[
  {
    "path": ".dockerignore",
    "content": "lego.exe\n.lego\n.gitcookies\n.idea\n.vscode/\ndist/\nbuilds/\ndocs/\n"
  },
  {
    "path": ".gitattributes",
    "content": "**/zz_gen_*.*   linguist-generated\ndocs/data/zz_cli_help.toml linguist-generated\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: 🐞 Bug Report\ndescription: Create a report to help us improve.\nlabels: [bug]\nbody:\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: Welcome\n      options:\n        - label: Yes, I'm using a binary release within the two latest releases.\n          required: true\n        - label: Yes, I've searched for similar issues on GitHub and didn't find any.\n          required: true\n        - label: Yes, I've included all information below (version, config, etc).\n          required: true\n\n  - type: textarea\n    id: expected\n    attributes:\n      label: What did you expect to see?\n      placeholder: Description.\n    validations:\n      required: true\n\n  - type: textarea\n    id: current\n    attributes:\n      label: What did you see instead?\n      placeholder: Description.\n    validations:\n      required: true\n\n  - type: dropdown\n    id: type\n    attributes:\n      label: How do you use lego?\n      options:\n        - I don't know\n        - Library\n        - Binary\n        - Docker image\n        - Through Traefik\n        - Through Caddy\n        - Through Terraform ACME provider\n        - Through Bitnami\n        - Through 1Panel\n        - Through Zoraxy\n        - Through Certimate\n        - go install\n        - Other\n    validations:\n      required: true\n\n  - type: textarea\n    id: steps\n    attributes:\n      label: Reproduction steps\n      description: \"How do you trigger this bug? Please walk us through it step by step.\"\n      placeholder: |\n        1. ...\n        2. ...\n        3. ...\n        ...\n    validations:\n      required: true\n\n  - type: textarea\n    id: version\n    attributes:\n      label: Effective version of lego\n      description: |-\n        `latest` or `dev` are not effective versions.\n        ```console\n        $ lego --version\n        ```\n      placeholder: Paste output here\n      render: console\n    validations:\n      required: true\n\n  - type: textarea\n    id: logs\n    attributes:\n      label: Logs\n      value: |-\n        <details>\n\n        ```console\n        # paste output here\n        ```\n\n        </details>\n    validations:\n      required: true\n\n  - type: textarea\n    id: go-env\n    attributes:\n      label: Go environment (if applicable)\n      value: |-\n        <details>\n\n        ```console\n        $ go version && go env\n        # paste output here\n        ```\n\n        </details>\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: ❓ Questions\n    url: https://github.com/go-acme/lego/discussions\n    about: If you have a question, or are looking for advice, please post on our Discussions section!\n  - name: 📖 Documentation\n    url: https://go-acme.github.io/lego/\n    about: Please take a look to our documentation.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: 💡 Feature request\ndescription: Suggest an idea for this project.\nbody:\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: Welcome\n      options:\n        - label: Yes, I've searched for similar issues on GitHub and didn't find any.\n          required: true\n\n  - type: dropdown\n    id: type\n    attributes:\n      label: How do you use lego?\n      options:\n        - I don't know\n        - Library\n        - Binary\n        - Docker image\n        - Through Traefik\n        - Through Caddy\n        - Through Terraform ACME provider\n        - Through Bitnami\n        - Through 1Panel\n        - Through Zoraxy\n        - Through Certimate\n        - go install\n        - Other\n    validations:\n      required: true\n\n  - type: input\n    id: version\n    attributes:\n      label: Effective version of lego\n      description: \"`latest` or `dev` are not effective versions.\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Detailed Description\n      placeholder: Description.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/new_dns_provider.yml",
    "content": "name: 🧩 New DNS provider support\ndescription: Request for the support of a new DNS provider.\ntitle: \"Support for provider: <put the name of your provider>\"\nlabels: [enhancement, new-provider]\nbody:\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: Welcome\n      options:\n        - label: Yes, I've searched for similar issues on GitHub and didn't find any.\n          required: true\n        - label: Yes, the DNS provider exposes a public API.\n          required: true\n        - label: Yes, I know that the lego maintainers don't have an account in all DNS providers in the world.\n          required: true\n\n  - type: checkboxes\n    id: pr\n    attributes:\n      label: Implementation\n      options:\n        - label: Yes, I'm able to create a pull request and be able to maintain the implementation.\n          required: false\n        - label: Yes, I can test an implementation with the help of the maintainers if someone creates a pull request.\n          required: false\n\n  - type: dropdown\n    id: type\n    attributes:\n      label: How do you use lego?\n      options:\n        - I don't know\n        - Library\n        - Binary\n        - Docker image\n        - Through Traefik\n        - Through Caddy\n        - Through Terraform ACME provider\n        - Through Bitnami\n        - Through 1Panel\n        - Through Zoraxy\n        - Through Certimate\n        - go install\n        - Other\n    validations:\n      required: true\n\n  - type: dropdown\n    id: profile\n    attributes:\n      label: Who are you?\n      options:\n        - A customer of this DNS provider\n        - An employee of this DNS provider\n        - Other (please explain)\n    validations:\n      required: true\n\n  - type: input\n    id: provider-link\n    attributes:\n      label: Link to the DNS provider\n      placeholder: Put your link here.\n    validations:\n      required: true\n\n  - type: input\n    id: api-link\n    attributes:\n      label:  Link to the API documentation\n      placeholder: Put your link here.\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected\n    attributes:\n      label: Additional Notes\n      placeholder: Your notes.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/mnp.md",
    "content": "PULL REQUEST TEMPLATE FOR MAINTAINERS ONLY.\n\nhttps://github.com/go-acme/lego/compare/master...ldez:branch?quick_pull=1&title=Add+DNS+provider+for+&labels=enhancement,area/dnsprovider,state/need-user-tests&template=mnp.md\n\n?quick_pull=1&title=Add+DNS+provider+for+&labels=enhancement,area/dnsprovider,state/need-user-tests&template=mnp.md\n\n---\n\n- [x] adds a description to your PR\n- [x] have a homogeneous design with the other providers\n- [ ] add tests (units)\n- [ ] add tests (\"live\")\n- [ ] add a provider descriptor\n- [ ] generate CLI help, documentation, and readme.\n- [ ] be able to do: _(and put the output of this command to a comment)_\n  ```bash\n  make build\n  rm -rf .lego\n\n  EXAMPLE_USERNAME=xxx \\\n  ./dist/lego -m your_email@example.com --dns EXAMPLE -d *.example.com -d example.com -s https://acme-staging-v02.api.letsencrypt.org/directory run\n  ```\n  Note the wildcard domain is important.\n- [ ] pass the linter\n- [ ] do `go mod tidy`\n\nPing @xxx, can you run the command (with your domain, email, credentials, etc.)?\n\nCloses #\n\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\n\nIMPORTANT:\n\n1. Create an issue and wait for a maintainer to approve it BEFORE opening a pull request.\n2. Don't open a work-in-progress pull request. If you open a PR, the PR must be ready to be reviewed.\n3. If a pull request doesn't follow one of the previous elements, it will be closed.\n\nAlso, pull requests from a fork inside a GitHub organization are not allowed because of access limitation on them.\nOnly pull requests from personal forks are allowed. \n\n-->\n"
  },
  {
    "path": ".github/workflows/documentation.yml",
    "content": "name: Documentation\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n\n  doc:\n    name: Build and deploy documentation\n    runs-on: ubuntu-latest\n    env:\n      GO_VERSION: stable\n      HUGO_VERSION: 0.148.2\n      CGO_ENABLED: 0\n\n    steps:\n\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: actions/setup-go@v6\n        with:\n          go-version: ${{ env.GO_VERSION }}\n\n      - name: Generate DNS docs\n        run: make generate-dns\n\n      - name: Install Hugo\n        run: |\n          wget -O /tmp/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_Linux-amd64.deb\n          sudo dpkg -i /tmp/hugo.deb\n\n      - name: Build Documentation\n        run: make docs-build\n\n      # https://github.com/marketplace/actions/github-pages\n      - name: Deploy to GitHub Pages\n        uses: crazy-max/ghaction-github-pages@v4\n        with:\n          target_branch: gh-pages\n          build_dir: docs/public\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/go-cross.yml",
    "content": "name: Go Matrix\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\njobs:\n\n  cross:\n    name: Go\n    runs-on: ${{ matrix.os }}\n    env:\n      CGO_ENABLED: 0\n\n    strategy:\n      matrix:\n        go-version: [ oldstable, stable ]\n        os: [ubuntu-latest, macos-latest, windows-latest]\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Test\n        run: go test -v -cover ./...\n\n      - name: Build\n        run: go build -v -ldflags \"-s -w\" -trimpath -o ./dist/lego ./cmd/lego/\n"
  },
  {
    "path": ".github/workflows/pr.yml",
    "content": "name: Main\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\njobs:\n\n  main:\n    name: Main Process\n    runs-on: ubuntu-latest\n    env:\n      GO_VERSION: stable\n      GOLANGCI_LINT_VERSION: v2.10\n      HUGO_VERSION: 0.148.2\n      CGO_ENABLED: 0\n      LEGO_E2E_TESTS: CI\n      MEMCACHED_HOSTS: localhost:11211\n\n    steps:\n\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: actions/setup-go@v6\n        with:\n          go-version: ${{ env.GO_VERSION }}\n\n      - name: Check and get dependencies\n        run: |\n          go mod tidy --diff\n\n      - name: Generate and Check generated elements\n        run: |\n          make generate-dns\n          git diff --exit-code\n\n      - uses: golangci/golangci-lint-action@v9\n        with:\n          version: ${{ env.GOLANGCI_LINT_VERSION }}\n          install-only: true\n\n      - name: Install Pebble\n        run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.9.0\n\n      - name: Install challtestsrv\n        run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.9.0\n\n      - name: Set up a Memcached server\n        run: docker run -d --rm -p 11211:11211 memcached:1.6-alpine\n\n      - name: Make\n        run: |\n          make\n          make clean\n\n      - name: Install Hugo\n        run: |\n          wget -O /tmp/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_Linux-amd64.deb\n          sudo dpkg -i /tmp/hugo.deb\n\n      - name: Build Documentation\n        run: make docs-build\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - v*\n\npermissions:\n  # Allow the workflow to write attestations.\n  id-token: write\n  attestations: write\n\njobs:\n\n  release:\n    name: Release version\n    runs-on: ubuntu-latest\n    env:\n      GO_VERSION: stable\n      CGO_ENABLED: 0\n\n    steps:\n      # temporary workaround for an error in free disk space action\n      # https://github.com/jlumbroso/free-disk-space/issues/14\n      - name: Update Package List and Remove Dotnet\n        run: |\n          sudo apt-get update\n          sudo apt-get remove -y '^dotnet-.*'\n\n      # https://github.com/marketplace/actions/free-disk-space-ubuntu\n      - name: Free Disk Space\n        uses: jlumbroso/free-disk-space@main\n        with:\n          # this might remove tools that are actually needed\n          tool-cache: false\n\n          # all of these default to true\n          android: true\n          dotnet: true\n          haskell: true\n          large-packages: true\n          docker-images: true\n          swap-storage: false\n\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - uses: actions/setup-go@v6\n        with:\n          go-version: ${{ env.GO_VERSION }}\n\n      - name: Docker Login\n        env:\n          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}\n          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}\n        run: echo \"${DOCKER_PASSWORD}\" | docker login --username \"${DOCKER_USERNAME}\" --password-stdin\n\n      - name: Install snapcraft\n        run: sudo snap install snapcraft --classic\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v2\n\n      # https://goreleaser.com/ci/actions/\n      - name: Run GoReleaser\n        id: goreleaser\n        uses: goreleaser/goreleaser-action@v6\n        with:\n          version: v2.13.0\n          args: release -p 1 --clean --timeout=90m\n        env:\n          GITHUB_TOKEN: ${{ secrets.GH_TOKEN_REPO }}\n          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}\n          AUR_KEY: ${{ secrets.AUR_KEY }}\n\n      - uses: actions/attest-build-provenance@v3\n        with:\n          subject-checksums: ./dist/lego_${{ fromJSON(steps.goreleaser.outputs.metadata).version }}_checksums.txt\n          github-token: ${{ secrets.GH_TOKEN_REPO }}\n      - uses: actions/attest-build-provenance@v3\n        with:\n          subject-checksums: ./dist/digests.txt\n          github-token: ${{ secrets.GH_TOKEN_REPO }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".lego\n.gitcookies\n.idea\n.vscode/\ndist/\nbuilds/\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\n\nformatters:\n  enable:\n    - gci\n    - gofmt\n    - gofumpt\n    - goimports\n  settings:\n    gofumpt:\n      extra-rules: true\n    gofmt:\n      rewrite-rules:\n        - pattern: 'interface{}'\n          replacement: 'any'\n\nlinters:\n  default: all\n  disable:\n    - wsl # Deprecated\n    - bodyclose\n    - canonicalheader\n    - contextcheck\n    - cyclop # duplicate of gocyclo\n    - dupl # not relevant\n    - err113 # not relevant\n    - errchkjson\n    - errname\n    - exhaustive # not relevant\n    - exhaustruct # not relevant\n    - forbidigo\n    - forcetypeassert\n    - gosec\n    - gosmopolitan # not relevant\n    - ireturn # not relevant\n    - lll\n    - makezero # not relevant\n    - mnd\n    - musttag # false-positive https://github.com/junk1tm/musttag/issues/17\n    - nestif # too many false-positive\n    - nilnil # not relevant\n    - nlreturn # not relevant\n    - noctx\n    - noinlineerr # too strict\n    - nonamedreturns\n    - paralleltest # not relevant\n    - prealloc # too many false-positive\n    - rowserrcheck # not relevant (SQL)\n    - sqlclosecheck # not relevant (SQL)\n    - tagliatelle\n    - testpackage # not relevant\n    - tparallel # not relevant\n    - varnamelen # not relevant\n    - wrapcheck\n\n  settings:\n    depguard:\n      rules:\n        main:\n          deny:\n            - pkg: github.com/instana/testify\n              desc: not allowed\n            - pkg: github.com/pkg/errors\n              desc: Should be replaced by standard lib errors package\n    funlen:\n      lines: -1\n      statements: 50\n    goconst:\n      min-len: 3\n      min-occurrences: 3\n    gocritic:\n      disabled-checks:\n        - paramTypeCombine # already handle by gofumpt.extra-rules\n        - whyNoLint # already handle by nonolint\n        - unnamedResult\n        - hugeParam\n        - sloppyReassign\n        - rangeValCopy\n        - octalLiteral\n        - ptrToRefParam\n        - appendAssign\n        - ruleguard\n        - httpNoBody\n        - exposedSyncMutex\n      enabled-tags:\n        - diagnostic\n        - style\n        - performance\n    gocyclo:\n      min-complexity: 12\n    godox:\n      keywords:\n        - FIXME\n    govet:\n      disable:\n        - fieldalignment\n      enable-all: true\n      settings:\n        printf:\n          funcs:\n            - Print\n            - Printf\n            - Warn\n            - Warnf\n            - Fatal\n            - Fatalf\n    misspell:\n      locale: US\n      ignore-rules:\n        - internetbs\n    perfsprint:\n      err-error: true\n      errorf: true\n      sprintf1: true\n      strconcat: false\n    revive:\n      rules:\n        - name: struct-tag\n        - name: blank-imports\n        - name: context-as-argument\n        - name: context-keys-type\n        - name: dot-imports\n        - name: error-return\n        - name: error-strings\n        - name: error-naming\n        - name: exported\n          disabled: true\n        - name: if-return\n        - name: increment-decrement\n        - name: var-naming\n        - name: var-declaration\n        - name: package-comments\n          disabled: true\n        - name: range\n        - name: receiver-naming\n        - name: time-naming\n        - name: unexported-return\n        - name: indent-error-flow\n        - name: errorf\n        - name: empty-block\n        - name: superfluous-else\n        - name: unused-parameter\n          disabled: true\n        - name: unreachable-code\n        - name: redefines-builtin-id\n    tagalign:\n      align: false\n      order:\n        - xml\n        - json\n        - yaml\n        - yml\n        - toml\n        - mapstructure\n        - url\n    testifylint:\n      disable:\n        - require-error\n        - go-require\n    usetesting:\n      os-setenv: false # we already have a test \"framework\" to handle env vars\n    funcorder:\n      struct-method: false\n\n  exclusions:\n    warn-unused: true\n    presets:\n      - comments\n      - std-error-handling\n    paths:\n      # Those elements are related to code borrowed from the official HuaweiCloud API client.\n      - providers/dns/huaweicloud/internal\n    rules:\n      - path: (.+)_test.go\n        linters:\n          - funlen\n          - goconst\n          - maintidx\n      - path: (.+)_test.go\n        text: Error return value of `fmt.Fprintln` is not checked\n        linters:\n          - errcheck\n      - text: \"var-naming: avoid meaningless package names\"\n        linters:\n          - revive\n      - text: \"var-naming: avoid package names that conflict with Go standard library package names\"\n        linters:\n          - revive\n      - path: certcrypto/crypto.go\n        text: (tlsFeatureExtensionOID|ocspMustStapleFeature) is a global variable\n        linters:\n          - gochecknoglobals\n      - path: challenge/dns01/nameserver.go\n        text: (defaultNameservers|recursiveNameservers|fqdnSoaCache|muFqdnSoaCache) is a global variable\n        linters:\n          - gochecknoglobals\n      - path: challenge/dns01/nameserver_.+.go\n        text: dnsTimeout is a global variable\n        linters:\n          - gochecknoglobals\n      - path: challenge/dns01/precheck.go\n        text: defaultNameserverPort is a global variable\n        linters:\n          - gochecknoglobals\n      - path: challenge/http01/domain_matcher.go\n        text: cyclomatic complexity \\d+ of func `parseForwardedHeader` is high\n        linters:\n          - gocyclo\n      - path: challenge/http01/domain_matcher.go\n        text: Function 'parseForwardedHeader' has too many statements\n        linters:\n          - funlen\n      - path: challenge/tlsalpn01/tls_alpn_challenge.go\n        text: idPeAcmeIdentifierV1 is a global variable\n        linters:\n          - gochecknoglobals\n      - path: log/logger.go\n        text: Logger is a global variable\n        linters:\n          - gochecknoglobals\n      - path: e2e/(dnschallenge/)?[\\d\\w]+_test.go\n        text: load is a global variable\n        linters:\n          - gochecknoglobals\n      - path: providers/(dns|http)/([\\d\\w]+/)*[\\d\\w]+_test.go\n        text: envTest is a global variable\n        linters:\n          - gochecknoglobals\n      - path: providers/dns/namecheap/namecheap_test.go\n        text: testCases is a global variable\n        linters:\n          - gochecknoglobals\n      - path: providers/dns/namecheap/transport.go\n        text: (envProxyOnce|envProxyFuncValue) is a global variable\n        linters:\n          - gochecknoglobals\n      - path: providers/dns/acmedns/mock_test.go\n        text: egTestAccount is a global variable\n        linters:\n          - gochecknoglobals\n      - path: providers/http/memcached/memcached_test.go\n        text: memcachedHosts is a global variable\n        linters:\n          - gochecknoglobals\n      - path: providers/dns/checkdomain/internal/types.go\n        text: '`payed` is a misspelling of `paid`'\n        linters:\n          - misspell\n      - path: platform/tester/env_test.go\n        linters:\n          - thelper\n      - path: providers/dns/oraclecloud/oraclecloud_test.go\n        text: 'SA1019: x509.EncryptPEMBlock has been deprecated since Go 1.16'\n        linters:\n          - staticcheck\n      - path: providers/dns/sakuracloud/wrapper.go\n        text: mu is a global variable\n        linters:\n          - gochecknoglobals\n      - path: cmd/cmd_renew.go\n        text: cyclomatic complexity \\d+ of func `(renewForDomains|renewForCSR)` is high\n        linters:\n          - gocyclo\n      - path: cmd/cmd_renew.go\n        text: Function 'renewForDomains' has too many statements\n        linters:\n          - funlen\n      - path: providers/dns/cpanel/cpanel.go\n        text: cyclomatic complexity 13 of func `\\(\\*DNSProvider\\)\\.CleanUp` is high\n        linters:\n          - gocyclo\n      - path: providers/dns/manual/manual.go\n        text: 'SA1019: dns01.DNSProviderManual is deprecated'\n        linters:\n          - staticcheck\n      # Those elements have been replaced by non-exposed structures.\n      - path: providers/dns/linode/linode_test.go\n        text: 'SA1019: linodego\\.(DomainsPagedResponse|DomainRecordsPagedResponse) is deprecated'\n        linters:\n          - staticcheck\n\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "version: 2\n\nproject_name: lego\n\nbuilds:\n  - binary: lego\n\n    main: ./cmd/lego/\n    env:\n      - CGO_ENABLED=0\n    flags:\n      - -trimpath\n    ldflags:\n      - -s -w -X main.version={{.Version}}\n\n    goos:\n      - linux\n      - darwin\n      - windows\n      - freebsd\n      - openbsd\n      - solaris\n    goarch:\n      - amd64\n      - 386\n      - arm\n      - arm64\n      - mips\n      - mipsle\n      - mips64\n      - mips64le\n    goarm:\n      - 7\n      - 6\n      - 5\n    gomips:\n      - hardfloat\n      - softfloat\n\n    ignore:\n      - goos: darwin\n        goarch: 386\n      - goos: openbsd\n        goarch: arm\n      # Deprecated in go1.25, Removed in go1.26\n      # https://go.dev/doc/go1.25#windows\n      - goos: windows\n        goarch: arm\n\nchangelog:\n  sort: asc\n  filters:\n    exclude:\n      - '(?i)^chore:'\n      - '(?i)^Detach v[\\d|.]+'\n      - '(?i)^Prepare release v[\\d|.]+'\n\nrelease:\n  skip_upload: false\n  github:\n    owner: 'go-acme'\n    name: 'lego'\n  header: |\n    lego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️\n    \n    Everybody thinks that the others will donate, but in the end, nobody does.\n    \n    So if you think that lego is worth it, please consider [donating](https://donate.ldez.dev).\n    \n    For key updates, see the [changelog](https://github.com/go-acme/lego/blob/HEAD/CHANGELOG.md#v{{ .Major }}{{ .Minor }}{{ .Patch }}).\n\narchives:\n  - id: lego\n    name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}'\n    formats: ['tar.gz']\n    format_overrides:\n      - goos: windows\n        formats: ['zip']\n    files:\n      - LICENSE\n      - CHANGELOG.md\n\ndockers_v2:\n  - images:\n      - 'goacme/lego'\n    dockerfile: buildx.Dockerfile\n    platforms:\n      - linux/amd64\n      - linux/arm64\n      - linux/arm/v7\n    tags:\n      - 'latest'\n      - 'v{{ .Major }}'\n      - 'v{{ .Major }}.{{ .Minor }}'\n      - '{{ .Tag }}'\n    labels:\n      # https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys\n      'org.opencontainers.image.title': '{{.ProjectName}}'\n      'org.opencontainers.image.description': 'Lets Encrypt/ACME client and library written in Go'\n      'org.opencontainers.image.source': '{{.GitURL}}'\n      'org.opencontainers.image.url': '{{.GitURL}}'\n      'org.opencontainers.image.documentation': 'https://go-acme.github.io/lego'\n      'org.opencontainers.image.created': '{{.Date}}'\n      'org.opencontainers.image.revision': '{{.FullCommit}}'\n      'org.opencontainers.image.version': '{{.Version}}'\n\nsnapcrafts:\n  - name_template: \"{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}\"\n    disable: false\n    publish: true\n    grade: stable\n    confinement: strict\n    license: MIT\n    base: core22\n    summary: Lego is a Let's Encrypt/ACME client.\n    description: |\n      Lego is a Let's Encrypt/ACME client written in Go.\n      \n      The lego snap makes it easy to install and use Lego on any Linux distribution that supports snaps.\n      \n      Usage:\n      * `sudo snap install lego`\n      * `sudo lego --email=\"you@example.com\" --domains=\"example.com\" --server=https://acme-staging-v02.api.letsencrypt.org/directory --http --http.port :8080 run\n    apps:\n      lego:\n        command: lego\n        environment:\n          LEGO_PATH: /var/snap/lego/common/.lego\n        plugs:\n          - network-bind\n\naurs:\n  - description: \"Let s Encrypt client and ACME library written in Go\"\n    skip_upload: false\n    homepage: https://go-acme.github.io/lego/\n    name: 'lego-bin'\n    provides:\n      - lego\n    maintainers:\n      - \"Fernandez Ludovic <lfernandez dot dev at gmail dot com>\"\n    license: APACHE\n    private_key: \"{{ .Env.AUR_KEY }}\"\n    git_url: \"ssh://aur@aur.archlinux.org/lego-bin.git\"\n    commit_author:\n      name: ldez\n      email: ldez@users.noreply.github.com\n    package: |-\n      # Bin\n      install -Dm755 \"./lego\" \"${pkgdir}/usr/bin/lego\"\n\n      # License\n      install -Dm644 \"./LICENSE\" \"${pkgdir}/usr/share/licenses/lego/LICENSE\"\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nlego is an independent, free, open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️\n\nEverybody thinks that the others will donate, but in the end, nobody does.\n\nSo if you think that lego is worth it, please consider [donating](https://donate.ldez.dev).\n\n## v4.33.0\n\n- Release date: 2026-03-19\n- Tag: [v4.33.0](https://github.com/go-acme/lego/releases/tag/v4.33.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Excedo\n- **[dnsprovider]** Add DNS provider for EuroDNS\n- **[dnsprovider]** Add DNS provider for Czechia\n\n### Changed\n\n- **[lib]** feat: allow to Unwrap obtainError\n\n### Fixed\n\n- **[dnsprovider]** liara: add support for team ID\n- **[dnsprovider]** gigahostno: remove unused Zone fields\n\n## v4.32.0\n\n- Release date: 2026-02-19\n- Tag: [v4.32.0](https://github.com/go-acme/lego/releases/tag/v4.32.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for ArtFiles\n- **[dnsprovider]** Add DNS provider for Leaseweb\n- **[dnsprovider]** Add DNS provider for FusionLayer NameSurfer\n- **[dnsprovider]** Add DNS provider for DDNSS\n- **[dnsprovider]** Add DNS provider for Bluecat v2\n- **[dnsprovider]** Add DNS provider for TodayNIC/时代互联\n- **[dnsprovider]** Add DNS provider for DNSExit\n- **[dnsprovider]** alidns: add line record option\n\n### Changed\n\n- **[dnsprovider]** azure: reinforces deprecation\n- **[dnsprovider]** allinkl: detect zone through API\n\n### Fixed\n\n- **[ari]** fix: implement parsing for Retry-After header according to RFC 7231\n- **[dnsprovider]** namesurfer: fix updateDNSHost\n- **[dnsprovider]** timewebcloud: fix subdomain support\n- **[dnsprovider]** fix: deduplicate authz for DNS01 challenge\n- **[lib,cli]** fix: use IPs to define the main domain\n- **[lib]** fix: preserve domain order\n\n## v4.31.0\n\n- Release date: 2026-01-08\n- Tag: [v4.31.0](https://github.com/go-acme/lego/releases/tag/v4.31.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for ISPConfig\n- **[dnsprovider]** Add DNS Provider for ISPConfig (DDNS Module)\n- **[dnsprovider]** Add DNS provider for Alwaysdata\n- **[dnsprovider]** Add DNS provider for JDCloud\n- **[dnsprovider]** Add DNS provider for 35.com/三五互联\n- **[dnsprovider]** f5xc: add an option to configure the domain of the server\n\n### Changed\n\n- **[lib]** feat: improve ACME error types\n- **[dnsprovider,cname]** namedotcom: follow CNAME\n\n### Fixed\n\n- **[dnsprovider]** hetzner: fix compatibility with _FILE suffix\n- **[dnsprovider]** gandiv5: fix API Key header\n\n## v4.30.1\n\n- Release date: 2025-12-16\n- Tag: [v4.30.1](https://github.com/go-acme/lego/releases/tag/v4.30.1)\n\nDue to an error related to `aliyun/credentials-go`, some artifacts of the v4.30.0 release have not been published.\n\nThis release contains the same things as v4.30.0.\n\n## v4.30.0\n\n- Release date: 2025-12-16\n- Tag: [v4.30.0](https://github.com/go-acme/lego/releases/tag/v4.30.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Ionos Cloud\n- **[dnsprovider]** Add DNS provider for Virtualname\n- **[dnsprovider]** Add DNS Provider for Neodigit\n- **[dnsprovider]** Add DNS provider for Syse.no\n- **[dnsprovider]** Add DNS provider for Gravity\n- **[dnsprovider]** Add DNS provider for hosting.nl\n\n### Changed\n\n- **[cli]** feat: remove email requirement\n\n### Fixed\n\n- **[dnsprovider]** autodns: use the right response structure\n\n## v4.29.0\n\n- Release date: 2025-11-29\n- Tag: [v4.29.0](https://github.com/go-acme/lego/releases/tag/v4.29.0)\n\n### Added\n \n- **[dnsprovider]** Add DNS provider for United-Domains\n- **[dnsprovider]** Add DNS provider for Gigahost.no\n- **[dnsprovider]** Add DNS provider for EdgeCenter\n- **[dnsprovider]** Add DNS provider for AlibabaCloud ESA\n- **[dnsprovider]** edgeone: add zones mapping\n- **[dnsprovider]** namecheap: add experimental proxy support\n\n### Changed\n\n- **[dnsprovider]** gandiv5: update base API URL\n\n### Fixed\n\n- **[dnsprovider]** hetzner: use int64 for IDs\n- **[dnsprovider]** baiducloud: pagination and TTL\n- **[dnsprovider]** inwx: fix API breaking changes with record IDs\n\n## v4.28.1\n\n- Release date: 2025-11-06\n- Tag: [v4.28.1](https://github.com/go-acme/lego/releases/tag/v4.28.1)\n\n### Fixed\n\n- **[cli]** fix: skip nil response\n\n## v4.28.0\n\n- Release date: 2025-10-31\n- Tag: [v4.28.0](https://github.com/go-acme/lego/releases/tag/v4.28.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Anexia\n- **[dnsprovider]** Add DNS provider for webnames.ca\n- **[dnsprovider]** webnames: rename to webnamesru to avoid ambiguity with webnamesca\n\n### Changed\n\n- **[dnsprovider,log]** hetzner: add deprecation logs\n- **[dnsprovider]** iwantmyname: provider deprecation\n- **[cli]** improve retryable HTTP client error handling\n\n### Fixed\n\n- **[dnsprovider]** hostinger: fix record update\n\n## v4.27.0\n\n- Release date: 2025-10-17\n- Tag: [v4.27.0](https://github.com/go-acme/lego/releases/tag/v4.27.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Octenium\n- **[dnsprovider]** Add DNS provider for Hostinger\n- **[dnsprovider]** Add DNS provider for Beget.com\n\n### Changed\n\n- **[cli]** support `--private-key` with a PKCS#8 keypair\n- **[dnsprovider]** hetzner: update to new API\n- **[dnsprovider]** otc: adds option to use private zone\n\n### Fixed\n\n- **[lib]** fix: deduplicate order identifiers\n\n## v4.26.0\n\n- Release date: 2025-09-13\n- Tag: [v4.26.0](https://github.com/go-acme/lego/releases/tag/v4.26.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for KeyHelp\n- **[dnsprovider]** Add DNS provider for Binary Lane\n- **[dnsprovider]** Add DNS provider for Tencent EdgeOne\n- **[dnsprovider]** azuredns: pipeline credential support\n- **[dnsprovider]** oraclecloud: handle instance_principal authentication\n\n### Changed\n\n- **[dnsprovider]** oraclecloud: add env var aliases\n- **[dnsprovider]** simply: update to API v2\n- **[lib,cli]** EAB: fallback to base64.URLEncoding\n\n### Fixed\n\n- **[dnsprovider]** selectelv2: add missing options\n\n## v4.25.2\n\n- Release date: 2025-08-06\n- Tag: [v4.25.2](https://github.com/go-acme/lego/releases/tag/v4.25.2)\n\n### Changed\n\n- **[cli,log]** log when dynamic renew date not yet reached\n\n### Fixed\n\n- **[cli]** fix: remove wrong env var\n- **[lib,cli]** fix: enforce HTTPS to the ACME server\n\n## v4.25.1\n\n- Release date: 2025-07-21\n- Tag: [v4.25.1](https://github.com/go-acme/lego/releases/tag/v4.25.1)\n\n### Fixed\n\n- **[cli]** fix: wrong CLI flag type\n\n## v4.25.0\n\n- Release date: 2025-07-21\n- Tag: [v4.25.0](https://github.com/go-acme/lego/releases/tag/v4.25.0)\n\nThe binary size of this release is about ~50% smaller compared to previous releases.\n\nThis 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).\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for ZoneEdit\n- **[cli]** Add an option to define dynamically the renew date\n- **[lib,cli]** Add an option to disable common name in CSR\n\n### Changed\n\n- **[dnsprovider]** vinyldns: add an option to add quotes around the TXT record value\n- **[dnsprovider]** ionos: increase default propagation timeout\n\n### Fixed\n\n- **[cli]** fix: enforce domain into renewal command\n\n## v4.24.0\n\n- Release date: 2025-07-07\n- Tag: [v4.24.0](https://github.com/go-acme/lego/releases/tag/v4.24.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Azion\n- **[dnsprovider]** Add DNS provider for DynDnsFree.de\n- **[dnsprovider]** Add DNS provider for ConoHa v3\n- **[dnsprovider]** Add DNS provider for RU Center\n- **[dnsprovider]** gcloud: add service account impersonation\n\n### Changed\n\n- **[dnsprovider]** pdns: improve error messages\n- **[dnsprovider]** cloudflare: add quotation marks to TXT record\n- **[dnsprovider]** googledomains: provider deprecation\n- **[dnsprovider]** mijnhost: improve record filter\n\n### Fixed\n\n- **[dnsprovider]** exoscale: fix find record\n- **[dnsprovider]** nicmanager: fix mode env var name and value\n- **[lib,cli]** Check order identifiers difference between client and server\n\n## v4.23.1\n\n- Release date: 2025-04-16\n- Tag: [v4.23.1](https://github.com/go-acme/lego/releases/tag/v4.23.1)\n\nDue to an error related to Snapcraft, some artifacts of the v4.23.0 release have not been published.\n\nThis release contains the same things as v4.23.0. \n\n## v4.23.0\n\n- Release date: 2025-04-16\n- Tag: [v4.23.0](https://github.com/go-acme/lego/releases/tag/v4.23.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Active24\n- **[dnsprovider]** Add DNS provider for BookMyName\n- **[dnsprovider]** Add DNS provider for Axelname\n- **[dnsprovider]** Add DNS provider for Baidu Cloud\n- **[dnsprovider]** Add DNS provider for Metaregistrar\n- **[dnsprovider]** Add DNS provider for F5 XC\n- **[dnsprovider]** Add INFOBLOX_CA_CERTIFICATE option\n- **[dnsprovider]** route53: adds option to use private zone\n- **[dnsprovider]** edgedns: add account switch key option\n- **[dnsprovider]** infoblox: update API client to v2\n- **[lib,cli]** Add delay option for TLSALPN challenge\n\n### Changed\n\n- **[dnsprovider]** designate: speed up API requests by using filters\n- **[dnsprovider]** cloudflare: make base URL configurable\n- **[dnsprovider]** websupport: migrate to API v2\n- **[dnsprovider]** dnssimple: use GetZone\n\n### Fixed\n\n- **[ari]** Fix retry on `alreadyReplaced` error\n- **[cli,log]** Fix malformed log messages\n- **[cli]** Kill hook when the command is stuck\n- **[dnsprovider]** pdns: fix TXT record cleanup for wildcard domains\n- **[dnsprovider]** allinkl: remove `ReturnInfo`\n\n## v4.22.2\n\n- Release date: 2025-02-17\n- Tag: [v4.22.2](https://github.com/go-acme/lego/releases/tag/v4.22.2)\n\n### Fixed\n\n- **[dnsprovider]** acme-dns: use new registred account\n\n## v4.22.1\n\n- Release date: 2025-02-17\n- Tag: [v4.22.1](https://github.com/go-acme/lego/releases/tag/v4.22.1)\n\n### Fixed\n\n- **[dnsprovider]** acme-dns: continue the process when the CNAME is handled by the storage\n\n### Added\n\n## v4.22.0\n\n- Release date: 2025-02-17\n- Tag: [v4.22.0](https://github.com/go-acme/lego/releases/tag/v4.22.0)\n\n### Added\n\n- **[cli]** Add `--private-key` flag to set the private key.\n- **[cli]** Add `LEGO_DEBUG_ACME_HTTP_CLIENT` environment variable to debug the calls to the ACME server.\n- **[cli]** Add `LEGO_EMAIL` environment variable for specifying email.\n- **[cli]** Add `--hook-timeout` flag to run and renew commands.\n- **[dnsprovider]** Add DNS provider for myaddr.{tools,dev,io}\n- **[dnsprovider]** Add DNS provider for Spaceship\n- **[dnsprovider]** acme-dns: add HTTP storage\n- **[lib,cli,httpprovider]** Add `--http.delay` option for HTTP challenge.\n- **[lib,cli,profiles]** Add support for Profiles Extension.\n- **[lib]** Add an option to set CSR email addresses\n\n### Changed\n\n- **[lib]** rewrite status management\n- **[dnsprovider]** docs: improve units and default values\n\n### Removed\n\n- **[dnsprovider]** netcup: remove TTL option\n\n### Fixed\n\n- **[cli,log]** remove extra debug logs\n\n## v4.21.0\n\n- Release date: 2024-12-20\n- Tag: [v4.21.0](https://github.com/go-acme/lego/releases/tag/v4.21.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Rainyun/雨云\n- **[dnsprovider]** Add DNS provider for West.cn/西部数码\n- **[dnsprovider]** Add DNS provider for ManageEngine CloudDNS\n- **[cli]** feat: add --force-cert-domains flag to renew\n\n### Fixed\n\n- **[cli]** create client only when needed\n- **[cli]** clone the transport with tls-skip-verify\n- **[cli]** use retryable client for ACME server calls\n- **[dnsprovider]** bunny: fix zone detection\n- **[dnsprovider]** inwx: delete only the TXT record related to the DNS challenge\n- **[dnsprovider]** infomaniak: increase default propagation timeout\n- **[dnsprovider]** dnsmadeeasy: use default transport\n- **[dnsprovider]** netcup: increase default propagation values\n- **[dnsprovider]** otc: use default transport\n\n## v4.20.4\n\n- Release date: 2024-11-21\n- Tag: [v4.20.4](https://github.com/go-acme/lego/releases/tag/v4.20.4)\n\nPublish the Snap to the Snapcraft stable channel.\n\n## v4.20.3\n\n- Release date: 2024-11-21\n- Tag: [v4.20.3](https://github.com/go-acme/lego/releases/tag/v4.20.3)\n\n### Fixed\n\n- **[dnsprovider]** technitium: fix status code handling\n- **[dnsprovider]** directadmin: fix timeout configuration\n- **[httpprovider]** fix: HTTP server IPv6 matching\n\n## v4.20.2\n\n- Release date: 2024-11-11\n- Tag: [v4.20.2](https://github.com/go-acme/lego/releases/tag/v4.20.2)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Technitium\n- **[dnsprovider]** Add DNS provider for Regfish\n- **[dnsprovider]** Add DNS provider for Timeweb Cloud\n- **[dnsprovider]** Add DNS provider for Volcano Engine\n- **[dnsprovider]** Add DNS provider for Core-Networks\n- **[dnsprovider]** rfc2136: add support for tsig-keygen generated file\n- **[cli]** Add option to skip the TLS verification of the ACME server\n- Add documentation for env var only options\n\n### Changed\n\n- **[cli,ari]** Attempt to check ARI unless explicitly disabled\n- **[dnsprovider]** Improve propagation check error messages\n- **[dnsprovider]** cloudxns: provider deprecation\n- **[dnsprovider]** brandit: provider deprecation\n\n### Fixed\n\n- **[dnsprovider]** regru: update authentication method\n- **[dnsprovider]** selectelv2: fix non-ASCII domain\n- **[dnsprovider]** limacity: fix error message\n- **[dnsprovider]** volcengine: set API information within the default configuration\n- **[log]** Parse printf verbs in log line output\n\n## v4.20.1\n\n- Release date: 2024-11-11\n\nCancelled due to CI failure.\n\n## v4.20.0\n\n- Release date: 2024-11-11\n\nCancelled due to CI failure.\n\n## v4.19.2\n\n- Release date: 2024-10-06\n- Tag: [v4.19.2](https://github.com/go-acme/lego/releases/tag/v4.19.2)\n\n### Fixed\n\n- **[lib]** go1.22 compatibility\n\n## v4.19.1\n\n- Release date: 2024-10-06\n- Tag: [v4.19.1](https://github.com/go-acme/lego/releases/tag/v4.19.1)\n\n### Fixed\n\n- **[dnsprovider]** selectelv2: use baseURL from configuration\n- **[dnsprovider]** epik: add User-Agent\n\n## v4.19.0\n\n- Release date: 2024-10-03\n- Tag: [v4.19.0](https://github.com/go-acme/lego/releases/tag/v4.19.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for HuaweiCloud\n- **[dnsprovider]** Add DNS provider for SelfHost.(de|eu)\n- **[lib,cli,dnsprovider]** Add `dns.propagation-rns` option\n- **[cli,dnsprovider]** Add `dns.propagation-wait` flag\n- **[lib,dnsprovider]** Add `PropagationWait` function\n\n### Changed\n\n- **[dnsprovider]** ionos: follow CNAME\n- **[lib,dnsprovider]** Reducing the lock strength of the soa cache entry\n- **[lib,cli,dnsprovider]** Deprecation of `dns.disable-cp`, replaced by `dns.propagation-disable-ans`.\n\n### Fixed\n\n- **[dnsprovider]** Use UTC instead of GMT when possible\n- **[dnsprovider]** namesilo: restrict CleanUp\n- **[dnsprovider]** godaddy: fix cleanup\n\n## v4.18.0\n\n- Release date: 2024-08-30\n- Tag: [v4.18.0](https://github.com/go-acme/lego/releases/tag/v4.18.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for mijn.host\n- **[dnsprovider]** Add DNS provider for Lima-City\n- **[dnsprovider]** Add DNS provider for DirectAdmin\n- **[dnsprovider]** Add DNS provider for Mittwald\n- **[lib,cli]** feat: add option to handle the overall request limit\n- **[lib]** feat: expose certificates pool creation\n\n### Changed\n\n- **[cli]** feat: add LEGO_ISSUER_CERT_PATH to run hook\n- **[dnsprovider]** bluecat: skip deploy\n- **[dnsprovider]** ovh: allow to use ovh.conf file\n- **[dnsprovider]** designate: allow manually overwriting DNS zone\n\n### Fixed\n\n- **[ari]** fix: avoid Int63n panic in ShouldRenewAt()\n\n## v4.17.4\n\n- Release date: 2024-06-12\n- Tag: [v4.17.4](https://github.com/go-acme/lego/releases/tag/v4.17.4)\n\n### Fixed\n\n- **[dnsprovider]** Update dependencies\n\n## v4.17.3\n\n- Release date: 2024-05-28\n- Tag: [v4.17.3](https://github.com/go-acme/lego/releases/tag/v4.17.3)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Selectel v2\n- **[dnsprovider]** route53: adds option to not wait for changes\n- **[dnsprovider]** ovh: add OAuth2 authentication\n- **[dnsprovider]** azuredns: use TenantID also for cli authentication\n- **[dnsprovider]** godaddy: documentation about new API limitations\n- **[cli]** feat: add LEGO_ISSUER_CERT_PATH to hook\n\n### Changed\n\n- **[dnsprovider]** dode: update API URL\n- **[dnsprovider]** exec: stream command output\n- **[dnsprovider]** oracle: update API client\n- **[dnsprovider]** azuredns: servicediscovery for zones\n- **[dnsprovider]** scaleway: add alternative env var names\n- **[dnsprovider]** exoscale: simplify record creation\n- **[dnsprovider]** httpnet: add provider to NewDNSChallengeProviderByName\n- **[cli]** feat: fills LEGO_CERT_PFX_PATH and LEGO_CERT_PEM_PATH only when needed\n- **[lib,ari]** feat: renewal retry after value\n\n### Fixed\n\n- **[dnsprovider]** pdns: reconstruct zone URLs to enable non-root folder API endpoints\n- **[dnsprovider]** alidns: fix link to API documentation\n\n## v4.17.2\n\n- Release date: 2024-05-28\n\nCanceled due to a release failure related to Snapcraft.\n\nThe Snapcraft release are disabled for now.\n\n## v4.17.1\n\n- Release date: 2024-05-28\n\nCanceled due to a release failure related to oci-go-sdk.\n\nThe module `github.com/oracle/oci-go-sdk/v65` uses `github.com/gofrs/flock` but flock doesn't support some platform (like Solaris):\n- https://github.com/gofrs/flock/issues/60\n\nDue to that we will remove the Solaris build.\n\n## v4.17.0\n\n- Release date: 2024-05-28\n\nCanceled due to a release failure related to Snapcraft.\n\n## v4.16.1\n\n- Release date: 2024-03-10\n- Tag: [v4.16.1](https://github.com/go-acme/lego/releases/tag/v4.16.1)\n\n### Fixed\n\n- **[cli,ari]** fix: don't generate ARI cert ID if ARI is not enable\n\n## v4.16.0\n\n- Release date: 2024-03-09\n- Tag: [v4.16.0](https://github.com/go-acme/lego/releases/tag/v4.16.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Shellrent\n- **[dnsprovider]** Add DNS provider for Mail-in-a-Box\n- **[dnsprovider]** Add DNS provider for CPanel and WHM\n\n### Changed\n\n- **[lib,ari]** Implement 'replaces' field in newOrder and draft-ietf-acme-ari-03 CertID changes\n- **[log]** feat: improve errors and logs related to DNS call\n- **[lib]** update to go-jose/go-jose/v4 v4.0.1\n\n### Fixed\n\n- **[dnsprovider]** nifcloud: fix bug in case of same auth zone\n- **[dnsprovider]** bunny: Support delegated subdomains\n- **[dnsprovider]** easydns: fix zone detection\n- **[dnsprovider]** ns1: fix record creation\n\n## v4.15.0\n\n- Release date: 2024-01-28\n- Tag: [v4.15.0](https://github.com/go-acme/lego/releases/tag/v4.15.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for http.net\n- **[dnsprovider]** Add DNS provider for Webnames\n\n### Changed\n\n- **[cli]** Add environment variable for specifying alternate directory URL\n- **[cli]** Add format option for PFX encoding\n- **[lib]** Support simplified issuance for very long domain names at Let's Encrypt\n- **[lib]** Update CertID format as per draft-ietf-acme-ari-02\n- **[dnsprovider]** azuredns: allow OIDC authentication\n- **[dnsprovider]** azuredns: provide the ability to select authentication methods\n- **[dnsprovider]** efficientip: add insecure skip verify option\n- **[dnsprovider]** gandiv5: add Personal Access Token support\n- **[dnsprovider]** gcloud: support GCE_ZONE_ID to bypass zone list\n- **[dnsprovider]** liquidweb: add LWAPI_ prefix for env vars\n- **[dnsprovider]** liquidweb: detect zone automatically\n- **[dnsprovider]** pdns: optional custom API version\n- **[dnsprovider]** regru: client certificate support\n- **[dnsprovider]** regru: HTTP method changed to POST\n- **[dnsprovider]** scaleway: add cname support\n\n### Fixed\n\n- **[dnsprovider]** cloudru: change default URLs\n- **[dnsprovider]** constellix: follow rate limiting headers\n- **[dnsprovider]** desec: increase default propagation interval\n- **[dnsprovider]** gandiv5: Add \"Bearer\" prefix to the auth header\n- **[dnsprovider]** inwx: improve sleep calculation\n- **[dnsprovider]** inwx: wait before generating new TOTP TANs\n- **[dnsprovider]** ionos: fix DNS record removal\n- **[dnsprovider]** ipv64: remove unused option\n- **[dnsprovider]** nifcloud: fix API requests\n- **[dnsprovider]** otc: sequential challenge\n\n## v4.14.1\n\n- Release date: 2023-09-20\n- Tag: [v4.14.1](https://github.com/go-acme/lego/releases/tag/v4.14.1)\n\n### Fixed\n\n- **[dnsprovider]** bunny: fix zone detection\n- **[dnsprovider]** bunny: use NRDCG fork\n- **[dnsprovider]** ovh: update client to v1.4.2\n\n## v4.14.1\n\n- Release date: 2023-09-19\n\nCancelled due to CI failure.\n\n## v4.14.0\n\n- Release date: 2023-08-20\n- Tag: [v4.14.0](https://github.com/go-acme/lego/releases/tag/v4.14.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Yandex 360\n- **[dnsprovider]** Add DNS provider for cloud.ru\n- **[httpprovider]** Adding S3 support for HTTP domain validation\n\n### Changed\n\n- **[cli]** Allow to set EAB kid and hmac via environment variables\n- **[dnsprovider]** Migrate to aws-sdk-go-v2 (lightsail, route53)\n\n### Fixed\n\n- **[dnsprovider]** nearlyfreespeech: fix authentication\n- **[dnsprovider]** pdns: fix notify\n- **[dnsprovider]** route53: avoid unexpected records deletion\n\n## v4.13.3\n\n- Release date: 2023-07-25\n- Tag: [v4.13.3](https://github.com/go-acme/lego/releases/tag/v4.13.3)\n\n### Fixed\n\n- **[dnsprovider]** azuredns: fix configuration from env vars\n- **[dnsprovider]** gcore: change API domain\n\n## v4.13.2\n\n- Release date: 2023-07-21\n- Tag: [v4.13.2](https://github.com/go-acme/lego/releases/tag/v4.13.2)\n\n### Fixed\n\n- **[dnsprovider]** servercow: fix regression\n\n## v4.13.1\n\n- Release date: 2023-07-20\n- Tag: [v4.13.1](https://github.com/go-acme/lego/releases/tag/v4.13.1)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for IPv64\n- **[dnsprovider]** Add DNS provider for Metaname\n- **[dnsprovider]** Add DNS provider for RcodeZero\n- **[dnsprovider]** Add DNS provider for Efficient IP\n- **[dnsprovider]** azure: new implementation based on the new API client\n- **[lib]** Experimental option to force DNS queries to use TCP\n\n### Changed\n\n- **[dnsprovider]** cloudflare: update api client to v0.70.0\n\n### Fixed\n\n- **[dnsprovider,cname]** fix: ensure case-insensitive comparison of CNAME records\n- **[cli]** fix: list command\n- **[lib]** fix: ARI explanationURL\n\n## v4.13.0\n\n- Release date: 2023-07-20\n\nCancelled due to a CI issue (no space left on device).\n\n## v4.12.2\n\n- Release date: 2023-06-19\n- Tag: [v4.12.2](https://github.com/go-acme/lego/releases/tag/v4.12.2)\n\n### Fixed\n\n- **[dnsprovider]** dnsmadeeasy: fix DeleteRecord\n- **[lib]** fix: read status code from response\n\n## v4.12.1\n\n- Release date: 2023-06-06\n- Tag: [v4.12.1](https://github.com/go-acme/lego/releases/tag/v4.12.1)\n\n### Fixed\n\n- **[dnsprovider]** pdns: fix record value\n\n## v4.12.0\n\n- Release date: 2023-05-28\n- Tag: [v4.12.0](https://github.com/go-acme/lego/releases/tag/v4.12.0)\n\n### Added\n\n- **[lib,cli]** Initial ACME Renewal Info (ARI) Implementation\n- **[dnsprovider]** Add DNS provider for Derak Cloud\n- **[dnsprovider]** route53: pass ExternalID property to STS:AssumeRole API operation\n- **[lib,cli]** Support custom duration for certificate\n\n### Changed\n\n- **[dnsprovider]** Refactor DNS provider and client implementations\n\n### Fixed\n\n- **[dnsprovider]** autodns: fixes wrong zone in api call if CNAME is used\n- **[cli]** fix: archive only domain-related files on revoke\n\n## v4.11.0\n\n- Release date: 2023-05-02\n- Tag: [v4.11.0](https://github.com/go-acme/lego/releases/tag/v4.11.0)\n\n### Added\n\n- **[lib]** Support for certificate with raw IP SAN (RFC8738)\n- **[dnsprovider]** Add Brandit.com as DNS provider\n- **[dnsprovider]** Add DNS provider for Bunny\n- **[dnsprovider]** Add DNS provider for Nodion\n- **[dnsprovider]** Add Google Domains as DNS provider\n- **[dnsprovider]** Add DNS provider for Plesk\n\n### Changed\n\n- **[cli]** feat: add LEGO_CERT_PEM_PATH and LEGO_CERT_PFX_PATH to run hook\n- **[lib,cli]** feat: add RSA 3072\n- **[dnsprovider]** gcloud: update google APIs to latest version\n- **[lib,dnsprovider,cname]** chore: replace GetRecord by GetChallengeInfo\n\n### Fixed\n\n- **[dnsprovider]** rimuhosting: fix API base URL\n\n## v4.10.2\n\n- Release date: 2023-02-26\n- Tag: [v4.10.2](https://github.com/go-acme/lego/releases/tag/v4.10.2)\n\nFix Docker image builds.\n\n## v4.10.1\n\n- Release date: 2023-02-25\n- Tag: [v4.10.1](https://github.com/go-acme/lego/releases/tag/v4.10.1)\n\n### Fixed\n\n- **[dnsprovider,cname]** acmedns: fix CNAME support\n- **[dnsprovider]** dynu: fix subdomain support\n\n## v4.10.0\n\n- Release date: 2023-02-10\n- Tag: [v4.10.0](https://github.com/go-acme/lego/releases/tag/v4.10.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for dnsHome.de\n- **[dnsprovider]** Add DNS provider for Liara\n- **[dnsprovider]** Add DNS provider for UltraDNS\n- **[dnsprovider]** Add DNS provider for Websupport\n\n### Changed\n\n- **[dnsprovider]** ibmcloud: add support for subdomains\n- **[dnsprovider]** infomaniak: CNAME support\n- **[dnsprovider]** namesilo: add cleanup before add a DNS record\n- **[dnsprovider]** route53: Allow static credentials to be supplied\n- **[dnsprovider]** tencentcloud: support punycode domain\n\n### Fixed\n\n- **[dnsprovider]** alidns: filter on record type\n- **[dnsprovider]** arvancloud: replace arvancloud.com by arvancloud.ir\n- **[dnsprovider]** hetzner: improve zone ID detection\n- **[dnsprovider]** luadns: removed dot suffix from authzone while searching for zone\n- **[dnsprovider]** pdns: fix usage of notify only when zone kind is Master or Slave\n- **[dnsprovider]** return an error when extracting record name\n\n## v4.9.1\n\n- Release date: 2022-11-25\n- Tag: [v4.9.1](https://github.com/go-acme/lego/releases/tag/v4.9.1)\n\n### Changed\n\n- **[lib,cname]** cname: add log about CNAME entries\n- **[dnsprovider]** regru: improve error handling\n\n### Fixed\n\n- **[dnsprovider,cname]** fix CNAME support for multiple DNS providers\n- **[dnsprovider,cname]** duckdns: fix CNAME support\n- **[dnsprovider,cname]** oraclecloud: use fqdn to resolve zone\n- **[dnsprovider]** hurricane: fix CNAME support\n- **[lib,cname]** cname: stop trying to traverse cname if none have been found\n\n## v4.9.0\n\n- Release date: 2022-10-03\n- Tag: [v4.9.0](https://github.com/go-acme/lego/releases/tag/v4.9.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for CIVO\n- **[dnsprovider]** Add DNS provider for VK Cloud\n- **[dnsprovider]** Add DNS provider for YandexCloud\n- **[dnsprovider]** digitalocean: configurable base URL\n- **[dnsprovider]** loopia: add configurable API endpoint\n- **[dnsprovider]** pdns: notify secondary servers after updates\n\n### Changed\n\n- **[dnsprovider]** allinkl: removed deprecated sha1 hashing\n- **[dnsprovider]** auroradns: update authentification\n- **[dnsprovider]** dnspod: deprecated. Use Tencent Cloud instead.\n- **[dnsprovider]** exoscale: migrate to API v2 endpoints\n- **[dnsprovider]** gcloud: update golang.org/x/oauth2\n- **[dnsprovider]** lightsail: cleanup\n- **[dnsprovider]** sakuracloud: update api client library\n- **[cname]** take out CNAME support from experimental features\n- **[lib,cname]** add recursive CNAME lookup support\n- **[lib]** Remove embedded issuer certificates from issued certificate if bundle is false\n\n### Fixed\n\n- **[dnsprovider]** luadns: fix cname support\n- **[dnsprovider]** njalla: fix record id unmarshal error\n- **[dnsprovider]** tencentcloud: fix subdomain error\n\n## v4.8.0\n\n- Release date: 2022-06-30\n- Tag: [v4.8.0](https://github.com/go-acme/lego/releases/tag/v4.8.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Variomedia\n- **[dnsprovider]** Add NearlyFreeSpeech DNS Provider\n- **[cli]** Add a --user-agent flag to lego-cli\n\n### Changed\n\n- new logo\n- **[cli]** feat: sleep at renewal\n- **[cli]** cli/renew: skip random sleep if stdout is a terminal\n- **[dnsprovider]** hetzner: set min TTL to 60s\n- **[docs]** refactoring and cleanup\n\n## v4.7.0\n\n- Release date: 2022-05-27\n- Tag: [v4.7.0](https://github.com/go-acme/lego/releases/tag/v4.7.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for iwantmyname\n- **[dnsprovider]** Add DNS Provider for IIJ DNS Platform Service\n- **[dnsprovider]** Add DNS provider for Vercel\n- **[dnsprovider]** route53: add assume role ARN\n- **[dnsprovider]** dnsimple: add debug option\n- **[cli]** feat: add `LEGO_CERT_PEM_PATH` and `LEGO_CERT_PFX_PATH`\n\n### Changed\n\n- **[dnsprovider]** gcore: change dns api url\n- **[dnsprovider]** bluecat: rewrite provider implementation\n\n### Fixed\n\n- **[dnsprovider]** rfc2136: fix TSIG secret\n- **[dnsprovider]** tencentcloud: fix InvalidParameter.DomainInvalid error when using DNS challenges\n- **[lib]** fix: panic in certcrypto.ParsePEMPrivateKey\n\n## v4.6.0\n\n- Release date: 2022-01-18\n- Tag: [v4.6.0](https://github.com/go-acme/lego/releases/tag/v4.6.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for UKFast SafeDNS\n- **[dnsprovider]** Add DNS Provider for Tencent Cloud\n- **[dnsprovider]** azure: add support for Azure Private Zone DNS\n- **[dnsprovider]** exec: add sequence interval\n- **[cli]** Add a `--pfx`, and `--pfx.pas`s option to generate a PKCS#12 (`.pfx`) file.\n- **[lib]** Extended support of cert pool (`LEGO_CA_CERTIFICATES` and `LEGO_CA_SYSTEM_CERT_POOL`)\n- **[lib,httpprovider]** added uds capability to http challenge server\n\n### Changed\n\n- **[lib]** Extend validity of TLS-ALPN-01 certificates to 365 days\n- **[lib,cli]** Allows defining the reason for the certificate revocation\n\n### Fixed\n\n- **[dnsprovider]** mythicbeasts: fix token expiration\n- **[dnsprovider]** rackspace: change zone ID to string\n\n## v4.5.3\n\n- Release date: 2021-09-06\n- Tag: [v4.5.3](https://github.com/go-acme/lego/releases/tag/v4.5.3)\n\n### Fixed\n\n- **[lib,cli]** fix: missing preferred chain param for renew request\n\n## v4.5.2\n\n- Release date: 2021-09-01\n- Tag: [v4.5.2](https://github.com/go-acme/lego/releases/tag/v4.5.2)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for all-inkl\n- **[dnsprovider]** Add DNS provider for Epik\n- **[dnsprovider]** Add DNS provider for freemyip.com\n- **[dnsprovider]** Add DNS provider for g-core labs\n- **[dnsprovider]** Add DNS provider for hosttech\n- **[dnsprovider]** Add DNS Provider for IBM Cloud (SoftLayer)\n- **[dnsprovider]** Add DNS provider for Internet.bs\n- **[dnsprovider]** Add DNS provider for nicmanager\n\n### Changed\n\n- **[dnsprovider]** alidns: support ECS instance RAM role\n- **[dnsprovider]** alidns: support sts token credential\n- **[dnsprovider]** azure: zone name as environment variable\n- **[dnsprovider]** ovh: follow cname\n- **[lib,cli]** Add AlwaysDeactivateAuthorizations flag to ObtainRequest\n\n### Fixed\n\n- **[dnsprovider]** infomaniak: fix subzone support\n- **[dnsprovider]** edgedns: fix Present and CleanUp logic\n- **[dnsprovider]** lightsail: wrong Region env var name\n- **[lib]** lib: fix backoff in SolverManager\n- **[lib]** lib: use permanent error instead of context cancellation\n- **[dnsprovider]** desec: bump to v0.6.0\n\n## v4.5.1\n\n- Release date: 2021-10-01\n\nCancelled due to a CI issue, replaced by v4.5.2.\n\n## v4.5.0\n\n- Release date: 2021-09-30\n\nCancelled due to a CI issue, replaced by v4.5.2.\n\n## v4.4.0\n\n- Release date: 2021-06-08\n- Tag: [v4.4.0](https://github.com/go-acme/lego/releases/tag/v4.4.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Infoblox\n- **[dnsprovider]** Add DNS provider for Porkbun\n- **[dnsprovider]** Add DNS provider for Simply.com\n- **[dnsprovider]** Add DNS provider for Sonic\n- **[dnsprovider]** Add DNS provider for VinylDNS\n- **[dnsprovider]** Add DNS provider for wedos\n\n### Changed\n\n- **[cli]** log: Use stderr instead of stdout.\n- **[dnsprovider]** hostingde: autodetection of the zone name.\n- **[dnsprovider]** scaleway: use official SDK\n- **[dnsprovider]** powerdns: several improvements\n- **[lib]** lib: improve wait.For returns.\n\n### Fixed\n\n- **[dnsprovider]** hurricane: add API rate limiter.\n- **[dnsprovider]** hurricane: only treat first word of response body as response code\n- **[dnsprovider]** exoscale: fix DNS provider debugging\n- **[dnsprovider]** wedos: fix api call parameters\n- **[dnsprovider]** nifcloud: Get zone info from dns01.FindZoneByFqdn\n- **[cli,lib]** csr: Support the type `NEW CERTIFICATE REQUEST`\n\n## v4.3.1\n\n- Release date: 2021-03-12\n- Tag: [v4.3.1](https://github.com/go-acme/lego/releases/tag/v4.3.1)\n\n### Fixed\n\n- **[dnsprovider]** exoscale: fix dependency version.\n\n## v4.3.0\n\n- Release date: 2021-03-10\n- Tag: [v4.3.0](https://github.com/go-acme/lego/releases/tag/v4.3.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Njalla\n- **[dnsprovider]** Add DNS provider for Domeneshop\n- **[dnsprovider]** Add DNS provider for Hurricane Electric\n- **[dnsprovider]** designate: support for Openstack Application Credentials\n- **[dnsprovider]** edgedns: support for .edgerc file\n\n### Changed\n\n- **[dnsprovider]** infomaniak: Make error message more meaningful\n- **[dnsprovider]** cloudns: Improve reliability\n- **[dnsprovider]** rfc2163: Removed support for MD5 algorithm. The default algorithm is now SHA1.\n\n### Fixed\n\n- **[dnsprovider]** desec: fix error with default TTL\n- **[dnsprovider]** mythicbeasts: implement `ProviderTimeout`\n- **[dnsprovider]** dnspod: improve search accuracy when a domain have more than 100 records\n- **[lib]** Increase HTTP client timeouts\n- **[lib]** preferred chain only match root name\n\n## v4.2.0\n\n- Release date: 2021-01-24\n- Tag: [v4.2.0](https://github.com/go-acme/lego/releases/tag/v4.2.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Loopia\n- **[dnsprovider]** Add DNS provider for Ionos.\n\n### Changed\n\n- **[dnsprovider]** acme-dns: update cpu/goacmedns to v0.1.1.\n- **[dnsprovider]** inwx: Increase propagation timeout to 360s to improve robustness\n- **[dnsprovider]** vultr: Update to govultr v2 API\n- **[dnsprovider]** pdns: get exact zone instead of all zones\n\n### Fixed\n\n- **[dnsprovider]** vult, dnspod: fix default HTTP timeout.\n- **[dnsprovider]** pdns: URL request creation.\n- **[lib]** errors: Fix instance not being printed\n\n## v4.1.3\n\n- Release date: 2020-11-25\n- Tag: [v4.1.3](https://github.com/go-acme/lego/releases/tag/v4.1.3)\n\n### Fixed\n\n- **[dnsprovider]** azure: fix error handling.\n\n## v4.1.2\n\n- Release date: 2020-11-21\n- Tag: [v4.1.2](https://github.com/go-acme/lego/releases/tag/v4.1.2)\n\n### Fixed\n\n- **[lib]** fix: preferred chain support.\n\n## v4.1.1\n\n- Release date: 2020-11-19\n- Tag: [v4.1.1](https://github.com/go-acme/lego/releases/tag/v4.1.1)\n\n### Fixed\n\n- **[dnsprovider]** otc: select correct zone if multiple returned\n- **[dnsprovider]** azure: fix target must be a non-nil pointer\n\n## v4.1.0\n\n- Release date: 2020-11-06\n- Tag: [v4.1.0](https://github.com/go-acme/lego/releases/tag/v4.1.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Infomaniak\n- **[dnsprovider]** joker: add support for SVC API\n- **[dnsprovider]** gcloud: add an option to allow the use of private zones\n\n### Changed\n\n- **[dnsprovider]** rfc2136: ensure TSIG algorithm is fully qualified\n- **[dnsprovider]** designate: Deprecate OS_TENANT_NAME as required field\n\n### Fixed\n\n- **[lib]** acme/api: use postAsGet instead of post for AccountService.Get\n- **[lib]** fix: use http.Header.Set method instead of Add.\n\n## v4.0.1\n\n- Release date: 2020-09-03\n- Tag: [v4.0.1](https://github.com/go-acme/lego/releases/tag/v4.0.1)\n\n### Fixed\n\n- **[dnsprovider]** exoscale: change dependency version.\n\n## v4.0.0\n\n- Release date: 2020-09-02\n- Tag: [v4.0.0](https://github.com/go-acme/lego/releases/tag/v4.0.0)\n\n### Added\n\n- **[cli], [lib]** Support \"alternate\" certificate links for selecting different signing Chains\n\n### Changed\n\n- **[cli]** Replaces `ec384` by `ec256` as default key-type\n- **[lib]** Changes `ObtainForCSR` method signature\n\n### Removed\n\n- **[dnsprovider]** Replaces FastDNS by EdgeDNS\n- **[dnsprovider]** Removes old Linode provider\n- **[lib]** Removes `AddPreCheck` function\n\n## v3.9.0\n\n- Release date: 2020-09-01\n- Tag: [v3.9.0](https://github.com/go-acme/lego/releases/tag/v3.9.0)\n\n### Added\n\n- **[dnsprovider]** Add Akamai Edgedns. Deprecate FastDNS\n- **[dnsprovider]** Add DNS provider for HyperOne\n\n### Changed\n\n- **[dnsprovider]** designate: add support for Openstack clouds.yaml\n- **[dnsprovider]** azure: allow selecting environments\n- **[dnsprovider]** desec: applies API rate limits.\n\n### Fixed\n\n- **[dnsprovider]** namesilo: fix cleanup.\n\n## v3.8.0\n\n- Release date: 2020-07-02\n- Tag: [v3.8.0](https://github.com/go-acme/lego/releases/tag/v3.8.0)\n\n### Added\n\n- **[cli]** cli: add hook on the run command.\n- **[dnsprovider]** inwx: Two-Factor-Authentication\n- **[dnsprovider]** Add DNS provider for ArvanCloud\n\n### Changed\n\n- **[dnsprovider]** vultr: bumping govultr version\n- **[dnsprovider]** desec: improve error logs.\n- **[lib]** Ensures the return of a location during account updates\n- **[dnsprovider]** route53: Document all AWS credential environment variables\n\n### Fixed\n\n- **[dnsprovider]** stackpath: fix subdomain support.\n- **[dnsprovider]** arvandcloud: fix record name.\n- **[dnsprovider]** fix: multi-va.\n- **[dnsprovider]** constellix: fix search records API call.\n- **[dnsprovider]** hetzner: fix record name.\n- **[lib]** Registrar.ResolveAccountByKey: Fix malformed request\n\n## v3.7.0\n\n- Release date: 2020-05-11\n- Tag: [v3.7.0](https://github.com/go-acme/lego/releases/tag/v3.7.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Netlify.\n- **[dnsprovider]** Add DNS provider for deSEC.io\n- **[dnsprovider]** Add DNS provider for LuaDNS\n- **[dnsprovider]** Adding Hetzner DNS provider\n- **[dnsprovider]** Add DNS provider for Mythic beasts DNSv2\n- **[dnsprovider]** Add DNS provider for Yandex.\n\n### Changed\n\n- **[dnsprovider]** Upgrade DNSimple client to 0.60.0\n- **[dnsprovider]** update aws sdk\n\n### Fixed\n\n- **[dnsprovider]** autodns: removes TXT records during CleanUp.\n- **[dnsprovider]** Fix exoscale HTTP timeout\n- **[cli]** fix: renew path information.\n- **[cli]** Fix account storage location warning message\n\n## v3.6.0\n\n- Release date: 2020-04-24\n- Tag: [v3.6.0](https://github.com/go-acme/lego/releases/tag/v3.6.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for CloudDNS.\n- **[dnsprovider]** alicloud: add support for domain with punycode\n- **[dnsprovider]** cloudns: Add subuser support\n- **[cli]** Information about renewed certificates are now passed to the renew hook\n\n### Changed\n\n- **[dnsprovider]** acmedns: Update cpu/goacmedns v0.0.1 -&gt; v0.0.2\n- **[dnsprovider]** alicloud: update sdk dependency version to v1.61.112\n- **[dnsprovider]** azure: Allow for the use of MSI\n- **[dnsprovider]** constellix: improve challenge.\n- **[dnsprovider]** godaddy: allow parallel solve.\n- **[dnsprovider]** namedotcom: get the actual registered domain, so we can remove just that from the hostname to be created\n- **[dnsprovider]** transip: updated the client to v6\n\n### Fixed\n\n- **[dnsprovider]** ns1: fix missing domain in log \n- **[dnsprovider]** rimuhosting: use HTTP client from config.\n\n## v3.5.0\n\n- Release date: 2020-03-15\n- Tag: [v3.5.0](https://github.com/go-acme/lego/releases/tag/v3.5.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Dynu.\n- **[dnsprovider]** Add DNS provider for reg.ru\n- **[dnsprovider]** Add DNS provider for Zonomi and RimuHosting.\n- **[cli]** Building binaries for arm 6 and 5\n- **[cli]** Uses CGO_ENABLED=0\n- **[cli]** Multi-arch Docker image.\n- **[cli]** Adds `--name` flag to list command.\n\n### Changed\n\n- **[lib]** lib: Improve cleanup log messages.\n- **[lib]** Wrap errors.\n\n### Fixed\n\n- **[dnsprovider]** azure: pass AZURE_CLIENT_SECRET_FILE to autorest.Authorizer\n- **[dnsprovider]** gcloud: fixes issues when used with GKE Workload Identity\n- **[dnsprovider]** oraclecloud: fix subdomain support\n\n## v3.4.0\n\n- Release date: 2020-02-25\n- Tag: [v3.4.0](https://github.com/go-acme/lego/releases/tag/v3.4.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Constellix\n- **[dnsprovider]** Add DNS provider for Servercow.\n- **[dnsprovider]** Add DNS provider for Scaleway\n- **[cli]** Add \"LEGO_PATH\" environment variable\n\n### Changed\n\n- **[dnsprovider]** route53: allow custom client to be provided\n- **[dnsprovider]** namecheap: allow external domains\n- **[dnsprovider]** namecheap: add sandbox support.\n- **[dnsprovider]** ovh: Improve provider documentation\n- **[dnsprovider]** route53: Improve provider documentation\n\n### Fixed\n\n- **[dnsprovider]** zoneee: fix subdomains.\n- **[dnsprovider]** designate: Don't clean up managed records like SOA and NS\n- **[dnsprovider]** dnspod: update lib.\n- **[lib]** crypto: Treat CommonName as optional\n- **[lib]** chore: update cenkalti/backoff to v4.\n\n## v3.3.0\n\n- Release date: 2020-01-08\n- Tag: [v3.3.0](https://github.com/go-acme/lego/releases/tag/v3.3.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Checkdomain\n- **[lib]** Add support to update account\n\n### Changed\n\n- **[dnsprovider]** gcloud: Auto-detection of the project ID.\n- **[lib]** Successfully parse private key PEM blocks\n\n### Fixed\n\n- **[dnsprovider]** Update dnspod, because of API breaking changes.\n\n## v3.2.0\n\n- Release date: 2019-11-10\n- Tag: [v3.2.0](https://github.com/go-acme/lego/releases/tag/v3.2.0)\n\n### Added\n\n- **[dnsprovider]** Add support for autodns\n\n### Changed\n\n- **[dnsprovider]** httpreq: Allow use environment vars from a `_FILE` file\n- **[lib]** Don't deactivate valid authorizations\n- **[lib]** Expose more SOA fields found by dns01.FindZoneByFqdn\n\n### Fixed\n\n- **[dnsprovider]** use token as unique ID.\n\n## v3.1.0\n\n- Release date: 2019-10-07\n- Tag: [v3.1.0](https://github.com/go-acme/lego/releases/tag/v3.1.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for Liquid Web\n- **[dnsprovider]** cloudflare: add support for API tokens\n- **[cli]** feat: ease operation behind proxy servers\n\n### Changed\n\n- **[dnsprovider]** cloudflare: update client\n- **[dnsprovider]** linodev4: propagation timeout configuration.\n\n### Fixed\n\n- **[dnsprovider]** ovh: fix int overflow.\n- **[dnsprovider]** bindman: fix client version.\n\n## v3.0.2\n\n- Release date: 2019-08-15\n- Tag: [v3.0.2](https://github.com/go-acme/lego/releases/tag/v3.0.2)\n\n### Fixed\n\n- Invalid pseudo version (related to Cloudflare client).\n\n## v3.0.1\n\n- Release date: 2019-08-14\n- Tag: [v3.0.1](https://github.com/go-acme/lego/releases/tag/v3.0.1)\n\nThere was a problem when creating the tag v3.0.1, this tag has been invalidated.\n\n## v3.0.0\n\n- Release date: 2019-08-05\n- Tag: [v3.0.0](https://github.com/go-acme/lego/releases/tag/v3.0.0)\n\n### Changed\n\n- migrate to go module (new import github.com/go-acme/lego/v3/)\n- update DNS clients\n\n## v2.7.2\n\n- Release date: 2019-07-30\n- Tag: [v2.7.2](https://github.com/go-acme/lego/releases/tag/v2.7.2)\n\n### Fixed\n\n- **[dnsprovider]** vultr: quote TXT record\n\n## v2.7.1\n\n- Release date: 2019-07-22\n- Tag: [v2.7.1](https://github.com/go-acme/lego/releases/tag/v2.7.1)\n\n### Fixed\n\n- **[dnsprovider]** vultr: invalid record type.\n\n## v2.7.0\n\n- Release date: 2019-07-17\n- Tag: [v2.7.0](https://github.com/go-acme/lego/releases/tag/v2.7.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS provider for namesilo\n- **[dnsprovider]** Add DNS provider for versio.nl\n\n### Changed\n\n- **[dnsprovider]** Update DNS providers libs.\n- **[dnsprovider]** joker: support username and password.\n- **[dnsprovider]** Vultr: Switch to official client\n\n### Fixed\n\n- **[dnsprovider]** otc: Prevent sending empty body.\n\n## v2.6.0\n\n- Release date: 2019-05-27\n- Tag: [v2.6.0](https://github.com/go-acme/lego/releases/tag/v2.6.0)\n\n### Added\n\n- **[dnsprovider]** Add support for Joker.com DMAPI\n- **[dnsprovider]** Add support for Bindman DNS provider\n- **[dnsprovider]** Add support for EasyDNS\n- **[lib]** Get an existing certificate by URL\n\n### Changed\n\n- **[dnsprovider]** digitalocean: LEGO_EXPERIMENTAL_CNAME_SUPPORT support\n- **[dnsprovider]** gcloud: Use fqdn to get zone Present/CleanUp\n- **[dnsprovider]** exec: serial behavior\n- **[dnsprovider]** manual: serial behavior.\n- **[dnsprovider]** Strip newlines when reading environment variables from `_FILE` suffixed files.\n\n### Fixed\n\n- **[cli]** fix: cli disable-cp option.\n- **[dnsprovider]** gcloud: fix zone visibility.\n\n## v2.5.0\n\n- Release date: 2019-04-17\n- Tag: [v2.5.0](https://github.com/go-acme/lego/releases/tag/v2.5.0)\n\n### Added\n\n- **[cli]** Adds renew hook\n- **[dnsprovider]** Adds 'Since' to DNS providers documentation\n\n### Changed\n\n- **[dnsprovider]** gcloud: use public DNS zones\n- **[dnsprovider]** route53: enhance documentation.\n\n### Fixed\n\n- **[dnsprovider]** cloudns: fix TTL and status validation\n- **[dnsprovider]** sakuracloud: supports concurrent update\n- **[dnsprovider]** Disable authz when solve fail.\n- Add tzdata to the Docker image.\n\n## v2.4.0\n\n- Release date: 2019-03-25\n- Tag: [v2.4.0](https://github.com/go-acme/lego/releases/tag/v2.4.0)\n\nMigrate from xenolf/lego to go-acme/lego.\n\n### Added\n\n- **[dnsprovider]** Add DNS Provider for Domain Offensive (do.de)\n- **[dnsprovider]** Adds information about '_FILE' suffix.\n\n### Fixed\n\n- **[cli,dnsprovider]** Add 'manual' provider to the output of dnshelp\n- **[dnsprovider]** hostingde: Use provided ZoneName instead of domain\n- **[dnsprovider]** pdns: fix wildcard with SANs\n\n## v2.3.0\n\n- Release date: 2019-03-11\n- Tag: [v2.3.0](https://github.com/go-acme/lego/releases/tag/v2.3.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS Provider for ClouDNS.net\n- **[dnsprovider]** Add DNS Provider for Oracle Cloud\n\n### Changed\n\n- **[cli]** Adds log when no renewal.\n- **[dnsprovider,lib]** Add a mechanism to wrap a PreCheckFunc\n- **[dnsprovider]** oraclecloud: better way to get private key.\n- **[dnsprovider]** exoscale: update library\n\n### Fixed\n\n- **[dnsprovider]** OVH: Refresh zone after deleting challenge record\n- **[dnsprovider]** oraclecloud: ttl config and timeout \n- **[dnsprovider]** hostingde: fix client fails if customer has no access to dns-groups\n- **[dnsprovider]** vscale: getting sub-domain\n- **[dnsprovider]** selectel: getting sub-domain\n- **[dnsprovider]** vscale: fix TXT records clean up\n- **[dnsprovider]** selectel: fix TXT records clean up\n\n## v2.2.0\n\n- Release date: 2019-02-08\n- Tag: [v2.2.0](https://github.com/go-acme/lego/releases/tag/v2.2.0)\n\n### Added\n\n- **[dnsprovider]** Add support for Openstack Designate as a DNS provider\n- **[dnsprovider]** gcloud: Option to specify gcloud service account json by env as string\n- **[experimental feature]** Resolve CNAME when creating dns-01 challenge. To enable: set `LEGO_EXPERIMENTAL_CNAME_SUPPORT` to `true`.\n \n### Changed\n\n- **[cli]** Applies Let’s Encrypt’s recommendation about renew. The option `--days` of the command `renew` has a new default value (`30`)\n- **[lib]** Uses a jittered exponential backoff\n\n### Fixed\n\n- **[cli]** CLI and key type.\n- **[dnsprovider]** httpreq: Endpoint with path.\n- **[dnsprovider]** fastdns: Do not overwrite existing TXT records\n- Log wildcard domain correctly in validation\n\n## v2.1.0\n\n- Release date: 2019-01-24\n- Tag: [v2.1.0](https://github.com/go-acme/lego/releases/tag/v2.1.0)\n\n### Added\n\n- **[dnsprovider]** Add support for zone.ee as a DNS provider.\n\n### Changed\n\n- **[dnsprovider]** nifcloud: Change DNS base url.\n- **[dnsprovider]** gcloud: More detailed information about Google Cloud DNS.\n\n### Fixed\n\n- **[lib]** fix: OCSP, set HTTP client.\n- **[dnsprovider]** alicloud: fix pagination.\n- **[dnsprovider]** namecheap: fix panic.\n\n## v2.0.0\n\n- Release date: 2019-01-09\n- Tag: [v2.0.0](https://github.com/go-acme/lego/releases/tag/v2.0.0)\n\n### Added\n\n- **[cli,lib]** Option to disable the complete propagation Requirement\n- **[lib,cli]** Support non-ascii domain name (punnycode)\n- **[cli,lib]** Add configurable timeout when obtaining certificates\n- **[cli]** Archive revoked certificates\n- **[cli]** Add command to list certificates.\n- **[cli]** support for renew with CSR\n- **[cli]** add SAN on renew\n- **[lib]** Adds `Remove` for challenges\n- **[lib]** Add version to xenolf-acme in User-Agent.\n- **[dnsprovider]** The ability for a DNS provider to solve the challenge sequentially\n- **[dnsprovider]** Add DNS provider for \"HTTP request\".\n- **[dnsprovider]** Add DNS Provider for Vscale\n- **[dnsprovider]** Add DNS Provider for TransIP\n- **[dnsprovider]** Add DNS Provider for inwx\n- **[dnsprovider]** alidns: add support to handle more than 20 domains\n\n### Changed\n\n- **[lib]** Check all challenges in a predictable order\n- **[lib]** Poll authz URL instead of challenge URL\n- **[lib]** Check all nameservers in a predictable order\n- **[lib]** Logs every iteration of waiting for the propagation\n- **[cli]** `--http`: enable HTTP challenge **important**\n- **[cli]** `--http.port`: previously named `--http`\n- **[cli]** `--http.webroot`: previously named `--webroot`\n- **[cli]** `--http.memcached-host`: previously named `--memcached-host`\n- **[cli]** `--tls`: enable TLS challenge **important**\n- **[cli]** `--tls.port`:  previously named `--tls`\n- **[cli]** `--dns.resolvers`: previously named `--dns-resolvers`\n- **[cli]** the option `--days` of the command `renew` has default value (`15`)\n- **[dnsprovider]** gcloud: Use GCE_PROJECT for project always, if specified\n\n### Removed\n\n- **[lib]** Remove `SetHTTP01Address`\n- **[lib]** Remove `SetTLSALPN01Address`\n- **[lib]** Remove `Exclude`\n- **[cli]** Remove `--exclude`, `-x` \n\n### Fixed\n\n- **[lib]** Fixes revocation for subdomains and non-ascii domains\n- **[lib]** Disable pending authorizations\n- **[dnsprovider]** transip: concurrent access to the API.\n- **[dnsprovider]** gcloud: fix for wildcard\n- **[dnsprovider]** Azure: Do not overwrite existing TXT records\n- **[dnsprovider]** fix: Cloudflare error.\n\n## v1.2.0\n\n- Release date: 2018-11-04\n- Tag: [v1.2.0](https://github.com/go-acme/lego/releases/tag/v1.2.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS Provider for ConoHa DNS\n- **[dnsprovider]** Add DNS Provider for MyDNS.jp\n- **[dnsprovider]** Add DNS Provider for Selectel\n\n### Fixed\n\n- **[dnsprovider]** netcup: make unmarshalling of api-responses more lenient.\n\n### Changed\n\n- **[dnsprovider]** aurora: change DNS client\n- **[dnsprovider]** azure: update auth to support instance metadata service\n- **[dnsprovider]** dnsmadeeasy: log response body on error\n- **[lib]** TLS-ALPN-01: Update idPeAcmeIdentifierV1, draft refs.\n- **[lib]** Do not send a JWS body when POSTing challenges.\n- **[lib]** Support POST-as-GET.\n\n## v1.1.0\n\n- Release date: 2018-10-16\n- Tag: [v1.1.0](https://github.com/go-acme/lego/releases/tag/v1.1.0)\n\n### Added\n\n- **[lib]** TLS-ALPN-01 Challenge\n- **[cli]** Add filename parameter\n- **[dnsprovider]** Allow to configure TTL, interval and timeout\n- **[dnsprovider]** Add support for reading DNS provider setup from files\n- **[dnsprovider]** Add DNS Provider for ACME-DNS\n- **[dnsprovider]** Add DNS Provider for ALIYUN DNS\n- **[dnsprovider]** Add DNS Provider for DreamHost\n- **[dnsprovider]** Add DNS provider for hosting.de\n- **[dnsprovider]** Add DNS Provider for IIJ\n- **[dnsprovider]** Add DNS Provider for netcup\n- **[dnsprovider]** Add DNS Provider for NIFCLOUD DNS\n- **[dnsprovider]** Add DNS Provider for SAKURA Cloud\n- **[dnsprovider]** Add DNS Provider for Stackpath\n- **[dnsprovider]** Add DNS Provider for VegaDNS\n- **[dnsprovider]** exec: add EXEC_MODE=RAW support.\n- **[dnsprovider]** cloudflare: support for CF_API_KEY and CF_API_EMAIL\n\n### Fixed\n\n- **[lib]** Don't trust identifiers order.\n- **[lib]** Fix missing issuer certificates from Let's Encrypt\n- **[dnsprovider]** duckdns: fix TXT record update url\n- **[dnsprovider]** duckdns: fix subsubdomain\n- **[dnsprovider]** gcloud: update findTxtRecords to use Name=fqdn and Type=TXT\n- **[dnsprovider]** lightsail: Fix Domain does not exist error\n- **[dnsprovider]** ns1: use the authoritative zone and not the domain name\n- **[dnsprovider]** ovh: check error to avoid panic due to nil client\n\n### Changed\n\n- **[lib]** Submit all dns records up front, then validate serially\n\n## v1.0.0\n\n- Release date: 2018-05-30\n- Tag: [v1.0.0](https://github.com/go-acme/lego/releases/tag/v1.0.0)\n\n### Changed\n\n- **[lib]** ACME v2 Support.\n- **[dnsprovider]** Renamed `/providers/dns/googlecloud` to `/providers/dns/gcloud`.\n- **[dnsprovider]** Modified Google Cloud provider `gcloud.NewDNSProviderServiceAccount` function to extract the project id directly from the service account file.\n- **[dnsprovider]** Made errors more verbose for the Cloudflare provider.\n\n## v0.5.0\n\n- Release date: 2018-05-29\n- Tag: [v0.5.0](https://github.com/go-acme/lego/releases/tag/v0.5.0)\n\n### Added\n\n- **[dnsprovider]** Add DNS challenge provider `exec`\n- **[dnsprovider]** Add DNS Provider for Akamai FastDNS\n- **[dnsprovider]** Add DNS Provider for Bluecat DNS\n- **[dnsprovider]** Add DNS Provider for CloudXNS\n- **[dnsprovider]** Add DNS Provider for Duck DNS\n- **[dnsprovider]** Add DNS Provider for Gandi Beta Platform (LiveDNS)\n- **[dnsprovider]** Add DNS Provider for GleSYS API\n- **[dnsprovider]** Add DNS Provider for GoDaddy\n- **[dnsprovider]** Add DNS Provider for Lightsail\n- **[dnsprovider]** Add DNS Provider for Name.com\n\n### Fixed\n\n- **[dnsprovider]** Azure: Added missing environment variable in the comments\n- **[dnsprovider]** PowerDNS: Fix zone URL, add leading slash.\n- **[dnsprovider]** DNSimple: Fix api\n- **[cli]** Correct help text for `--dns-resolvers` default.\n- **[cli]** renew/revoke - don't panic on wrong account.\n- **[lib]** Fix zone detection for cross-zone cnames.\n- **[lib]** Use proxies from environment when making outbound http connections.\n\n### Changed\n\n- **[lib]** Users of an effective top-level domain can use the DNS challenge.\n- **[dnsprovider]** Azure: Refactor to work with new Azure SDK version.\n- **[dnsprovider]** Cloudflare and Azure: Adding output of which envvars are missing.\n- **[dnsprovider]** Dyn DNS: Slightly improve provider error reporting.\n- **[dnsprovider]** Exoscale: update to latest egoscale version.\n- **[dnsprovider]** Route53: Use NewSessionWithOptions instead of deprecated New.\n\n## 0.4.1\n\n- Release date: 2017-09-26\n- Tag: [0.4.1](https://github.com/go-acme/lego/releases/tag/0.4.1)\n\n### Added\n\n- lib: A new DNS provider for OTC.\n- lib: The `AWS_HOSTED_ZONE_ID` environment variable for the Route53 DNS provider to directly specify the zone.\n- lib: The `RFC2136_TIMEOUT` environment variable to make the timeout for the RFC2136 provider configurable.\n- lib: The `GCE_SERVICE_ACCOUNT_FILE` environment variable to specify a service account file for the Google Cloud DNS provider.\n\n### Fixed\n\n- lib: Fixed an authentication issue with the latest Azure SDK.\n\n## 0.4.0\n\n- Release date: 2017-07-13\n- Tag: [0.4.0](https://github.com/go-acme/lego/releases/tag/0.4.0)\n\n### Added\n\n- CLI: The `--http-timeout` switch. This allows for an override of the default client HTTP timeout.\n- lib: The `HTTPClient` field. This allows for an override of the default HTTP timeout for library HTTP requests.\n- CLI: The `--dns-timeout` switch. This allows for an override of the default DNS timeout for library DNS requests.\n- lib: The `DNSTimeout` switch. This allows for an override of the default client DNS timeout.\n- lib: The `QueryRegistration` function on `acme.Client`. This performs a POST on the client registration's URI and gets the updated registration info.\n- lib: The `DeleteRegistration` function on `acme.Client`. This deletes the registration as currently configured in the client.\n- lib: The `ObtainCertificateForCSR` function on `acme.Client`. The function allows to request a certificate for an already existing CSR.\n- CLI: The `--csr` switch. Allows to use already existing CSRs for certificate requests on the command line.\n- CLI: The `--pem` flag. This will change the certificate output, so it outputs a .pem file concatanating the .key and .crt files together.\n- CLI: The `--dns-resolvers` flag. Allows for users to override the default DNS servers used for recursive lookup.\n- lib: Added a memcached provider for the HTTP challenge.\n- CLI: The `--memcached-host` flag. This allows to use memcached for challenge storage.\n- CLI: The `--must-staple` flag. This enables OCSP must staple in the generated CSR.\n- lib: The library will now honor entries in your resolv.conf.\n- lib: Added a field `IssuerCertificate` to the `CertificateResource` struct.\n- lib: A new DNS provider for OVH.\n- lib: A new DNS provider for DNSMadeEasy.\n- lib: A new DNS provider for Linode.\n- lib: A new DNS provider for AuroraDNS.\n- lib: A new DNS provider for NS1.\n- lib: A new DNS provider for Azure DNS.\n- lib: A new DNS provider for Rackspace DNS.\n- lib: A new DNS provider for Exoscale DNS.\n- lib: A new DNS provider for DNSPod.\n\n### Changed\n\n- lib: Exported the `PreCheckDNS` field so library users can manage the DNS check in tests.\n- lib: The library will now skip challenge solving if a valid Authz already exists.\n\n### Removed\n\n- lib: The library will no longer check for auto-renewed certificates. This has been removed from the spec and is not supported in Boulder.\n\n### Fixed\n\n- lib: Fix a problem with the Route53 provider where it was possible the verification was published to a private zone.\n- lib: Loading an account from file should fail if an integral part is nil\n- lib: Fix a potential issue where the Dyn provider could resolve to an incorrect zone.\n- lib: If a registration encounteres a conflict, the old registration is now recovered.\n- CLI: The account.json file no longer has the executable flag set.\n- lib: Made the client registration more robust in case of a 403 HTTP response.\n- lib: Fixed an issue with zone lookups when they have a CNAME in another zone.\n- lib: Fixed the lookup for the authoritative zone for Google Cloud.\n- lib: Fixed a race condition in the nonce store.\n- lib: The Google Cloud provider now removes old entries before trying to add new ones.\n- lib: Fixed a condition where we could stall due to an early error condition.\n- lib: Fixed an issue where Authz object could end up in an active state after an error condition.\n\n## 0.3.1\n\n- Release date: 2016-04-19\n- Tag: [0.3.1](https://github.com/go-acme/lego/releases/tag/0.3.1)\n\n### Added\n\n- lib: A new DNS provider for Vultr.\n\n### Fixed\n\n- lib: DNS Provider for DigitalOcean could not handle subdomains properly.\n- lib: handleHTTPError should only try to JSON decode error messages with the right content type.\n- lib: The propagation checker for the DNS challenge would not retry on send errors.\n\n## 0.3.0\n\n- Release date: 2016-03-19\n- Tag: [0.3.0](https://github.com/go-acme/lego/releases/tag/0.3.0)\n\n### Added\n\n- CLI: The `--dns` switch. To include the DNS challenge for consideration. When using this switch, all other solvers are disabled. Supported are the following solvers: cloudflare, digitalocean, dnsimple, dyn, gandi, googlecloud, namecheap, route53, rfc2136 and manual.\n- CLI: The `--accept-tos`  switch. Indicates your acceptance of the Let's Encrypt terms of service without prompting you.\n- CLI: The `--webroot` switch. The HTTP-01 challenge may now be completed by dropping a file into a webroot. When using this switch, all other solvers are disabled.\n- CLI: The `--key-type` switch. This replaces the `--rsa-key-size` switch and supports the following key types: EC256, EC384, RSA2048, RSA4096 and RSA8192.\n- CLI: The `--dnshelp` switch. This displays a more in-depth help topic for DNS solvers.\n- CLI: The `--no-bundle` sub switch for the `run` and `renew` commands. When this switch is set, the CLI will not bundle the issuer certificate with your certificate.\n- lib: A new type for challenge identifiers `Challenge`\n- lib: A new interface for custom challenge providers `acme.ChallengeProvider`\n- lib: A new interface for DNS-01 providers to allow for custom timeouts for the validation function `acme.ChallengeProviderTimeout`\n- lib: SetChallengeProvider function. Pass a challenge identifier and a Provider to replace the default behaviour of a challenge.\n- lib: The DNS-01 challenge has been implemented with modular solvers using the `ChallengeProvider` interface. Included solvers are: cloudflare, digitalocean, dnsimple, gandi, namecheap, route53, rfc2136 and manual.\n- lib: The `acme.KeyType` type was added and is used for the configuration of crypto parameters for RSA and EC keys. Valid KeyTypes are: EC256, EC384, RSA2048, RSA4096 and RSA8192.\n\n### Changed\n\n- lib: ExcludeChallenges now expects to be passed an array of `Challenge` types.\n- lib: HTTP-01 now supports custom solvers using the `ChallengeProvider` interface.\n- lib: TLS-SNI-01 now supports custom solvers using the `ChallengeProvider` interface.\n- lib: The `GetPrivateKey` function in the `acme.User` interface is now expected to return a `crypto.PrivateKey` instead of an `rsa.PrivateKey` for EC compat.\n- lib: The `acme.NewClient` function now expects an `acme.KeyType` instead of the keyBits parameter.\n \n### Removed\n\n- CLI: The `rsa-key-size` switch was removed in favor of `key-type` to support EC keys.\n\n### Fixed\n\n- lib: Fixed a race condition in HTTP-01\n- lib: Fixed an issue where status codes on ACME challenge responses could lead to no action being taken.\n- lib: Fixed a regression when calling the Renew function with a SAN certificate.\n\n## 0.2.0\n\n- Release date: 2016-01-09\n- Tag: [0.2.0](https://github.com/go-acme/lego/releases/tag/0.2.0)\n\n### Added\n\n- CLI: The `--exclude` or `-x` switch. To exclude a challenge from being solved.\n- CLI: The `--http` switch. To set the listen address and port of HTTP based challenges. Supports `host:port` and `:port` for any interface.\n- CLI: The `--tls` switch. To set the listen address and port of TLS based challenges. Supports `host:port` and `:port` for any interface.\n- CLI: The `--reuse-key` switch for the `renew` operation. This lets you reuse an existing private key for renewals.\n- lib: ExcludeChallenges function. Pass an array of challenge identifiers to exclude them from solving.\n- lib: SetHTTPAddress function. Pass a port to set the listen port for HTTP based challenges.\n- lib: SetTLSAddress function. Pass a port to set the listen port of TLS based challenges.\n- lib: acme.UserAgent variable. Use this to customize the user agent on all requests sent by lego.\n\n### Changed\n\n- lib: NewClient does no longer accept the optPort parameter\n- lib: ObtainCertificate now returns a SAN certificate if you pass more than one domain.\n- lib: GetOCSPForCert now returns the parsed OCSP response instead of just the status.\n- lib: ObtainCertificate has a new parameter `privKey crypto.PrivateKey` which lets you reuse an existing private key for new certificates.\n- lib: RenewCertificate now expects the PrivateKey property of the CertificateResource to be set only if you want to reuse the key.\n\n### Removed\n\n- CLI: The `--port` switch was removed.\n- lib: RenewCertificate does no longer offer to also revoke your old certificate.\n\n### Fixed\n\n- CLI: Fix logic using the `--days` parameter for renew\n\n## 0.1.1\n\n- Release date: 2015-12-18\n- Tag: [0.1.1](https://github.com/go-acme/lego/releases/tag/0.1.1)\n\n### Added\n\n- CLI: Added a way to automate renewal through a cronjob using the --days parameter to renew\n\n### Changed\n\n- lib: Improved log output on challenge failures.\n\n### Fixed\n\n- CLI: The short parameter for domains would not get accepted\n- CLI: The cli did not return proper exit codes on error library errors.\n- lib: RenewCertificate did not properly renew SAN certificates.\n\n### Security\n\n- lib: Fix possible DOS on GetOCSPForCert\n\n## 0.1.0\n\n- Release date: 2015-12-03\n- Tag: [0.1.0](https://github.com/go-acme/lego/releases/tag/0.1.0)\n\nInitial release\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# How to contribute to lego\n\nContributions in the form of patches and proposals are essential to keep lego great and to make it even better.\nTo ensure a great and easy experience for everyone, please review the few guidelines in this document.\n\n## Bug reports\n\n- Use the issue search to see if the issue has already been reported.\n- Also look for closed issues to see if your issue has already been fixed.\n- If both of the above do not apply, create a new issue and include as much information as possible.\n\nBug reports should include all information a person could need to reproduce your problem without the need to\nfollow up for more information. If possible, provide detailed steps for us to reproduce it, the expected behavior and the actual behavior.\n\n## Feature proposals and requests\n\nFeature requests are welcome and should be discussed in an issue.\nPlease keep proposals focused on one thing at a time and be as detailed as possible.\nIt is up to you to make a strong point about your proposal and convince us of the merits and the added complexity of this feature.\n\n## Pull requests\n\nCreate an issue and wait for a maintainer to approve it BEFORE opening a pull request.\n\nPatches, new features and improvements are a great way to help the project.\nPlease keep them focused on one thing and do not include unrelated commits.\n\nAll pull requests that alter the behavior of the program,\nadd new behavior or somehow alter code in a non-trivial way should **always** include tests.\n\n**IMPORTANT**: By submitting a patch, you agree to allow the project owners to license your work under the terms of the [MIT License](LICENSE).\n\n### How to create a pull request\n\nRequirements:\n\n- `go` v1.24+\n- environment variable: `GO111MODULE=on`\n\nFirst, you have to install [GoLang](https://golang.org/doc/install) and [golangci-lint](https://github.com/golangci/golangci-lint#install).\n\n```bash\n# clone your fork\ngit clone git@github.com:YOUR_USERNAME/lego.git\ncd lego\n\n# Add the go-acme/lego remote\ngit remote add upstream git@github.com:go-acme/lego.git\ngit fetch upstream\n```\n\n```bash\n# Create your branch\ngit switch -c my-feature\n\n## Create your code ##\n```\n\n```bash\n# Linters\nmake checks\n# Tests\nmake test\n# Compile\nmake build\n```\n\n```bash\n# push your branch\ngit push -u origin my-feature\n\n## create a pull request on GitHub ##\n```\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1-alpine as builder\n\nRUN apk --no-cache --no-progress add make git\n\nWORKDIR /go/lego\n\nENV GO111MODULE on\n\n# Download go modules\nCOPY go.mod .\nCOPY go.sum .\nRUN go mod download\n\nCOPY . .\nRUN make build\n\nFROM alpine:3\nRUN apk update \\\n    && apk add --no-cache ca-certificates tzdata \\\n    && update-ca-certificates\n\nCOPY --from=builder /go/lego/dist/lego /usr/bin/lego\n\nENTRYPOINT [ \"/usr/bin/lego\" ]\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017-2024 Ludovic Fernandez\nCopyright (c) 2015-2017 Sebastian Erhart\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: clean checks test build image e2e fmt\n\nexport GO111MODULE=on\nexport CGO_ENABLED=0\n\nLEGO_IMAGE := goacme/lego\nMAIN_DIRECTORY := ./cmd/lego/\n\nBIN_OUTPUT := $(if $(filter $(shell go env GOOS), windows), dist/lego.exe, dist/lego)\n\nTAG_NAME := $(shell git tag -l --contains HEAD)\nSHA := $(shell git rev-parse HEAD)\nVERSION := $(if $(TAG_NAME),$(TAG_NAME),$(SHA))\n\ndefault: clean generate-dns checks test build\n\nclean:\n\t@echo BIN_OUTPUT: ${BIN_OUTPUT}\n\trm -rf dist/ builds/ cover.out\n\nbuild: clean\n\t@echo Version: $(VERSION)\n\tgo build -trimpath -ldflags '-X \"main.version=${VERSION}\"' -o ${BIN_OUTPUT} ${MAIN_DIRECTORY}\n\nimage:\n\t@echo Version: $(VERSION)\n\tdocker build -t $(LEGO_IMAGE) .\n\ntest: clean\n\tgo test -v -cover ./...\n\ne2e: clean\n\tLEGO_E2E_TESTS=local go test -count=1 -v ./e2e/...\n\nchecks:\n\tgolangci-lint run\n\n# Release helper\n.PHONY: patch minor major detach\n\npatch:\n\tgo run ./internal/releaser/ release -m patch\n\nminor:\n\tgo run ./internal/releaser/ release -m minor\n\nmajor:\n\tgo run ./internal/releaser/ release -m major\n\ndetach:\n\tgo run ./internal/releaser/ detach\n\n# Docs\n.PHONY: docs-build docs-serve docs-themes\n\ndocs-build: generate-dns\n\t@make -C ./docs build\n\ndocs-serve: generate-dns\n\t@make -C ./docs serve\n\ndocs-themes:\n\t@make -C ./docs hugo-themes\n\n# DNS Documentation\n.PHONY: generate-dns validate-doc\n\ngenerate-dns:\n\tgo generate ./...\n\nvalidate-doc: generate-dns\nvalidate-doc: DOC_DIRECTORIES := ./docs/ ./cmd/\nvalidate-doc:\n\t@if git diff --exit-code --quiet $(DOC_DIRECTORIES) 2>/dev/null; then \\\n\t\techo 'All documentation changes are done the right way.'; \\\n\telse \\\n\t\techo 'The documentation must be regenerated, please use `make generate-dns`.'; \\\n\t\tgit status --porcelain -- $(DOC_DIRECTORIES) 2>/dev/null; \\\n\t\texit 2; \\\n\tfi\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <img alt=\"lego logo\" src=\"./docs/static/images/lego-logo.min.svg\">\n  <p>Automatic Certificates and HTTPS for everyone.</p>\n</div>\n\n# Lego\n\n[ACME](https://www.rfc-editor.org/rfc/rfc8555.html) client and library for Let's Encrypt and other ACME CAs written in Go.\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/go-acme/lego/v4.svg)](https://pkg.go.dev/github.com/go-acme/lego/v4)\n[![Build Status](https://github.com//go-acme/lego/workflows/Main/badge.svg?branch=master)](https://github.com//go-acme/lego/actions)\n[![Docker Pulls](https://img.shields.io/docker/pulls/goacme/lego.svg)](https://hub.docker.com/r/goacme/lego/)\n\nlego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️\n\nEverybody thinks that the others will donate, but in the end, nobody does.\n\nSo if you think that lego is worth it, please consider [donating](https://donate.ldez.dev).\n\n## Features\n\n- ACME v2 [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html)\n  - Support [RFC 8737](https://www.rfc-editor.org/rfc/rfc8737.html): TLS Application‑Layer Protocol Negotiation (ALPN) Challenge Extension\n  - Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): certificates for IP addresses\n  - Support [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html): Renewal Information (ARI) Extension\n  - Support [draft-ietf-acme-profiles-00](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/): Profiles Extension\n- Comes with about [180 DNS providers](https://go-acme.github.io/lego/dns)\n- Register with CA\n- Obtain certificates, both from scratch or with an existing CSR\n- Renew certificates\n- Revoke certificates\n- Robust implementation of ACME challenges:\n  - HTTP (http-01)\n  - DNS (dns-01)\n  - TLS (tls-alpn-01)\n- SAN certificate support\n- [CNAME support](https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme.html) by default\n- [Custom challenge solvers](https://go-acme.github.io/lego/usage/library/writing-a-challenge-solver/)\n- Certificate bundling\n- OCSP helper function\n\n## Installation\n\nHow to [install](https://go-acme.github.io/lego/installation/).\n\n## Usage\n\n- as a [CLI](https://go-acme.github.io/lego/usage/cli)\n- as a [library](https://go-acme.github.io/lego/usage/library)\n\n## Documentation\n\nDocumentation is hosted live at https://go-acme.github.io/lego/.\n\n## DNS providers\n\nDetailed documentation is available [here](https://go-acme.github.io/lego/dns).\n\nIf 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).\n\n<!-- START DNS PROVIDERS LIST -->\n\n<table><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/com35/\">35.com/三五互联</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/active24/\">Active24</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/edgedns/\">Akamai EdgeDNS</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/alidns/\">Alibaba Cloud DNS</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/aliesa/\">AlibabaCloud ESA</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/allinkl/\">all-inkl</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/alwaysdata/\">Alwaysdata</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/lightsail/\">Amazon Lightsail</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/route53/\">Amazon Route 53</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/anexia/\">Anexia CloudDNS</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/safedns/\">ANS SafeDNS</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/artfiles/\">ArtFiles</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/arvancloud/\">ArvanCloud</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/auroradns/\">Aurora DNS</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/autodns/\">Autodns</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/axelname/\">Axelname</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/azion/\">Azion</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/azure/\">Azure (deprecated)</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/azuredns/\">Azure DNS</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/baiducloud/\">Baidu Cloud</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/beget/\">Beget.com</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/binarylane/\">Binary Lane</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/bindman/\">Bindman</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/bluecat/\">Bluecat</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/bluecatv2/\">Bluecat v2</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/bookmyname/\">BookMyName</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/brandit/\">Brandit (deprecated)</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/bunny/\">Bunny</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/checkdomain/\">Checkdomain</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/civo/\">Civo</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/cloudru/\">Cloud.ru</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/clouddns/\">CloudDNS</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/cloudflare/\">Cloudflare</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/cloudns/\">ClouDNS</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/cloudxns/\">CloudXNS (Deprecated)</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/conoha/\">ConoHa v2</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/conohav3/\">ConoHa v3</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/constellix/\">Constellix</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/corenetworks/\">Core-Networks</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/cpanel/\">CPanel/WHM</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/czechia/\">Czechia</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/ddnss/\">DDnss (DynDNS Service)</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/derak/\">Derak Cloud</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/desec/\">deSEC.io</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/designate/\">Designate DNSaaS for Openstack</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/digitalocean/\">Digital Ocean</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/directadmin/\">DirectAdmin</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/dnsmadeeasy/\">DNS Made Easy</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/dnsexit/\">DNSExit</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/dnshomede/\">dnsHome.de</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/dnsimple/\">DNSimple</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/dnspod/\">DNSPod (deprecated)</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/dode/\">Domain Offensive (do.de)</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/domeneshop/\">Domeneshop</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/dreamhost/\">DreamHost</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/duckdns/\">Duck DNS</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/dyn/\">Dyn</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/dyndnsfree/\">DynDnsFree.de</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/dynu/\">Dynu</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/easydns/\">EasyDNS</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/edgecenter/\">EdgeCenter</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/efficientip/\">Efficient IP</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/epik/\">Epik</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/eurodns/\">EuroDNS</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/excedo/\">Excedo</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/exoscale/\">Exoscale</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/exec/\">External program</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/f5xc/\">F5 XC</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/freemyip/\">freemyip.com</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/namesurfer/\">FusionLayer NameSurfer</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/gcore/\">G-Core</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/gandi/\">Gandi</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/gandiv5/\">Gandi Live DNS (v5)</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/gigahostno/\">Gigahost.no</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/glesys/\">Glesys</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/godaddy/\">Go Daddy</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/gcloud/\">Google Cloud</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/googledomains/\">Google Domains</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/gravity/\">Gravity</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/hetzner/\">Hetzner</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/hostingde/\">Hosting.de</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/hostingnl/\">Hosting.nl</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/hostinger/\">Hostinger</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/hosttech/\">Hosttech</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/httpreq/\">HTTP request</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/httpnet/\">http.net</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/huaweicloud/\">Huawei Cloud</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/hurricane/\">Hurricane Electric DNS</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/hyperone/\">HyperOne</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/ibmcloud/\">IBM Cloud (SoftLayer)</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/iijdpf/\">IIJ DNS Platform Service</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/infoblox/\">Infoblox</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/infomaniak/\">Infomaniak</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/iij/\">Internet Initiative Japan</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/internetbs/\">Internet.bs</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/inwx/\">INWX</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/ionos/\">Ionos</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/ionoscloud/\">Ionos Cloud</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/ipv64/\">IPv64</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/ispconfig/\">ISPConfig 3</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/ispconfigddns/\">ISPConfig 3 - Dynamic DNS (DDNS) Module</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/iwantmyname/\">iwantmyname (Deprecated)</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/jdcloud/\">JD Cloud</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/joker/\">Joker</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/acme-dns/\">Joohoi&#39;s ACME-DNS</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/keyhelp/\">KeyHelp</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/leaseweb/\">Leaseweb</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/liara/\">Liara</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/limacity/\">Lima-City</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/linode/\">Linode (v4)</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/liquidweb/\">Liquid Web</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/loopia/\">Loopia</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/luadns/\">LuaDNS</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/mailinabox/\">Mail-in-a-Box</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/manageengine/\">ManageEngine CloudDNS</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/manual/\">Manual</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/metaname/\">Metaname</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/metaregistrar/\">Metaregistrar</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/mijnhost/\">mijn.host</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/mittwald/\">Mittwald</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/myaddr/\">myaddr.{tools,dev,io}</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/mydnsjp/\">MyDNS.jp</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/mythicbeasts/\">MythicBeasts</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/namedotcom/\">Name.com</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/namecheap/\">Namecheap</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/namesilo/\">Namesilo</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/nearlyfreespeech/\">NearlyFreeSpeech.NET</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/neodigit/\">Neodigit</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/netcup/\">Netcup</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/netlify/\">Netlify</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/nicmanager/\">Nicmanager</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/nifcloud/\">NIFCloud</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/njalla/\">Njalla</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/nodion/\">Nodion</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/ns1/\">NS1</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/octenium/\">Octenium</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/otc/\">Open Telekom Cloud</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/oraclecloud/\">Oracle Cloud</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/ovh/\">OVH</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/plesk/\">plesk.com</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/porkbun/\">Porkbun</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/pdns/\">PowerDNS</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/rackspace/\">Rackspace</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/rainyun/\">Rain Yun/雨云</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/rcodezero/\">RcodeZero</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/regru/\">reg.ru</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/regfish/\">Regfish</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/rfc2136/\">RFC2136</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/rimuhosting/\">RimuHosting</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/nicru/\">RU CENTER</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/sakuracloud/\">Sakura Cloud</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/scaleway/\">Scaleway</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/selectel/\">Selectel</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/selectelv2/\">Selectel v2</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/selfhostde/\">SelfHost.(de|eu)</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/servercow/\">Servercow</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/shellrent/\">Shellrent</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/simply/\">Simply.com</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/sonic/\">Sonic</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/spaceship/\">Spaceship</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/stackpath/\">Stackpath</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/syse/\">Syse</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/technitium/\">Technitium</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/tencentcloud/\">Tencent Cloud DNS</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/edgeone/\">Tencent EdgeOne</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/timewebcloud/\">Timeweb Cloud</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/todaynic/\">TodayNIC/时代互联</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/transip/\">TransIP</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/ultradns/\">Ultradns</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/uniteddomains/\">United-Domains</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/variomedia/\">Variomedia</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/vegadns/\">VegaDNS</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/vercel/\">Vercel</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/versio/\">Versio.[nl|eu|uk]</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/vinyldns/\">VinylDNS</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/virtualname/\">Virtualname</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/vkcloud/\">VK Cloud</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/volcengine/\">Volcano Engine/火山引擎</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/vscale/\">Vscale</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/vultr/\">Vultr</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/webnamesca/\">webnames.ca</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/webnames/\">webnames.ru</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/websupport/\">Websupport</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/wedos/\">WEDOS</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/westcn/\">West.cn/西部数码</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/yandex360/\">Yandex 360</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/yandexcloud/\">Yandex Cloud</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/yandex/\">Yandex PDD</a></td>\n</tr><tr>\n  <td><a href=\"https://go-acme.github.io/lego/dns/zoneee/\">Zone.ee</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/zoneedit/\">ZoneEdit</a></td>\n  <td><a href=\"https://go-acme.github.io/lego/dns/zonomi/\">Zonomi</a></td>\n  <td></td>\n</tr></table>\n\n<!-- END DNS PROVIDERS LIST -->\n\nIf 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).\n"
  },
  {
    "path": "acme/api/account.go",
    "content": "package api\n\nimport (\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n)\n\ntype AccountService service\n\n// New Creates a new account.\nfunc (a *AccountService) New(req acme.Account) (acme.ExtendedAccount, error) {\n\tvar account acme.Account\n\n\tresp, err := a.core.post(a.core.GetDirectory().NewAccountURL, req, &account)\n\tlocation := getLocation(resp)\n\n\tif location != \"\" {\n\t\ta.core.jws.SetKid(location)\n\t}\n\n\tif err != nil {\n\t\treturn acme.ExtendedAccount{Location: location}, err\n\t}\n\n\treturn acme.ExtendedAccount{Account: account, Location: location}, nil\n}\n\n// NewEAB Creates a new account with an External Account Binding.\nfunc (a *AccountService) NewEAB(accMsg acme.Account, kid, hmacEncoded string) (acme.ExtendedAccount, error) {\n\thmac, err := decodeEABHmac(hmacEncoded)\n\tif err != nil {\n\t\treturn acme.ExtendedAccount{}, err\n\t}\n\n\teabJWS, err := a.core.signEABContent(a.core.GetDirectory().NewAccountURL, kid, hmac)\n\tif err != nil {\n\t\treturn acme.ExtendedAccount{}, fmt.Errorf(\"acme: error signing eab content: %w\", err)\n\t}\n\n\taccMsg.ExternalAccountBinding = eabJWS\n\n\treturn a.New(accMsg)\n}\n\n// Get Retrieves an account.\nfunc (a *AccountService) Get(accountURL string) (acme.Account, error) {\n\tif accountURL == \"\" {\n\t\treturn acme.Account{}, errors.New(\"account[get]: empty URL\")\n\t}\n\n\tvar account acme.Account\n\n\t_, err := a.core.postAsGet(accountURL, &account)\n\tif err != nil {\n\t\treturn acme.Account{}, err\n\t}\n\n\treturn account, nil\n}\n\n// Update Updates an account.\nfunc (a *AccountService) Update(accountURL string, req acme.Account) (acme.Account, error) {\n\tif accountURL == \"\" {\n\t\treturn acme.Account{}, errors.New(\"account[update]: empty URL\")\n\t}\n\n\tvar account acme.Account\n\n\t_, err := a.core.post(accountURL, req, &account)\n\tif err != nil {\n\t\treturn acme.Account{}, err\n\t}\n\n\treturn account, nil\n}\n\n// Deactivate Deactivates an account.\nfunc (a *AccountService) Deactivate(accountURL string) error {\n\tif accountURL == \"\" {\n\t\treturn errors.New(\"account[deactivate]: empty URL\")\n\t}\n\n\treq := acme.Account{Status: acme.StatusDeactivated}\n\t_, err := a.core.post(accountURL, req, nil)\n\n\treturn err\n}\n\nfunc decodeEABHmac(hmacEncoded string) ([]byte, error) {\n\thmac, errRaw := base64.RawURLEncoding.DecodeString(hmacEncoded)\n\tif errRaw == nil {\n\t\treturn hmac, nil\n\t}\n\n\thmac, err := base64.URLEncoding.DecodeString(hmacEncoded)\n\tif err == nil {\n\t\treturn hmac, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"acme: could not decode hmac key: %w\", errors.Join(errRaw, err))\n}\n"
  },
  {
    "path": "acme/api/account_test.go",
    "content": "package api\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_decodeEABHmac(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc string\n\t\thmac string\n\t}{\n\t\t{\n\t\t\tdesc: \"RawURLEncoding\",\n\t\t\thmac: \"BAEDAgQCBQcGCAUDDDMBAAIRAwQhEjEFQVFhEyJxgTIGFJGhsUIjJBVSwWIzNHKC0UMHJZJT8OHx\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"URLEncoding\",\n\t\t\thmac: \"nKTo9Hu8fpCqWPXx-25LVbZrJWxcHISsr4qHrRR0j5U=\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tv, err := decodeEABHmac(test.hmac)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.NotEmpty(t, v)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/api/api.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api/internal/nonces\"\n\t\"github.com/go-acme/lego/v4/acme/api/internal/secure\"\n\t\"github.com/go-acme/lego/v4/acme/api/internal/sender\"\n\t\"github.com/go-acme/lego/v4/log\"\n)\n\n// Core ACME/LE core API.\ntype Core struct {\n\tdoer         *sender.Doer\n\tnonceManager *nonces.Manager\n\tjws          *secure.JWS\n\tdirectory    acme.Directory\n\tHTTPClient   *http.Client\n\n\tcommon         service // Reuse a single struct instead of allocating one for each service on the heap.\n\tAccounts       *AccountService\n\tAuthorizations *AuthorizationService\n\tCertificates   *CertificateService\n\tChallenges     *ChallengeService\n\tOrders         *OrderService\n}\n\n// New Creates a new Core.\nfunc New(httpClient *http.Client, userAgent, caDirURL, kid string, privateKey crypto.PrivateKey) (*Core, error) {\n\tdoer := sender.NewDoer(httpClient, userAgent)\n\n\tdir, err := getDirectory(doer, caDirURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnonceManager := nonces.NewManager(doer, dir.NewNonceURL)\n\n\tjws := secure.NewJWS(privateKey, kid, nonceManager)\n\n\tc := &Core{doer: doer, nonceManager: nonceManager, jws: jws, directory: dir, HTTPClient: httpClient}\n\n\tc.common.core = c\n\tc.Accounts = (*AccountService)(&c.common)\n\tc.Authorizations = (*AuthorizationService)(&c.common)\n\tc.Certificates = (*CertificateService)(&c.common)\n\tc.Challenges = (*ChallengeService)(&c.common)\n\tc.Orders = (*OrderService)(&c.common)\n\n\treturn c, nil\n}\n\n// post performs an HTTP POST request and parses the response body as JSON,\n// into the provided respBody object.\nfunc (a *Core) post(uri string, reqBody, response any) (*http.Response, error) {\n\tcontent, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn nil, errors.New(\"failed to marshal message\")\n\t}\n\n\treturn a.retrievablePost(uri, content, response)\n}\n\n// postAsGet performs an HTTP POST (\"POST-as-GET\") request.\n// https://www.rfc-editor.org/rfc/rfc8555.html#section-6.3\nfunc (a *Core) postAsGet(uri string, response any) (*http.Response, error) {\n\treturn a.retrievablePost(uri, []byte{}, response)\n}\n\nfunc (a *Core) retrievablePost(uri string, content []byte, response any) (*http.Response, error) {\n\tctx := context.Background()\n\n\t// during tests, allow to support ~90% of bad nonce with a minimum of attempts.\n\tbo := backoff.NewExponentialBackOff()\n\tbo.InitialInterval = 200 * time.Millisecond\n\tbo.MaxInterval = 5 * time.Second\n\n\toperation := func() (*http.Response, error) {\n\t\tresp, err := a.signedPost(uri, content, response)\n\t\tif err != nil {\n\t\t\t// Retry if the nonce was invalidated\n\t\t\tvar e *acme.NonceError\n\t\t\tif errors.As(err, &e) {\n\t\t\t\treturn resp, err\n\t\t\t}\n\n\t\t\treturn resp, backoff.Permanent(err)\n\t\t}\n\n\t\treturn resp, nil\n\t}\n\n\tnotify := func(err error, duration time.Duration) {\n\t\tlog.Infof(\"retry due to: %v\", err)\n\t}\n\n\treturn backoff.Retry(ctx, operation,\n\t\tbackoff.WithBackOff(bo),\n\t\tbackoff.WithMaxElapsedTime(20*time.Second),\n\t\tbackoff.WithNotify(notify))\n}\n\nfunc (a *Core) signedPost(uri string, content []byte, response any) (*http.Response, error) {\n\tsignedContent, err := a.jws.SignContent(uri, content)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to post JWS message: failed to sign content: %w\", err)\n\t}\n\n\tsignedBody := bytes.NewBufferString(signedContent.FullSerialize())\n\n\tresp, err := a.doer.Post(uri, signedBody, \"application/jose+json\", response)\n\n\t// nonceErr is ignored to keep the root error.\n\tnonce, nonceErr := nonces.GetFromResponse(resp)\n\tif nonceErr == nil {\n\t\ta.nonceManager.Push(nonce)\n\t}\n\n\treturn resp, err\n}\n\nfunc (a *Core) signEABContent(newAccountURL, kid string, hmac []byte) ([]byte, error) {\n\teabJWS, err := a.jws.SignEABContent(newAccountURL, kid, hmac)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn []byte(eabJWS.FullSerialize()), nil\n}\n\n// GetKeyAuthorization Gets the key authorization.\nfunc (a *Core) GetKeyAuthorization(token string) (string, error) {\n\treturn a.jws.GetKeyAuthorization(token)\n}\n\nfunc (a *Core) GetDirectory() acme.Directory {\n\treturn a.directory\n}\n\nfunc getDirectory(do *sender.Doer, caDirURL string) (acme.Directory, error) {\n\tvar dir acme.Directory\n\tif _, err := do.Get(caDirURL, &dir); err != nil {\n\t\treturn dir, fmt.Errorf(\"get directory at '%s': %w\", caDirURL, err)\n\t}\n\n\tif dir.NewAccountURL == \"\" {\n\t\treturn dir, errors.New(\"directory missing new registration URL\")\n\t}\n\n\tif dir.NewOrderURL == \"\" {\n\t\treturn dir, errors.New(\"directory missing new order URL\")\n\t}\n\n\treturn dir, nil\n}\n"
  },
  {
    "path": "acme/api/authorization.go",
    "content": "package api\n\nimport (\n\t\"errors\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n)\n\ntype AuthorizationService service\n\n// Get Gets an authorization.\nfunc (c *AuthorizationService) Get(authzURL string) (acme.Authorization, error) {\n\tif authzURL == \"\" {\n\t\treturn acme.Authorization{}, errors.New(\"authorization[get]: empty URL\")\n\t}\n\n\tvar authz acme.Authorization\n\n\t_, err := c.core.postAsGet(authzURL, &authz)\n\tif err != nil {\n\t\treturn acme.Authorization{}, err\n\t}\n\n\treturn authz, nil\n}\n\n// Deactivate Deactivates an authorization.\nfunc (c *AuthorizationService) Deactivate(authzURL string) error {\n\tif authzURL == \"\" {\n\t\treturn errors.New(\"authorization[deactivate]: empty URL\")\n\t}\n\n\tvar disabledAuth acme.Authorization\n\n\t_, err := c.core.post(authzURL, acme.Authorization{Status: acme.StatusDeactivated}, &disabledAuth)\n\n\treturn err\n}\n"
  },
  {
    "path": "acme/api/certificate.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n)\n\n// maxBodySize is the maximum size of body that we will read.\nconst maxBodySize = 1024 * 1024\n\ntype CertificateService service\n\n// Get Returns the certificate and the issuer certificate.\n// 'bundle' is only applied if the issuer is provided by the 'up' link.\nfunc (c *CertificateService) Get(certURL string, bundle bool) ([]byte, []byte, error) {\n\tcert, _, err := c.get(certURL, bundle)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn cert.Cert, cert.Issuer, nil\n}\n\n// GetAll the certificates and the alternate certificates.\n// bundle' is only applied if the issuer is provided by the 'up' link.\nfunc (c *CertificateService) GetAll(certURL string, bundle bool) (map[string]*acme.RawCertificate, error) {\n\tcert, headers, err := c.get(certURL, bundle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcerts := map[string]*acme.RawCertificate{certURL: cert}\n\n\t// URLs of \"alternate\" link relation\n\t// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2\n\talts := getLinks(headers, \"alternate\")\n\n\tfor _, alt := range alts {\n\t\taltCert, _, err := c.get(alt, bundle)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tcerts[alt] = altCert\n\t}\n\n\treturn certs, nil\n}\n\n// Revoke Revokes a certificate.\nfunc (c *CertificateService) Revoke(req acme.RevokeCertMessage) error {\n\t_, err := c.core.post(c.core.GetDirectory().RevokeCertURL, req, nil)\n\treturn err\n}\n\n// get Returns the certificate and the \"up\" link.\nfunc (c *CertificateService) get(certURL string, bundle bool) (*acme.RawCertificate, http.Header, error) {\n\tif certURL == \"\" {\n\t\treturn nil, nil, errors.New(\"certificate[get]: empty URL\")\n\t}\n\n\tresp, err := c.core.postAsGet(certURL, nil)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tdata, err := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))\n\tif err != nil {\n\t\treturn nil, resp.Header, err\n\t}\n\n\tcert := c.getCertificateChain(data, bundle)\n\n\treturn cert, resp.Header, err\n}\n\n// getCertificateChain Returns the certificate and the issuer certificate.\nfunc (c *CertificateService) getCertificateChain(cert []byte, bundle bool) *acme.RawCertificate {\n\t// Get issuerCert from bundled response from Let's Encrypt\n\t// See https://community.letsencrypt.org/t/acme-v2-no-up-link-in-response/64962\n\t_, issuer := pem.Decode(cert)\n\n\t// If bundle is false, we want to return a single certificate.\n\t// To do this, we remove the issuer cert(s) from the issued cert.\n\tif !bundle {\n\t\tcert = bytes.TrimSuffix(cert, issuer)\n\t}\n\n\treturn &acme.RawCertificate{Cert: cert, Issuer: issuer}\n}\n"
  },
  {
    "path": "acme/api/certificate_test.go",
    "content": "package api\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst certResponseMock = `-----BEGIN CERTIFICATE-----\nMIIDEDCCAfigAwIBAgIHPhckqW5fPDANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQD\nEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDM5NWU2MTAeFw0xODExMDcxNzQ2NTZa\nFw0yMzExMDcxNzQ2NTZaMBMxETAPBgNVBAMTCGFjbWUud3RmMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtLNKvZXD20XPUQCWYSK9rUSKxD9Eb0c9fag\nbxOxOkLRTgL8LH6yln+bxc3MrHDou4PpDUdeo2CyOQu3CKsTS5mrH3NXYHu0H7p5\ny3riOJTHnfkGKLT9LciGz7GkXd62nvNP57bOf5Sk4P2M+Qbxd0hPTSfu52740LSy\n144cnxe2P1aDYehrEp6nYCESuyD/CtUHTo0qwJmzIy163Sp3rSs15BuCPyhySnE3\nBJ8Ggv+qC6D5I1932DfSqyQJ79iq/HRm0Fn84am3KwvRlUfWxabmsUGARXoqCgnE\nzcbJVOZKewv0zlQJpfac+b+Imj6Lvt1TGjIz2mVyefYgLx8gwwIDAQABo1QwUjAO\nBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwG\nA1UdEwEB/wQCMAAwEwYDVR0RBAwwCoIIYWNtZS53dGYwDQYJKoZIhvcNAQELBQAD\nggEBABB/0iYhmfPSQot5RaeeovQnsqYjI5ryQK2cwzW6qcTJfv8N6+p6XkqF1+W4\njXZjrQP8MvgO9KNWlvx12vhINE6wubk88L+2piAi5uS2QejmZbXpyYB9s+oPqlk9\nIDvfdlVYOqvYAhSx7ggGi+j73mjZVtjAavP6dKuu475ZCeq+NIC15RpbbikWKtYE\nHBJ7BW8XQKx67iHGx8ygHTDLbREL80Bck3oUm7wIYGMoNijD6RBl25p4gYl9dzOd\nTqGl5hW/1P5hMbgEzHbr4O3BfWqU2g7tV36TASy3jbC3ONFRNNYrpEZ1AL3+cUri\nOPPkKtAKAbQkKbUIfsHpBZjKZMU=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE\nAxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw\nNzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl\nNjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT\nSZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh\n0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen\nSRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx\nHAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt\nD1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu\nmB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD\nAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA\nupU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm\niUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd\nQqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ\nwlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv\nrzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2\n7R4IbHGnj0BJA2vMYC4hSw==\n-----END CERTIFICATE-----\n`\n\nconst issuerMock = `-----BEGIN CERTIFICATE-----\nMIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE\nAxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw\nNzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl\nNjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT\nSZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh\n0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen\nSRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx\nHAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt\nD1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu\nmB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD\nAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA\nupU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm\niUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd\nQqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ\nwlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv\nrzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2\n7R4IbHGnj0BJA2vMYC4hSw==\n-----END CERTIFICATE-----\n`\n\nfunc TestCertificateService_Get_issuerRelUp(t *testing.T) {\n\tserver := tester.MockACMEServer().\n\t\tRoute(\"POST /certificate\", servermock.RawStringResponse(certResponseMock)).\n\t\tBuildHTTPS(t)\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", key)\n\trequire.NoError(t, err)\n\n\tcert, issuer, err := core.Certificates.Get(server.URL+\"/certificate\", true)\n\trequire.NoError(t, err)\n\tassert.Equal(t, certResponseMock, string(cert), \"Certificate\")\n\tassert.Equal(t, issuerMock, string(issuer), \"IssuerCertificate\")\n}\n\nfunc TestCertificateService_Get_embeddedIssuer(t *testing.T) {\n\tserver := tester.MockACMEServer().\n\t\tRoute(\"POST /certificate\", servermock.RawStringResponse(certResponseMock)).\n\t\tBuildHTTPS(t)\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", key)\n\trequire.NoError(t, err)\n\n\tcert, issuer, err := core.Certificates.Get(server.URL+\"/certificate\", true)\n\trequire.NoError(t, err)\n\tassert.Equal(t, certResponseMock, string(cert), \"Certificate\")\n\tassert.Equal(t, issuerMock, string(issuer), \"IssuerCertificate\")\n}\n"
  },
  {
    "path": "acme/api/challenge.go",
    "content": "package api\n\nimport (\n\t\"errors\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n)\n\ntype ChallengeService service\n\n// New Creates a challenge.\nfunc (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) {\n\tif chlgURL == \"\" {\n\t\treturn acme.ExtendedChallenge{}, errors.New(\"challenge[new]: empty URL\")\n\t}\n\n\t// Challenge initiation is done by sending a JWS payload containing the trivial JSON object `{}`.\n\t// We use an empty struct instance as the postJSON payload here to achieve this result.\n\tvar chlng acme.ExtendedChallenge\n\n\tresp, err := c.core.post(chlgURL, struct{}{}, &chlng)\n\tif err != nil {\n\t\treturn acme.ExtendedChallenge{}, err\n\t}\n\n\tchlng.AuthorizationURL = getLink(resp.Header, \"up\")\n\tchlng.RetryAfter = getRetryAfter(resp)\n\n\treturn chlng, nil\n}\n\n// Get Gets a challenge.\nfunc (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) {\n\tif chlgURL == \"\" {\n\t\treturn acme.ExtendedChallenge{}, errors.New(\"challenge[get]: empty URL\")\n\t}\n\n\tvar chlng acme.ExtendedChallenge\n\n\tresp, err := c.core.postAsGet(chlgURL, &chlng)\n\tif err != nil {\n\t\treturn acme.ExtendedChallenge{}, err\n\t}\n\n\tchlng.AuthorizationURL = getLink(resp.Header, \"up\")\n\tchlng.RetryAfter = getRetryAfter(resp)\n\n\treturn chlng, nil\n}\n"
  },
  {
    "path": "acme/api/identifier.go",
    "content": "package api\n\nimport (\n\t\"cmp\"\n\t\"net\"\n\t\"slices\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n)\n\nfunc createIdentifiers(domains []string) []acme.Identifier {\n\tuniqIdentifiers := make(map[string]struct{})\n\n\tvar identifiers []acme.Identifier\n\n\tfor _, domain := range domains {\n\t\tif _, ok := uniqIdentifiers[domain]; ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tident := acme.Identifier{Value: domain, Type: \"dns\"}\n\n\t\tif net.ParseIP(domain) != nil {\n\t\t\tident.Type = \"ip\"\n\t\t}\n\n\t\tidentifiers = append(identifiers, ident)\n\n\t\tuniqIdentifiers[domain] = struct{}{}\n\t}\n\n\treturn identifiers\n}\n\n// compareIdentifiers compares 2 slices of [acme.Identifier].\nfunc compareIdentifiers(a, b []acme.Identifier) int {\n\t// Clones slices to avoid modifying original slices.\n\tright := slices.Clone(a)\n\tleft := slices.Clone(b)\n\n\tslices.SortStableFunc(right, compareIdentifier)\n\tslices.SortStableFunc(left, compareIdentifier)\n\n\treturn slices.CompareFunc(right, left, compareIdentifier)\n}\n\nfunc compareIdentifier(right, left acme.Identifier) int {\n\treturn cmp.Or(\n\t\tcmp.Compare(right.Type, left.Type),\n\t\tcmp.Compare(right.Value, left.Value),\n\t)\n}\n"
  },
  {
    "path": "acme/api/identifier_test.go",
    "content": "package api\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_compareIdentifiers(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ta, b     []acme.Identifier\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tdesc: \"identical identifiers\",\n\t\t\ta: []acme.Identifier{\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"*.example.com\"},\n\t\t\t},\n\t\t\tb: []acme.Identifier{\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"*.example.com\"},\n\t\t\t},\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tdesc: \"identical identifiers but different order\",\n\t\t\ta: []acme.Identifier{\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"*.example.com\"},\n\t\t\t},\n\t\t\tb: []acme.Identifier{\n\t\t\t\t{Type: \"dns\", Value: \"*.example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t},\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tdesc: \"duplicate identifiers\",\n\t\t\ta: []acme.Identifier{\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"*.example.com\"},\n\t\t\t},\n\t\t\tb: []acme.Identifier{\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t},\n\t\t\texpected: -1,\n\t\t},\n\t\t{\n\t\t\tdesc: \"different identifier values\",\n\t\t\ta: []acme.Identifier{\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"*.example.com\"},\n\t\t\t},\n\t\t\tb: []acme.Identifier{\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"*.example.org\"},\n\t\t\t},\n\t\t\texpected: -1,\n\t\t},\n\t\t{\n\t\t\tdesc: \"different identifier types\",\n\t\t\ta: []acme.Identifier{\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"*.example.com\"},\n\t\t\t},\n\t\t\tb: []acme.Identifier{\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t{Type: \"ip\", Value: \"*.example.com\"},\n\t\t\t},\n\t\t\texpected: -1,\n\t\t},\n\t\t{\n\t\t\tdesc: \"different number of identifiers a>b\",\n\t\t\ta: []acme.Identifier{\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"*.example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"example.org\"},\n\t\t\t},\n\t\t\tb: []acme.Identifier{\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"*.example.com\"},\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tdesc: \"different number of identifiers b>a\",\n\t\t\ta: []acme.Identifier{\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"*.example.com\"},\n\t\t\t},\n\t\t\tb: []acme.Identifier{\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"*.example.com\"},\n\t\t\t\t{Type: \"dns\", Value: \"example.org\"},\n\t\t\t},\n\t\t\texpected: -1,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassert.Equal(t, test.expected, compareIdentifiers(test.a, test.b))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/api/internal/nonces/nonce_manager.go",
    "content": "package nonces\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/go-acme/lego/v4/acme/api/internal/sender\"\n)\n\n// Manager Manages nonces.\ntype Manager struct {\n\tsync.Mutex\n\n\tdo       *sender.Doer\n\tnonceURL string\n\tnonces   []string\n}\n\n// NewManager Creates a new Manager.\nfunc NewManager(do *sender.Doer, nonceURL string) *Manager {\n\treturn &Manager{\n\t\tdo:       do,\n\t\tnonceURL: nonceURL,\n\t}\n}\n\n// Pop Pops a nonce.\nfunc (n *Manager) Pop() (string, bool) {\n\tn.Lock()\n\tdefer n.Unlock()\n\n\tif len(n.nonces) == 0 {\n\t\treturn \"\", false\n\t}\n\n\tnonce := n.nonces[len(n.nonces)-1]\n\tn.nonces = n.nonces[:len(n.nonces)-1]\n\n\treturn nonce, true\n}\n\n// Push Pushes a nonce.\nfunc (n *Manager) Push(nonce string) {\n\tn.Lock()\n\tdefer n.Unlock()\n\n\tn.nonces = append(n.nonces, nonce)\n}\n\n// Nonce implement jose.NonceSource.\nfunc (n *Manager) Nonce() (string, error) {\n\tif nonce, ok := n.Pop(); ok {\n\t\treturn nonce, nil\n\t}\n\n\treturn n.getNonce()\n}\n\nfunc (n *Manager) getNonce() (string, error) {\n\tresp, err := n.do.Head(n.nonceURL)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get nonce from HTTP HEAD: %w\", err)\n\t}\n\n\treturn GetFromResponse(resp)\n}\n\n// GetFromResponse Extracts a nonce from an HTTP response.\nfunc GetFromResponse(resp *http.Response) (string, error) {\n\tif resp == nil {\n\t\treturn \"\", errors.New(\"nil response\")\n\t}\n\n\tnonce := resp.Header.Get(\"Replay-Nonce\")\n\tif nonce == \"\" {\n\t\treturn \"\", errors.New(\"server did not respond with a proper nonce header\")\n\t}\n\n\treturn nonce, nil\n}\n"
  },
  {
    "path": "acme/api/internal/nonces/nonce_manager_test.go",
    "content": "package nonces\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api/internal/sender\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n)\n\nfunc TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {\n\tmanager := servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*Manager, error) {\n\t\t\tdoer := sender.NewDoer(server.Client(), \"lego-test\")\n\n\t\t\treturn NewManager(doer, server.URL), nil\n\t\t}).\n\t\tRoute(\"HEAD /\", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\ttime.Sleep(250 * time.Millisecond)\n\n\t\t\trw.Header().Set(\"Replay-Nonce\", \"12345\")\n\t\t\trw.Header().Set(\"Retry-After\", \"0\")\n\n\t\t\tservermock.JSONEncode(&acme.Challenge{Type: \"http-01\", Status: \"Valid\", URL: \"https://example.com/\", Token: \"token\"}).ServeHTTP(rw, req)\n\t\t})).\n\t\tBuildHTTPS(t)\n\n\tch := make(chan bool)\n\tresultCh := make(chan bool)\n\n\tgo func() {\n\t\t_, errN := manager.Nonce()\n\t\tif errN != nil {\n\t\t\tt.Log(errN)\n\t\t}\n\n\t\tch <- true\n\t}()\n\tgo func() {\n\t\t_, errN := manager.Nonce()\n\t\tif errN != nil {\n\t\t\tt.Log(errN)\n\t\t}\n\n\t\tch <- true\n\t}()\n\tgo func() {\n\t\t<-ch\n\t\t<-ch\n\n\t\tresultCh <- true\n\t}()\n\n\tselect {\n\tcase <-resultCh:\n\tcase <-time.After(500 * time.Millisecond):\n\t\tt.Fatal(\"JWS is probably holding a lock while making HTTP request\")\n\t}\n}\n"
  },
  {
    "path": "acme/api/internal/secure/jws.go",
    "content": "package secure\n\nimport (\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rsa\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/acme/api/internal/nonces\"\n\tjose \"github.com/go-jose/go-jose/v4\"\n)\n\n// JWS Represents a JWS.\ntype JWS struct {\n\tprivKey crypto.PrivateKey\n\tkid     string // Key identifier\n\tnonces  *nonces.Manager\n}\n\n// NewJWS Create a new JWS.\nfunc NewJWS(privateKey crypto.PrivateKey, kid string, nonceManager *nonces.Manager) *JWS {\n\treturn &JWS{\n\t\tprivKey: privateKey,\n\t\tnonces:  nonceManager,\n\t\tkid:     kid,\n\t}\n}\n\n// SetKid Sets a key identifier.\nfunc (j *JWS) SetKid(kid string) {\n\tj.kid = kid\n}\n\n// SignContent Signs a content with the JWS.\nfunc (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, error) {\n\tvar alg jose.SignatureAlgorithm\n\n\tswitch k := j.privKey.(type) {\n\tcase *rsa.PrivateKey:\n\t\talg = jose.RS256\n\tcase *ecdsa.PrivateKey:\n\t\tif k.Curve == elliptic.P256() {\n\t\t\talg = jose.ES256\n\t\t} else if k.Curve == elliptic.P384() {\n\t\t\talg = jose.ES384\n\t\t}\n\t}\n\n\tsignKey := jose.SigningKey{\n\t\tAlgorithm: alg,\n\t\tKey:       jose.JSONWebKey{Key: j.privKey, KeyID: j.kid},\n\t}\n\n\toptions := jose.SignerOptions{\n\t\tNonceSource: j.nonces,\n\t\tExtraHeaders: map[jose.HeaderKey]any{\n\t\t\t\"url\": url,\n\t\t},\n\t}\n\n\tif j.kid == \"\" {\n\t\toptions.EmbedJWK = true\n\t}\n\n\tsigner, err := jose.NewSigner(signKey, &options)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create jose signer: %w\", err)\n\t}\n\n\tsigned, err := signer.Sign(content)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to sign content: %w\", err)\n\t}\n\n\treturn signed, nil\n}\n\n// SignEABContent Signs an external account binding content with the JWS.\nfunc (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) {\n\tjwk := jose.JSONWebKey{Key: j.privKey}\n\n\tjwkJSON, err := jwk.Public().MarshalJSON()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"acme: error encoding eab jwk key: %w\", err)\n\t}\n\n\tsigner, err := jose.NewSigner(\n\t\tjose.SigningKey{Algorithm: jose.HS256, Key: hmac},\n\t\t&jose.SignerOptions{\n\t\t\tEmbedJWK: false,\n\t\t\tExtraHeaders: map[jose.HeaderKey]any{\n\t\t\t\t\"kid\": kid,\n\t\t\t\t\"url\": url,\n\t\t\t},\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create External Account Binding jose signer: %w\", err)\n\t}\n\n\tsigned, err := signer.Sign(jwkJSON)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to External Account Binding sign content: %w\", err)\n\t}\n\n\treturn signed, nil\n}\n\n// GetKeyAuthorization Gets the key authorization for a token.\nfunc (j *JWS) GetKeyAuthorization(token string) (string, error) {\n\tvar publicKey crypto.PublicKey\n\n\tswitch k := j.privKey.(type) {\n\tcase *ecdsa.PrivateKey:\n\t\tpublicKey = k.Public()\n\tcase *rsa.PrivateKey:\n\t\tpublicKey = k.Public()\n\t}\n\n\t// Generate the Key Authorization for the challenge\n\tjwk := &jose.JSONWebKey{Key: publicKey}\n\n\tthumbBytes, err := jwk.Thumbprint(crypto.SHA256)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// unpad the base64URL\n\tkeyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes)\n\n\treturn token + \".\" + keyThumb, nil\n}\n"
  },
  {
    "path": "acme/api/internal/secure/jws_test.go",
    "content": "package secure\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api/internal/nonces\"\n\t\"github.com/go-acme/lego/v4/acme/api/internal/sender\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n)\n\nfunc TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {\n\tmanager := servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*nonces.Manager, error) {\n\t\t\tdoer := sender.NewDoer(server.Client(), \"lego-test\")\n\n\t\t\treturn nonces.NewManager(doer, server.URL), nil\n\t\t}).\n\t\tRoute(\"HEAD /\", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\ttime.Sleep(250 * time.Millisecond)\n\n\t\t\trw.Header().Set(\"Replay-Nonce\", \"12345\")\n\t\t\trw.Header().Set(\"Retry-After\", \"0\")\n\n\t\t\tservermock.JSONEncode(&acme.Challenge{Type: \"http-01\", Status: \"Valid\", URL: \"https://example.com/\", Token: \"token\"}).ServeHTTP(rw, req)\n\t\t})).\n\t\tBuildHTTPS(t)\n\n\tch := make(chan bool)\n\tresultCh := make(chan bool)\n\n\tgo func() {\n\t\t_, errN := manager.Nonce()\n\t\tif errN != nil {\n\t\t\tt.Log(errN)\n\t\t}\n\n\t\tch <- true\n\t}()\n\tgo func() {\n\t\t_, errN := manager.Nonce()\n\t\tif errN != nil {\n\t\t\tt.Log(errN)\n\t\t}\n\n\t\tch <- true\n\t}()\n\tgo func() {\n\t\t<-ch\n\t\t<-ch\n\n\t\tresultCh <- true\n\t}()\n\n\tselect {\n\tcase <-resultCh:\n\tcase <-time.After(500 * time.Millisecond):\n\t\tt.Fatal(\"JWS is probably holding a lock while making HTTP request\")\n\t}\n}\n"
  },
  {
    "path": "acme/api/internal/sender/sender.go",
    "content": "package sender\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n)\n\ntype RequestOption func(*http.Request) error\n\nfunc contentType(ct string) RequestOption {\n\treturn func(req *http.Request) error {\n\t\treq.Header.Set(\"Content-Type\", ct)\n\t\treturn nil\n\t}\n}\n\ntype Doer struct {\n\thttpClient *http.Client\n\tuserAgent  string\n}\n\n// NewDoer Creates a new Doer.\nfunc NewDoer(client *http.Client, userAgent string) *Doer {\n\tclient.Transport = newHTTPSOnly(client)\n\n\treturn &Doer{\n\t\thttpClient: client,\n\t\tuserAgent:  userAgent,\n\t}\n}\n\n// Get performs a GET request with a proper User-Agent string.\n// If \"response\" is not provided, callers should close resp.Body when done reading from it.\nfunc (d *Doer) Get(url string, response any) (*http.Response, error) {\n\treq, err := d.newRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn d.do(req, response)\n}\n\n// Head performs a HEAD request with a proper User-Agent string.\n// The response body (resp.Body) is already closed when this function returns.\nfunc (d *Doer) Head(url string) (*http.Response, error) {\n\treq, err := d.newRequest(http.MethodHead, url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn d.do(req, nil)\n}\n\n// Post performs a POST request with a proper User-Agent string.\n// If \"response\" is not provided, callers should close resp.Body when done reading from it.\nfunc (d *Doer) Post(url string, body io.Reader, bodyType string, response any) (*http.Response, error) {\n\treq, err := d.newRequest(http.MethodPost, url, body, contentType(bodyType))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn d.do(req, response)\n}\n\nfunc (d *Doer) newRequest(method, uri string, body io.Reader, opts ...RequestOption) (*http.Request, error) {\n\treq, err := http.NewRequest(method, uri, body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"User-Agent\", d.formatUserAgent())\n\n\tfor _, opt := range opts {\n\t\terr = opt(req)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\t}\n\n\treturn req, nil\n}\n\nfunc (d *Doer) do(req *http.Request, response any) (*http.Response, error) {\n\tresp, err := d.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = checkError(req, resp); err != nil {\n\t\treturn resp, err\n\t}\n\n\tif response != nil {\n\t\traw, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn resp, err\n\t\t}\n\n\t\tdefer resp.Body.Close()\n\n\t\terr = json.Unmarshal(raw, response)\n\t\tif err != nil {\n\t\t\treturn resp, fmt.Errorf(\"failed to unmarshal %q to type %T: %w\", raw, response, err)\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n\n// formatUserAgent builds and returns the User-Agent string to use in requests.\nfunc (d *Doer) formatUserAgent() string {\n\tua := fmt.Sprintf(\"%s %s (%s; %s; %s)\", d.userAgent, ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH)\n\treturn strings.TrimSpace(ua)\n}\n\nfunc checkError(req *http.Request, resp *http.Response) error {\n\tif resp.StatusCode < http.StatusBadRequest {\n\t\treturn nil\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%d :: %s :: %s :: %w\", resp.StatusCode, req.Method, req.URL, err)\n\t}\n\n\tvar errorDetails *acme.ProblemDetails\n\n\terr = json.Unmarshal(body, &errorDetails)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%d ::%s :: %s :: %w :: %s\", resp.StatusCode, req.Method, req.URL, err, string(body))\n\t}\n\n\terrorDetails.Method = req.Method\n\terrorDetails.URL = req.URL.String()\n\n\tif errorDetails.HTTPStatus == 0 {\n\t\terrorDetails.HTTPStatus = resp.StatusCode\n\t}\n\n\t// Check for errors we handle specifically\n\tswitch {\n\tcase errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr:\n\t\treturn &acme.NonceError{ProblemDetails: errorDetails}\n\n\tcase errorDetails.HTTPStatus == http.StatusConflict && errorDetails.Type == acme.AlreadyReplacedErr:\n\t\treturn &acme.AlreadyReplacedError{ProblemDetails: errorDetails}\n\n\tcase errorDetails.HTTPStatus == http.StatusTooManyRequests && errorDetails.Type == acme.RateLimitedErr:\n\t\treturn &acme.RateLimitedError{\n\t\t\tProblemDetails: errorDetails,\n\t\t\tRetryAfter:     resp.Header.Get(\"Retry-After\"),\n\t\t}\n\n\tdefault:\n\t\treturn errorDetails\n\t}\n}\n\ntype httpsOnly struct {\n\trt http.RoundTripper\n}\n\nfunc newHTTPSOnly(client *http.Client) *httpsOnly {\n\tif client.Transport == nil {\n\t\treturn &httpsOnly{rt: http.DefaultTransport}\n\t}\n\n\treturn &httpsOnly{rt: client.Transport}\n}\n\n// RoundTrip ensure HTTPS is used.\n// Each ACME function is accomplished by the client sending a sequence of HTTPS requests to the server [RFC2818],\n// carrying JSON messages [RFC8259].\n// Use of HTTPS is REQUIRED.\n// https://datatracker.ietf.org/doc/html/rfc8555#section-6.1\nfunc (r *httpsOnly) RoundTrip(req *http.Request) (*http.Response, error) {\n\tif req.URL.Scheme != \"https\" {\n\t\treturn nil, fmt.Errorf(\"HTTPS is required: %s\", req.URL)\n\t}\n\n\treturn r.rt.RoundTrip(req)\n}\n"
  },
  {
    "path": "acme/api/internal/sender/sender_test.go",
    "content": "package sender\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDo_UserAgentOnAllHTTPMethod(t *testing.T) {\n\tvar ua, method string\n\n\tserver := httptest.NewTLSServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {\n\t\tua = r.Header.Get(\"User-Agent\")\n\t\tmethod = r.Method\n\t}))\n\tt.Cleanup(server.Close)\n\n\tdoer := NewDoer(server.Client(), \"\")\n\n\ttestCases := []struct {\n\t\tmethod string\n\t\tcall   func(u string) (*http.Response, error)\n\t}{\n\t\t{\n\t\t\tmethod: http.MethodGet,\n\t\t\tcall: func(u string) (*http.Response, error) {\n\t\t\t\treturn doer.Get(u, nil)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmethod: http.MethodHead,\n\t\t\tcall:   doer.Head,\n\t\t},\n\t\t{\n\t\t\tmethod: http.MethodPost,\n\t\t\tcall: func(u string) (*http.Response, error) {\n\t\t\t\treturn doer.Post(u, strings.NewReader(\"falalalala\"), \"text/plain\", nil)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.method, func(t *testing.T) {\n\t\t\t_, err := test.call(server.URL)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.method, method)\n\t\t\tassert.Contains(t, ua, ourUserAgent, \"User-Agent\")\n\t\t})\n\t}\n}\n\nfunc TestDo_CustomUserAgent(t *testing.T) {\n\tcustomUA := \"MyApp/1.2.3\"\n\tdoer := NewDoer(http.DefaultClient, customUA)\n\n\tua := doer.formatUserAgent()\n\tassert.Contains(t, ua, ourUserAgent)\n\tassert.Contains(t, ua, customUA)\n\n\tif strings.HasSuffix(ua, \" \") {\n\t\tt.Errorf(\"UA should not have trailing spaces; got '%s'\", ua)\n\t}\n\n\tassert.Len(t, strings.Split(ua, \" \"), 5)\n}\n\nfunc TestDo_failWithHTTP(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))\n\tt.Cleanup(server.Close)\n\n\tsender := NewDoer(server.Client(), \"test\")\n\n\t_, err := sender.Post(server.URL, strings.NewReader(\"data\"), \"text/plain\", nil)\n\trequire.ErrorContains(t, err, \"HTTPS is required: http://\")\n}\n\nfunc Test_checkError(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc   string\n\t\tresp   *http.Response\n\t\tassert func(t *testing.T, err error)\n\t}{\n\t\t{\n\t\t\tdesc: \"default\",\n\t\t\tresp: &http.Response{\n\t\t\t\tStatusCode: http.StatusNotFound,\n\t\t\t\tBody:       io.NopCloser(bytes.NewBufferString(`{\"type\":\"urn:ietf:params:acme:error:example\",\"detail\":\"message\",\"status\":404}`)),\n\t\t\t},\n\t\t\tassert: errorAs[*acme.ProblemDetails],\n\t\t},\n\t\t{\n\t\t\tdesc: \"badNonce\",\n\t\t\tresp: &http.Response{\n\t\t\t\tStatusCode: http.StatusBadRequest,\n\t\t\t\tBody:       io.NopCloser(bytes.NewBufferString(`{\"type\":\"urn:ietf:params:acme:error:badNonce\",\"detail\":\"message\",\"status\":400}`)),\n\t\t\t},\n\t\t\tassert: errorAs[*acme.NonceError],\n\t\t},\n\t\t{\n\t\t\tdesc: \"alreadyReplaced\",\n\t\t\tresp: &http.Response{\n\t\t\t\tStatusCode: http.StatusConflict,\n\t\t\t\tBody:       io.NopCloser(bytes.NewBufferString(`{\"type\":\"urn:ietf:params:acme:error:alreadyReplaced\",\"detail\":\"message\",\"status\":409}`)),\n\t\t\t},\n\t\t\tassert: errorAs[*acme.AlreadyReplacedError],\n\t\t},\n\t\t{\n\t\t\tdesc: \"rateLimited\",\n\t\t\tresp: &http.Response{\n\t\t\t\tStatusCode: http.StatusConflict,\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\"Retry-After\": []string{\"1\"},\n\t\t\t\t},\n\t\t\t\tBody: io.NopCloser(bytes.NewBufferString(`{\"type\":\"urn:ietf:params:acme:error:rateLimited\",\"detail\":\"message\",\"status\":429}`)),\n\t\t\t},\n\t\t\tassert: errorAs[*acme.RateLimitedError],\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\treq := httptest.NewRequestWithContext(t.Context(), http.MethodPost, \"https://example.com\", nil)\n\n\t\t\terr := checkError(req, test.resp)\n\t\t\trequire.Error(t, err)\n\n\t\t\tpb := &acme.ProblemDetails{}\n\t\t\tassert.ErrorAs(t, err, &pb)\n\n\t\t\ttest.assert(t, err)\n\t\t})\n\t}\n}\n\nfunc errorAs[T error](t *testing.T, err error) {\n\tt.Helper()\n\n\tvar zero T\n\tassert.ErrorAs(t, err, &zero)\n}\n"
  },
  {
    "path": "acme/api/internal/sender/useragent.go",
    "content": "// Code generated by 'internal/releaser'; DO NOT EDIT.\n\npackage sender\n\nconst (\n\t// ourUserAgent is the User-Agent of this underlying library package.\n\tourUserAgent = \"xenolf-acme/4.33.0\"\n\n\t// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.\n\t// values: detach|release\n\t// NOTE: Update this with each tagged release.\n\tourUserAgentComment = \"detach\"\n)\n"
  },
  {
    "path": "acme/api/order.go",
    "content": "package api\n\nimport (\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n)\n\n// OrderOptions used to create an order (optional).\ntype OrderOptions struct {\n\tNotBefore time.Time\n\tNotAfter  time.Time\n\n\t// A string uniquely identifying the profile\n\t// which will be used to affect issuance of the certificate requested by this Order.\n\t// - https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4\n\tProfile string\n\n\t// A string uniquely identifying a previously-issued certificate which this\n\t// order is intended to replace.\n\t// - https://www.rfc-editor.org/rfc/rfc9773.html#section-5\n\tReplacesCertID string\n}\n\ntype OrderService service\n\n// New Creates a new order.\nfunc (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) {\n\treturn o.NewWithOptions(domains, nil)\n}\n\n// NewWithOptions Creates a new order.\nfunc (o *OrderService) NewWithOptions(domains []string, opts *OrderOptions) (acme.ExtendedOrder, error) {\n\torderReq := acme.Order{Identifiers: createIdentifiers(domains)}\n\n\tif opts != nil {\n\t\tif !opts.NotAfter.IsZero() {\n\t\t\torderReq.NotAfter = opts.NotAfter.Format(time.RFC3339)\n\t\t}\n\n\t\tif !opts.NotBefore.IsZero() {\n\t\t\torderReq.NotBefore = opts.NotBefore.Format(time.RFC3339)\n\t\t}\n\n\t\tif o.core.GetDirectory().RenewalInfo != \"\" {\n\t\t\torderReq.Replaces = opts.ReplacesCertID\n\t\t}\n\n\t\tif opts.Profile != \"\" {\n\t\t\torderReq.Profile = opts.Profile\n\t\t}\n\t}\n\n\tvar order acme.Order\n\n\tresp, err := o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order)\n\tif err != nil {\n\t\tare := &acme.AlreadyReplacedError{}\n\t\tif !errors.As(err, &are) {\n\t\t\treturn acme.ExtendedOrder{}, err\n\t\t}\n\n\t\t// If the Server rejects the request because the identified certificate has already been marked as replaced,\n\t\t// it MUST return an HTTP 409 (Conflict) with a problem document of type \"alreadyReplaced\" (see Section 7.4).\n\t\t// https://www.rfc-editor.org/rfc/rfc9773.html#section-5\n\t\torderReq.Replaces = \"\"\n\n\t\tresp, err = o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order)\n\t\tif err != nil {\n\t\t\treturn acme.ExtendedOrder{}, err\n\t\t}\n\t}\n\n\t// The server MUST return an error if it cannot fulfill the request as specified,\n\t// and it MUST NOT issue a certificate with contents other than those requested.\n\t// If the server requires the request to be modified in a certain way,\n\t// it should indicate the required changes using an appropriate error type and description.\n\t// https://www.rfc-editor.org/rfc/rfc8555#section-7.4\n\t//\n\t// Some ACME servers don't return an error,\n\t// and/or change the order identifiers in the response,\n\t// so we need to ensure that the identifiers are the same as requested.\n\t// Deduplication by the server is allowed.\n\tif compareIdentifiers(orderReq.Identifiers, order.Identifiers) != 0 {\n\t\t// Sorts identifiers to avoid error message ambiguities about the order of the identifiers.\n\t\tslices.SortStableFunc(orderReq.Identifiers, compareIdentifier)\n\t\tslices.SortStableFunc(order.Identifiers, compareIdentifier)\n\n\t\treturn acme.ExtendedOrder{},\n\t\t\tfmt.Errorf(\"order identifiers have been modified by the ACME server (RFC8555 §7.4): %+v != %+v\",\n\t\t\t\torderReq.Identifiers, order.Identifiers)\n\t}\n\n\treturn acme.ExtendedOrder{\n\t\tOrder:    order,\n\t\tLocation: resp.Header.Get(\"Location\"),\n\t}, nil\n}\n\n// Get Gets an order.\nfunc (o *OrderService) Get(orderURL string) (acme.ExtendedOrder, error) {\n\tif orderURL == \"\" {\n\t\treturn acme.ExtendedOrder{}, errors.New(\"order[get]: empty URL\")\n\t}\n\n\tvar order acme.Order\n\n\t_, err := o.core.postAsGet(orderURL, &order)\n\tif err != nil {\n\t\treturn acme.ExtendedOrder{}, err\n\t}\n\n\treturn acme.ExtendedOrder{Order: order}, nil\n}\n\n// UpdateForCSR Updates an order for a CSR.\nfunc (o *OrderService) UpdateForCSR(orderURL string, csr []byte) (acme.ExtendedOrder, error) {\n\tcsrMsg := acme.CSRMessage{\n\t\tCsr: base64.RawURLEncoding.EncodeToString(csr),\n\t}\n\n\tvar order acme.Order\n\n\t_, err := o.core.post(orderURL, csrMsg, &order)\n\tif err != nil {\n\t\treturn acme.ExtendedOrder{}, err\n\t}\n\n\tif order.Status == acme.StatusInvalid {\n\t\treturn acme.ExtendedOrder{}, fmt.Errorf(\"invalid order: %w\", order.Err())\n\t}\n\n\treturn acme.ExtendedOrder{Order: order}, nil\n}\n"
  },
  {
    "path": "acme/api/order_test.go",
    "content": "package api\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-jose/go-jose/v4\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestOrderService_NewWithOptions(t *testing.T) {\n\t// small value keeps test fast\n\tprivateKey, errK := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, errK, \"Could not generate test key\")\n\n\tserver := tester.MockACMEServer().\n\t\tRoute(\"POST /newOrder\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\tbody, err := readSignedBody(req, privateKey)\n\t\t\t\tif err != nil {\n\t\t\t\t\thttp.Error(rw, err.Error(), http.StatusBadRequest)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\torder := acme.Order{}\n\n\t\t\t\terr = json.Unmarshal(body, &order)\n\t\t\t\tif err != nil {\n\t\t\t\t\thttp.Error(rw, err.Error(), http.StatusBadRequest)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tservermock.JSONEncode(acme.Order{\n\t\t\t\t\tStatus:         acme.StatusValid,\n\t\t\t\t\tExpires:        order.Expires,\n\t\t\t\t\tIdentifiers:    order.Identifiers,\n\t\t\t\t\tProfile:        order.Profile,\n\t\t\t\t\tNotBefore:      order.NotBefore,\n\t\t\t\t\tNotAfter:       order.NotAfter,\n\t\t\t\t\tError:          order.Error,\n\t\t\t\t\tAuthorizations: order.Authorizations,\n\t\t\t\t\tFinalize:       order.Finalize,\n\t\t\t\t\tCertificate:    order.Certificate,\n\t\t\t\t\tReplaces:       order.Replaces,\n\t\t\t\t}).ServeHTTP(rw, req)\n\t\t\t})).\n\t\tBuildHTTPS(t)\n\n\tcore, err := New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", privateKey)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\topts     *OrderOptions\n\t\texpected acme.ExtendedOrder\n\t}{\n\t\t{\n\t\t\tdesc: \"simple\",\n\t\t\texpected: acme.ExtendedOrder{\n\t\t\t\tOrder: acme.Order{\n\t\t\t\t\tStatus:      \"valid\",\n\t\t\t\t\tIdentifiers: []acme.Identifier{{Type: \"dns\", Value: \"example.com\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"with options\",\n\t\t\topts: &OrderOptions{\n\t\t\t\tNotBefore: time.Date(2023, 1, 1, 1, 0, 0, 0, time.UTC),\n\t\t\t\tNotAfter:  time.Date(2023, 1, 2, 1, 0, 0, 0, time.UTC),\n\t\t\t},\n\t\t\texpected: acme.ExtendedOrder{\n\t\t\t\tOrder: acme.Order{\n\t\t\t\t\tStatus:      \"valid\",\n\t\t\t\t\tIdentifiers: []acme.Identifier{{Type: \"dns\", Value: \"example.com\"}},\n\t\t\t\t\tNotBefore:   \"2023-01-01T01:00:00Z\",\n\t\t\t\t\tNotAfter:    \"2023-01-02T01:00:00Z\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\torder, err := core.Orders.NewWithOptions([]string{\"example.com\"}, test.opts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.expected, order)\n\t\t})\n\t}\n}\n\nfunc readSignedBody(r *http.Request, privateKey *rsa.PrivateKey) ([]byte, error) {\n\treqBody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsigAlgs := []jose.SignatureAlgorithm{jose.RS256}\n\n\tjws, err := jose.ParseSigned(string(reqBody), sigAlgs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbody, err := jws.Verify(&jose.JSONWebKey{\n\t\tKey:       privateKey.Public(),\n\t\tAlgorithm: \"RSA\",\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn body, nil\n}\n"
  },
  {
    "path": "acme/api/renewal.go",
    "content": "package api\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n)\n\n// ErrNoARI is returned when the server does not advertise a renewal info endpoint.\nvar ErrNoARI = errors.New(\"renewalInfo[get/post]: server does not advertise a renewal info endpoint\")\n\n// GetRenewalInfo GETs renewal information for a certificate from the renewalInfo endpoint.\n// This is used to determine if a certificate needs to be renewed.\n//\n// Note: this endpoint is part of a draft specification, not all ACME servers will implement it.\n// This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint.\n//\n// https://www.rfc-editor.org/rfc/rfc9773.html\nfunc (c *CertificateService) GetRenewalInfo(certID string) (*http.Response, error) {\n\tif c.core.GetDirectory().RenewalInfo == \"\" {\n\t\treturn nil, ErrNoARI\n\t}\n\n\tif certID == \"\" {\n\t\treturn nil, errors.New(\"renewalInfo[get]: 'certID' cannot be empty\")\n\t}\n\n\treturn c.core.HTTPClient.Get(c.core.GetDirectory().RenewalInfo + \"/\" + certID)\n}\n"
  },
  {
    "path": "acme/api/service.go",
    "content": "package api\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"time\"\n)\n\ntype service struct {\n\tcore *Core\n}\n\n// getLink get a rel into the Link header.\nfunc getLink(header http.Header, rel string) string {\n\tlinks := getLinks(header, rel)\n\tif len(links) < 1 {\n\t\treturn \"\"\n\t}\n\n\treturn links[0]\n}\n\nfunc getLinks(header http.Header, rel string) []string {\n\tlinkExpr := regexp.MustCompile(`<(.+?)>(?:;[^;]+)*?;\\s*rel=\"(.+?)\"`)\n\n\tvar links []string\n\n\tfor _, link := range header[\"Link\"] {\n\t\tfor _, m := range linkExpr.FindAllStringSubmatch(link, -1) {\n\t\t\tif len(m) != 3 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif m[2] == rel {\n\t\t\t\tlinks = append(links, m[1])\n\t\t\t}\n\t\t}\n\t}\n\n\treturn links\n}\n\n// getLocation get the value of the header Location.\nfunc getLocation(resp *http.Response) string {\n\tif resp == nil {\n\t\treturn \"\"\n\t}\n\n\treturn resp.Header.Get(\"Location\")\n}\n\n// getRetryAfter get the value of the header Retry-After.\nfunc getRetryAfter(resp *http.Response) string {\n\tif resp == nil {\n\t\treturn \"\"\n\t}\n\n\treturn resp.Header.Get(\"Retry-After\")\n}\n\n// ParseRetryAfter parses the Retry-After header value according to RFC 7231.\n// The header can be either delay-seconds (numeric) or HTTP-date (RFC 1123 format).\n// https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.3\n// Returns the duration until the retry time.\n// TODO(ldez): unexposed this function in v5.\nfunc ParseRetryAfter(value string) (time.Duration, error) {\n\tif value == \"\" {\n\t\treturn 0, nil\n\t}\n\n\tif seconds, err := strconv.ParseInt(value, 10, 64); err == nil {\n\t\treturn time.Duration(seconds) * time.Second, nil\n\t}\n\n\tif retryTime, err := time.Parse(time.RFC1123, value); err == nil {\n\t\tduration := time.Until(retryTime)\n\t\tif duration < 0 {\n\t\t\treturn 0, nil\n\t\t}\n\n\t\treturn duration, nil\n\t}\n\n\treturn 0, fmt.Errorf(\"invalid Retry-After value: %q\", value)\n}\n"
  },
  {
    "path": "acme/api/service_test.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_getLink(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\theader   http.Header\n\t\trelName  string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\theader: http.Header{\n\t\t\t\t\"Link\": []string{`<https://acme-staging-v02.api.letsencrypt.org/next>; rel=\"next\", <https://acme-staging-v02.api.letsencrypt.org/up?query>; rel=\"up\"`},\n\t\t\t},\n\t\t\trelName:  \"up\",\n\t\t\texpected: \"https://acme-staging-v02.api.letsencrypt.org/up?query\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"success several lines\",\n\t\t\theader: http.Header{\n\t\t\t\t\"Link\": []string{`<https://acme-staging-v02.api.letsencrypt.org/next>; rel=\"next\"`, `<https://acme-staging-v02.api.letsencrypt.org/up?query>; rel=\"up\"`},\n\t\t\t},\n\t\t\trelName:  \"up\",\n\t\t\texpected: \"https://acme-staging-v02.api.letsencrypt.org/up?query\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"no link\",\n\t\t\theader:   http.Header{},\n\t\t\trelName:  \"up\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"no header\",\n\t\t\trelName:  \"up\",\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlink := getLink(test.header, test.relName)\n\n\t\t\tassert.Equal(t, test.expected, link)\n\t\t})\n\t}\n}\n\nfunc TestParseRetryAfter(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tvalue    string\n\t\texpected time.Duration\n\t}{\n\t\t{\n\t\t\tdesc:     \"empty header value\",\n\t\t\tvalue:    \"\",\n\t\t\texpected: time.Duration(0),\n\t\t},\n\t\t{\n\t\t\tdesc:     \"delay-seconds\",\n\t\t\tvalue:    \"123\",\n\t\t\texpected: 123 * time.Second,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"HTTP-date\",\n\t\t\tvalue:    time.Now().Add(3 * time.Second).Format(time.RFC1123),\n\t\t\texpected: 3 * time.Second,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\trt, err := ParseRetryAfter(test.value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.InDelta(t, test.expected.Seconds(), rt.Seconds(), 1)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/commons.go",
    "content": "// Package acme contains all objects related the ACME endpoints.\n// https://www.rfc-editor.org/rfc/rfc8555.html\npackage acme\n\nimport (\n\t\"encoding/json\"\n\t\"time\"\n)\n\n// ACME status values of Account, Order, Authorization and Challenge objects.\n// See https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.6 for details.\nconst (\n\tStatusDeactivated = \"deactivated\"\n\tStatusExpired     = \"expired\"\n\tStatusInvalid     = \"invalid\"\n\tStatusPending     = \"pending\"\n\tStatusProcessing  = \"processing\"\n\tStatusReady       = \"ready\"\n\tStatusRevoked     = \"revoked\"\n\tStatusUnknown     = \"unknown\"\n\tStatusValid       = \"valid\"\n)\n\n// CRL reason codes as defined in RFC 5280.\n// https://datatracker.ietf.org/doc/html/rfc5280#section-5.3.1\nconst (\n\tCRLReasonUnspecified          uint = 0\n\tCRLReasonKeyCompromise        uint = 1\n\tCRLReasonCACompromise         uint = 2\n\tCRLReasonAffiliationChanged   uint = 3\n\tCRLReasonSuperseded           uint = 4\n\tCRLReasonCessationOfOperation uint = 5\n\tCRLReasonCertificateHold      uint = 6\n\tCRLReasonRemoveFromCRL        uint = 8\n\tCRLReasonPrivilegeWithdrawn   uint = 9\n\tCRLReasonAACompromise         uint = 10\n)\n\n// Directory the ACME directory object.\n// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1\n// - https://www.rfc-editor.org/rfc/rfc9773.html\ntype Directory struct {\n\tNewNonceURL   string `json:\"newNonce\"`\n\tNewAccountURL string `json:\"newAccount\"`\n\tNewOrderURL   string `json:\"newOrder\"`\n\tNewAuthzURL   string `json:\"newAuthz\"`\n\tRevokeCertURL string `json:\"revokeCert\"`\n\tKeyChangeURL  string `json:\"keyChange\"`\n\tMeta          Meta   `json:\"meta\"`\n\tRenewalInfo   string `json:\"renewalInfo\"`\n}\n\n// Meta the ACME meta object (related to Directory).\n// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1\ntype Meta struct {\n\t// termsOfService (optional, string):\n\t// A URL identifying the current terms of service.\n\tTermsOfService string `json:\"termsOfService\"`\n\n\t// website (optional, string):\n\t// An HTTP or HTTPS URL locating a website providing more information about the ACME server.\n\tWebsite string `json:\"website\"`\n\n\t// caaIdentities (optional, array of string):\n\t// The hostnames that the ACME server recognizes as referring to itself\n\t// for the purposes of CAA record validation as defined in [RFC6844].\n\t// Each string MUST represent the same sequence of ASCII code points\n\t// that the server will expect to see as the \"Issuer Domain Name\" in a CAA issue or issuewild property tag.\n\t// This allows clients to determine the correct issuer domain name to use when configuring CAA records.\n\tCaaIdentities []string `json:\"caaIdentities\"`\n\n\t// externalAccountRequired (optional, boolean):\n\t// If this field is present and set to \"true\",\n\t// then the CA requires that all new-account requests include an \"externalAccountBinding\" field\n\t// associating the new account with an external account.\n\tExternalAccountRequired bool `json:\"externalAccountRequired\"`\n\n\t// profiles (optional, object):\n\t// A map of profile names to human-readable descriptions of those profiles.\n\t// https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-3\n\tProfiles map[string]string `json:\"profiles\"`\n}\n\n// ExtendedAccount an extended Account.\ntype ExtendedAccount struct {\n\tAccount\n\n\t// Contains the value of the response header `Location`\n\tLocation string `json:\"-\"`\n}\n\n// Account the ACME account Object.\n// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.2\n// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3\ntype Account struct {\n\t// status (required, string):\n\t// The status of this account.\n\t// Possible values are: \"valid\", \"deactivated\", and \"revoked\".\n\t// The value \"deactivated\" should be used to indicate client-initiated deactivation\n\t// whereas \"revoked\" should be used to indicate server-initiated deactivation. (See Section 7.1.6)\n\tStatus string `json:\"status,omitempty\"`\n\n\t// contact (optional, array of string):\n\t// An array of URLs that the server can use to contact the client for issues related to this account.\n\t// For example, the server may wish to notify the client about server-initiated revocation or certificate expiration.\n\t// For information on supported URL schemes, see Section 7.3\n\tContact []string `json:\"contact,omitempty\"`\n\n\t// termsOfServiceAgreed (optional, boolean):\n\t// Including this field in a new-account request,\n\t// with a value of true, indicates the client's agreement with the terms of service.\n\t// This field is not updateable by the client.\n\tTermsOfServiceAgreed bool `json:\"termsOfServiceAgreed,omitempty\"`\n\n\t// orders (required, string):\n\t// A URL from which a list of orders submitted by this account can be fetched via a POST-as-GET request,\n\t// as described in Section 7.1.2.1.\n\tOrders string `json:\"orders,omitempty\"`\n\n\t// onlyReturnExisting (optional, boolean):\n\t// If this field is present with the value \"true\",\n\t// then the server MUST NOT create a new account if one does not already exist.\n\t// This allows a client to look up an account URL based on an account key (see Section 7.3.1).\n\tOnlyReturnExisting bool `json:\"onlyReturnExisting,omitempty\"`\n\n\t// externalAccountBinding (optional, object):\n\t// An optional field for binding the new account with an existing non-ACME account (see Section 7.3.4).\n\tExternalAccountBinding json.RawMessage `json:\"externalAccountBinding,omitempty\"`\n}\n\n// ExtendedOrder a extended Order.\ntype ExtendedOrder struct {\n\tOrder\n\n\t// The order URL, contains the value of the response header `Location`\n\tLocation string `json:\"-\"`\n}\n\n// Order the ACME order Object.\n// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.3\ntype Order struct {\n\t// status (required, string):\n\t// The status of this order.\n\t// Possible values are: \"pending\", \"ready\", \"processing\", \"valid\", and \"invalid\".\n\tStatus string `json:\"status,omitempty\"`\n\n\t// expires (optional, string):\n\t// The timestamp after which the server will consider this order invalid,\n\t// encoded in the format specified in RFC 3339 [RFC3339].\n\t// This field is REQUIRED for objects with \"pending\" or \"valid\" in the status field.\n\tExpires string `json:\"expires,omitempty\"`\n\n\t// identifiers (required, array of object):\n\t// An array of identifier objects that the order pertains to.\n\tIdentifiers []Identifier `json:\"identifiers\"`\n\n\t// profile (string, optional):\n\t// A string uniquely identifying the profile\n\t// which will be used to affect issuance of the certificate requested by this Order.\n\t// https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4\n\tProfile string `json:\"profile,omitempty\"`\n\n\t// notBefore (optional, string):\n\t// The requested value of the notBefore field in the certificate,\n\t// in the date format defined in [RFC3339].\n\tNotBefore string `json:\"notBefore,omitempty\"`\n\n\t// notAfter (optional, string):\n\t// The requested value of the notAfter field in the certificate,\n\t// in the date format defined in [RFC3339].\n\tNotAfter string `json:\"notAfter,omitempty\"`\n\n\t// error (optional, object):\n\t// The error that occurred while processing the order, if any.\n\t// This field is structured as a problem document [RFC7807].\n\tError *ProblemDetails `json:\"error,omitempty\"`\n\n\t// authorizations (required, array of string):\n\t// For pending orders,\n\t// the authorizations that the client needs to complete before the requested certificate can be issued (see Section 7.5),\n\t// including unexpired authorizations that the client has completed in the past for identifiers specified in the order.\n\t// The authorizations required are dictated by server policy\n\t// and there may not be a 1:1 relationship between the order identifiers and the authorizations required.\n\t// For final orders (in the \"valid\" or \"invalid\" state), the authorizations that were completed.\n\t// Each entry is a URL from which an authorization can be fetched with a POST-as-GET request.\n\tAuthorizations []string `json:\"authorizations,omitempty\"`\n\n\t// finalize (required, string):\n\t// A URL that a CSR must be POSTed to once all of the order's authorizations are satisfied to finalize the order.\n\t// The result of a successful finalization will be the population of the certificate URL for the order.\n\tFinalize string `json:\"finalize,omitempty\"`\n\n\t// certificate (optional, string):\n\t// A URL for the certificate that has been issued in response to this order\n\tCertificate string `json:\"certificate,omitempty\"`\n\n\t// replaces (optional, string):\n\t// replaces (string, optional): A string uniquely identifying a\n\t// previously-issued certificate which this order is intended to replace.\n\t// - https://www.rfc-editor.org/rfc/rfc9773.html#section-5\n\tReplaces string `json:\"replaces,omitempty\"`\n}\n\nfunc (r *Order) Err() error {\n\tif r.Error != nil {\n\t\treturn r.Error\n\t}\n\n\treturn nil\n}\n\n// Authorization the ACME authorization object.\n// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.4\ntype Authorization struct {\n\t// status (required, string):\n\t// The status of this authorization.\n\t// Possible values are: \"pending\", \"valid\", \"invalid\", \"deactivated\", \"expired\", and \"revoked\".\n\tStatus string `json:\"status\"`\n\n\t// expires (optional, string):\n\t// The timestamp after which the server will consider this authorization invalid,\n\t// encoded in the format specified in RFC 3339 [RFC3339].\n\t// This field is REQUIRED for objects with \"valid\" in the \"status\" field.\n\tExpires time.Time `json:\"expires,omitzero\"`\n\n\t// identifier (required, object):\n\t// The identifier that the account is authorized to represent\n\tIdentifier Identifier `json:\"identifier\"`\n\n\t// challenges (required, array of objects):\n\t// For pending authorizations, the challenges that the client can fulfill in order to prove possession of the identifier.\n\t// For valid authorizations, the challenge that was validated.\n\t// For invalid authorizations, the challenge that was attempted and failed.\n\t// Each array entry is an object with parameters required to validate the challenge.\n\t// A client should attempt to fulfill one of these challenges,\n\t// and a server should consider any one of the challenges sufficient to make the authorization valid.\n\tChallenges []Challenge `json:\"challenges,omitempty\"`\n\n\t// wildcard (optional, boolean):\n\t// For authorizations created as a result of a newOrder request containing a DNS identifier\n\t// with a value that contained a wildcard prefix this field MUST be present, and true.\n\tWildcard bool `json:\"wildcard,omitempty\"`\n}\n\n// ExtendedChallenge a extended Challenge.\ntype ExtendedChallenge struct {\n\tChallenge\n\n\t// Contains the value of the response header `Retry-After`\n\tRetryAfter string `json:\"-\"`\n\t// Contains the value of the response header `Link` rel=\"up\"\n\tAuthorizationURL string `json:\"-\"`\n}\n\n// Challenge the ACME challenge object.\n// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.5\n// - https://www.rfc-editor.org/rfc/rfc8555.html#section-8\ntype Challenge struct {\n\t// type (required, string):\n\t// The type of challenge encoded in the object.\n\tType string `json:\"type\"`\n\n\t// url (required, string):\n\t// The URL to which a response can be posted.\n\tURL string `json:\"url\"`\n\n\t// status (required, string):\n\t// The status of this challenge. Possible values are: \"pending\", \"processing\", \"valid\", and \"invalid\".\n\tStatus string `json:\"status\"`\n\n\t// validated (optional, string):\n\t// The time at which the server validated this challenge,\n\t// encoded in the format specified in RFC 3339 [RFC3339].\n\t// This field is REQUIRED if the \"status\" field is \"valid\".\n\tValidated time.Time `json:\"validated,omitzero\"`\n\n\t// error (optional, object):\n\t// Error that occurred while the server was validating the challenge, if any,\n\t// structured as a problem document [RFC7807].\n\t// Multiple errors can be indicated by using subproblems Section 6.7.1.\n\t// A challenge object with an error MUST have status equal to \"invalid\".\n\tError *ProblemDetails `json:\"error,omitempty\"`\n\n\t// token (required, string):\n\t// A random value that uniquely identifies the challenge.\n\t// This value MUST have at least 128 bits of entropy.\n\t// It MUST NOT contain any characters outside the base64url alphabet,\n\t// and MUST NOT include base64 padding characters (\"=\").\n\t// See [RFC4086] for additional information on randomness requirements.\n\t// https://www.rfc-editor.org/rfc/rfc8555.html#section-8.3\n\t// https://www.rfc-editor.org/rfc/rfc8555.html#section-8.4\n\tToken string `json:\"token\"`\n\n\t// https://www.rfc-editor.org/rfc/rfc8555.html#section-8.1\n\tKeyAuthorization string `json:\"keyAuthorization\"`\n}\n\nfunc (c *Challenge) Err() error {\n\tif c.Error != nil {\n\t\treturn c.Error\n\t}\n\n\treturn nil\n}\n\n// Identifier the ACME identifier object.\n// - https://www.rfc-editor.org/rfc/rfc8555.html#section-9.7.7\ntype Identifier struct {\n\tType  string `json:\"type\"`\n\tValue string `json:\"value\"`\n}\n\n// CSRMessage Certificate Signing Request.\n// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4\ntype CSRMessage struct {\n\t// csr (required, string):\n\t// A CSR encoding the parameters for the certificate being requested [RFC2986].\n\t// The CSR is sent in the base64url-encoded version of the DER format.\n\t// (Note: Because this field uses base64url, and does not include headers, it is different from PEM.).\n\tCsr string `json:\"csr\"`\n}\n\n// RevokeCertMessage a certificate revocation message.\n// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.6\n// - https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1\ntype RevokeCertMessage struct {\n\t// certificate (required, string):\n\t// The certificate to be revoked, in the base64url-encoded version of the DER format.\n\t// (Note: Because this field uses base64url, and does not include headers, it is different from PEM.)\n\tCertificate string `json:\"certificate\"`\n\n\t// reason (optional, int):\n\t// One of the revocation reasonCodes defined in Section 5.3.1 of [RFC5280] to be used when generating OCSP responses and CRLs.\n\t// If this field is not set the server SHOULD omit the reasonCode CRL entry extension when generating OCSP responses and CRLs.\n\t// The server MAY disallow a subset of reasonCodes from being used by the user.\n\t// If a request contains a disallowed reasonCode the server MUST reject it with the error type \"urn:ietf:params:acme:error:badRevocationReason\".\n\t// The problem document detail SHOULD indicate which reasonCodes are allowed.\n\tReason *uint `json:\"reason,omitempty\"`\n}\n\n// RawCertificate raw data of a certificate.\ntype RawCertificate struct {\n\tCert   []byte\n\tIssuer []byte\n}\n\n// Window is a window of time.\ntype Window struct {\n\tStart time.Time `json:\"start\"`\n\tEnd   time.Time `json:\"end\"`\n}\n\n// RenewalInfoResponse is the response to GET requests made the renewalInfo endpoint.\n// - (4.1. Getting Renewal Information) https://www.rfc-editor.org/rfc/rfc9773.html\ntype RenewalInfoResponse struct {\n\t// SuggestedWindow contains two fields, start and end,\n\t// whose values are timestamps which bound the window of time in which the CA recommends renewing the certificate.\n\tSuggestedWindow Window `json:\"suggestedWindow\"`\n\t//\tExplanationURL is an optional URL pointing to a page which may explain why the suggested renewal window is what it is.\n\t//\tFor example, it may be a page explaining the CA's dynamic load-balancing strategy,\n\t//\tor a page documenting which certificates are affected by a mass revocation event.\n\t//\tCallers SHOULD provide this URL to their operator, if present.\n\tExplanationURL string `json:\"explanationURL\"`\n}\n\n// RenewalInfoUpdateRequest is the JWS payload for POST requests made to the renewalInfo endpoint.\n// - (4.2. RenewalInfo Objects) https://www.rfc-editor.org/rfc/rfc9773.html#section-4.2\ntype RenewalInfoUpdateRequest struct {\n\t// CertID is a composite string in the format: base64url(AKI) || '.' || base64url(Serial), where AKI is the\n\t// certificate's authority key identifier and Serial is the certificate's serial number. For details, see:\n\t// https://www.rfc-editor.org/rfc/rfc9773.html#section-4.1\n\tCertID string `json:\"certID\"`\n\t// Replaced is required and indicates whether or not the client considers the certificate to have been replaced.\n\t// A certificate is considered replaced when its revocation would not disrupt any ongoing services,\n\t// for instance because it has been renewed and the new certificate is in use, or because it is no longer in use.\n\t// Clients SHOULD NOT send a request where this value is false.\n\tReplaced bool `json:\"replaced\"`\n}\n"
  },
  {
    "path": "acme/errors.go",
    "content": "package acme\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// Errors types.\nconst (\n\terrNS              = \"urn:ietf:params:acme:error:\"\n\tBadNonceErr        = errNS + \"badNonce\"\n\tAlreadyReplacedErr = errNS + \"alreadyReplaced\"\n\tRateLimitedErr     = errNS + \"rateLimited\"\n)\n\n// ProblemDetails the problem details object.\n// - https://www.rfc-editor.org/rfc/rfc7807.html#section-3.1\n// - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3.3\ntype ProblemDetails struct {\n\tType        string       `json:\"type,omitempty\"`\n\tDetail      string       `json:\"detail,omitempty\"`\n\tHTTPStatus  int          `json:\"status,omitempty\"`\n\tInstance    string       `json:\"instance,omitempty\"`\n\tSubProblems []SubProblem `json:\"subproblems,omitempty\"`\n\n\t// additional values to have a better error message (Not defined by the RFC)\n\tMethod string `json:\"method,omitempty\"`\n\tURL    string `json:\"url,omitempty\"`\n}\n\nfunc (p *ProblemDetails) Error() string {\n\tmsg := new(strings.Builder)\n\n\t_, _ = fmt.Fprintf(msg, \"acme: error: %d\", p.HTTPStatus)\n\n\tif p.Method != \"\" || p.URL != \"\" {\n\t\t_, _ = fmt.Fprintf(msg, \" :: %s :: %s\", p.Method, p.URL)\n\t}\n\n\t_, _ = fmt.Fprintf(msg, \" :: %s :: %s\", p.Type, p.Detail)\n\n\tfor _, sub := range p.SubProblems {\n\t\t_, _ = fmt.Fprintf(msg, \", problem: %q :: %s\", sub.Type, sub.Detail)\n\t}\n\n\tif p.Instance != \"\" {\n\t\tmsg.WriteString(\", url: \" + p.Instance)\n\t}\n\n\treturn msg.String()\n}\n\n// SubProblem a \"subproblems\".\n// - https://www.rfc-editor.org/rfc/rfc8555.html#section-6.7.1\ntype SubProblem struct {\n\tType       string     `json:\"type,omitempty\"`\n\tDetail     string     `json:\"detail,omitempty\"`\n\tIdentifier Identifier `json:\"identifier\"`\n}\n\n// NonceError represents the error which is returned\n// if the nonce sent by the client was not accepted by the server.\ntype NonceError struct {\n\t*ProblemDetails\n}\n\nfunc (e *NonceError) Unwrap() error {\n\treturn e.ProblemDetails\n}\n\n// AlreadyReplacedError represents the error which is returned\n// if the Server rejects the request because the identified certificate has already been marked as replaced.\n// - https://www.rfc-editor.org/rfc/rfc9773.html#section-5\ntype AlreadyReplacedError struct {\n\t*ProblemDetails\n}\n\nfunc (e *AlreadyReplacedError) Unwrap() error {\n\treturn e.ProblemDetails\n}\n\n// RateLimitedError represents the error which is returned\n// if the server rejects the request because the client has exceeded the rate limit.\n// - https://www.rfc-editor.org/rfc/rfc8555.html#section-6.6\ntype RateLimitedError struct {\n\t*ProblemDetails\n\n\tRetryAfter string\n}\n\nfunc (e *RateLimitedError) Unwrap() error {\n\treturn e.ProblemDetails\n}\n"
  },
  {
    "path": "buildx.Dockerfile",
    "content": "# syntax=docker/dockerfile:1.4\nFROM alpine:3\n\nARG TARGETPLATFORM\n\nRUN apk --no-cache --no-progress add git ca-certificates tzdata \\\n    && rm -rf /var/cache/apk/*\n\nCOPY $TARGETPLATFORM/lego /\n\nENTRYPOINT [\"/lego\"]\nEXPOSE 80\n"
  },
  {
    "path": "certcrypto/crypto.go",
    "content": "package certcrypto\n\nimport (\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"golang.org/x/crypto/ocsp\"\n)\n\n// Constants for all key types we support.\nconst (\n\tEC256   = KeyType(\"P256\")\n\tEC384   = KeyType(\"P384\")\n\tRSA2048 = KeyType(\"2048\")\n\tRSA3072 = KeyType(\"3072\")\n\tRSA4096 = KeyType(\"4096\")\n\tRSA8192 = KeyType(\"8192\")\n)\n\nconst (\n\t// OCSPGood means that the certificate is valid.\n\tOCSPGood = ocsp.Good\n\t// OCSPRevoked means that the certificate has been deliberately revoked.\n\tOCSPRevoked = ocsp.Revoked\n\t// OCSPUnknown means that the OCSP responder doesn't know about the certificate.\n\tOCSPUnknown = ocsp.Unknown\n\t// OCSPServerFailed means that the OCSP responder failed to process the request.\n\tOCSPServerFailed = ocsp.ServerFailed\n)\n\n// Constants for OCSP must staple.\nvar (\n\ttlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}\n\tocspMustStapleFeature  = []byte{0x30, 0x03, 0x02, 0x01, 0x05}\n)\n\n// KeyType represents the key algo as well as the key size or curve to use.\ntype KeyType string\n\ntype DERCertificateBytes []byte\n\n// ParsePEMBundle parses a certificate bundle from top to bottom and returns\n// a slice of x509 certificates. This function will error if no certificates are found.\nfunc ParsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {\n\tvar (\n\t\tcertificates []*x509.Certificate\n\t\tcertDERBlock *pem.Block\n\t)\n\n\tfor {\n\t\tcertDERBlock, bundle = pem.Decode(bundle)\n\t\tif certDERBlock == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif certDERBlock.Type == \"CERTIFICATE\" {\n\t\t\tcert, err := x509.ParseCertificate(certDERBlock.Bytes)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tcertificates = append(certificates, cert)\n\t\t}\n\t}\n\n\tif len(certificates) == 0 {\n\t\treturn nil, errors.New(\"no certificates were found while parsing the bundle\")\n\t}\n\n\treturn certificates, nil\n}\n\n// ParsePEMPrivateKey parses a private key from key, which is a PEM block.\n// Borrowed from Go standard library, to handle various private key and PEM block types.\n// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308\n// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238\nfunc ParsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) {\n\tkeyBlockDER, _ := pem.Decode(key)\n\tif keyBlockDER == nil {\n\t\treturn nil, errors.New(\"invalid PEM block\")\n\t}\n\n\tif keyBlockDER.Type != \"PRIVATE KEY\" && !strings.HasSuffix(keyBlockDER.Type, \" PRIVATE KEY\") {\n\t\treturn nil, fmt.Errorf(\"unknown PEM header %q\", keyBlockDER.Type)\n\t}\n\n\tif key, err := x509.ParsePKCS1PrivateKey(keyBlockDER.Bytes); err == nil {\n\t\treturn key, nil\n\t}\n\n\tif key, err := x509.ParsePKCS8PrivateKey(keyBlockDER.Bytes); err == nil {\n\t\tswitch key := key.(type) {\n\t\tcase *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey:\n\t\t\treturn key, nil\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"found unknown private key type in PKCS#8 wrapping: %T\", key)\n\t\t}\n\t}\n\n\tif key, err := x509.ParseECPrivateKey(keyBlockDER.Bytes); err == nil {\n\t\treturn key, nil\n\t}\n\n\treturn nil, errors.New(\"failed to parse private key\")\n}\n\nfunc GeneratePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {\n\tswitch keyType {\n\tcase EC256:\n\t\treturn ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tcase EC384:\n\t\treturn ecdsa.GenerateKey(elliptic.P384(), rand.Reader)\n\tcase RSA2048:\n\t\treturn rsa.GenerateKey(rand.Reader, 2048)\n\tcase RSA3072:\n\t\treturn rsa.GenerateKey(rand.Reader, 3072)\n\tcase RSA4096:\n\t\treturn rsa.GenerateKey(rand.Reader, 4096)\n\tcase RSA8192:\n\t\treturn rsa.GenerateKey(rand.Reader, 8192)\n\t}\n\n\treturn nil, fmt.Errorf(\"invalid KeyType: %s\", keyType)\n}\n\n// Deprecated: uses [CreateCSR] instead.\nfunc GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {\n\treturn CreateCSR(privateKey, CSROptions{\n\t\tDomain:     domain,\n\t\tSAN:        san,\n\t\tMustStaple: mustStaple,\n\t})\n}\n\ntype CSROptions struct {\n\tDomain         string\n\tSAN            []string\n\tMustStaple     bool\n\tEmailAddresses []string\n}\n\nfunc CreateCSR(privateKey crypto.PrivateKey, opts CSROptions) ([]byte, error) {\n\tvar (\n\t\tdnsNames    []string\n\t\tipAddresses []net.IP\n\t)\n\n\tfor _, altname := range opts.SAN {\n\t\tif ip := net.ParseIP(altname); ip != nil {\n\t\t\tipAddresses = append(ipAddresses, ip)\n\t\t} else {\n\t\t\tdnsNames = append(dnsNames, altname)\n\t\t}\n\t}\n\n\ttemplate := x509.CertificateRequest{\n\t\tSubject:        pkix.Name{CommonName: opts.Domain},\n\t\tDNSNames:       dnsNames,\n\t\tEmailAddresses: opts.EmailAddresses,\n\t\tIPAddresses:    ipAddresses,\n\t}\n\n\tif opts.MustStaple {\n\t\ttemplate.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{\n\t\t\tId:    tlsFeatureExtensionOID,\n\t\t\tValue: ocspMustStapleFeature,\n\t\t})\n\t}\n\n\treturn x509.CreateCertificateRequest(rand.Reader, &template, privateKey)\n}\n\nfunc PEMEncode(data any) []byte {\n\treturn pem.EncodeToMemory(PEMBlock(data))\n}\n\nfunc PEMBlock(data any) *pem.Block {\n\tvar pemBlock *pem.Block\n\n\tswitch key := data.(type) {\n\tcase *ecdsa.PrivateKey:\n\t\tkeyBytes, _ := x509.MarshalECPrivateKey(key)\n\t\tpemBlock = &pem.Block{Type: \"EC PRIVATE KEY\", Bytes: keyBytes}\n\tcase *rsa.PrivateKey:\n\t\tpemBlock = &pem.Block{Type: \"RSA PRIVATE KEY\", Bytes: x509.MarshalPKCS1PrivateKey(key)}\n\tcase *x509.CertificateRequest:\n\t\tpemBlock = &pem.Block{Type: \"CERTIFICATE REQUEST\", Bytes: key.Raw}\n\tcase DERCertificateBytes:\n\t\tpemBlock = &pem.Block{Type: \"CERTIFICATE\", Bytes: []byte(data.(DERCertificateBytes))}\n\t}\n\n\treturn pemBlock\n}\n\nfunc pemDecode(data []byte) (*pem.Block, error) {\n\tpemBlock, _ := pem.Decode(data)\n\tif pemBlock == nil {\n\t\treturn nil, errors.New(\"PEM decode did not yield a valid block. Is the certificate in the right format?\")\n\t}\n\n\treturn pemBlock, nil\n}\n\nfunc PemDecodeTox509CSR(data []byte) (*x509.CertificateRequest, error) {\n\tpemBlock, err := pemDecode(data)\n\tif pemBlock == nil {\n\t\treturn nil, err\n\t}\n\n\tif pemBlock.Type != \"CERTIFICATE REQUEST\" && pemBlock.Type != \"NEW CERTIFICATE REQUEST\" {\n\t\treturn nil, errors.New(\"PEM block is not a certificate request\")\n\t}\n\n\treturn x509.ParseCertificateRequest(pemBlock.Bytes)\n}\n\n// ParsePEMCertificate returns Certificate from a PEM encoded certificate.\n// The certificate has to be PEM encoded. Any other encodings like DER will fail.\nfunc ParsePEMCertificate(cert []byte) (*x509.Certificate, error) {\n\tpemBlock, err := pemDecode(cert)\n\tif pemBlock == nil {\n\t\treturn nil, err\n\t}\n\n\t// from a DER encoded certificate\n\treturn x509.ParseCertificate(pemBlock.Bytes)\n}\n\nfunc GetCertificateMainDomain(cert *x509.Certificate) (string, error) {\n\treturn getMainDomain(cert.Subject, cert.DNSNames, cert.IPAddresses)\n}\n\nfunc GetCSRMainDomain(cert *x509.CertificateRequest) (string, error) {\n\treturn getMainDomain(cert.Subject, cert.DNSNames, cert.IPAddresses)\n}\n\nfunc getMainDomain(subject pkix.Name, dnsNames []string, ips []net.IP) (string, error) {\n\tif subject.CommonName == \"\" && len(dnsNames) == 0 && len(ips) == 0 {\n\t\treturn \"\", errors.New(\"missing domain\")\n\t}\n\n\tif subject.CommonName != \"\" {\n\t\treturn subject.CommonName, nil\n\t}\n\n\tif len(dnsNames) > 0 {\n\t\treturn dnsNames[0], nil\n\t}\n\n\treturn ips[0].String(), nil\n}\n\nfunc ExtractDomains(cert *x509.Certificate) []string {\n\tvar domains []string\n\tif cert.Subject.CommonName != \"\" {\n\t\tdomains = append(domains, cert.Subject.CommonName)\n\t}\n\n\t// Check for SAN certificate\n\tfor _, sanDomain := range cert.DNSNames {\n\t\tif sanDomain == cert.Subject.CommonName {\n\t\t\tcontinue\n\t\t}\n\n\t\tdomains = append(domains, sanDomain)\n\t}\n\n\tcommonNameIP := net.ParseIP(cert.Subject.CommonName)\n\tfor _, sanIP := range cert.IPAddresses {\n\t\tif !commonNameIP.Equal(sanIP) {\n\t\t\tdomains = append(domains, sanIP.String())\n\t\t}\n\t}\n\n\treturn domains\n}\n\nfunc ExtractDomainsCSR(csr *x509.CertificateRequest) []string {\n\tvar domains []string\n\tif csr.Subject.CommonName != \"\" {\n\t\tdomains = append(domains, csr.Subject.CommonName)\n\t}\n\n\t// loop over the SubjectAltName DNS names\n\tfor _, sanName := range csr.DNSNames {\n\t\tif slices.Contains(domains, sanName) {\n\t\t\t// Duplicate; skip this name\n\t\t\tcontinue\n\t\t}\n\n\t\t// Name is unique\n\t\tdomains = append(domains, sanName)\n\t}\n\n\tcnip := net.ParseIP(csr.Subject.CommonName)\n\tfor _, sanIP := range csr.IPAddresses {\n\t\tif !cnip.Equal(sanIP) {\n\t\t\tdomains = append(domains, sanIP.String())\n\t\t}\n\t}\n\n\treturn domains\n}\n\nfunc GeneratePemCert(privateKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) {\n\tderBytes, err := generateDerCert(privateKey, time.Time{}, domain, extensions)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: derBytes}), nil\n}\n\nfunc generateDerCert(privateKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]byte, error) {\n\tserialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)\n\n\tserialNumber, err := rand.Int(rand.Reader, serialNumberLimit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif expiration.IsZero() {\n\t\texpiration = time.Now().AddDate(1, 0, 0)\n\t}\n\n\ttemplate := x509.Certificate{\n\t\tSerialNumber: serialNumber,\n\t\tSubject: pkix.Name{\n\t\t\tCommonName: \"ACME Challenge TEMP\",\n\t\t},\n\t\tNotBefore: time.Now(),\n\t\tNotAfter:  expiration,\n\n\t\tKeyUsage:              x509.KeyUsageKeyEncipherment,\n\t\tBasicConstraintsValid: true,\n\t\tExtraExtensions:       extensions,\n\t}\n\n\t// handling SAN filling as type suspected\n\tif ip := net.ParseIP(domain); ip != nil {\n\t\ttemplate.IPAddresses = []net.IP{ip}\n\t} else {\n\t\ttemplate.DNSNames = []string{domain}\n\t}\n\n\treturn x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)\n}\n"
  },
  {
    "path": "certcrypto/crypto_test.go",
    "content": "package certcrypto\n\nimport (\n\t\"bytes\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"encoding/pem\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestDomain1 = \"lego.example\"\n\ttestDomain2 = \"a.lego.example\"\n\ttestDomain3 = \"b.lego.example\"\n\ttestDomain4 = \"c.lego.example\"\n)\n\nfunc TestGeneratePrivateKey(t *testing.T) {\n\tkey, err := GeneratePrivateKey(RSA2048)\n\trequire.NoError(t, err, \"Error generating private key\")\n\n\tassert.NotNil(t, key)\n}\n\nfunc TestGenerateCSR(t *testing.T) {\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err, \"Error generating private key\")\n\n\ttype expected struct {\n\t\tlen   int\n\t\terror bool\n\t}\n\n\ttestCases := []struct {\n\t\tdesc       string\n\t\tprivateKey crypto.PrivateKey\n\t\topts       CSROptions\n\t\texpected   expected\n\t}{\n\t\t{\n\t\t\tdesc:       \"without SAN (nil)\",\n\t\t\tprivateKey: privateKey,\n\t\t\topts: CSROptions{\n\t\t\t\tDomain:     testDomain1,\n\t\t\t\tMustStaple: true,\n\t\t\t},\n\t\t\texpected: expected{len: 382},\n\t\t},\n\t\t{\n\t\t\tdesc:       \"without SAN (empty)\",\n\t\t\tprivateKey: privateKey,\n\t\t\topts: CSROptions{\n\t\t\t\tDomain:     testDomain1,\n\t\t\t\tSAN:        []string{},\n\t\t\t\tMustStaple: true,\n\t\t\t},\n\t\t\texpected: expected{len: 382},\n\t\t},\n\t\t{\n\t\t\tdesc:       \"with SAN\",\n\t\t\tprivateKey: privateKey,\n\t\t\topts: CSROptions{\n\t\t\t\tDomain:     testDomain1,\n\t\t\t\tSAN:        []string{testDomain2, testDomain3, testDomain4},\n\t\t\t\tMustStaple: true,\n\t\t\t},\n\t\t\texpected: expected{len: 442},\n\t\t},\n\t\t{\n\t\t\tdesc:       \"no domain\",\n\t\t\tprivateKey: privateKey,\n\t\t\topts: CSROptions{\n\t\t\t\tDomain:     \"\",\n\t\t\t\tMustStaple: true,\n\t\t\t},\n\t\t\texpected: expected{len: 359},\n\t\t},\n\t\t{\n\t\t\tdesc:       \"no domain with SAN\",\n\t\t\tprivateKey: privateKey,\n\t\t\topts: CSROptions{\n\t\t\t\tDomain:     \"\",\n\t\t\t\tSAN:        []string{testDomain2, testDomain3, testDomain4},\n\t\t\t\tMustStaple: true,\n\t\t\t},\n\t\t\texpected: expected{len: 419},\n\t\t},\n\t\t{\n\t\t\tdesc:       \"private key nil\",\n\t\t\tprivateKey: nil,\n\t\t\topts: CSROptions{\n\t\t\t\tDomain:     testDomain1,\n\t\t\t\tMustStaple: true,\n\t\t\t},\n\t\t\texpected: expected{error: true},\n\t\t},\n\t\t{\n\t\t\tdesc:       \"with email addresses\",\n\t\t\tprivateKey: privateKey,\n\t\t\topts: CSROptions{\n\t\t\t\tDomain:         \"example.com\",\n\t\t\t\tSAN:            []string{\"example.org\"},\n\t\t\t\tEmailAddresses: []string{\"foo@example.com\", \"bar@example.com\"},\n\t\t\t},\n\t\t\texpected: expected{len: 421},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tcsr, err := CreateCSR(test.privateKey, test.opts)\n\n\t\t\tif test.expected.error {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, \"Error generating CSR\")\n\n\t\t\t\tassert.NotEmpty(t, csr)\n\t\t\t\tassert.Len(t, csr, test.expected.len)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPEMEncode(t *testing.T) {\n\tkey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err, \"Error generating private key\")\n\n\tdata := PEMEncode(key)\n\trequire.NotNil(t, data)\n\n\tp, rest := pem.Decode(data)\n\n\tassert.Equal(t, \"RSA PRIVATE KEY\", p.Type)\n\tassert.Empty(t, rest)\n\tassert.Empty(t, p.Headers)\n}\n\nfunc TestParsePEMCertificate(t *testing.T) {\n\tprivateKey, err := GeneratePrivateKey(RSA2048)\n\trequire.NoError(t, err, \"Error generating private key\")\n\n\texpiration := time.Now().Add(365).Round(time.Second)\n\tcertBytes, err := generateDerCert(privateKey.(*rsa.PrivateKey), expiration, \"test.com\", nil)\n\trequire.NoError(t, err, \"Error generating cert\")\n\n\tbuf := bytes.NewBufferString(\"TestingRSAIsSoMuchFun\")\n\n\t// Some random string should return an error.\n\tcert, err := ParsePEMCertificate(buf.Bytes())\n\trequire.Errorf(t, err, \"returned %v\", cert)\n\n\t// A DER encoded certificate should return an error.\n\t_, err = ParsePEMCertificate(certBytes)\n\trequire.Error(t, err, \"Expected to return an error for DER certificates\")\n\n\t// A PEM encoded certificate should work ok.\n\tpemCert := PEMEncode(DERCertificateBytes(certBytes))\n\tcert, err = ParsePEMCertificate(pemCert)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, expiration.UTC(), cert.NotAfter)\n}\n\nfunc TestParsePEMPrivateKey(t *testing.T) {\n\tprivateKey, err := GeneratePrivateKey(RSA2048)\n\trequire.NoError(t, err, \"Error generating private key\")\n\n\tpemPrivateKey := PEMEncode(privateKey)\n\n\t// Decoding a key should work and create an identical RSA key to the original,\n\t// ignoring precomputed values.\n\tdecoded, err := ParsePEMPrivateKey(pemPrivateKey)\n\trequire.NoError(t, err)\n\n\tdecodedRsaPrivateKey := decoded.(*rsa.PrivateKey)\n\trequire.True(t, decodedRsaPrivateKey.Equal(privateKey))\n\n\t// Decoding a PEM block that doesn't contain a private key should error\n\t_, err = ParsePEMPrivateKey(pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\"}))\n\trequire.Errorf(t, err, \"Expected to return an error for non-private key input\")\n\n\t// Decoding a PEM block that doesn't actually contain a key should error\n\t_, err = ParsePEMPrivateKey(pem.EncodeToMemory(&pem.Block{Type: \"PRIVATE KEY\"}))\n\trequire.Errorf(t, err, \"Expected to return an error for empty input\")\n\n\t// Decoding non-PEM input should return an error\n\t_, err = ParsePEMPrivateKey([]byte(\"This is not PEM\"))\n\trequire.Errorf(t, err, \"Expected to return an error for non-PEM input\")\n}\n"
  },
  {
    "path": "certificate/authorization.go",
    "content": "package certificate\n\nimport (\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/log\"\n)\n\nfunc (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authorization, error) {\n\tresc, errc := make(chan acme.Authorization), make(chan domainError)\n\n\tdelay := time.Second / time.Duration(c.overallRequestLimit)\n\n\tfor _, authzURL := range order.Authorizations {\n\t\ttime.Sleep(delay)\n\n\t\tgo func(authzURL string) {\n\t\t\tauthz, err := c.core.Authorizations.Get(authzURL)\n\t\t\tif err != nil {\n\t\t\t\terrc <- domainError{Domain: authz.Identifier.Value, Error: err}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tresc <- authz\n\t\t}(authzURL)\n\t}\n\n\tvar responses []acme.Authorization\n\n\tfailures := newObtainError()\n\n\tfor range len(order.Authorizations) {\n\t\tselect {\n\t\tcase res := <-resc:\n\t\t\tresponses = append(responses, res)\n\t\tcase err := <-errc:\n\t\t\tfailures.Add(err.Domain, err.Error)\n\t\t}\n\t}\n\n\tfor i, auth := range order.Authorizations {\n\t\tlog.Infof(\"[%s] AuthURL: %s\", order.Identifiers[i].Value, auth)\n\t}\n\n\tclose(resc)\n\tclose(errc)\n\n\treturn responses, failures.Join()\n}\n\nfunc (c *Certifier) deactivateAuthorizations(order acme.ExtendedOrder, force bool) {\n\tfor _, authzURL := range order.Authorizations {\n\t\tauth, err := c.core.Authorizations.Get(authzURL)\n\t\tif err != nil {\n\t\t\tlog.Infof(\"Unable to get the authorization for %s: %v\", authzURL, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tif auth.Status == acme.StatusValid && !force {\n\t\t\tlog.Infof(\"Skipping deactivating of valid auth: %s\", authzURL)\n\t\t\tcontinue\n\t\t}\n\n\t\tlog.Infof(\"Deactivating auth: %s\", authzURL)\n\n\t\tif c.core.Authorizations.Deactivate(authzURL) != nil {\n\t\t\tlog.Infof(\"Unable to deactivate the authorization: %s\", authzURL)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "certificate/certificates.go",
    "content": "package certificate\n\nimport (\n\t\"bytes\"\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n\t\"golang.org/x/crypto/ocsp\"\n\t\"golang.org/x/net/idna\"\n)\n\nconst (\n\t// DefaultOverallRequestLimit is the overall number of request per second\n\t// limited on the \"new-reg\", \"new-authz\" and \"new-cert\" endpoints.\n\t// From the documentation the limitation is 20 requests per second,\n\t// but using 20 as value doesn't work but 18 do.\n\t// https://letsencrypt.org/docs/rate-limits/\n\t// ZeroSSL has a limit of 7.\n\t// https://help.zerossl.com/hc/en-us/articles/17864245480093-Advantages-over-Using-Let-s-Encrypt#h_01HT4Z1JCJFJQFJ1M3P7S085Q9\n\tDefaultOverallRequestLimit = 18\n)\n\n// maxBodySize is the maximum size of body that we will read.\nconst maxBodySize = 1024 * 1024\n\n// Resource represents a CA issued certificate.\n// PrivateKey, Certificate and IssuerCertificate are all\n// already PEM encoded and can be directly written to disk.\n// Certificate may be a certificate bundle,\n// depending on the options supplied to create it.\ntype Resource struct {\n\tDomain            string `json:\"domain\"`\n\tCertURL           string `json:\"certUrl\"`\n\tCertStableURL     string `json:\"certStableUrl\"`\n\tPrivateKey        []byte `json:\"-\"`\n\tCertificate       []byte `json:\"-\"`\n\tIssuerCertificate []byte `json:\"-\"`\n\tCSR               []byte `json:\"-\"`\n}\n\n// ObtainRequest The request to obtain certificate.\n//\n// The first domain in domains is used for the CommonName field of the certificate,\n// all other domains are added using the Subject Alternate Names extension.\n//\n// A new private key is generated for every invocation of the function Obtain.\n// If you do not want that you can supply your own private key in the privateKey parameter.\n// If this parameter is non-nil it will be used instead of generating a new one.\n//\n// If `Bundle` is true, the `[]byte` contains both the issuer certificate and your issued certificate as a bundle.\n//\n// If `AlwaysDeactivateAuthorizations` is true, the authorizations are also relinquished if the obtain request was successful.\n// See https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2.\ntype ObtainRequest struct {\n\tDomains        []string\n\tPrivateKey     crypto.PrivateKey\n\tMustStaple     bool\n\tEmailAddresses []string\n\n\tNotBefore      time.Time\n\tNotAfter       time.Time\n\tBundle         bool\n\tPreferredChain string\n\n\t// A string uniquely identifying the profile\n\t// which will be used to affect issuance of the certificate requested by this Order.\n\t// - https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4\n\tProfile string\n\n\tAlwaysDeactivateAuthorizations bool\n\n\t// A string uniquely identifying a previously-issued certificate which this\n\t// order is intended to replace.\n\t// - https://www.rfc-editor.org/rfc/rfc9773.html#section-5\n\tReplacesCertID string\n}\n\n// ObtainForCSRRequest The request to obtain a certificate matching the CSR passed into it.\n//\n// If `Bundle` is true, the `[]byte` contains both the issuer certificate and your issued certificate as a bundle.\n//\n// If `AlwaysDeactivateAuthorizations` is true, the authorizations are also relinquished if the obtain request was successful.\n// See https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2.\ntype ObtainForCSRRequest struct {\n\tCSR *x509.CertificateRequest\n\n\tPrivateKey crypto.PrivateKey\n\n\tNotBefore      time.Time\n\tNotAfter       time.Time\n\tBundle         bool\n\tPreferredChain string\n\n\t// A string uniquely identifying the profile\n\t// which will be used to affect issuance of the certificate requested by this Order.\n\t// - https://www.ietf.org/id/draft-ietf-acme-profiles-00.html#section-4\n\tProfile string\n\n\tAlwaysDeactivateAuthorizations bool\n\n\t// A string uniquely identifying a previously-issued certificate which this\n\t// order is intended to replace.\n\t// - https://www.rfc-editor.org/rfc/rfc9773.html#section-5\n\tReplacesCertID string\n}\n\ntype resolver interface {\n\tSolve(authorizations []acme.Authorization) error\n}\n\ntype CertifierOptions struct {\n\tKeyType             certcrypto.KeyType\n\tTimeout             time.Duration\n\tOverallRequestLimit int\n\tDisableCommonName   bool\n}\n\n// Certifier A service to obtain/renew/revoke certificates.\ntype Certifier struct {\n\tcore                *api.Core\n\tresolver            resolver\n\toptions             CertifierOptions\n\toverallRequestLimit int\n}\n\n// NewCertifier creates a Certifier.\nfunc NewCertifier(core *api.Core, resolver resolver, options CertifierOptions) *Certifier {\n\tc := &Certifier{\n\t\tcore:     core,\n\t\tresolver: resolver,\n\t\toptions:  options,\n\t}\n\n\tc.overallRequestLimit = options.OverallRequestLimit\n\tif c.overallRequestLimit <= 0 {\n\t\tc.overallRequestLimit = DefaultOverallRequestLimit\n\t}\n\n\treturn c\n}\n\n// Obtain tries to obtain a single certificate using all domains passed into it.\n//\n// This function will never return a partial certificate.\n// If one domain in the list fails, the whole certificate will fail.\nfunc (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {\n\tif len(request.Domains) == 0 {\n\t\treturn nil, errors.New(\"no domains to obtain a certificate for\")\n\t}\n\n\tdomains := sanitizeDomain(request.Domains)\n\n\tif request.Bundle {\n\t\tlog.Infof(\"[%s] acme: Obtaining bundled SAN certificate\", strings.Join(domains, \", \"))\n\t} else {\n\t\tlog.Infof(\"[%s] acme: Obtaining SAN certificate\", strings.Join(domains, \", \"))\n\t}\n\n\torderOpts := &api.OrderOptions{\n\t\tNotBefore:      request.NotBefore,\n\t\tNotAfter:       request.NotAfter,\n\t\tProfile:        request.Profile,\n\t\tReplacesCertID: request.ReplacesCertID,\n\t}\n\n\torder, err := c.core.Orders.NewWithOptions(domains, orderOpts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tauthz, err := c.getAuthorizations(order)\n\tif err != nil {\n\t\t// If any challenge fails, return. Do not generate partial SAN certificates.\n\t\tc.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations)\n\t\treturn nil, err\n\t}\n\n\terr = c.resolver.Solve(authz)\n\tif err != nil {\n\t\t// If any challenge fails, return. Do not generate partial SAN certificates.\n\t\tc.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations)\n\t\treturn nil, err\n\t}\n\n\tlog.Infof(\"[%s] acme: Validations succeeded; requesting certificates\", strings.Join(domains, \", \"))\n\n\tfailures := newObtainError()\n\n\tcert, err := c.getForOrder(domains, order, request)\n\tif err != nil {\n\t\tfor _, auth := range authz {\n\t\t\tfailures.Add(challenge.GetTargetedDomain(auth), err)\n\t\t}\n\t}\n\n\tif request.AlwaysDeactivateAuthorizations {\n\t\tc.deactivateAuthorizations(order, true)\n\t}\n\n\treturn cert, failures.Join()\n}\n\n// ObtainForCSR tries to obtain a certificate matching the CSR passed into it.\n//\n// The domains are inferred from the CommonName and SubjectAltNames, if any.\n// The private key for this CSR is not required.\n//\n// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.\n//\n// This function will never return a partial certificate.\n// If one domain in the list fails, the whole certificate will fail.\nfunc (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error) {\n\tif request.CSR == nil {\n\t\treturn nil, errors.New(\"cannot obtain resource for CSR: CSR is missing\")\n\t}\n\n\t// figure out what domains it concerns\n\t// start with the common name\n\tdomains := certcrypto.ExtractDomainsCSR(request.CSR)\n\n\tif request.Bundle {\n\t\tlog.Infof(\"[%s] acme: Obtaining bundled SAN certificate given a CSR\", strings.Join(domains, \", \"))\n\t} else {\n\t\tlog.Infof(\"[%s] acme: Obtaining SAN certificate given a CSR\", strings.Join(domains, \", \"))\n\t}\n\n\torderOpts := &api.OrderOptions{\n\t\tNotBefore:      request.NotBefore,\n\t\tNotAfter:       request.NotAfter,\n\t\tProfile:        request.Profile,\n\t\tReplacesCertID: request.ReplacesCertID,\n\t}\n\n\torder, err := c.core.Orders.NewWithOptions(domains, orderOpts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tauthz, err := c.getAuthorizations(order)\n\tif err != nil {\n\t\t// If any challenge fails, return. Do not generate partial SAN certificates.\n\t\tc.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations)\n\t\treturn nil, err\n\t}\n\n\terr = c.resolver.Solve(authz)\n\tif err != nil {\n\t\t// If any challenge fails, return. Do not generate partial SAN certificates.\n\t\tc.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations)\n\t\treturn nil, err\n\t}\n\n\tlog.Infof(\"[%s] acme: Validations succeeded; requesting certificates\", strings.Join(domains, \", \"))\n\n\tfailures := newObtainError()\n\n\tvar privateKey []byte\n\tif request.PrivateKey != nil {\n\t\tprivateKey = certcrypto.PEMEncode(request.PrivateKey)\n\t}\n\n\tcert, err := c.getForCSR(domains, order, request.Bundle, request.CSR.Raw, privateKey, request.PreferredChain)\n\tif err != nil {\n\t\tfor _, auth := range authz {\n\t\t\tfailures.Add(challenge.GetTargetedDomain(auth), err)\n\t\t}\n\t}\n\n\tif request.AlwaysDeactivateAuthorizations {\n\t\tc.deactivateAuthorizations(order, true)\n\t}\n\n\tif cert != nil {\n\t\t// Add the CSR to the certificate so that it can be used for renewals.\n\t\tcert.CSR = certcrypto.PEMEncode(request.CSR)\n\t}\n\n\treturn cert, failures.Join()\n}\n\nfunc (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, request ObtainRequest) (*Resource, error) {\n\tprivateKey := request.PrivateKey\n\n\tif privateKey == nil {\n\t\tvar err error\n\n\t\tprivateKey, err = certcrypto.GeneratePrivateKey(c.options.KeyType)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tcommonName := \"\"\n\tif len(domains[0]) <= 64 && !c.options.DisableCommonName {\n\t\tcommonName = domains[0]\n\t}\n\n\t// RFC8555 Section 7.4 \"Applying for Certificate Issuance\"\n\t// https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4\n\t// says:\n\t//   Clients SHOULD NOT make any assumptions about the sort order of\n\t//   \"identifiers\" or \"authorizations\" elements in the returned order\n\t//   object.\n\n\tvar san []string\n\tif commonName != \"\" {\n\t\tsan = append(san, commonName)\n\t}\n\n\tfor _, auth := range order.Identifiers {\n\t\tif auth.Value != commonName {\n\t\t\tsan = append(san, auth.Value)\n\t\t}\n\t}\n\n\tcsrOptions := certcrypto.CSROptions{\n\t\tDomain:         commonName,\n\t\tSAN:            san,\n\t\tMustStaple:     request.MustStaple,\n\t\tEmailAddresses: request.EmailAddresses,\n\t}\n\n\tcsr, err := certcrypto.CreateCSR(privateKey, csrOptions)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn c.getForCSR(domains, order, request.Bundle, csr, certcrypto.PEMEncode(privateKey), request.PreferredChain)\n}\n\nfunc (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle bool, csr, privateKeyPem []byte, preferredChain string) (*Resource, error) {\n\trespOrder, err := c.core.Orders.UpdateForCSR(order.Finalize, csr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcertRes := &Resource{\n\t\tDomain:     domains[0],\n\t\tCertURL:    respOrder.Certificate,\n\t\tPrivateKey: privateKeyPem,\n\t}\n\n\tif respOrder.Status == acme.StatusValid {\n\t\t// if the certificate is available right away, shortcut!\n\t\tok, errR := c.checkResponse(respOrder, certRes, bundle, preferredChain)\n\t\tif errR != nil {\n\t\t\treturn nil, errR\n\t\t}\n\n\t\tif ok {\n\t\t\treturn certRes, nil\n\t\t}\n\t}\n\n\ttimeout := c.options.Timeout\n\tif c.options.Timeout <= 0 {\n\t\ttimeout = 30 * time.Second\n\t}\n\n\terr = wait.For(\"certificate\", timeout, timeout/60, func() (bool, error) {\n\t\tord, errW := c.core.Orders.Get(order.Location)\n\t\tif errW != nil {\n\t\t\treturn false, errW\n\t\t}\n\n\t\tdone, errW := c.checkResponse(ord, certRes, bundle, preferredChain)\n\t\tif errW != nil {\n\t\t\treturn false, errW\n\t\t}\n\n\t\treturn done, nil\n\t})\n\n\treturn certRes, err\n}\n\n// checkResponse checks to see if the certificate is ready and a link is contained in the response.\n//\n// If so, loads it into certRes and returns true.\n// If the cert is not yet ready, it returns false.\n//\n// The certRes input should already have the Domain (common name) field populated.\n//\n// If bundle is true, the certificate will be bundled with the issuer's cert.\nfunc (c *Certifier) checkResponse(order acme.ExtendedOrder, certRes *Resource, bundle bool, preferredChain string) (bool, error) {\n\tvalid, err := checkOrderStatus(order)\n\tif err != nil || !valid {\n\t\treturn valid, err\n\t}\n\n\tcerts, err := c.core.Certificates.GetAll(order.Certificate, bundle)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// Set the default certificate\n\tcertRes.IssuerCertificate = certs[order.Certificate].Issuer\n\tcertRes.Certificate = certs[order.Certificate].Cert\n\tcertRes.CertURL = order.Certificate\n\tcertRes.CertStableURL = order.Certificate\n\n\tif preferredChain == \"\" {\n\t\tlog.Infof(\"[%s] Server responded with a certificate.\", certRes.Domain)\n\n\t\treturn true, nil\n\t}\n\n\tfor link, cert := range certs {\n\t\tok, err := hasPreferredChain(cert.Issuer, preferredChain)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tif ok {\n\t\t\tlog.Infof(\"[%s] Server responded with a certificate for the preferred certificate chains %q.\", certRes.Domain, preferredChain)\n\n\t\t\tcertRes.IssuerCertificate = cert.Issuer\n\t\t\tcertRes.Certificate = cert.Cert\n\t\t\tcertRes.CertURL = link\n\t\t\tcertRes.CertStableURL = link\n\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\tlog.Infof(\"lego has been configured to prefer certificate chains with issuer %q, but no chain from the CA matched this issuer. Using the default certificate chain instead.\", preferredChain)\n\n\treturn true, nil\n}\n\n// Revoke takes a PEM encoded certificate or bundle and tries to revoke it at the CA.\nfunc (c *Certifier) Revoke(cert []byte) error {\n\treturn c.RevokeWithReason(cert, nil)\n}\n\n// RevokeWithReason takes a PEM encoded certificate or bundle and tries to revoke it at the CA.\nfunc (c *Certifier) RevokeWithReason(cert []byte, reason *uint) error {\n\tcertificates, err := certcrypto.ParsePEMBundle(cert)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tx509Cert := certificates[0]\n\tif x509Cert.IsCA {\n\t\treturn errors.New(\"certificate bundle starts with a CA certificate\")\n\t}\n\n\trevokeMsg := acme.RevokeCertMessage{\n\t\tCertificate: base64.RawURLEncoding.EncodeToString(x509Cert.Raw),\n\t\tReason:      reason,\n\t}\n\n\treturn c.core.Certificates.Revoke(revokeMsg)\n}\n\n// RenewOptions options used by Certifier.RenewWithOptions.\ntype RenewOptions struct {\n\tNotBefore time.Time\n\tNotAfter  time.Time\n\t// If true, the []byte contains both the issuer certificate and your issued certificate as a bundle.\n\tBundle         bool\n\tPreferredChain string\n\n\tProfile string\n\n\tAlwaysDeactivateAuthorizations bool\n\t// Not supported for CSR request.\n\tMustStaple     bool\n\tEmailAddresses []string\n}\n\n// Renew takes a Resource and tries to renew the certificate.\n//\n// If the renewal process succeeds, the new certificate will be returned in a new CertResource.\n// Please be aware that this function will return a new certificate in ANY case that is not an error.\n// If the server does not provide us with a new cert on a GET request to the CertURL\n// this function will start a new-cert flow where a new certificate gets generated.\n//\n// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.\n//\n// For private key reuse the PrivateKey property of the passed in Resource should be non-nil.\n//\n// Deprecated: use RenewWithOptions instead.\nfunc (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool, preferredChain string) (*Resource, error) {\n\treturn c.RenewWithOptions(certRes, &RenewOptions{\n\t\tBundle:         bundle,\n\t\tPreferredChain: preferredChain,\n\t\tMustStaple:     mustStaple,\n\t})\n}\n\n// RenewWithOptions takes a Resource and tries to renew the certificate.\n//\n// If the renewal process succeeds, the new certificate will be returned in a new CertResource.\n// Please be aware that this function will return a new certificate in ANY case that is not an error.\n// If the server does not provide us with a new cert on a GET request to the CertURL\n// this function will start a new-cert flow where a new certificate gets generated.\n//\n// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.\n//\n// For private key reuse the PrivateKey property of the passed in Resource should be non-nil.\nfunc (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (*Resource, error) {\n\t// Input certificate is PEM encoded.\n\t// Decode it here as we may need the decoded cert later on in the renewal process.\n\t// The input may be a bundle or a single certificate.\n\tcertificates, err := certcrypto.ParsePEMBundle(certRes.Certificate)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tx509Cert := certificates[0]\n\tif x509Cert.IsCA {\n\t\treturn nil, fmt.Errorf(\"[%s] Certificate bundle starts with a CA certificate\", certRes.Domain)\n\t}\n\n\t// This is just meant to be informal for the user.\n\ttimeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())\n\tlog.Infof(\"[%s] acme: Trying renewal with %d hours remaining\", certRes.Domain, int(timeLeft.Hours()))\n\n\t// We always need to request a new certificate to renew.\n\t// Start by checking to see if the certificate was based off a CSR,\n\t// and use that if it's defined.\n\tif len(certRes.CSR) > 0 {\n\t\tcsr, errP := certcrypto.PemDecodeTox509CSR(certRes.CSR)\n\t\tif errP != nil {\n\t\t\treturn nil, errP\n\t\t}\n\n\t\trequest := ObtainForCSRRequest{CSR: csr}\n\n\t\tif options != nil {\n\t\t\trequest.NotBefore = options.NotBefore\n\t\t\trequest.NotAfter = options.NotAfter\n\t\t\trequest.Bundle = options.Bundle\n\t\t\trequest.PreferredChain = options.PreferredChain\n\t\t\trequest.Profile = options.Profile\n\t\t\trequest.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations\n\t\t}\n\n\t\treturn c.ObtainForCSR(request)\n\t}\n\n\tvar privateKey crypto.PrivateKey\n\tif certRes.PrivateKey != nil {\n\t\tprivateKey, err = certcrypto.ParsePEMPrivateKey(certRes.PrivateKey)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\trequest := ObtainRequest{\n\t\tDomains:    certcrypto.ExtractDomains(x509Cert),\n\t\tPrivateKey: privateKey,\n\t}\n\n\tif options != nil {\n\t\trequest.MustStaple = options.MustStaple\n\t\trequest.NotBefore = options.NotBefore\n\t\trequest.NotAfter = options.NotAfter\n\t\trequest.Bundle = options.Bundle\n\t\trequest.PreferredChain = options.PreferredChain\n\t\trequest.EmailAddresses = options.EmailAddresses\n\t\trequest.Profile = options.Profile\n\t\trequest.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations\n\t}\n\n\treturn c.Obtain(request)\n}\n\n// GetOCSP takes a PEM encoded cert or cert bundle returning the raw OCSP response,\n// the parsed response, and an error, if any.\n//\n// The returned []byte can be passed directly into the OCSPStaple property of a tls.Certificate.\n// If the bundle only contains the issued certificate,\n// this function will try to get the issuer certificate from the IssuingCertificateURL in the certificate.\n//\n// If the []byte and/or ocsp.Response return values are nil, the OCSP status may be assumed OCSPUnknown.\nfunc (c *Certifier) GetOCSP(bundle []byte) ([]byte, *ocsp.Response, error) {\n\tcertificates, err := certcrypto.ParsePEMBundle(bundle)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// We expect the certificate slice to be ordered downwards the chain.\n\t// SRV CRT -> CA. We need to pull the leaf and issuer certs out of it,\n\t// which should always be the first two certificates.\n\t// If there's no OCSP server listed in the leaf cert, there's nothing to do.\n\t// And if we have only one certificate so far, we need to get the issuer cert.\n\n\tissuedCert := certificates[0]\n\n\tif len(issuedCert.OCSPServer) == 0 {\n\t\treturn nil, nil, errors.New(\"no OCSP server specified in cert\")\n\t}\n\n\tif len(certificates) == 1 {\n\t\t// TODO: build fallback. If this fails, check the remaining array entries.\n\t\tif len(issuedCert.IssuingCertificateURL) == 0 {\n\t\t\treturn nil, nil, errors.New(\"no issuing certificate URL\")\n\t\t}\n\n\t\tresp, errC := c.core.HTTPClient.Get(issuedCert.IssuingCertificateURL[0])\n\t\tif errC != nil {\n\t\t\treturn nil, nil, errC\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tissuerBytes, errC := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))\n\t\tif errC != nil {\n\t\t\treturn nil, nil, errC\n\t\t}\n\n\t\tissuerCert, errC := x509.ParseCertificate(issuerBytes)\n\t\tif errC != nil {\n\t\t\treturn nil, nil, errC\n\t\t}\n\n\t\t// Insert it into the slice on position 0\n\t\t// We want it ordered right SRV CRT -> CA\n\t\tcertificates = append(certificates, issuerCert)\n\t}\n\n\tissuerCert := certificates[1]\n\n\t// Finally kick off the OCSP request.\n\tocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tresp, err := c.core.HTTPClient.Post(issuedCert.OCSPServer[0], \"application/ocsp-request\", bytes.NewReader(ocspReq))\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tocspResBytes, err := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn ocspResBytes, ocspRes, nil\n}\n\n// Get attempts to fetch the certificate at the supplied URL.\n// The URL is the same as what would normally be supplied at the Resource's CertURL.\n//\n// The returned Resource will not have the PrivateKey and CSR fields populated as these will not be available.\n//\n// If bundle is true, the Certificate field in the returned Resource includes the issuer certificate.\nfunc (c *Certifier) Get(url string, bundle bool) (*Resource, error) {\n\tcert, issuer, err := c.core.Certificates.Get(url, bundle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Parse the returned cert bundle so that we can grab the domain from the common name.\n\tx509Certs, err := certcrypto.ParsePEMBundle(cert)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdomain, err := certcrypto.GetCertificateMainDomain(x509Certs[0])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Resource{\n\t\tDomain:            domain,\n\t\tCertificate:       cert,\n\t\tIssuerCertificate: issuer,\n\t\tCertURL:           url,\n\t\tCertStableURL:     url,\n\t}, nil\n}\n\nfunc hasPreferredChain(issuer []byte, preferredChain string) (bool, error) {\n\tcerts, err := certcrypto.ParsePEMBundle(issuer)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\ttopCert := certs[len(certs)-1]\n\n\tif topCert.Issuer.CommonName == preferredChain {\n\t\treturn true, nil\n\t}\n\n\treturn false, nil\n}\n\nfunc checkOrderStatus(order acme.ExtendedOrder) (bool, error) {\n\tswitch order.Status {\n\tcase acme.StatusValid:\n\t\treturn true, nil\n\tcase acme.StatusInvalid:\n\t\treturn false, fmt.Errorf(\"invalid order: %w\", order.Err())\n\tdefault:\n\t\treturn false, nil\n\t}\n}\n\n// https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.4\n// The domain name MUST be encoded in the form in which it would appear in a certificate.\n// That is, it MUST be encoded according to the rules in Section 7 of [RFC5280].\n//\n// https://www.rfc-editor.org/rfc/rfc5280.html#section-7\nfunc sanitizeDomain(domains []string) []string {\n\tvar sanitizedDomains []string\n\n\tfor _, domain := range domains {\n\t\tsanitizedDomain, err := idna.ToASCII(domain)\n\t\tif err != nil {\n\t\t\tlog.Infof(\"skip domain %q: unable to sanitize (punnycode): %v\", domain, err)\n\t\t} else {\n\t\t\tsanitizedDomains = append(sanitizedDomains, sanitizedDomain)\n\t\t}\n\t}\n\n\treturn sanitizedDomains\n}\n"
  },
  {
    "path": "certificate/certificates_test.go",
    "content": "package certificate\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst certResponseNoBundleMock = `-----BEGIN CERTIFICATE-----\nMIIDEDCCAfigAwIBAgIHPhckqW5fPDANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQD\nEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDM5NWU2MTAeFw0xODExMDcxNzQ2NTZa\nFw0yMzExMDcxNzQ2NTZaMBMxETAPBgNVBAMTCGFjbWUud3RmMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtLNKvZXD20XPUQCWYSK9rUSKxD9Eb0c9fag\nbxOxOkLRTgL8LH6yln+bxc3MrHDou4PpDUdeo2CyOQu3CKsTS5mrH3NXYHu0H7p5\ny3riOJTHnfkGKLT9LciGz7GkXd62nvNP57bOf5Sk4P2M+Qbxd0hPTSfu52740LSy\n144cnxe2P1aDYehrEp6nYCESuyD/CtUHTo0qwJmzIy163Sp3rSs15BuCPyhySnE3\nBJ8Ggv+qC6D5I1932DfSqyQJ79iq/HRm0Fn84am3KwvRlUfWxabmsUGARXoqCgnE\nzcbJVOZKewv0zlQJpfac+b+Imj6Lvt1TGjIz2mVyefYgLx8gwwIDAQABo1QwUjAO\nBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwG\nA1UdEwEB/wQCMAAwEwYDVR0RBAwwCoIIYWNtZS53dGYwDQYJKoZIhvcNAQELBQAD\nggEBABB/0iYhmfPSQot5RaeeovQnsqYjI5ryQK2cwzW6qcTJfv8N6+p6XkqF1+W4\njXZjrQP8MvgO9KNWlvx12vhINE6wubk88L+2piAi5uS2QejmZbXpyYB9s+oPqlk9\nIDvfdlVYOqvYAhSx7ggGi+j73mjZVtjAavP6dKuu475ZCeq+NIC15RpbbikWKtYE\nHBJ7BW8XQKx67iHGx8ygHTDLbREL80Bck3oUm7wIYGMoNijD6RBl25p4gYl9dzOd\nTqGl5hW/1P5hMbgEzHbr4O3BfWqU2g7tV36TASy3jbC3ONFRNNYrpEZ1AL3+cUri\nOPPkKtAKAbQkKbUIfsHpBZjKZMU=\n-----END CERTIFICATE-----\n`\n\nconst certResponseMock = `-----BEGIN CERTIFICATE-----\nMIIDEDCCAfigAwIBAgIHPhckqW5fPDANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQD\nEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDM5NWU2MTAeFw0xODExMDcxNzQ2NTZa\nFw0yMzExMDcxNzQ2NTZaMBMxETAPBgNVBAMTCGFjbWUud3RmMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtLNKvZXD20XPUQCWYSK9rUSKxD9Eb0c9fag\nbxOxOkLRTgL8LH6yln+bxc3MrHDou4PpDUdeo2CyOQu3CKsTS5mrH3NXYHu0H7p5\ny3riOJTHnfkGKLT9LciGz7GkXd62nvNP57bOf5Sk4P2M+Qbxd0hPTSfu52740LSy\n144cnxe2P1aDYehrEp6nYCESuyD/CtUHTo0qwJmzIy163Sp3rSs15BuCPyhySnE3\nBJ8Ggv+qC6D5I1932DfSqyQJ79iq/HRm0Fn84am3KwvRlUfWxabmsUGARXoqCgnE\nzcbJVOZKewv0zlQJpfac+b+Imj6Lvt1TGjIz2mVyefYgLx8gwwIDAQABo1QwUjAO\nBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwG\nA1UdEwEB/wQCMAAwEwYDVR0RBAwwCoIIYWNtZS53dGYwDQYJKoZIhvcNAQELBQAD\nggEBABB/0iYhmfPSQot5RaeeovQnsqYjI5ryQK2cwzW6qcTJfv8N6+p6XkqF1+W4\njXZjrQP8MvgO9KNWlvx12vhINE6wubk88L+2piAi5uS2QejmZbXpyYB9s+oPqlk9\nIDvfdlVYOqvYAhSx7ggGi+j73mjZVtjAavP6dKuu475ZCeq+NIC15RpbbikWKtYE\nHBJ7BW8XQKx67iHGx8ygHTDLbREL80Bck3oUm7wIYGMoNijD6RBl25p4gYl9dzOd\nTqGl5hW/1P5hMbgEzHbr4O3BfWqU2g7tV36TASy3jbC3ONFRNNYrpEZ1AL3+cUri\nOPPkKtAKAbQkKbUIfsHpBZjKZMU=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE\nAxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw\nNzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl\nNjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT\nSZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh\n0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen\nSRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx\nHAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt\nD1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu\nmB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD\nAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA\nupU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm\niUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd\nQqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ\nwlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv\nrzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2\n7R4IbHGnj0BJA2vMYC4hSw==\n-----END CERTIFICATE-----\n`\n\nconst issuerMock = `-----BEGIN CERTIFICATE-----\nMIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE\nAxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw\nNzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl\nNjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT\nSZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh\n0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen\nSRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx\nHAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt\nD1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu\nmB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD\nAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA\nupU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm\niUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd\nQqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ\nwlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv\nrzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2\n7R4IbHGnj0BJA2vMYC4hSw==\n-----END CERTIFICATE-----\n`\n\nconst certResponseMock2 = `\n-----BEGIN CERTIFICATE-----\nMIIFUzCCBDugAwIBAgISA/z9btaZCSo/qlVwmJrHpoyPMA0GCSqGSIb3DQEBCwUA\nMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD\nExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDA3MjUwNjUxNDRaFw0y\nMDEwMjMwNjUxNDRaMBgxFjAUBgNVBAMTDW5hdHVyZS5nbG9iYWwwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN/PF8lWub3i+lO3CLl/HJAM86pQH9hWej\nWhci1PPNzKyEByJq2psNLCO1W1mXK3ClWSyifptCf7+AAFAOoBojPMwjaKMziw1M\nBxAQiX8MzZLv4Hr4Uk08cQX31QHiEpOv4pMHqB0UpodTYY10dZnDdyJHaGKzxfJh\nnQPYIVto+UegcVu9iZIDow7ugoT2Gh8nB8jOAc4wtBgmylgeAFmYR6QZ4PYSYFh0\nDLZGGB1WuU/4YC5OciwTDv5EiqP3KM3NdkmGhPY0A3jcTrjN+HhcE4pYBtG1wHi8\nPEuqqKyCLa3AjHq4WrZyCCkCMXPbIDS1Qt7botDmUZr/26xJZnl5AgMBAAGjggJj\nMIICXzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF\nBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFFm72Cv7LnjVhcLqUujrykUr70lF\nMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMw\nYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9y\nZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9y\nZy8wGAYDVR0RBBEwD4INbmF0dXJlLmdsb2JhbDBMBgNVHSAERTBDMAgGBmeBDAEC\nATA3BgsrBgEEAYLfEwEBATAoMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNl\nbmNyeXB0Lm9yZzCCAQUGCisGAQQB1nkCBAIEgfYEgfMA8QB3ALIeBcyLos2KIE6H\nZvkruYolIGdr2vpw57JJUy3vi5BeAAABc4T006IAAAQDAEgwRgIhAPEEvCEMkekD\n8XLDaxHPnJ85UZL72JqGgNK+7I/NdFNuAiEA5D78b4V1YsD8wvWz/sk6Ks8VgjED\neKGl/TyXwKEpzEIAdgBvU3asMfAxGdiZAKRRFf93FRwR2QLBACkGjbIImjfZEwAA\nAXOE9NPrAAAEAwBHMEUCIAu4YFfGZIN/P+0eRG0krSddHKCSf6rqr6aVqUWkJY3F\nAiEAz0HkTe0alED1gW9nEAJ1qqK1MLMjRM8SsUv9Is86+CwwDQYJKoZIhvcNAQEL\nBQADggEBAGriSVi9YuBnm50w84gjlinmeGdvxgugblIoEqKoXd3d5/zx0DvW9Tm6\nYGfXsvAJUSCag7dZ/s/PEu23jKNdFoaBmDaUHHKnUwbWWF7/ptYZ+YuDVGOJo8PL\nCULNfUMon20rPU9smzW4BFDBZ6KmX/r4Q8cQ7FLOqKdcng0yMcqIfq4cBxEvd0uQ\npHR3AwCjAIGpV6Q9WHHiHx+SEd/Xc18Z5pXa9m3Rz4i6Mfv+AYLtnsZDxcH81cVM\n7rYp80vhXM9tFd4wyrqLuaVZgYD1ylxTYpTI7sijIq4Sl984f3IPA/olN+zK6E8d\nEbiufIcKeju/aSellDzzBabEo80YT4o=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\nPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\nEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\nrz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\nOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\nxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\naeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\nSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\nikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\nAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\nR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\nJDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\nOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n-----END CERTIFICATE-----\n`\n\nconst issuerMock2 = `-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\nPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\nEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\nrz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\nOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\nxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\naeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\nSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\nikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\nAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\nR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\nJDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\nOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n-----END CERTIFICATE-----\n`\n\nfunc Test_checkResponse(t *testing.T) {\n\tserver := tester.MockACMEServer().\n\t\tRoute(\"POST /certificate\", servermock.RawStringResponse(certResponseMock)).\n\t\tBuildHTTPS(t)\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", key)\n\trequire.NoError(t, err)\n\n\tcertifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})\n\n\torder := acme.ExtendedOrder{\n\t\tOrder: acme.Order{\n\t\t\tStatus:      acme.StatusValid,\n\t\t\tCertificate: server.URL + \"/certificate\",\n\t\t},\n\t}\n\tcertRes := &Resource{}\n\n\tvalid, err := certifier.checkResponse(order, certRes, true, \"\")\n\trequire.NoError(t, err)\n\tassert.True(t, valid)\n\tassert.NotNil(t, certRes)\n\tassert.Empty(t, certRes.Domain)\n\tassert.Contains(t, certRes.CertStableURL, \"/certificate\")\n\tassert.Contains(t, certRes.CertURL, \"/certificate\")\n\tassert.Nil(t, certRes.CSR)\n\tassert.Nil(t, certRes.PrivateKey)\n\tassert.Equal(t, certResponseMock, string(certRes.Certificate), \"Certificate\")\n\tassert.Equal(t, issuerMock, string(certRes.IssuerCertificate), \"IssuerCertificate\")\n}\n\nfunc Test_checkResponse_issuerRelUp(t *testing.T) {\n\tserver := tester.MockACMEServer().\n\t\tRoute(\"POST /certificate\", servermock.RawStringResponse(certResponseMock)).\n\t\tBuildHTTPS(t)\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", key)\n\trequire.NoError(t, err)\n\n\tcertifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})\n\n\torder := acme.ExtendedOrder{\n\t\tOrder: acme.Order{\n\t\t\tStatus:      acme.StatusValid,\n\t\t\tCertificate: server.URL + \"/certificate\",\n\t\t},\n\t}\n\tcertRes := &Resource{}\n\n\tvalid, err := certifier.checkResponse(order, certRes, true, \"\")\n\trequire.NoError(t, err)\n\tassert.True(t, valid)\n\tassert.NotNil(t, certRes)\n\tassert.Empty(t, certRes.Domain)\n\tassert.Contains(t, certRes.CertStableURL, \"/certificate\")\n\tassert.Contains(t, certRes.CertURL, \"/certificate\")\n\tassert.Nil(t, certRes.CSR)\n\tassert.Nil(t, certRes.PrivateKey)\n\tassert.Equal(t, certResponseMock, string(certRes.Certificate), \"Certificate\")\n\tassert.Equal(t, issuerMock, string(certRes.IssuerCertificate), \"IssuerCertificate\")\n}\n\nfunc Test_checkResponse_no_bundle(t *testing.T) {\n\tserver := tester.MockACMEServer().\n\t\tRoute(\"POST /certificate\", servermock.RawStringResponse(certResponseMock)).\n\t\tBuildHTTPS(t)\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", key)\n\trequire.NoError(t, err)\n\n\tcertifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})\n\n\torder := acme.ExtendedOrder{\n\t\tOrder: acme.Order{\n\t\t\tStatus:      acme.StatusValid,\n\t\t\tCertificate: server.URL + \"/certificate\",\n\t\t},\n\t}\n\tcertRes := &Resource{}\n\n\tvalid, err := certifier.checkResponse(order, certRes, false, \"\")\n\trequire.NoError(t, err)\n\tassert.True(t, valid)\n\tassert.NotNil(t, certRes)\n\tassert.Empty(t, certRes.Domain)\n\tassert.Contains(t, certRes.CertStableURL, \"/certificate\")\n\tassert.Contains(t, certRes.CertURL, \"/certificate\")\n\tassert.Nil(t, certRes.CSR)\n\tassert.Nil(t, certRes.PrivateKey)\n\tassert.Equal(t, certResponseNoBundleMock, string(certRes.Certificate), \"Certificate\")\n\tassert.Equal(t, issuerMock, string(certRes.IssuerCertificate), \"IssuerCertificate\")\n}\n\nfunc Test_checkResponse_alternate(t *testing.T) {\n\tserver := tester.MockACMEServer().\n\t\tRoute(\"POST /certificate\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\trw.Header().Add(\"Link\",\n\t\t\t\t\tfmt.Sprintf(`<https://%s/certificate/1>;title=\"foo\";rel=\"alternate\"`, req.Context().Value(http.LocalAddrContextKey)))\n\n\t\t\t\tservermock.RawStringResponse(certResponseMock).ServeHTTP(rw, req)\n\t\t\t})).\n\t\tRoute(\"/certificate/1\", servermock.RawStringResponse(certResponseMock2)).\n\t\tBuildHTTPS(t)\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", key)\n\trequire.NoError(t, err)\n\n\tcertifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})\n\n\torder := acme.ExtendedOrder{\n\t\tOrder: acme.Order{\n\t\t\tStatus:      acme.StatusValid,\n\t\t\tCertificate: server.URL + \"/certificate\",\n\t\t},\n\t}\n\tcertRes := &Resource{\n\t\tDomain: \"example.com\",\n\t}\n\n\tvalid, err := certifier.checkResponse(order, certRes, true, \"DST Root CA X3\")\n\trequire.NoError(t, err)\n\n\tassert.True(t, valid)\n\tassert.NotNil(t, certRes)\n\tassert.Equal(t, \"example.com\", certRes.Domain)\n\tassert.Contains(t, certRes.CertStableURL, \"/certificate/1\")\n\tassert.Contains(t, certRes.CertURL, \"/certificate/1\")\n\tassert.Nil(t, certRes.CSR)\n\tassert.Nil(t, certRes.PrivateKey)\n\tassert.Equal(t, certResponseMock2, string(certRes.Certificate), \"Certificate\")\n\tassert.Equal(t, issuerMock2, string(certRes.IssuerCertificate), \"IssuerCertificate\")\n}\n\nfunc Test_Get(t *testing.T) {\n\tserver := tester.MockACMEServer().\n\t\tRoute(\"POST /acme/cert/test-cert\", servermock.RawStringResponse(certResponseMock)).\n\t\tBuildHTTPS(t)\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", key)\n\trequire.NoError(t, err)\n\n\tcertifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})\n\n\tcertRes, err := certifier.Get(server.URL+\"/acme/cert/test-cert\", true)\n\trequire.NoError(t, err)\n\n\tassert.NotNil(t, certRes)\n\tassert.Equal(t, \"acme.wtf\", certRes.Domain)\n\tassert.Equal(t, server.URL+\"/acme/cert/test-cert\", certRes.CertStableURL)\n\tassert.Equal(t, server.URL+\"/acme/cert/test-cert\", certRes.CertURL)\n\tassert.Nil(t, certRes.CSR)\n\tassert.Nil(t, certRes.PrivateKey)\n\tassert.Equal(t, certResponseMock, string(certRes.Certificate), \"Certificate\")\n\tassert.Equal(t, issuerMock, string(certRes.IssuerCertificate), \"IssuerCertificate\")\n}\n\nfunc Test_checkOrderStatus(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc       string\n\t\torder      acme.Order\n\t\trequireErr require.ErrorAssertionFunc\n\t\texpected   bool\n\t}{\n\t\t{\n\t\t\tdesc:       \"status valid\",\n\t\t\torder:      acme.Order{Status: acme.StatusValid},\n\t\t\trequireErr: require.NoError,\n\t\t\texpected:   true,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"status invalid\",\n\t\t\torder:      acme.Order{Status: acme.StatusInvalid},\n\t\t\trequireErr: require.Error,\n\t\t\texpected:   false,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"status invalid with error\",\n\t\t\torder:      acme.Order{Status: acme.StatusInvalid, Error: &acme.ProblemDetails{}},\n\t\t\trequireErr: require.Error,\n\t\t\texpected:   false,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"unknown status\",\n\t\t\torder:      acme.Order{Status: \"foo\"},\n\t\t\trequireErr: require.NoError,\n\t\t\texpected:   false,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tstatus, err := checkOrderStatus(acme.ExtendedOrder{Order: test.order})\n\t\t\ttest.requireErr(t, err)\n\n\t\t\tassert.Equal(t, test.expected, status)\n\t\t})\n\t}\n}\n\ntype resolverMock struct {\n\terror error\n}\n\nfunc (r *resolverMock) Solve(_ []acme.Authorization) error {\n\treturn r.error\n}\n"
  },
  {
    "path": "certificate/errors.go",
    "content": "package certificate\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n)\n\ntype obtainError struct {\n\tdata map[string]error\n}\n\nfunc newObtainError() *obtainError {\n\treturn &obtainError{data: make(map[string]error)}\n}\n\nfunc (e *obtainError) Add(domain string, err error) {\n\te.data[domain] = err\n}\n\nfunc (e *obtainError) Join() error {\n\tif e == nil {\n\t\treturn nil\n\t}\n\n\tif len(e.data) == 0 {\n\t\treturn nil\n\t}\n\n\tvar err error\n\tfor d, e := range e.data {\n\t\terr = errors.Join(err, fmt.Errorf(\"%s: %w\", d, e))\n\t}\n\n\treturn fmt.Errorf(\"error: one or more domains had a problem:\\n%w\", err)\n}\n\ntype domainError struct {\n\tDomain string\n\tError  error\n}\n"
  },
  {
    "path": "certificate/errors_test.go",
    "content": "package certificate\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype TomatoError struct{}\n\nfunc (t TomatoError) Error() string {\n\treturn \"tomato\"\n}\n\ntype CarrotError struct{}\n\nfunc (t CarrotError) Error() string {\n\treturn \"carrot\"\n}\n\nfunc Test_obtainError_Join(t *testing.T) {\n\tfailures := newObtainError()\n\n\tfailures.Add(\"example.com\", &TomatoError{})\n\n\terr := failures.Join()\n\n\tto := &TomatoError{}\n\trequire.ErrorAs(t, err, &to)\n}\n\nfunc Test_obtainError_Join_multiple_domains(t *testing.T) {\n\tfailures := newObtainError()\n\n\tfailures.Add(\"example.com\", &TomatoError{})\n\tfailures.Add(\"example.org\", &CarrotError{})\n\n\terr := failures.Join()\n\n\tto := &TomatoError{}\n\trequire.ErrorAs(t, err, &to)\n\n\tca := &CarrotError{}\n\trequire.ErrorAs(t, err, &ca)\n}\n\nfunc Test_obtainError_Join_no_error(t *testing.T) {\n\tfailures := newObtainError()\n\n\trequire.NoError(t, failures.Join())\n}\n\nfunc Test_obtainError_Join_same_domain(t *testing.T) {\n\tfailures := newObtainError()\n\n\tfailures.Add(\"example.com\", &TomatoError{})\n\tfailures.Add(\"example.com\", &CarrotError{})\n\n\terr := failures.Join()\n\n\tto := &TomatoError{}\n\tif errors.As(err, &to) {\n\t\trequire.Fail(t, \"TomatoError should be overridden by CarrotError\")\n\t}\n\n\tca := &CarrotError{}\n\trequire.ErrorAs(t, err, &ca)\n}\n"
  },
  {
    "path": "certificate/renewal.go",
    "content": "package certificate\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/asn1\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api\"\n)\n\n// RenewalInfoRequest contains the necessary renewal information.\ntype RenewalInfoRequest struct {\n\tCert *x509.Certificate\n}\n\n// RenewalInfoResponse is a wrapper around acme.RenewalInfoResponse that provides a method for determining when to renew a certificate.\ntype RenewalInfoResponse struct {\n\tacme.RenewalInfoResponse\n\n\t// RetryAfter header indicating the polling interval that the ACME server recommends.\n\t// Conforming clients SHOULD query the renewalInfo URL again after the RetryAfter period has passed,\n\t// as the server may provide a different suggestedWindow.\n\t// https://www.rfc-editor.org/rfc/rfc9773.html#section-4.2\n\tRetryAfter time.Duration\n}\n\n// ShouldRenewAt determines the optimal renewal time based on the current time (UTC),renewal window suggest by ARI, and the client's willingness to sleep.\n// 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.\n// This method implements the RECOMMENDED algorithm described in RFC 9773.\n//\n// - (4.1-11. Getting Renewal Information) https://www.rfc-editor.org/rfc/rfc9773.html\nfunc (r *RenewalInfoResponse) ShouldRenewAt(now time.Time, willingToSleep time.Duration) *time.Time {\n\t// Explicitly convert all times to UTC.\n\tnow = now.UTC()\n\tstart := r.SuggestedWindow.Start.UTC()\n\tend := r.SuggestedWindow.End.UTC()\n\n\t// Select a uniform random time within the suggested window.\n\trt := start\n\tif window := end.Sub(start); window > 0 {\n\t\trandomDuration := time.Duration(rand.Int63n(int64(window)))\n\t\trt = rt.Add(randomDuration)\n\t}\n\n\t// If the selected time is in the past, attempt renewal immediately.\n\tif rt.Before(now) {\n\t\treturn &now\n\t}\n\n\t// Otherwise, if the client can schedule itself to attempt renewal at exactly the selected time, do so.\n\twillingToSleepUntil := now.Add(willingToSleep)\n\tif willingToSleepUntil.After(rt) || willingToSleepUntil.Equal(rt) {\n\t\treturn &rt\n\t}\n\n\t// TODO: Otherwise, if the selected time is before the next time that the client would wake up normally, attempt renewal immediately.\n\n\t// Otherwise, sleep until the next normal wake time, re-check ARI, and return to Step 1.\n\treturn nil\n}\n\n// GetRenewalInfo sends a request to the ACME server's renewalInfo endpoint to obtain a suggested renewal window.\n// The caller MUST provide the certificate and issuer certificate for the certificate they wish to renew.\n// The caller should attempt to renew the certificate at the time indicated by the ShouldRenewAt method of the returned RenewalInfoResponse object.\n//\n// Note: this endpoint is part of a draft specification, not all ACME servers will implement it.\n// This method will return api.ErrNoARI if the server does not advertise a renewal info endpoint.\n//\n// https://www.rfc-editor.org/rfc/rfc9773.html\nfunc (c *Certifier) GetRenewalInfo(req RenewalInfoRequest) (*RenewalInfoResponse, error) {\n\tcertID, err := MakeARICertID(req.Cert)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error making certID: %w\", err)\n\t}\n\n\tresp, err := c.core.Certificates.GetRenewalInfo(certID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tvar info RenewalInfoResponse\n\n\terr = json.NewDecoder(resp.Body).Decode(&info)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif retry := resp.Header.Get(\"Retry-After\"); retry != \"\" {\n\t\tinfo.RetryAfter, err = api.ParseRetryAfter(retry)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse Retry-After header: %w\", err)\n\t\t}\n\t}\n\n\treturn &info, nil\n}\n\n// MakeARICertID constructs a certificate identifier as described in RFC 9773, section 4.1.\nfunc MakeARICertID(leaf *x509.Certificate) (string, error) {\n\tif leaf == nil {\n\t\treturn \"\", errors.New(\"leaf certificate is nil\")\n\t}\n\n\t// Marshal the Serial Number into DER.\n\tder, err := asn1.Marshal(leaf.SerialNumber)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Check if the DER encoded bytes are sufficient (at least 3 bytes: tag,\n\t// length, and value).\n\tif len(der) < 3 {\n\t\treturn \"\", errors.New(\"invalid DER encoding of serial number\")\n\t}\n\n\t// Extract only the integer bytes from the DER encoded Serial Number\n\t// Skipping the first 2 bytes (tag and length).\n\tserial := base64.RawURLEncoding.EncodeToString(der[2:])\n\n\t// Convert the Authority Key Identifier to base64url encoding without\n\t// padding.\n\taki := base64.RawURLEncoding.EncodeToString(leaf.AuthorityKeyId)\n\n\t// Construct the final identifier by concatenating AKI and Serial Number.\n\treturn fmt.Sprintf(\"%s.%s\", aki, serial), nil\n}\n"
  },
  {
    "path": "certificate/renewal_test.go",
    "content": "package certificate\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tariLeafPEM = `-----BEGIN CERTIFICATE-----\nMIIBQzCB66ADAgECAgUAh2VDITAKBggqhkjOPQQDAjAVMRMwEQYDVQQDEwpFeGFt\ncGxlIENBMCIYDzAwMDEwMTAxMDAwMDAwWhgPMDAwMTAxMDEwMDAwMDBaMBYxFDAS\nBgNVBAMTC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeBZu\n7cbpAYNXZLbbh8rNIzuOoqOOtmxA1v7cRm//AwyMwWxyHz4zfwmBhcSrf47NUAFf\nqzLQ2PPQxdTXREYEnKMjMCEwHwYDVR0jBBgwFoAUaYhba4dGQEHhs3uEe6CuLN4B\nyNQwCgYIKoZIzj0EAwIDRwAwRAIge09+S5TZAlw5tgtiVvuERV6cT4mfutXIlwTb\n+FYN/8oCIClDsqBklhB9KAelFiYt9+6FDj3z4KGVelYM5MdsO3pK\n-----END CERTIFICATE-----`\n\tariLeafCertID = \"aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE\"\n)\n\nfunc Test_makeCertID(t *testing.T) {\n\tleaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))\n\trequire.NoError(t, err)\n\n\tactual, err := MakeARICertID(leaf)\n\trequire.NoError(t, err)\n\tassert.Equal(t, ariLeafCertID, actual)\n}\n\nfunc TestCertifier_GetRenewalInfo(t *testing.T) {\n\tleaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))\n\trequire.NoError(t, err)\n\n\t// Test with a fake API.\n\tserver := tester.MockACMEServer().\n\t\tRoute(\"GET /renewalInfo/\"+ariLeafCertID,\n\t\t\tservermock.RawStringResponse(`{\n\t\t\t\t\"suggestedWindow\": {\n\t\t\t\t\t\"start\": \"2020-03-17T17:51:09Z\",\n\t\t\t\t\t\"end\": \"2020-03-17T18:21:09Z\"\n\t\t\t\t},\n\t\t\t\t\"explanationUrl\": \"https://aricapable.ca.example/docs/renewal-advice/\"\n\t\t\t}\n\t\t}`).\n\t\t\t\tWithHeader(\"Content-Type\", \"application/json\").\n\t\t\t\tWithHeader(\"Retry-After\", \"21600\")).\n\t\tBuildHTTPS(t)\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", key)\n\trequire.NoError(t, err)\n\n\tcertifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})\n\n\tri, err := certifier.GetRenewalInfo(RenewalInfoRequest{leaf})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, ri)\n\tassert.Equal(t, \"2020-03-17T17:51:09Z\", ri.SuggestedWindow.Start.Format(time.RFC3339))\n\tassert.Equal(t, \"2020-03-17T18:21:09Z\", ri.SuggestedWindow.End.Format(time.RFC3339))\n\tassert.Equal(t, \"https://aricapable.ca.example/docs/renewal-advice/\", ri.ExplanationURL)\n\tassert.Equal(t, time.Duration(21600000000000), ri.RetryAfter)\n}\n\nfunc TestCertifier_GetRenewalInfo_retryAfter(t *testing.T) {\n\tleaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))\n\trequire.NoError(t, err)\n\n\tserver := tester.MockACMEServer().\n\t\tRoute(\"GET /renewalInfo/\"+ariLeafCertID,\n\t\t\tservermock.RawStringResponse(`{\n\t\t\t\t\"suggestedWindow\": {\n\t\t\t\t\t\"start\": \"2020-03-17T17:51:09Z\",\n\t\t\t\t\t\"end\": \"2020-03-17T18:21:09Z\"\n\t\t\t\t},\n\t\t\t\t\"explanationUrl\": \"https://aricapable.ca.example/docs/renewal-advice/\"\n\t\t\t}\n\t\t}`).\n\t\t\t\tWithHeader(\"Content-Type\", \"application/json\").\n\t\t\t\tWithHeader(\"Retry-After\", time.Now().UTC().Add(6*time.Hour).Format(time.RFC1123))).\n\t\tBuildHTTPS(t)\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", key)\n\trequire.NoError(t, err)\n\n\tcertifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})\n\n\tri, err := certifier.GetRenewalInfo(RenewalInfoRequest{leaf})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, ri)\n\tassert.Equal(t, \"2020-03-17T17:51:09Z\", ri.SuggestedWindow.Start.Format(time.RFC3339))\n\tassert.Equal(t, \"2020-03-17T18:21:09Z\", ri.SuggestedWindow.End.Format(time.RFC3339))\n\tassert.Equal(t, \"https://aricapable.ca.example/docs/renewal-advice/\", ri.ExplanationURL)\n\n\tassert.InDelta(t, 6, ri.RetryAfter.Hours(), 0.001)\n}\n\nfunc TestCertifier_GetRenewalInfo_errors(t *testing.T) {\n\tleaf, err := certcrypto.ParsePEMCertificate([]byte(ariLeafPEM))\n\trequire.NoError(t, err)\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\ttestCases := []struct {\n\t\tdesc    string\n\t\ttimeout time.Duration\n\t\trequest RenewalInfoRequest\n\t\thandler http.HandlerFunc\n\t}{\n\t\t{\n\t\t\tdesc:    \"API timeout\",\n\t\t\ttimeout: 500 * time.Millisecond, // HTTP client that times out after 500ms.\n\t\t\trequest: RenewalInfoRequest{leaf},\n\t\t\thandler: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t// API that takes 2ms to respond.\n\t\t\t\ttime.Sleep(2 * time.Millisecond)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:    \"API error\",\n\t\t\trequest: RenewalInfoRequest{leaf},\n\t\t\thandler: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t// API that responds with error instead of renewal info.\n\t\t\t\thttp.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tserver := tester.MockACMEServer().\n\t\t\t\tRoute(\"GET /renewalInfo/\"+ariLeafCertID, test.handler).\n\t\t\t\tBuildHTTPS(t)\n\n\t\t\tclient := server.Client()\n\n\t\t\tif test.timeout != 0 {\n\t\t\t\tclient.Timeout = test.timeout\n\t\t\t}\n\n\t\t\tcore, err := api.New(client, \"lego-test\", server.URL+\"/dir\", \"\", key)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcertifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048})\n\n\t\t\tresponse, err := certifier.GetRenewalInfo(test.request)\n\t\t\trequire.Error(t, err)\n\t\t\tassert.Nil(t, response)\n\t\t})\n\t}\n}\n\nfunc TestRenewalInfoResponse_ShouldRenew(t *testing.T) {\n\tnow := time.Now().UTC()\n\n\tt.Run(\"Window is in the past\", func(t *testing.T) {\n\t\tri := RenewalInfoResponse{\n\t\t\tRenewalInfoResponse: acme.RenewalInfoResponse{\n\t\t\t\tSuggestedWindow: acme.Window{\n\t\t\t\t\tStart: now.Add(-2 * time.Hour),\n\t\t\t\t\tEnd:   now.Add(-1 * time.Hour),\n\t\t\t\t},\n\t\t\t\tExplanationURL: \"\",\n\t\t\t},\n\t\t\tRetryAfter: 0,\n\t\t}\n\n\t\trt := ri.ShouldRenewAt(now, 0)\n\t\trequire.NotNil(t, rt)\n\t\tassert.Equal(t, now, *rt)\n\t})\n\n\tt.Run(\"Window is in the future\", func(t *testing.T) {\n\t\tri := RenewalInfoResponse{\n\t\t\tRenewalInfoResponse: acme.RenewalInfoResponse{\n\t\t\t\tSuggestedWindow: acme.Window{\n\t\t\t\t\tStart: now.Add(1 * time.Hour),\n\t\t\t\t\tEnd:   now.Add(2 * time.Hour),\n\t\t\t\t},\n\t\t\t\tExplanationURL: \"\",\n\t\t\t},\n\t\t\tRetryAfter: 0,\n\t\t}\n\n\t\trt := ri.ShouldRenewAt(now, 0)\n\t\tassert.Nil(t, rt)\n\t})\n\n\tt.Run(\"Window is in the future, but caller is willing to sleep\", func(t *testing.T) {\n\t\tri := RenewalInfoResponse{\n\t\t\tRenewalInfoResponse: acme.RenewalInfoResponse{\n\t\t\t\tSuggestedWindow: acme.Window{\n\t\t\t\t\tStart: now.Add(1 * time.Hour),\n\t\t\t\t\tEnd:   now.Add(2 * time.Hour),\n\t\t\t\t},\n\t\t\t\tExplanationURL: \"\",\n\t\t\t},\n\t\t\tRetryAfter: 0,\n\t\t}\n\n\t\trt := ri.ShouldRenewAt(now, 2*time.Hour)\n\t\trequire.NotNil(t, rt)\n\t\tassert.True(t, rt.Before(now.Add(2*time.Hour)))\n\t})\n\n\tt.Run(\"Window is in the future, but caller isn't willing to sleep long enough\", func(t *testing.T) {\n\t\tri := RenewalInfoResponse{\n\t\t\tRenewalInfoResponse: acme.RenewalInfoResponse{\n\t\t\t\tSuggestedWindow: acme.Window{\n\t\t\t\t\tStart: now.Add(1 * time.Hour),\n\t\t\t\t\tEnd:   now.Add(2 * time.Hour),\n\t\t\t\t},\n\t\t\t\tExplanationURL: \"\",\n\t\t\t},\n\t\t\tRetryAfter: 0,\n\t\t}\n\n\t\trt := ri.ShouldRenewAt(now, 59*time.Minute)\n\t\tassert.Nil(t, rt)\n\t})\n}\n"
  },
  {
    "path": "challenge/challenges.go",
    "content": "package challenge\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n)\n\n// Type is a string that identifies a particular challenge type and version of ACME challenge.\ntype Type string\n\nconst (\n\t// HTTP01 is the \"http-01\" ACME challenge https://www.rfc-editor.org/rfc/rfc8555.html#section-8.3\n\t// Note: ChallengePath returns the URL path to fulfill this challenge.\n\tHTTP01 = Type(\"http-01\")\n\n\t// DNS01 is the \"dns-01\" ACME challenge https://www.rfc-editor.org/rfc/rfc8555.html#section-8.4\n\t// Note: GetRecord returns a DNS record which will fulfill this challenge.\n\tDNS01 = Type(\"dns-01\")\n\n\t// TLSALPN01 is the \"tls-alpn-01\" ACME challenge https://www.rfc-editor.org/rfc/rfc8737.html\n\tTLSALPN01 = Type(\"tls-alpn-01\")\n)\n\nfunc (t Type) String() string {\n\treturn string(t)\n}\n\nfunc FindChallenge(chlgType Type, authz acme.Authorization) (acme.Challenge, error) {\n\tfor _, chlg := range authz.Challenges {\n\t\tif chlg.Type == string(chlgType) {\n\t\t\treturn chlg, nil\n\t\t}\n\t}\n\n\treturn acme.Challenge{}, fmt.Errorf(\"[%s] acme: unable to find challenge %s\", GetTargetedDomain(authz), chlgType)\n}\n\nfunc GetTargetedDomain(authz acme.Authorization) string {\n\tif authz.Wildcard {\n\t\treturn \"*.\" + authz.Identifier.Value\n\t}\n\n\treturn authz.Identifier.Value\n}\n"
  },
  {
    "path": "challenge/dns01/cname.go",
    "content": "package dns01\n\nimport (\n\t\"strings\"\n\n\t\"github.com/miekg/dns\"\n)\n\n// Update FQDN with CNAME if any.\nfunc updateDomainWithCName(r *dns.Msg, fqdn string) string {\n\tfor _, rr := range r.Answer {\n\t\tif cn, ok := rr.(*dns.CNAME); ok {\n\t\t\tif strings.EqualFold(cn.Hdr.Name, fqdn) {\n\t\t\t\treturn cn.Target\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fqdn\n}\n"
  },
  {
    "path": "challenge/dns01/cname_test.go",
    "content": "package dns01\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_updateDomainWithCName_caseInsensitive(t *testing.T) {\n\tqname := \"_acme-challenge.uppercase-test.example.com.\"\n\tcnameTarget := \"_acme-challenge.uppercase-test.cname-target.example.com.\"\n\n\tmsg := &dns.Msg{\n\t\tMsgHdr: dns.MsgHdr{\n\t\t\tAuthoritative: true,\n\t\t},\n\t\tAnswer: []dns.RR{\n\t\t\t&dns.CNAME{\n\t\t\t\tHdr: dns.RR_Header{\n\t\t\t\t\tName:   strings.ToUpper(qname), // CNAME names are case-insensitive\n\t\t\t\t\tRrtype: dns.TypeCNAME,\n\t\t\t\t\tClass:  dns.ClassINET,\n\t\t\t\t\tTtl:    3600,\n\t\t\t\t},\n\t\t\t\tTarget: cnameTarget,\n\t\t\t},\n\t\t},\n\t}\n\n\tfqdn := updateDomainWithCName(msg, qname)\n\n\tassert.Equal(t, cnameTarget, fqdn)\n}\n"
  },
  {
    "path": "challenge/dns01/dns_challenge.go",
    "content": "package dns01\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n\t\"github.com/miekg/dns\"\n)\n\nconst (\n\t// DefaultPropagationTimeout default propagation timeout.\n\tDefaultPropagationTimeout = 60 * time.Second\n\n\t// DefaultPollingInterval default polling interval.\n\tDefaultPollingInterval = 2 * time.Second\n\n\t// DefaultTTL default TTL.\n\tDefaultTTL = 120\n)\n\ntype ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error\n\ntype ChallengeOption func(*Challenge) error\n\n// CondOption Conditional challenge option.\nfunc CondOption(condition bool, opt ChallengeOption) ChallengeOption {\n\tif !condition {\n\t\t// NoOp options\n\t\treturn func(*Challenge) error {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn opt\n}\n\n// Challenge implements the dns-01 challenge.\ntype Challenge struct {\n\tcore       *api.Core\n\tvalidate   ValidateFunc\n\tprovider   challenge.Provider\n\tpreCheck   preCheck\n\tdnsTimeout time.Duration\n}\n\nfunc NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge {\n\tchlg := &Challenge{\n\t\tcore:       core,\n\t\tvalidate:   validate,\n\t\tprovider:   provider,\n\t\tpreCheck:   newPreCheck(),\n\t\tdnsTimeout: 10 * time.Second,\n\t}\n\n\tfor _, opt := range opts {\n\t\terr := opt(chlg)\n\t\tif err != nil {\n\t\t\tlog.Infof(\"challenge option error: %v\", err)\n\t\t}\n\t}\n\n\treturn chlg\n}\n\n// PreSolve just submits the txt record to the dns provider.\n// It does not validate record propagation, or do anything at all with the acme server.\nfunc (c *Challenge) PreSolve(authz acme.Authorization) error {\n\tdomain := challenge.GetTargetedDomain(authz)\n\tlog.Infof(\"[%s] acme: Preparing to solve DNS-01\", domain)\n\n\tchlng, err := challenge.FindChallenge(challenge.DNS01, authz)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif c.provider == nil {\n\t\treturn fmt.Errorf(\"[%s] acme: no DNS Provider configured\", domain)\n\t}\n\n\t// Generate the Key Authorization for the challenge\n\tkeyAuth, err := c.core.GetKeyAuthorization(chlng.Token)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"[%s] acme: error presenting token: %w\", domain, err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Challenge) Solve(authz acme.Authorization) error {\n\tdomain := challenge.GetTargetedDomain(authz)\n\tlog.Infof(\"[%s] acme: Trying to solve DNS-01\", domain)\n\n\tchlng, err := challenge.FindChallenge(challenge.DNS01, authz)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Generate the Key Authorization for the challenge\n\tkeyAuth, err := c.core.GetKeyAuthorization(chlng.Token)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tinfo := GetChallengeInfo(authz.Identifier.Value, keyAuth)\n\n\tvar timeout, interval time.Duration\n\n\tswitch provider := c.provider.(type) {\n\tcase challenge.ProviderTimeout:\n\t\ttimeout, interval = provider.Timeout()\n\tdefault:\n\t\ttimeout, interval = DefaultPropagationTimeout, DefaultPollingInterval\n\t}\n\n\tlog.Infof(\"[%s] acme: Checking DNS record propagation. [nameservers=%s]\", domain, strings.Join(recursiveNameservers, \",\"))\n\n\ttime.Sleep(interval)\n\n\terr = wait.For(\"propagation\", timeout, interval, func() (bool, error) {\n\t\tstop, errP := c.preCheck.call(domain, info.EffectiveFQDN, info.Value)\n\t\tif !stop || errP != nil {\n\t\t\tlog.Infof(\"[%s] acme: Waiting for DNS record propagation.\", domain)\n\t\t}\n\n\t\treturn stop, errP\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tchlng.KeyAuthorization = keyAuth\n\n\treturn c.validate(c.core, domain, chlng)\n}\n\n// CleanUp cleans the challenge.\nfunc (c *Challenge) CleanUp(authz acme.Authorization) error {\n\tlog.Infof(\"[%s] acme: Cleaning DNS-01 challenge\", challenge.GetTargetedDomain(authz))\n\n\tchlng, err := challenge.FindChallenge(challenge.DNS01, authz)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tkeyAuth, err := c.core.GetKeyAuthorization(chlng.Token)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth)\n}\n\nfunc (c *Challenge) Sequential() (bool, time.Duration) {\n\tif p, ok := c.provider.(sequential); ok {\n\t\treturn ok, p.Sequential()\n\t}\n\n\treturn false, 0\n}\n\ntype sequential interface {\n\tSequential() time.Duration\n}\n\n// GetRecord returns a DNS record which will fulfill the `dns-01` challenge.\n//\n// Deprecated: use GetChallengeInfo instead.\nfunc GetRecord(domain, keyAuth string) (fqdn, value string) {\n\tinfo := GetChallengeInfo(domain, keyAuth)\n\n\treturn info.EffectiveFQDN, info.Value\n}\n\n// ChallengeInfo contains the information use to create the TXT record.\ntype ChallengeInfo struct {\n\t// FQDN is the full-qualified challenge domain (i.e. `_acme-challenge.[domain].`)\n\tFQDN string\n\n\t// EffectiveFQDN contains the resulting FQDN after the CNAMEs resolutions.\n\tEffectiveFQDN string\n\n\t// Value contains the value for the TXT record.\n\tValue string\n}\n\n// GetChallengeInfo returns information used to create a DNS record which will fulfill the `dns-01` challenge.\nfunc GetChallengeInfo(domain, keyAuth string) ChallengeInfo {\n\tkeyAuthShaBytes := sha256.Sum256([]byte(keyAuth))\n\t// base64URL encoding without padding\n\tvalue := base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])\n\n\tok, _ := strconv.ParseBool(os.Getenv(\"LEGO_DISABLE_CNAME_SUPPORT\"))\n\n\treturn ChallengeInfo{\n\t\tValue:         value,\n\t\tFQDN:          getChallengeFQDN(domain, false),\n\t\tEffectiveFQDN: getChallengeFQDN(domain, !ok),\n\t}\n}\n\nfunc getChallengeFQDN(domain string, followCNAME bool) string {\n\tfqdn := fmt.Sprintf(\"_acme-challenge.%s.\", domain)\n\n\tif !followCNAME {\n\t\treturn fqdn\n\t}\n\n\t// recursion counter so it doesn't spin out of control\n\tfor range 50 {\n\t\t// Keep following CNAMEs\n\t\tr, err := dnsQuery(fqdn, dns.TypeCNAME, recursiveNameservers, true)\n\n\t\tif err != nil || r.Rcode != dns.RcodeSuccess {\n\t\t\t// No more CNAME records to follow, exit\n\t\t\tbreak\n\t\t}\n\n\t\t// Check if the domain has CNAME then use that\n\t\tcname := updateDomainWithCName(r, fqdn)\n\t\tif cname == fqdn {\n\t\t\tbreak\n\t\t}\n\n\t\tlog.Infof(\"Found CNAME entry for %q: %q\", fqdn, cname)\n\n\t\tfqdn = cname\n\t}\n\n\treturn fqdn\n}\n"
  },
  {
    "path": "challenge/dns01/dns_challenge_manual.go",
    "content": "package dns01\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n)\n\nconst (\n\tdnsTemplate = `%s %d IN TXT %q`\n)\n\n// DNSProviderManual is an implementation of the ChallengeProvider interface.\n// TODO(ldez): move this to providers/dns/manual\n//\n// Deprecated: Use the manual.DNSProvider instead.\ntype DNSProviderManual struct{}\n\n// NewDNSProviderManual returns a DNSProviderManual instance.\n//\n// Deprecated: Use the manual.NewDNSProvider instead.\nfunc NewDNSProviderManual() (*DNSProviderManual, error) {\n\treturn &DNSProviderManual{}, nil\n}\n\n// Present prints instructions for manually creating the TXT record.\nfunc (*DNSProviderManual) Present(domain, token, keyAuth string) error {\n\tinfo := GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"manual: could not find zone: %w\", err)\n\t}\n\n\tfmt.Printf(\"lego: Please create the following TXT record in your %s zone:\\n\", authZone)\n\tfmt.Printf(dnsTemplate+\"\\n\", info.EffectiveFQDN, DefaultTTL, info.Value)\n\tfmt.Printf(\"lego: Press 'Enter' when you are done\\n\")\n\n\t_, err = bufio.NewReader(os.Stdin).ReadBytes('\\n')\n\tif err != nil {\n\t\treturn fmt.Errorf(\"manual: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp prints instructions for manually removing the TXT record.\nfunc (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {\n\tinfo := GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"manual: could not find zone: %w\", err)\n\t}\n\n\tfmt.Printf(\"lego: You can now remove this TXT record from your %s zone:\\n\", authZone)\n\tfmt.Printf(dnsTemplate+\"\\n\", info.EffectiveFQDN, DefaultTTL, \"...\")\n\n\treturn nil\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProviderManual) Sequential() time.Duration {\n\treturn DefaultPropagationTimeout\n}\n"
  },
  {
    "path": "challenge/dns01/dns_challenge_test.go",
    "content": "package dns01\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/dnsmock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype providerMock struct {\n\tpresent, cleanUp error\n}\n\nfunc (p *providerMock) Present(domain, token, keyAuth string) error { return p.present }\nfunc (p *providerMock) CleanUp(domain, token, keyAuth string) error { return p.cleanUp }\n\ntype providerTimeoutMock struct {\n\tpresent, cleanUp  error\n\ttimeout, interval time.Duration\n}\n\nfunc (p *providerTimeoutMock) Present(domain, token, keyAuth string) error { return p.present }\nfunc (p *providerTimeoutMock) CleanUp(domain, token, keyAuth string) error { return p.cleanUp }\nfunc (p *providerTimeoutMock) Timeout() (time.Duration, time.Duration)     { return p.timeout, p.interval }\n\nfunc TestChallenge_PreSolve(t *testing.T) {\n\tserver := tester.MockACMEServer().BuildHTTPS(t)\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err)\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", privateKey)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tvalidate    ValidateFunc\n\t\tpreCheck    WrapPreCheckFunc\n\t\tprovider    challenge.Provider\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },\n\t\t\tprovider: &providerMock{},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"validate fail\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return errors.New(\"OOPS\") },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },\n\t\t\tprovider: &providerMock{\n\t\t\t\tpresent: nil,\n\t\t\t\tcleanUp: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"preCheck fail\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return false, errors.New(\"OOPS\") },\n\t\t\tprovider: &providerTimeoutMock{\n\t\t\t\ttimeout:  2 * time.Second,\n\t\t\t\tinterval: 500 * time.Millisecond,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"present fail\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },\n\t\t\tprovider: &providerMock{\n\t\t\t\tpresent: errors.New(\"OOPS\"),\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"cleanUp fail\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },\n\t\t\tprovider: &providerMock{\n\t\t\t\tcleanUp: errors.New(\"OOPS\"),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tchlg := NewChallenge(core, test.validate, test.provider, WrapPreCheck(test.preCheck))\n\n\t\t\tauthz := acme.Authorization{\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tValue: \"example.com\",\n\t\t\t\t},\n\t\t\t\tChallenges: []acme.Challenge{\n\t\t\t\t\t{Type: challenge.DNS01.String()},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\terr = chlg.PreSolve(authz)\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestChallenge_Solve(t *testing.T) {\n\tuseAsNameserver(t, dnsmock.NewServer().\n\t\tQuery(\"_acme-challenge.example.com. CNAME\", dnsmock.Noop).\n\t\tBuild(t))\n\n\tserver := tester.MockACMEServer().BuildHTTPS(t)\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err)\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", privateKey)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tvalidate    ValidateFunc\n\t\tpreCheck    WrapPreCheckFunc\n\t\tprovider    challenge.Provider\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },\n\t\t\tprovider: &providerMock{},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"validate fail\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return errors.New(\"OOPS\") },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },\n\t\t\tprovider: &providerMock{\n\t\t\t\tpresent: nil,\n\t\t\t\tcleanUp: nil,\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"preCheck fail\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return false, errors.New(\"OOPS\") },\n\t\t\tprovider: &providerTimeoutMock{\n\t\t\t\ttimeout:  2 * time.Second,\n\t\t\t\tinterval: 500 * time.Millisecond,\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"present fail\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },\n\t\t\tprovider: &providerMock{\n\t\t\t\tpresent: errors.New(\"OOPS\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"cleanUp fail\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },\n\t\t\tprovider: &providerMock{\n\t\t\t\tcleanUp: errors.New(\"OOPS\"),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tvar options []ChallengeOption\n\t\t\tif test.preCheck != nil {\n\t\t\t\toptions = append(options, WrapPreCheck(test.preCheck))\n\t\t\t}\n\n\t\t\tchlg := NewChallenge(core, test.validate, test.provider, options...)\n\n\t\t\tauthz := acme.Authorization{\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tValue: \"example.com\",\n\t\t\t\t},\n\t\t\t\tChallenges: []acme.Challenge{\n\t\t\t\t\t{Type: challenge.DNS01.String()},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\terr = chlg.Solve(authz)\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestChallenge_CleanUp(t *testing.T) {\n\tserver := tester.MockACMEServer().BuildHTTPS(t)\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err)\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", privateKey)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tvalidate    ValidateFunc\n\t\tpreCheck    WrapPreCheckFunc\n\t\tprovider    challenge.Provider\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },\n\t\t\tprovider: &providerMock{},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"validate fail\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return errors.New(\"OOPS\") },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },\n\t\t\tprovider: &providerMock{\n\t\t\t\tpresent: nil,\n\t\t\t\tcleanUp: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"preCheck fail\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return false, errors.New(\"OOPS\") },\n\t\t\tprovider: &providerTimeoutMock{\n\t\t\t\ttimeout:  2 * time.Second,\n\t\t\t\tinterval: 500 * time.Millisecond,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"present fail\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },\n\t\t\tprovider: &providerMock{\n\t\t\t\tpresent: errors.New(\"OOPS\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"cleanUp fail\",\n\t\t\tvalidate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil },\n\t\t\tpreCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil },\n\t\t\tprovider: &providerMock{\n\t\t\t\tcleanUp: errors.New(\"OOPS\"),\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tchlg := NewChallenge(core, test.validate, test.provider, WrapPreCheck(test.preCheck))\n\n\t\t\tauthz := acme.Authorization{\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tValue: \"example.com\",\n\t\t\t\t},\n\t\t\t\tChallenges: []acme.Challenge{\n\t\t\t\t\t{Type: challenge.DNS01.String()},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\terr = chlg.CleanUp(authz)\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetChallengeInfo(t *testing.T) {\n\tuseAsNameserver(t, dnsmock.NewServer().\n\t\tQuery(\"_acme-challenge.example.com. CNAME\", dnsmock.Noop).\n\t\tBuild(t))\n\n\tinfo := GetChallengeInfo(\"example.com\", \"123\")\n\n\texpected := ChallengeInfo{\n\t\tFQDN:          \"_acme-challenge.example.com.\",\n\t\tEffectiveFQDN: \"_acme-challenge.example.com.\",\n\t\tValue:         \"pmWkWSBCL51Bfkhn79xPuKBKHz__H6B-mY6G9_eieuM\",\n\t}\n\n\tassert.Equal(t, expected, info)\n}\n\nfunc TestGetChallengeInfo_CNAME(t *testing.T) {\n\tuseAsNameserver(t, dnsmock.NewServer().\n\t\tQuery(\"_acme-challenge.example.com. CNAME\", dnsmock.CNAME(\"example.org.\")).\n\t\tQuery(\"example.org. CNAME\", dnsmock.Noop).\n\t\tBuild(t))\n\n\tinfo := GetChallengeInfo(\"example.com\", \"123\")\n\n\texpected := ChallengeInfo{\n\t\tFQDN:          \"_acme-challenge.example.com.\",\n\t\tEffectiveFQDN: \"example.org.\",\n\t\tValue:         \"pmWkWSBCL51Bfkhn79xPuKBKHz__H6B-mY6G9_eieuM\",\n\t}\n\n\tassert.Equal(t, expected, info)\n}\n\nfunc TestGetChallengeInfo_CNAME_disabled(t *testing.T) {\n\tuseAsNameserver(t, dnsmock.NewServer().\n\t\t// Never called when the env var works.\n\t\tQuery(\"_acme-challenge.example.com. CNAME\", dnsmock.CNAME(\"example.org.\")).\n\t\tBuild(t))\n\n\tt.Setenv(\"LEGO_DISABLE_CNAME_SUPPORT\", \"true\")\n\n\tinfo := GetChallengeInfo(\"example.com\", \"123\")\n\n\texpected := ChallengeInfo{\n\t\tFQDN:          \"_acme-challenge.example.com.\",\n\t\tEffectiveFQDN: \"_acme-challenge.example.com.\",\n\t\tValue:         \"pmWkWSBCL51Bfkhn79xPuKBKHz__H6B-mY6G9_eieuM\",\n\t}\n\n\tassert.Equal(t, expected, info)\n}\n"
  },
  {
    "path": "challenge/dns01/domain.go",
    "content": "package dns01\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/miekg/dns\"\n)\n\n// ExtractSubDomain extracts the subdomain part from a domain and a zone.\nfunc ExtractSubDomain(domain, zone string) (string, error) {\n\tcanonDomain := dns.Fqdn(domain)\n\tcanonZone := dns.Fqdn(zone)\n\n\tif canonDomain == canonZone {\n\t\treturn \"\", fmt.Errorf(\"no subdomain because the domain and the zone are identical: %s\", canonDomain)\n\t}\n\n\tif !dns.IsSubDomain(canonZone, canonDomain) {\n\t\treturn \"\", fmt.Errorf(\"%s is not a subdomain of %s\", canonDomain, canonZone)\n\t}\n\n\treturn strings.TrimSuffix(canonDomain, \".\"+canonZone), nil\n}\n"
  },
  {
    "path": "challenge/dns01/domain_test.go",
    "content": "package dns01\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestExtractSubDomain(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tdomain   string\n\t\tzone     string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"no FQDN\",\n\t\t\tdomain:   \"_acme-challenge.example.com\",\n\t\t\tzone:     \"example.com\",\n\t\t\texpected: \"_acme-challenge\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"no FQDN zone\",\n\t\t\tdomain:   \"_acme-challenge.example.com.\",\n\t\t\tzone:     \"example.com\",\n\t\t\texpected: \"_acme-challenge\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"no FQDN domain\",\n\t\t\tdomain:   \"_acme-challenge.example.com\",\n\t\t\tzone:     \"example.com.\",\n\t\t\texpected: \"_acme-challenge\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"FQDN\",\n\t\t\tdomain:   \"_acme-challenge.example.com.\",\n\t\t\tzone:     \"example.com.\",\n\t\t\texpected: \"_acme-challenge\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"multi-level subdomain\",\n\t\t\tdomain:   \"_acme-challenge.one.example.com.\",\n\t\t\tzone:     \"example.com.\",\n\t\t\texpected: \"_acme-challenge.one\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tsubDomain, err := ExtractSubDomain(test.domain, test.zone)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.expected, subDomain)\n\t\t})\n\t}\n}\n\nfunc TestExtractSubDomain_errors(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc   string\n\t\tdomain string\n\t\tzone   string\n\t}{\n\t\t{\n\t\t\tdesc:   \"same domain\",\n\t\t\tdomain: \"example.com\",\n\t\t\tzone:   \"example.com\",\n\t\t},\n\t\t{\n\t\t\tdesc:   \"same domain, no FQDN zone\",\n\t\t\tdomain: \"example.com.\",\n\t\t\tzone:   \"example.com\",\n\t\t},\n\t\t{\n\t\t\tdesc:   \"same domain, no FQDN domain\",\n\t\t\tdomain: \"example.com\",\n\t\t\tzone:   \"example.com.\",\n\t\t},\n\t\t{\n\t\t\tdesc:   \"same domain, FQDN\",\n\t\t\tdomain: \"example.com.\",\n\t\t\tzone:   \"example.com.\",\n\t\t},\n\t\t{\n\t\t\tdesc:   \"zone and domain are unrelated\",\n\t\t\tdomain: \"_acme-challenge.example.com\",\n\t\t\tzone:   \"example.org\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t_, err := ExtractSubDomain(test.domain, test.zone)\n\t\t\trequire.Error(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "challenge/dns01/fixtures/resolv.conf.1",
    "content": "domain example.com\nnameserver 10.200.3.249\nnameserver 10.200.3.250:5353\nnameserver 2001:4860:4860::8844\nnameserver [10.0.0.1]:5353\n"
  },
  {
    "path": "challenge/dns01/fqdn.go",
    "content": "package dns01\n\nimport (\n\t\"iter\"\n\n\t\"github.com/miekg/dns\"\n)\n\n// ToFqdn converts the name into a fqdn appending a trailing dot.\n//\n// Deprecated: Use [github.com/miekg/dns.Fqdn] directly.\nfunc ToFqdn(name string) string {\n\treturn dns.Fqdn(name)\n}\n\n// UnFqdn converts the fqdn into a name removing the trailing dot.\nfunc UnFqdn(name string) string {\n\tn := len(name)\n\tif n != 0 && name[n-1] == '.' {\n\t\treturn name[:n-1]\n\t}\n\n\treturn name\n}\n\n// UnFqdnDomainsSeq generates a sequence of \"unFQDNed\" domain names derived from a domain (FQDN or not) in descending order.\nfunc UnFqdnDomainsSeq(fqdn string) iter.Seq[string] {\n\treturn func(yield func(string) bool) {\n\t\tif fqdn == \"\" {\n\t\t\treturn\n\t\t}\n\n\t\tfor _, index := range dns.Split(fqdn) {\n\t\t\tif !yield(UnFqdn(fqdn[index:])) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\n// DomainsSeq generates a sequence of domain names derived from a domain (FQDN or not) in descending order.\nfunc DomainsSeq(fqdn string) iter.Seq[string] {\n\treturn func(yield func(string) bool) {\n\t\tif fqdn == \"\" {\n\t\t\treturn\n\t\t}\n\n\t\tfor _, index := range dns.Split(fqdn) {\n\t\t\tif !yield(fqdn[index:]) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "challenge/dns01/fqdn_test.go",
    "content": "package dns01\n\nimport (\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestUnFqdn(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tfqdn     string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"simple\",\n\t\t\tfqdn:     \"foo.example.\",\n\t\t\texpected: \"foo.example\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"already domain\",\n\t\t\tfqdn:     \"foo.example\",\n\t\t\texpected: \"foo.example\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tdomain := UnFqdn(test.fqdn)\n\n\t\t\tassert.Equal(t, test.expected, domain)\n\t\t})\n\t}\n}\n\nfunc TestUnFqdnDomainsSeq(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tfqdn     string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tdesc:     \"empty\",\n\t\t\tfqdn:     \"\",\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"TLD\",\n\t\t\tfqdn:     \"com\",\n\t\t\texpected: []string{\"com\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"2 levels\",\n\t\t\tfqdn:     \"example.com\",\n\t\t\texpected: []string{\"example.com\", \"com\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"3 levels\",\n\t\t\tfqdn:     \"foo.example.com\",\n\t\t\texpected: []string{\"foo.example.com\", \"example.com\", \"com\"},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tfor name, suffix := range map[string]string{\"\": \"\", \" FQDN\": \".\"} { //nolint:gocritic\n\t\t\tt.Run(test.desc+name, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\tactual := slices.Collect(UnFqdnDomainsSeq(test.fqdn + suffix))\n\n\t\t\t\tassert.Equal(t, test.expected, actual)\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestDomainsSeq(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tfqdn     string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tdesc:     \"empty\",\n\t\t\tfqdn:     \"\",\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"empty FQDN\",\n\t\t\tfqdn:     \".\",\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"TLD FQDN\",\n\t\t\tfqdn:     \"com\",\n\t\t\texpected: []string{\"com\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"TLD\",\n\t\t\tfqdn:     \"com.\",\n\t\t\texpected: []string{\"com.\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"2 levels\",\n\t\t\tfqdn:     \"example.com\",\n\t\t\texpected: []string{\"example.com\", \"com\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"2 levels FQDN\",\n\t\t\tfqdn:     \"example.com.\",\n\t\t\texpected: []string{\"example.com.\", \"com.\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"3 levels\",\n\t\t\tfqdn:     \"foo.example.com\",\n\t\t\texpected: []string{\"foo.example.com\", \"example.com\", \"com\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"3 levels FQDN\",\n\t\t\tfqdn:     \"foo.example.com.\",\n\t\t\texpected: []string{\"foo.example.com.\", \"example.com.\", \"com.\"},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := slices.Collect(DomainsSeq(test.fqdn))\n\n\t\t\tassert.Equal(t, test.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "challenge/dns01/mock_test.go",
    "content": "package dns01\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc fakeNS(name, ns string) *dns.NS {\n\treturn &dns.NS{\n\t\tHdr: dns.RR_Header{Name: name, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 172800},\n\t\tNs:  ns,\n\t}\n}\n\nfunc fakeA(name, ip string) *dns.A {\n\treturn &dns.A{\n\t\tHdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 10},\n\t\tA:   net.ParseIP(ip),\n\t}\n}\n\nfunc fakeTXT(name, value string) *dns.TXT {\n\treturn &dns.TXT{\n\t\tHdr: dns.RR_Header{Name: name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 10},\n\t\tTxt: []string{value},\n\t}\n}\n\n// mockResolver modifies the default DNS resolver to use a custom network address during the test execution.\n// IMPORTANT: it modifying global variables.\nfunc mockResolver(t *testing.T, addr net.Addr) {\n\tt.Helper()\n\n\t_, port, err := net.SplitHostPort(addr.String())\n\trequire.NoError(t, err)\n\n\toriginalDefaultNameserverPort := defaultNameserverPort\n\n\tt.Cleanup(func() {\n\t\tdefaultNameserverPort = originalDefaultNameserverPort\n\t})\n\n\tdefaultNameserverPort = port\n\n\toriginalResolver := net.DefaultResolver\n\n\tt.Cleanup(func() {\n\t\tnet.DefaultResolver = originalResolver\n\t})\n\n\tnet.DefaultResolver = &net.Resolver{\n\t\tPreferGo: true,\n\t\tDial: func(ctx context.Context, network, address string) (net.Conn, error) {\n\t\t\td := net.Dialer{Timeout: 1 * time.Second}\n\n\t\t\treturn d.DialContext(ctx, network, addr.String())\n\t\t},\n\t}\n}\n\nfunc useAsNameserver(t *testing.T, addr net.Addr) {\n\tt.Helper()\n\n\tClearFqdnCache()\n\tt.Cleanup(func() {\n\t\tClearFqdnCache()\n\t})\n\n\toriginalRecursiveNameservers := recursiveNameservers\n\n\tt.Cleanup(func() {\n\t\trecursiveNameservers = originalRecursiveNameservers\n\t})\n\n\trecursiveNameservers = ParseNameservers([]string{addr.String()})\n}\n"
  },
  {
    "path": "challenge/dns01/nameserver.go",
    "content": "package dns01\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/miekg/dns\"\n)\n\nconst defaultResolvConf = \"/etc/resolv.conf\"\n\nvar fqdnSoaCache = &sync.Map{}\n\nvar defaultNameservers = []string{\n\t\"google-public-dns-a.google.com:53\",\n\t\"google-public-dns-b.google.com:53\",\n}\n\n// recursiveNameservers are used to pre-check DNS propagation.\nvar recursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)\n\n// soaCacheEntry holds a cached SOA record (only selected fields).\ntype soaCacheEntry struct {\n\tzone      string    // zone apex (a domain name)\n\tprimaryNs string    // primary nameserver for the zone apex\n\texpires   time.Time // time when this cache entry should be evicted\n}\n\nfunc newSoaCacheEntry(soa *dns.SOA) *soaCacheEntry {\n\treturn &soaCacheEntry{\n\t\tzone:      soa.Hdr.Name,\n\t\tprimaryNs: soa.Ns,\n\t\texpires:   time.Now().Add(time.Duration(soa.Refresh) * time.Second),\n\t}\n}\n\n// isExpired checks whether a cache entry should be considered expired.\nfunc (cache *soaCacheEntry) isExpired() bool {\n\treturn time.Now().After(cache.expires)\n}\n\n// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing.\nfunc ClearFqdnCache() {\n\t// TODO(ldez): use `fqdnSoaCache.Clear()` when updating to go1.23\n\tfqdnSoaCache.Range(func(k, v any) bool {\n\t\tfqdnSoaCache.Delete(k)\n\t\treturn true\n\t})\n}\n\nfunc AddDNSTimeout(timeout time.Duration) ChallengeOption {\n\treturn func(_ *Challenge) error {\n\t\tdnsTimeout = timeout\n\t\treturn nil\n\t}\n}\n\nfunc AddRecursiveNameservers(nameservers []string) ChallengeOption {\n\treturn func(_ *Challenge) error {\n\t\trecursiveNameservers = ParseNameservers(nameservers)\n\t\treturn nil\n\t}\n}\n\n// getNameservers attempts to get systems nameservers before falling back to the defaults.\nfunc getNameservers(path string, defaults []string) []string {\n\tconfig, err := dns.ClientConfigFromFile(path)\n\tif err != nil || len(config.Servers) == 0 {\n\t\treturn defaults\n\t}\n\n\treturn ParseNameservers(config.Servers)\n}\n\nfunc ParseNameservers(servers []string) []string {\n\tvar resolvers []string\n\n\tfor _, resolver := range servers {\n\t\t// ensure all servers have a port number\n\t\tif _, _, err := net.SplitHostPort(resolver); err != nil {\n\t\t\tresolvers = append(resolvers, net.JoinHostPort(resolver, \"53\"))\n\t\t} else {\n\t\t\tresolvers = append(resolvers, resolver)\n\t\t}\n\t}\n\n\treturn resolvers\n}\n\n// lookupNameservers returns the authoritative nameservers for the given fqdn.\nfunc lookupNameservers(fqdn string) ([]string, error) {\n\tvar authoritativeNss []string\n\n\tzone, err := FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tr, err := dnsQuery(zone, dns.TypeNS, recursiveNameservers, true)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"NS call failed: %w\", err)\n\t}\n\n\tfor _, rr := range r.Answer {\n\t\tif ns, ok := rr.(*dns.NS); ok {\n\t\t\tauthoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns))\n\t\t}\n\t}\n\n\tif len(authoritativeNss) > 0 {\n\t\treturn authoritativeNss, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"[zone=%s] could not determine authoritative nameservers\", zone)\n}\n\n// FindPrimaryNsByFqdn determines the primary nameserver of the zone apex for the given fqdn\n// by recursing up the domain labels until the nameserver returns a SOA record in the answer section.\nfunc FindPrimaryNsByFqdn(fqdn string) (string, error) {\n\treturn FindPrimaryNsByFqdnCustom(fqdn, recursiveNameservers)\n}\n\n// FindPrimaryNsByFqdnCustom determines the primary nameserver of the zone apex for the given fqdn\n// by recursing up the domain labels until the nameserver returns a SOA record in the answer section.\nfunc FindPrimaryNsByFqdnCustom(fqdn string, nameservers []string) (string, error) {\n\tsoa, err := lookupSoaByFqdn(fqdn, nameservers)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"[fqdn=%s] %w\", fqdn, err)\n\t}\n\n\treturn soa.primaryNs, nil\n}\n\n// FindZoneByFqdn determines the zone apex for the given fqdn\n// by recursing up the domain labels until the nameserver returns a SOA record in the answer section.\nfunc FindZoneByFqdn(fqdn string) (string, error) {\n\treturn FindZoneByFqdnCustom(fqdn, recursiveNameservers)\n}\n\n// FindZoneByFqdnCustom determines the zone apex for the given fqdn\n// by recursing up the domain labels until the nameserver returns a SOA record in the answer section.\nfunc FindZoneByFqdnCustom(fqdn string, nameservers []string) (string, error) {\n\tsoa, err := lookupSoaByFqdn(fqdn, nameservers)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"[fqdn=%s] %w\", fqdn, err)\n\t}\n\n\treturn soa.zone, nil\n}\n\nfunc lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {\n\t// Do we have it cached and is it still fresh?\n\tentAny, ok := fqdnSoaCache.Load(fqdn)\n\tif ok && entAny != nil {\n\t\tent, ok1 := entAny.(*soaCacheEntry)\n\t\tif ok1 && !ent.isExpired() {\n\t\t\treturn ent, nil\n\t\t}\n\t}\n\n\tent, err := fetchSoaByFqdn(fqdn, nameservers)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfqdnSoaCache.Store(fqdn, ent)\n\n\treturn ent, nil\n}\n\nfunc fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) {\n\tvar (\n\t\terr error\n\t\tr   *dns.Msg\n\t)\n\n\tfor domain := range DomainsSeq(fqdn) {\n\t\tr, err = dnsQuery(domain, dns.TypeSOA, nameservers, true)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif r == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch r.Rcode {\n\t\tcase dns.RcodeSuccess:\n\t\t\t// Check if we got a SOA RR in the answer section\n\t\t\tif len(r.Answer) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// CNAME records cannot/should not exist at the root of a zone.\n\t\t\t// So we skip a domain when a CNAME is found.\n\t\t\tif dnsMsgContainsCNAME(r) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfor _, ans := range r.Answer {\n\t\t\t\tif soa, ok := ans.(*dns.SOA); ok {\n\t\t\t\t\treturn newSoaCacheEntry(soa), nil\n\t\t\t\t}\n\t\t\t}\n\t\tcase dns.RcodeNameError:\n\t\t\t// NXDOMAIN\n\t\tdefault:\n\t\t\t// Any response code other than NOERROR and NXDOMAIN is treated as error\n\t\t\treturn nil, &DNSError{Message: fmt.Sprintf(\"unexpected response for '%s'\", domain), MsgOut: r}\n\t\t}\n\t}\n\n\treturn nil, &DNSError{Message: fmt.Sprintf(\"could not find the start of authority for '%s'\", fqdn), MsgOut: r, Err: err}\n}\n\n// dnsMsgContainsCNAME checks for a CNAME answer in msg.\nfunc dnsMsgContainsCNAME(msg *dns.Msg) bool {\n\treturn slices.ContainsFunc(msg.Answer, func(rr dns.RR) bool {\n\t\t_, ok := rr.(*dns.CNAME)\n\t\treturn ok\n\t})\n}\n\nfunc dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (*dns.Msg, error) {\n\tm := createDNSMsg(fqdn, rtype, recursive)\n\n\tif len(nameservers) == 0 {\n\t\treturn nil, &DNSError{Message: \"empty list of nameservers\"}\n\t}\n\n\tvar (\n\t\tr      *dns.Msg\n\t\terr    error\n\t\terrAll error\n\t)\n\n\tfor _, ns := range nameservers {\n\t\tr, err = sendDNSQuery(m, ns)\n\t\tif err == nil && len(r.Answer) > 0 {\n\t\t\tbreak\n\t\t}\n\n\t\terrAll = errors.Join(errAll, err)\n\t}\n\n\tif err != nil {\n\t\treturn r, errAll\n\t}\n\n\treturn r, nil\n}\n\nfunc createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg {\n\tm := new(dns.Msg)\n\tm.SetQuestion(fqdn, rtype)\n\tm.SetEdns0(4096, false)\n\n\tif !recursive {\n\t\tm.RecursionDesired = false\n\t}\n\n\treturn m\n}\n\nfunc sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) {\n\tif ok, _ := strconv.ParseBool(os.Getenv(\"LEGO_EXPERIMENTAL_DNS_TCP_ONLY\")); ok {\n\t\ttcp := &dns.Client{Net: \"tcp\", Timeout: dnsTimeout}\n\n\t\tr, _, err := tcp.Exchange(m, ns)\n\t\tif err != nil {\n\t\t\treturn r, &DNSError{Message: \"DNS call error\", MsgIn: m, NS: ns, Err: err}\n\t\t}\n\n\t\treturn r, nil\n\t}\n\n\tudp := &dns.Client{Net: \"udp\", Timeout: dnsTimeout}\n\tr, _, err := udp.Exchange(m, ns)\n\n\tif r != nil && r.Truncated {\n\t\ttcp := &dns.Client{Net: \"tcp\", Timeout: dnsTimeout}\n\t\t// If the TCP request succeeds, the \"err\" will reset to nil\n\t\tr, _, err = tcp.Exchange(m, ns)\n\t}\n\n\tif err != nil {\n\t\treturn r, &DNSError{Message: \"DNS call error\", MsgIn: m, NS: ns, Err: err}\n\t}\n\n\treturn r, nil\n}\n\n// DNSError error related to DNS calls.\ntype DNSError struct {\n\tMessage string\n\tNS      string\n\tMsgIn   *dns.Msg\n\tMsgOut  *dns.Msg\n\tErr     error\n}\n\nfunc (d *DNSError) Error() string {\n\tvar details []string\n\tif d.NS != \"\" {\n\t\tdetails = append(details, \"ns=\"+d.NS)\n\t}\n\n\tif d.MsgIn != nil && len(d.MsgIn.Question) > 0 {\n\t\tdetails = append(details, fmt.Sprintf(\"question='%s'\", formatQuestions(d.MsgIn.Question)))\n\t}\n\n\tif d.MsgOut != nil {\n\t\tif d.MsgIn == nil || len(d.MsgIn.Question) == 0 {\n\t\t\tdetails = append(details, fmt.Sprintf(\"question='%s'\", formatQuestions(d.MsgOut.Question)))\n\t\t}\n\n\t\tdetails = append(details, \"code=\"+dns.RcodeToString[d.MsgOut.Rcode])\n\t}\n\n\tmsg := \"DNS error\"\n\tif d.Message != \"\" {\n\t\tmsg = d.Message\n\t}\n\n\tif d.Err != nil {\n\t\tmsg += \": \" + d.Err.Error()\n\t}\n\n\tif len(details) > 0 {\n\t\tmsg += \" [\" + strings.Join(details, \", \") + \"]\"\n\t}\n\n\treturn msg\n}\n\nfunc (d *DNSError) Unwrap() error {\n\treturn d.Err\n}\n\nfunc formatQuestions(questions []dns.Question) string {\n\tvar parts []string\n\tfor _, question := range questions {\n\t\tparts = append(parts, strings.ReplaceAll(strings.TrimPrefix(question.String(), \";\"), \"\\t\", \" \"))\n\t}\n\n\treturn strings.Join(parts, \";\")\n}\n"
  },
  {
    "path": "challenge/dns01/nameserver_test.go",
    "content": "package dns01\n\nimport (\n\t\"errors\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/dnsmock\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_lookupNameserversOK(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tfakeDNSServer *dnsmock.Builder\n\t\tfqdn          string\n\t\texpected      []string\n\t}{\n\t\t{\n\t\t\tfqdn: \"en.wikipedia.org.localhost.\",\n\t\t\tfakeDNSServer: dnsmock.NewServer().\n\t\t\t\tQuery(\"en.wikipedia.org.localhost SOA\", dnsmock.CNAME(\"dyna.wikimedia.org.localhost\")).\n\t\t\t\tQuery(\"wikipedia.org.localhost SOA\", dnsmock.SOA(\"\")).\n\t\t\t\tQuery(\"wikipedia.org.localhost NS\",\n\t\t\t\t\tdnsmock.Answer(\n\t\t\t\t\t\tfakeNS(\"wikipedia.org.localhost.\", \"ns0.wikimedia.org.localhost.\"),\n\t\t\t\t\t\tfakeNS(\"wikipedia.org.localhost.\", \"ns1.wikimedia.org.localhost.\"),\n\t\t\t\t\t\tfakeNS(\"wikipedia.org.localhost.\", \"ns2.wikimedia.org.localhost.\"),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\texpected: []string{\"ns0.wikimedia.org.localhost.\", \"ns1.wikimedia.org.localhost.\", \"ns2.wikimedia.org.localhost.\"},\n\t\t},\n\t\t{\n\t\t\tfqdn: \"www.google.com.localhost.\",\n\t\t\tfakeDNSServer: dnsmock.NewServer().\n\t\t\t\tQuery(\"www.google.com.localhost. SOA\", dnsmock.Noop).\n\t\t\t\tQuery(\"google.com.localhost. SOA\", dnsmock.SOA(\"\")).\n\t\t\t\tQuery(\"google.com.localhost. NS\",\n\t\t\t\t\tdnsmock.Answer(\n\t\t\t\t\t\tfakeNS(\"google.com.localhost.\", \"ns1.google.com.localhost.\"),\n\t\t\t\t\t\tfakeNS(\"google.com.localhost.\", \"ns2.google.com.localhost.\"),\n\t\t\t\t\t\tfakeNS(\"google.com.localhost.\", \"ns3.google.com.localhost.\"),\n\t\t\t\t\t\tfakeNS(\"google.com.localhost.\", \"ns4.google.com.localhost.\"),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\texpected: []string{\"ns1.google.com.localhost.\", \"ns2.google.com.localhost.\", \"ns3.google.com.localhost.\", \"ns4.google.com.localhost.\"},\n\t\t},\n\t\t{\n\t\t\tfqdn: \"mail.proton.me.localhost.\",\n\t\t\tfakeDNSServer: dnsmock.NewServer().\n\t\t\t\tQuery(\"mail.proton.me.localhost. SOA\", dnsmock.Noop).\n\t\t\t\tQuery(\"proton.me.localhost. SOA\", dnsmock.SOA(\"\")).\n\t\t\t\tQuery(\"proton.me.localhost. NS\",\n\t\t\t\t\tdnsmock.Answer(\n\t\t\t\t\t\tfakeNS(\"proton.me.localhost.\", \"ns1.proton.me.localhost.\"),\n\t\t\t\t\t\tfakeNS(\"proton.me.localhost.\", \"ns2.proton.me.localhost.\"),\n\t\t\t\t\t\tfakeNS(\"proton.me.localhost.\", \"ns3.proton.me.localhost.\"),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\texpected: []string{\"ns1.proton.me.localhost.\", \"ns2.proton.me.localhost.\", \"ns3.proton.me.localhost.\"},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.fqdn, func(t *testing.T) {\n\t\t\tuseAsNameserver(t, test.fakeDNSServer.Build(t))\n\n\t\t\tnss, err := lookupNameservers(test.fqdn)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsort.Strings(nss)\n\t\t\tsort.Strings(test.expected)\n\n\t\t\tassert.Equal(t, test.expected, nss)\n\t\t})\n\t}\n}\n\nfunc Test_lookupNameserversErr(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tfqdn          string\n\t\tfakeDNSServer *dnsmock.Builder\n\t\terror         string\n\t}{\n\t\t{\n\t\t\tdesc: \"NXDOMAIN\",\n\t\t\tfqdn: \"example.invalid.\",\n\t\t\tfakeDNSServer: dnsmock.NewServer().\n\t\t\t\tQuery(\". SOA\", dnsmock.Error(dns.RcodeNameError)),\n\t\t\terror: \"could not find zone: [fqdn=example.invalid.] could not find the start of authority for 'example.invalid.' [question='invalid. IN  SOA', code=NXDOMAIN]\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"NS error\",\n\t\t\tfqdn: \"example.com.\",\n\t\t\tfakeDNSServer: dnsmock.NewServer().\n\t\t\t\tQuery(\"example.com. SOA\", dnsmock.SOA(\"\")).\n\t\t\t\tQuery(\"example.com. NS\", dnsmock.Error(dns.RcodeServerFailure)),\n\t\t\terror: \"[zone=example.com.] could not determine authoritative nameservers\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"empty NS\",\n\t\t\tfqdn: \"example.com.\",\n\t\t\tfakeDNSServer: dnsmock.NewServer().\n\t\t\t\tQuery(\"example.com. SOA\", dnsmock.SOA(\"\")).\n\t\t\t\tQuery(\"example.me NS\", dnsmock.Noop),\n\t\t\terror: \"[zone=example.com.] could not determine authoritative nameservers\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tuseAsNameserver(t, test.fakeDNSServer.Build(t))\n\n\t\t\t_, err := lookupNameservers(test.fqdn)\n\t\t\trequire.Error(t, err)\n\t\t\tassert.EqualError(t, err, test.error)\n\t\t})\n\t}\n}\n\ntype lookupSoaByFqdnTestCase struct {\n\tdesc          string\n\tfqdn          string\n\tzone          string\n\tprimaryNs     string\n\tnameservers   []string\n\texpectedError string\n}\n\nfunc lookupSoaByFqdnTestCases(t *testing.T) []lookupSoaByFqdnTestCase {\n\tt.Helper()\n\n\treturn []lookupSoaByFqdnTestCase{\n\t\t{\n\t\t\tdesc:      \"domain is a CNAME\",\n\t\t\tfqdn:      \"mail.example.com.\",\n\t\t\tzone:      \"example.com.\",\n\t\t\tprimaryNs: \"ns1.example.com.\",\n\t\t\tnameservers: []string{\n\t\t\t\tdnsmock.NewServer().\n\t\t\t\t\tQuery(\"mail.example.com. SOA\", dnsmock.CNAME(\"example.com.\")).\n\t\t\t\t\tQuery(\"example.com. SOA\", dnsmock.SOA(\"\")).\n\t\t\t\t\tBuild(t).\n\t\t\t\t\tString(),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:      \"domain is a non-existent subdomain\",\n\t\t\tfqdn:      \"foo.example.com.\",\n\t\t\tzone:      \"example.com.\",\n\t\t\tprimaryNs: \"ns1.example.com.\",\n\t\t\tnameservers: []string{\n\t\t\t\tdnsmock.NewServer().\n\t\t\t\t\tQuery(\"foo.example.com. SOA\", dnsmock.Error(dns.RcodeNameError)).\n\t\t\t\t\tQuery(\"example.com. SOA\", dnsmock.SOA(\"\")).\n\t\t\t\t\tBuild(t).\n\t\t\t\t\tString(),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:      \"domain is a eTLD\",\n\t\t\tfqdn:      \"example.com.ac.\",\n\t\t\tzone:      \"ac.\",\n\t\t\tprimaryNs: \"ns1.nic.ac.\",\n\t\t\tnameservers: []string{\n\t\t\t\tdnsmock.NewServer().\n\t\t\t\t\tQuery(\"example.com.ac. SOA\", dnsmock.Error(dns.RcodeNameError)).\n\t\t\t\t\tQuery(\"com.ac. SOA\", dnsmock.Error(dns.RcodeNameError)).\n\t\t\t\t\tQuery(\"ac. SOA\", dnsmock.SOA(\"\")).\n\t\t\t\t\tBuild(t).\n\t\t\t\t\tString(),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:      \"domain is a cross-zone CNAME\",\n\t\t\tfqdn:      \"cross-zone-example.example.com.\",\n\t\t\tzone:      \"example.com.\",\n\t\t\tprimaryNs: \"ns1.example.com.\",\n\t\t\tnameservers: []string{\n\t\t\t\tdnsmock.NewServer().\n\t\t\t\t\tQuery(\"cross-zone-example.example.com. SOA\", dnsmock.CNAME(\"example.org.\")).\n\t\t\t\t\tQuery(\"example.com. SOA\", dnsmock.SOA(\"\")).\n\t\t\t\t\tBuild(t).\n\t\t\t\t\tString(),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"NXDOMAIN\",\n\t\t\tfqdn: \"test.lego.invalid.\",\n\t\t\tzone: \"lego.invalid.\",\n\t\t\tnameservers: []string{\n\t\t\t\tdnsmock.NewServer().\n\t\t\t\t\tQuery(\"test.lego.invalid. SOA\", dnsmock.Error(dns.RcodeNameError)).\n\t\t\t\t\tQuery(\"lego.invalid. SOA\", dnsmock.Error(dns.RcodeNameError)).\n\t\t\t\t\tQuery(\"invalid. SOA\", dnsmock.Error(dns.RcodeNameError)).\n\t\t\t\t\tBuild(t).\n\t\t\t\t\tString(),\n\t\t\t},\n\t\t\texpectedError: `[fqdn=test.lego.invalid.] could not find the start of authority for 'test.lego.invalid.' [question='invalid. IN  SOA', code=NXDOMAIN]`,\n\t\t},\n\t\t{\n\t\t\tdesc:      \"several non existent nameservers\",\n\t\t\tfqdn:      \"mail.example.com.\",\n\t\t\tzone:      \"example.com.\",\n\t\t\tprimaryNs: \"ns1.example.com.\",\n\t\t\tnameservers: []string{\n\t\t\t\t\":7053\",\n\t\t\t\t\":8053\",\n\t\t\t\tdnsmock.NewServer().\n\t\t\t\t\tQuery(\"mail.example.com. SOA\", dnsmock.CNAME(\"example.com.\")).\n\t\t\t\t\tQuery(\"example.com. SOA\", dnsmock.SOA(\"\")).\n\t\t\t\t\tBuild(t).\n\t\t\t\t\tString(),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"only non-existent nameservers\",\n\t\t\tfqdn:        \"mail.example.com.\",\n\t\t\tzone:        \"example.com.\",\n\t\t\tnameservers: []string{\":7053\", \":8053\", \":9053\"},\n\t\t\t// use only the start of the message because the port changes with each call: 127.0.0.1:XXXXX->127.0.0.1:7053.\n\t\t\texpectedError: \"[fqdn=mail.example.com.] could not find the start of authority for 'mail.example.com.': DNS call error: read udp \",\n\t\t},\n\t\t{\n\t\t\tdesc:          \"no nameservers\",\n\t\t\tfqdn:          \"test.example.com.\",\n\t\t\tzone:          \"example.com.\",\n\t\t\tnameservers:   []string{},\n\t\t\texpectedError: \"[fqdn=test.example.com.] could not find the start of authority for 'test.example.com.': empty list of nameservers\",\n\t\t},\n\t}\n}\n\nfunc TestFindZoneByFqdnCustom(t *testing.T) {\n\tfor _, test := range lookupSoaByFqdnTestCases(t) {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tClearFqdnCache()\n\n\t\t\tzone, err := FindZoneByFqdnCustom(test.fqdn, test.nameservers)\n\t\t\tif test.expectedError != \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.ErrorContains(t, err, test.expectedError)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.zone, zone)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFindPrimaryNsByFqdnCustom(t *testing.T) {\n\tfor _, test := range lookupSoaByFqdnTestCases(t) {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tClearFqdnCache()\n\n\t\t\tns, err := FindPrimaryNsByFqdnCustom(test.fqdn, test.nameservers)\n\t\t\tif test.expectedError != \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.ErrorContains(t, err, test.expectedError)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.primaryNs, ns)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_getNameservers_ResolveConfServers(t *testing.T) {\n\ttestCases := []struct {\n\t\tfixture  string\n\t\texpected []string\n\t\tdefaults []string\n\t}{\n\t\t{\n\t\t\tfixture:  \"fixtures/resolv.conf.1\",\n\t\t\tdefaults: []string{\"127.0.0.1:53\"},\n\t\t\texpected: []string{\"10.200.3.249:53\", \"10.200.3.250:5353\", \"[2001:4860:4860::8844]:53\", \"[10.0.0.1]:5353\"},\n\t\t},\n\t\t{\n\t\t\tfixture:  \"fixtures/resolv.conf.nonexistant\",\n\t\t\tdefaults: []string{\"127.0.0.1:53\"},\n\t\t\texpected: []string{\"127.0.0.1:53\"},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.fixture, func(t *testing.T) {\n\t\t\tresult := getNameservers(test.fixture, test.defaults)\n\n\t\t\tsort.Strings(result)\n\t\t\tsort.Strings(test.expected)\n\n\t\t\tassert.Equal(t, test.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestDNSError_Error(t *testing.T) {\n\tmsgIn := createDNSMsg(\"example.com.\", dns.TypeTXT, true)\n\n\tmsgOut := createDNSMsg(\"example.org.\", dns.TypeSOA, true)\n\tmsgOut.Rcode = dns.RcodeNameError\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\terr      *DNSError\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"empty error\",\n\t\t\terr:      &DNSError{},\n\t\t\texpected: \"DNS error\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"all fields\",\n\t\t\terr: &DNSError{\n\t\t\t\tMessage: \"Oops\",\n\t\t\t\tNS:      \"example.com.\",\n\t\t\t\tMsgIn:   msgIn,\n\t\t\t\tMsgOut:  msgOut,\n\t\t\t\tErr:     errors.New(\"I did it again\"),\n\t\t\t},\n\t\t\texpected: \"Oops: I did it again [ns=example.com., question='example.com. IN  TXT', code=NXDOMAIN]\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"only NS\",\n\t\t\terr: &DNSError{\n\t\t\t\tNS: \"example.com.\",\n\t\t\t},\n\t\t\texpected: \"DNS error [ns=example.com.]\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"only MsgIn\",\n\t\t\terr: &DNSError{\n\t\t\t\tMsgIn: msgIn,\n\t\t\t},\n\t\t\texpected: \"DNS error [question='example.com. IN  TXT']\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"only MsgOut\",\n\t\t\terr: &DNSError{\n\t\t\t\tMsgOut: msgOut,\n\t\t\t},\n\t\t\texpected: \"DNS error [question='example.org. IN  SOA', code=NXDOMAIN]\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"only Err\",\n\t\t\terr: &DNSError{\n\t\t\t\tErr: errors.New(\"I did it again\"),\n\t\t\t},\n\t\t\texpected: \"DNS error: I did it again\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassert.EqualError(t, test.err, test.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "challenge/dns01/nameserver_unix.go",
    "content": "//go:build !windows\n\npackage dns01\n\nimport \"time\"\n\n// dnsTimeout is used to override the default DNS timeout of 10 seconds.\nvar dnsTimeout = 10 * time.Second\n"
  },
  {
    "path": "challenge/dns01/nameserver_windows.go",
    "content": "//go:build windows\n\npackage dns01\n\nimport \"time\"\n\n// dnsTimeout is used to override the default DNS timeout of 20 seconds.\nvar dnsTimeout = 20 * time.Second\n"
  },
  {
    "path": "challenge/dns01/precheck.go",
    "content": "package dns01\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/miekg/dns\"\n)\n\n// defaultNameserverPort used by authoritative NS.\n// This is for tests only.\nvar defaultNameserverPort = \"53\"\n\n// PreCheckFunc checks DNS propagation before notifying ACME that the DNS challenge is ready.\ntype PreCheckFunc func(fqdn, value string) (bool, error)\n\n// WrapPreCheckFunc wraps a PreCheckFunc in order to do extra operations before or after\n// the main check, put it in a loop, etc.\ntype WrapPreCheckFunc func(domain, fqdn, value string, check PreCheckFunc) (bool, error)\n\n// WrapPreCheck Allow to define checks before notifying ACME that the DNS challenge is ready.\nfunc WrapPreCheck(wrap WrapPreCheckFunc) ChallengeOption {\n\treturn func(chlg *Challenge) error {\n\t\tchlg.preCheck.checkFunc = wrap\n\t\treturn nil\n\t}\n}\n\n// DisableCompletePropagationRequirement obsolete.\n//\n// Deprecated: use DisableAuthoritativeNssPropagationRequirement instead.\nfunc DisableCompletePropagationRequirement() ChallengeOption {\n\treturn DisableAuthoritativeNssPropagationRequirement()\n}\n\nfunc DisableAuthoritativeNssPropagationRequirement() ChallengeOption {\n\treturn func(chlg *Challenge) error {\n\t\tchlg.preCheck.requireAuthoritativeNssPropagation = false\n\t\treturn nil\n\t}\n}\n\nfunc RecursiveNSsPropagationRequirement() ChallengeOption {\n\treturn func(chlg *Challenge) error {\n\t\tchlg.preCheck.requireRecursiveNssPropagation = true\n\t\treturn nil\n\t}\n}\n\nfunc PropagationWait(wait time.Duration, skipCheck bool) ChallengeOption {\n\treturn WrapPreCheck(func(domain, fqdn, value string, check PreCheckFunc) (bool, error) {\n\t\ttime.Sleep(wait)\n\n\t\tif skipCheck {\n\t\t\treturn true, nil\n\t\t}\n\n\t\treturn check(fqdn, value)\n\t})\n}\n\ntype preCheck struct {\n\t// checks DNS propagation before notifying ACME that the DNS challenge is ready.\n\tcheckFunc WrapPreCheckFunc\n\n\t// require the TXT record to be propagated to all authoritative name servers\n\trequireAuthoritativeNssPropagation bool\n\n\t// require the TXT record to be propagated to all recursive name servers\n\trequireRecursiveNssPropagation bool\n}\n\nfunc newPreCheck() preCheck {\n\treturn preCheck{\n\t\trequireAuthoritativeNssPropagation: true,\n\t}\n}\n\nfunc (p preCheck) call(domain, fqdn, value string) (bool, error) {\n\tif p.checkFunc == nil {\n\t\treturn p.checkDNSPropagation(fqdn, value)\n\t}\n\n\treturn p.checkFunc(domain, fqdn, value, p.checkDNSPropagation)\n}\n\n// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.\nfunc (p preCheck) checkDNSPropagation(fqdn, value string) (bool, error) {\n\t// Initial attempt to resolve at the recursive NS (require to get CNAME)\n\tr, err := dnsQuery(fqdn, dns.TypeTXT, recursiveNameservers, true)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"initial recursive nameserver: %w\", err)\n\t}\n\n\tif r.Rcode == dns.RcodeSuccess {\n\t\tfqdn = updateDomainWithCName(r, fqdn)\n\t}\n\n\tif p.requireRecursiveNssPropagation {\n\t\t_, err = checkNameserversPropagation(fqdn, value, recursiveNameservers, false)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"recursive nameservers: %w\", err)\n\t\t}\n\t}\n\n\tif !p.requireAuthoritativeNssPropagation {\n\t\treturn true, nil\n\t}\n\n\tauthoritativeNss, err := lookupNameservers(fqdn)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tfound, err := checkNameserversPropagation(fqdn, value, authoritativeNss, true)\n\tif err != nil {\n\t\treturn found, fmt.Errorf(\"authoritative nameservers: %w\", err)\n\t}\n\n\treturn found, nil\n}\n\n// checkNameserversPropagation queries each of the given nameservers for the expected TXT record.\nfunc checkNameserversPropagation(fqdn, value string, nameservers []string, addPort bool) (bool, error) {\n\tfor _, ns := range nameservers {\n\t\tif addPort {\n\t\t\tns = net.JoinHostPort(ns, defaultNameserverPort)\n\t\t}\n\n\t\tr, err := dnsQuery(fqdn, dns.TypeTXT, []string{ns}, false)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tif r.Rcode != dns.RcodeSuccess {\n\t\t\treturn false, fmt.Errorf(\"NS %s returned %s for %s\", ns, dns.RcodeToString[r.Rcode], fqdn)\n\t\t}\n\n\t\tvar records []string\n\n\t\tvar found bool\n\n\t\tfor _, rr := range r.Answer {\n\t\t\tif txt, ok := rr.(*dns.TXT); ok {\n\t\t\t\trecord := strings.Join(txt.Txt, \"\")\n\n\t\t\t\trecords = append(records, record)\n\t\t\t\tif record == value {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif !found {\n\t\t\treturn false, fmt.Errorf(\"NS %s did not return the expected TXT record [fqdn: %s, value: %s]: %s\", ns, fqdn, value, strings.Join(records, \" ,\"))\n\t\t}\n\t}\n\n\treturn true, nil\n}\n"
  },
  {
    "path": "challenge/dns01/precheck_test.go",
    "content": "package dns01\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/dnsmock\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_preCheck_checkDNSPropagation(t *testing.T) {\n\tmockResolver(t,\n\t\tdnsmock.NewServer().\n\t\t\tQuery(\"ns0.lego.localhost. A\",\n\t\t\t\tdnsmock.Answer(fakeA(\"ns0.lego.localhost.\", \"127.0.0.1\"))).\n\t\t\tQuery(\"ns1.lego.localhost. A\",\n\t\t\t\tdnsmock.Answer(fakeA(\"ns1.lego.localhost.\", \"127.0.0.1\"))).\n\t\t\tQuery(\"example.com. TXT\",\n\t\t\t\tdnsmock.Answer(\n\t\t\t\t\tfakeTXT(\"example.com.\", \"one\"),\n\t\t\t\t\tfakeTXT(\"example.com.\", \"two\"),\n\t\t\t\t\tfakeTXT(\"example.com.\", \"three\"),\n\t\t\t\t\tfakeTXT(\"example.com.\", \"four\"),\n\t\t\t\t\tfakeTXT(\"example.com.\", \"five\"),\n\t\t\t\t),\n\t\t\t).\n\t\t\tBuild(t),\n\t)\n\n\tuseAsNameserver(t,\n\t\tdnsmock.NewServer().\n\t\t\tQuery(\"acme-staging.api.example.com. SOA\", dnsmock.Error(dns.RcodeNameError)).\n\t\t\tQuery(\"api.example.com. SOA\", dnsmock.Error(dns.RcodeNameError)).\n\t\t\tQuery(\"example.com. SOA\", dnsmock.SOA(\"\")).\n\t\t\tQuery(\"example.com. NS\",\n\t\t\t\tdnsmock.Answer(\n\t\t\t\t\tfakeNS(\"example.com.\", \"ns0.lego.localhost.\"),\n\t\t\t\t\tfakeNS(\"example.com.\", \"ns1.lego.localhost.\"),\n\t\t\t\t),\n\t\t\t).\n\t\t\tBuild(t),\n\t)\n\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tfqdn          string\n\t\tvalue         string\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\tfqdn:  \"example.com.\",\n\t\t\tvalue: \"four\",\n\t\t},\n\t\t{\n\t\t\tdesc:          \"no matching TXT record\",\n\t\t\tfqdn:          \"acme-staging.api.example.com.\",\n\t\t\tvalue:         \"fe01=\",\n\t\t\texpectedError: \"did not return the expected TXT record [fqdn: acme-staging.api.example.com., value: fe01=]: one ,two ,three ,four ,five\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tClearFqdnCache()\n\n\t\t\tcheck := newPreCheck()\n\n\t\t\tok, err := check.checkDNSPropagation(test.fqdn, test.value)\n\t\t\tif test.expectedError != \"\" {\n\t\t\t\tassert.ErrorContainsf(t, err, test.expectedError, \"PreCheckDNS must fail for %s\", test.fqdn)\n\t\t\t\tassert.False(t, ok, \"PreCheckDNS must fail for %s\", test.fqdn)\n\t\t\t} else {\n\t\t\t\tassert.NoErrorf(t, err, \"PreCheckDNS failed for %s\", test.fqdn)\n\t\t\t\tassert.True(t, ok, \"PreCheckDNS failed for %s\", test.fqdn)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_checkNameserversPropagation_authoritativeNss(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tfqdn, value   string\n\t\tfakeDNSServer *dnsmock.Builder\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc: \"TXT RR w/ expected value\",\n\t\t\t// NS: asnums.routeviews.org.\n\t\t\tfqdn:  \"8.8.8.8.asn.routeviews.org.\",\n\t\t\tvalue: \"151698.8.8.024\",\n\t\t\tfakeDNSServer: dnsmock.NewServer().\n\t\t\t\tQuery(\"8.8.8.8.asn.routeviews.org. TXT\",\n\t\t\t\t\tdnsmock.Answer(\n\t\t\t\t\t\tfakeTXT(\"8.8.8.8.asn.routeviews.org.\", \"151698.8.8.024\"),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t},\n\t\t{\n\t\t\tdesc: \"TXT RR w/ unexpected value\",\n\t\t\t// NS: asnums.routeviews.org.\n\t\t\tfqdn:  \"8.8.8.8.asn.routeviews.org.\",\n\t\t\tvalue: \"fe01=\",\n\t\t\tfakeDNSServer: dnsmock.NewServer().\n\t\t\t\tQuery(\"8.8.8.8.asn.routeviews.org. TXT\",\n\t\t\t\t\tdnsmock.Answer(\n\t\t\t\t\t\tfakeTXT(\"8.8.8.8.asn.routeviews.org.\", \"15169\"),\n\t\t\t\t\t\tfakeTXT(\"8.8.8.8.asn.routeviews.org.\", \"8.8.8.0\"),\n\t\t\t\t\t\tfakeTXT(\"8.8.8.8.asn.routeviews.org.\", \"24\"),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\texpectedError: \"did not return the expected TXT record [fqdn: 8.8.8.8.asn.routeviews.org., value: fe01=]: 15169 ,8.8.8.0 ,24\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"No TXT RR\",\n\t\t\t// NS: ns2.google.com.\n\t\t\tfqdn:  \"ns1.google.com.\",\n\t\t\tvalue: \"fe01=\",\n\t\t\tfakeDNSServer: dnsmock.NewServer().\n\t\t\t\tQuery(\"ns1.google.com.\", dnsmock.Noop),\n\t\t\texpectedError: \"did not return the expected TXT record [fqdn: ns1.google.com., value: fe01=]: \",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tClearFqdnCache()\n\n\t\t\taddr := test.fakeDNSServer.Build(t)\n\n\t\t\tok, err := checkNameserversPropagation(test.fqdn, test.value, []string{addr.String()}, false)\n\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.True(t, ok)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.ErrorContains(t, err, test.expectedError)\n\t\t\t\tassert.False(t, ok)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "challenge/http01/domain_matcher.go",
    "content": "package http01\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"strings\"\n)\n\n// A domainMatcher tries to match a domain (the one we're requesting a certificate for)\n// in the HTTP request coming from the ACME validation servers.\n// This step is part of DNS rebind attack prevention,\n// where the webserver matches incoming requests to a list of domain the server acts authoritative for.\n//\n// The most simple check involves finding the domain in the HTTP Host header;\n// this is what hostMatcher does.\n// Use it, when the http01.ProviderServer is directly reachable from the internet,\n// or when it operates behind a transparent proxy.\n//\n// In many (reverse) proxy setups, Apache and NGINX traditionally move the Host header to a new header named X-Forwarded-Host.\n// Use arbitraryMatcher(\"X-Forwarded-Host\") in this case,\n// or the appropriate header name for other proxy servers.\n//\n// RFC7239 has standardized the different forwarding headers into a single header named Forwarded.\n// The header value has a different format, so you should use forwardedMatcher\n// when the http01.ProviderServer operates behind a RFC7239 compatible proxy.\n// https://www.rfc-editor.org/rfc/rfc7239.html\n//\n// Note: RFC7239 also reminds us, \"that an HTTP list [...] may be split over multiple header fields\" (section 7.1),\n// meaning that\n//\n//\tX-Header: a\n//\tX-Header: b\n//\n// is equal to\n//\n//\tX-Header: a, b\n//\n// All matcher implementations (explicitly not excluding arbitraryMatcher!)\n// have in common that they only match against the first value in such lists.\ntype domainMatcher interface {\n\t// matches checks whether the request is valid for the given domain.\n\tmatches(request *http.Request, domain string) bool\n\n\t// name returns the header name used in the check.\n\t// This is primarily used to create meaningful error messages.\n\tname() string\n}\n\n// hostMatcher checks whether (*net/http).Request.Host starts with a domain name.\ntype hostMatcher struct{}\n\nfunc (m *hostMatcher) name() string {\n\treturn \"Host\"\n}\n\nfunc (m *hostMatcher) matches(r *http.Request, domain string) bool {\n\treturn matchDomain(r.Host, domain)\n}\n\n// arbitraryMatcher checks whether the specified (*net/http.Request).Header value starts with a domain name.\ntype arbitraryMatcher string\n\nfunc (m arbitraryMatcher) name() string {\n\treturn string(m)\n}\n\nfunc (m arbitraryMatcher) matches(r *http.Request, domain string) bool {\n\treturn matchDomain(r.Header.Get(m.name()), domain)\n}\n\n// forwardedMatcher checks whether the Forwarded header contains a \"host\" element starting with a domain name.\n// See https://www.rfc-editor.org/rfc/rfc7239.html for details.\ntype forwardedMatcher struct{}\n\nfunc (m *forwardedMatcher) name() string {\n\treturn \"Forwarded\"\n}\n\nfunc (m *forwardedMatcher) matches(r *http.Request, domain string) bool {\n\tfwds, err := parseForwardedHeader(r.Header.Get(m.name()))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tif len(fwds) == 0 {\n\t\treturn false\n\t}\n\n\thost := fwds[0][\"host\"]\n\n\treturn matchDomain(host, domain)\n}\n\n// parsing requires some form of state machine.\nfunc parseForwardedHeader(s string) (elements []map[string]string, err error) {\n\tcur := make(map[string]string)\n\tkey := \"\"\n\tval := \"\"\n\tinquote := false\n\n\tpos := 0\n\n\tl := len(s)\n\tfor i := 0; i < l; i++ {\n\t\tr := rune(s[i])\n\n\t\tif inquote {\n\t\t\tif r == '\"' {\n\t\t\t\tcur[key] = s[pos:i]\n\t\t\t\tkey = \"\"\n\t\t\t\tpos = i\n\t\t\t\tinquote = false\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch {\n\t\tcase r == '\"': // start of quoted-string\n\t\t\tif key == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"unexpected quoted string as pos %d\", i)\n\t\t\t}\n\n\t\t\tinquote = true\n\t\t\tpos = i + 1\n\n\t\tcase r == ';': // end of forwarded-pair\n\t\t\tcur[key] = s[pos:i]\n\t\t\tkey = \"\"\n\t\t\ti = skipWS(s, i)\n\t\t\tpos = i + 1\n\n\t\tcase r == '=': // end of token\n\t\t\tkey = strings.ToLower(strings.TrimFunc(s[pos:i], isWS))\n\t\t\ti = skipWS(s, i)\n\t\t\tpos = i + 1\n\n\t\tcase r == ',': // end of forwarded-element\n\t\t\tif key != \"\" {\n\t\t\t\tval = s[pos:i]\n\t\t\t\tcur[key] = val\n\t\t\t}\n\n\t\t\telements = append(elements, cur)\n\t\t\tcur = make(map[string]string)\n\t\t\tkey = \"\"\n\t\t\tval = \"\"\n\n\t\t\ti = skipWS(s, i)\n\t\t\tpos = i + 1\n\t\tcase tchar(r) || isWS(r): // valid token character or whitespace\n\t\t\tcontinue\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"invalid token character at pos %d: %c\", i, r)\n\t\t}\n\t}\n\n\tif inquote {\n\t\treturn nil, fmt.Errorf(\"unterminated quoted-string at pos %d\", len(s))\n\t}\n\n\tif key != \"\" {\n\t\tif pos < len(s) {\n\t\t\tval = s[pos:]\n\t\t}\n\n\t\tcur[key] = val\n\t}\n\n\tif len(cur) > 0 {\n\t\telements = append(elements, cur)\n\t}\n\n\treturn elements, nil\n}\n\nfunc tchar(r rune) bool {\n\treturn strings.ContainsRune(\"!#$%&'*+-.^_`|~\", r) ||\n\t\t'0' <= r && r <= '9' ||\n\t\t'a' <= r && r <= 'z' ||\n\t\t'A' <= r && r <= 'Z'\n}\n\nfunc skipWS(s string, i int) int {\n\tfor isWS(rune(s[i+1])) {\n\t\ti++\n\t}\n\n\treturn i\n}\n\nfunc isWS(r rune) bool {\n\treturn strings.ContainsRune(\" \\t\\v\\r\\n\", r)\n}\n\nfunc matchDomain(src, domain string) bool {\n\taddr, err := netip.ParseAddr(domain)\n\tif err == nil && addr.Is6() {\n\t\tdomain = \"[\" + domain + \"]\"\n\t}\n\n\treturn strings.HasPrefix(src, domain)\n}\n"
  },
  {
    "path": "challenge/http01/domain_matcher_test.go",
    "content": "package http01\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_parseForwardedHeader(t *testing.T) {\n\ttestCases := []struct {\n\t\tname  string\n\t\tinput string\n\t\twant  []map[string]string\n\t\terr   string\n\t}{\n\t\t{\n\t\t\tname:  \"empty input\",\n\t\t\tinput: \"\",\n\t\t\twant:  nil,\n\t\t},\n\t\t{\n\t\t\tname:  \"simple case\",\n\t\t\tinput: `for=1.2.3.4;host=example.com; by=127.0.0.1`,\n\t\t\twant: []map[string]string{\n\t\t\t\t{\"for\": \"1.2.3.4\", \"host\": \"example.com\", \"by\": \"127.0.0.1\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"quoted-string\",\n\t\t\tinput: `foo=\"bar\"`,\n\t\t\twant: []map[string]string{\n\t\t\t\t{\"foo\": \"bar\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"multiple entries\",\n\t\t\tinput: `a=1, b=2; c=3, d=4`,\n\t\t\twant: []map[string]string{\n\t\t\t\t{\"a\": \"1\"},\n\t\t\t\t{\"b\": \"2\", \"c\": \"3\"},\n\t\t\t\t{\"d\": \"4\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"whitespace\",\n\t\t\tinput: \"   a =  1,\\tb\\n=\\r\\n2,c=\\\"   untrimmed  \\\"\",\n\t\t\twant: []map[string]string{\n\t\t\t\t{\"a\": \"1\"},\n\t\t\t\t{\"b\": \"2\"},\n\t\t\t\t{\"c\": \"   untrimmed  \"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"unterminated quote\",\n\t\t\tinput: `x=\"y`,\n\t\t\terr:   \"unterminated quoted-string\",\n\t\t},\n\t\t{\n\t\t\tname:  \"unexpected quote\",\n\t\t\tinput: `\"x=y\"`,\n\t\t\terr:   \"unexpected quote\",\n\t\t},\n\t\t{\n\t\t\tname:  \"invalid token\",\n\t\t\tinput: `a=b, ipv6=[fe80::1], x=y`,\n\t\t\terr:   \"invalid token character at pos 10: [\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual, err := parseForwardedHeader(test.input)\n\t\t\tif test.err == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.want, actual)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), test.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_hostMatcher_matches(t *testing.T) {\n\thm := &hostMatcher{}\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tdomain   string\n\t\treq      *http.Request\n\t\texpected assert.BoolAssertionFunc\n\t}{\n\t\t{\n\t\t\tdesc:     \"exact domain\",\n\t\t\tdomain:   \"example.com\",\n\t\t\treq:      httptest.NewRequest(http.MethodGet, \"http://example.com\", nil),\n\t\t\texpected: assert.True,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"request with path\",\n\t\t\tdomain:   \"example.com\",\n\t\t\treq:      httptest.NewRequest(http.MethodGet, \"http://example.com/foo/bar\", nil),\n\t\t\texpected: assert.True,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"ipv4\",\n\t\t\tdomain:   \"127.0.0.1\",\n\t\t\treq:      httptest.NewRequest(http.MethodGet, \"http://127.0.0.1\", nil),\n\t\t\texpected: assert.True,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"ipv6\",\n\t\t\tdomain:   \"2001:db8::1\",\n\t\t\treq:      httptest.NewRequest(http.MethodGet, \"http://[2001:db8::1]\", nil),\n\t\t\texpected: assert.True,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"ipv6 with brackets\",\n\t\t\tdomain:   \"[2001:db8::1]\",\n\t\t\treq:      httptest.NewRequest(http.MethodGet, \"http://[2001:db8::1]\", nil),\n\t\t\texpected: assert.True,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\thm.matches(test.req, test.domain)\n\n\t\t\ttest.expected(t, hm.matches(test.req, test.domain))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "challenge/http01/http_challenge.go",
    "content": "package http01\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/log\"\n)\n\ntype ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error\n\ntype ChallengeOption func(*Challenge) error\n\n// SetDelay sets a delay between the start of the HTTP server and the challenge validation.\nfunc SetDelay(delay time.Duration) ChallengeOption {\n\treturn func(chlg *Challenge) error {\n\t\tchlg.delay = delay\n\t\treturn nil\n\t}\n}\n\n// ChallengePath returns the URL path for the `http-01` challenge.\nfunc ChallengePath(token string) string {\n\treturn \"/.well-known/acme-challenge/\" + token\n}\n\ntype Challenge struct {\n\tcore     *api.Core\n\tvalidate ValidateFunc\n\tprovider challenge.Provider\n\tdelay    time.Duration\n}\n\nfunc NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge {\n\tchlg := &Challenge{\n\t\tcore:     core,\n\t\tvalidate: validate,\n\t\tprovider: provider,\n\t}\n\n\tfor _, opt := range opts {\n\t\terr := opt(chlg)\n\t\tif err != nil {\n\t\t\tlog.Infof(\"challenge option error: %v\", err)\n\t\t}\n\t}\n\n\treturn chlg\n}\n\nfunc (c *Challenge) SetProvider(provider challenge.Provider) {\n\tc.provider = provider\n}\n\nfunc (c *Challenge) Solve(authz acme.Authorization) error {\n\tdomain := challenge.GetTargetedDomain(authz)\n\tlog.Infof(\"[%s] acme: Trying to solve HTTP-01\", domain)\n\n\tchlng, err := challenge.FindChallenge(challenge.HTTP01, authz)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Generate the Key Authorization for the challenge\n\tkeyAuth, err := c.core.GetKeyAuthorization(chlng.Token)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"[%s] acme: error presenting token: %w\", domain, err)\n\t}\n\n\tdefer func() {\n\t\terr := c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"[%s] acme: cleaning up failed: %v\", domain, err)\n\t\t}\n\t}()\n\n\tif c.delay > 0 {\n\t\ttime.Sleep(c.delay)\n\t}\n\n\tchlng.KeyAuthorization = keyAuth\n\n\treturn c.validate(c.core, domain, chlng)\n}\n"
  },
  {
    "path": "challenge/http01/http_challenge_server.go",
    "content": "package http01\n\nimport (\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/go-acme/lego/v4/log\"\n)\n\n// ProviderServer implements ChallengeProvider for `http-01` challenge.\n// It may be instantiated without using the NewProviderServer function if\n// you want only to use the default values.\ntype ProviderServer struct {\n\taddress string\n\tnetwork string // must be valid argument to net.Listen\n\n\tsocketMode fs.FileMode\n\n\tmatcher  domainMatcher\n\tdone     chan bool\n\tlistener net.Listener\n}\n\n// NewProviderServer creates a new ProviderServer on the selected interface and port.\n// Setting iface and / or port to an empty string will make the server fall back to\n// the \"any\" interface and port 80 respectively.\nfunc NewProviderServer(iface, port string) *ProviderServer {\n\tif port == \"\" {\n\t\tport = \"80\"\n\t}\n\n\treturn &ProviderServer{network: \"tcp\", address: net.JoinHostPort(iface, port), matcher: &hostMatcher{}}\n}\n\nfunc NewUnixProviderServer(socketPath string, mode fs.FileMode) *ProviderServer {\n\treturn &ProviderServer{network: \"unix\", address: socketPath, socketMode: mode, matcher: &hostMatcher{}}\n}\n\n// Present starts a web server and makes the token available at `ChallengePath(token)` for web requests.\nfunc (s *ProviderServer) Present(domain, token, keyAuth string) error {\n\tvar err error\n\n\ts.listener, err = net.Listen(s.network, s.GetAddress())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not start HTTP server for challenge: %w\", err)\n\t}\n\n\tif s.network == \"unix\" {\n\t\tif err = os.Chmod(s.address, s.socketMode); err != nil {\n\t\t\treturn fmt.Errorf(\"chmod %s: %w\", s.address, err)\n\t\t}\n\t}\n\n\ts.done = make(chan bool)\n\n\tgo s.serve(domain, token, keyAuth)\n\n\treturn nil\n}\n\nfunc (s *ProviderServer) GetAddress() string {\n\treturn s.address\n}\n\n// CleanUp closes the HTTP server and removes the token from `ChallengePath(token)`.\nfunc (s *ProviderServer) CleanUp(domain, token, keyAuth string) error {\n\tif s.listener == nil {\n\t\treturn nil\n\t}\n\n\ts.listener.Close()\n\n\t<-s.done\n\n\treturn nil\n}\n\n// SetProxyHeader changes the validation of incoming requests.\n// By default, s matches the \"Host\" header value to the domain name.\n//\n// When the server runs behind a proxy server, this is not the correct place to look at;\n// Apache and NGINX have traditionally moved the original Host header into a new header named \"X-Forwarded-Host\".\n// Other webservers might use different names;\n// and RFC7239 has standardized a new header named \"Forwarded\" (with slightly different semantics).\n//\n// The exact behavior depends on the value of headerName:\n// - \"\" (the empty string) and \"Host\" will restore the default and only check the Host header\n// - \"Forwarded\" will look for a Forwarded header, and inspect it according to https://www.rfc-editor.org/rfc/rfc7239.html\n// - any other value will check the header value with the same name.\nfunc (s *ProviderServer) SetProxyHeader(headerName string) {\n\tswitch h := textproto.CanonicalMIMEHeaderKey(headerName); h {\n\tcase \"\", \"Host\":\n\t\ts.matcher = &hostMatcher{}\n\tcase \"Forwarded\":\n\t\ts.matcher = &forwardedMatcher{}\n\tdefault:\n\t\ts.matcher = arbitraryMatcher(h)\n\t}\n}\n\nfunc (s *ProviderServer) serve(domain, token, keyAuth string) {\n\tpath := ChallengePath(token)\n\n\t// The incoming request will be validated to prevent DNS rebind attacks.\n\t// We only respond with the keyAuth, when we're receiving a GET requests with\n\t// the \"Host\" header matching the domain (the latter is configurable though SetProxyHeader).\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method == http.MethodGet && s.matcher.matches(r, domain) {\n\t\t\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\n\t\t\t_, err := w.Write([]byte(keyAuth))\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tlog.Infof(\"[%s] Served key authentication\", domain)\n\n\t\t\treturn\n\t\t}\n\n\t\tlog.Warnf(\"Received request for domain %s with method %s but the domain did not match any challenge. Please ensure you are passing the %s header properly.\", r.Host, r.Method, s.matcher.name())\n\n\t\t_, err := w.Write([]byte(\"TEST\"))\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t})\n\n\thttpServer := &http.Server{Handler: mux}\n\n\t// Once httpServer is shut down\n\t// we don't want any lingering connections, so disable KeepAlives.\n\thttpServer.SetKeepAlivesEnabled(false)\n\n\terr := httpServer.Serve(s.listener)\n\tif err != nil && !strings.Contains(err.Error(), \"use of closed network connection\") {\n\t\tlog.Println(err)\n\t}\n\n\ts.done <- true\n}\n"
  },
  {
    "path": "challenge/http01/http_challenge_test.go",
    "content": "package http01\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestProviderServer_GetAddress(t *testing.T) {\n\tdir := t.TempDir()\n\tt.Cleanup(func() { _ = os.RemoveAll(dir) })\n\n\tsock := filepath.Join(dir, \"var\", \"run\", \"test\")\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tserver   *ProviderServer\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"TCP default address\",\n\t\t\tserver:   NewProviderServer(\"\", \"\"),\n\t\t\texpected: \":80\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"TCP with explicit port\",\n\t\t\tserver:   NewProviderServer(\"\", \"8080\"),\n\t\t\texpected: \":8080\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"TCP with host and port\",\n\t\t\tserver:   NewProviderServer(\"localhost\", \"8080\"),\n\t\t\texpected: \"localhost:8080\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"UDS socket\",\n\t\t\tserver:   NewUnixProviderServer(sock, fs.ModeSocket|0o666),\n\t\t\texpected: sock,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\taddress := test.server.GetAddress()\n\t\t\tassert.Equal(t, test.expected, address)\n\t\t})\n\t}\n}\n\nfunc TestChallenge(t *testing.T) {\n\tserver := tester.MockACMEServer().BuildHTTPS(t)\n\n\tproviderServer := NewProviderServer(\"\", \"23457\")\n\n\tvalidate := func(_ *api.Core, _ string, chlng acme.Challenge) error {\n\t\turi := \"http://localhost\" + providerServer.GetAddress() + ChallengePath(chlng.Token)\n\n\t\tresp, err := http.DefaultClient.Get(uri)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tif want := \"text/plain\"; resp.Header.Get(\"Content-Type\") != want {\n\t\t\tt.Errorf(\"Get(%q) Content-Type: got %q, want %q\", uri, resp.Header.Get(\"Content-Type\"), want)\n\t\t}\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tbodyStr := string(body)\n\n\t\tif bodyStr != chlng.KeyAuthorization {\n\t\t\tt.Errorf(\"Get(%q) Body: got %q, want %q\", uri, bodyStr, chlng.KeyAuthorization)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", privateKey)\n\trequire.NoError(t, err)\n\n\tsolver := NewChallenge(core, validate, providerServer)\n\n\tauthz := acme.Authorization{\n\t\tIdentifier: acme.Identifier{\n\t\t\tValue: \"localhost:23457\",\n\t\t},\n\t\tChallenges: []acme.Challenge{\n\t\t\t{Type: challenge.HTTP01.String(), Token: \"http1\"},\n\t\t},\n\t}\n\n\terr = solver.Solve(authz)\n\trequire.NoError(t, err)\n}\n\nfunc TestChallengeUnix(t *testing.T) {\n\tif runtime.GOOS != \"linux\" {\n\t\tt.Skip(\"only for UNIX systems\")\n\t}\n\n\tserver := tester.MockACMEServer().BuildHTTPS(t)\n\n\tdir := t.TempDir()\n\tt.Cleanup(func() { _ = os.RemoveAll(dir) })\n\n\tsocket := filepath.Join(dir, \"lego-challenge-test.sock\")\n\n\tproviderServer := NewUnixProviderServer(socket, fs.ModeSocket|0o666)\n\n\tvalidate := func(_ *api.Core, _ string, chlng acme.Challenge) error {\n\t\t// any uri will do, as we hijack the dial\n\t\turi := \"http://localhost\" + ChallengePath(chlng.Token)\n\n\t\tclient := &http.Client{Transport: &http.Transport{\n\t\t\tDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\t\treturn net.Dial(\"unix\", socket)\n\t\t\t},\n\t\t}}\n\n\t\tresp, err := client.Get(uri)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdefer resp.Body.Close()\n\n\t\tif want := \"text/plain\"; resp.Header.Get(\"Content-Type\") != want {\n\t\t\tt.Errorf(\"Get(%q) Content-Type: got %q, want %q\", uri, resp.Header.Get(\"Content-Type\"), want)\n\t\t}\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tbodyStr := string(body)\n\n\t\tif bodyStr != chlng.KeyAuthorization {\n\t\t\tt.Errorf(\"Get(%q) Body: got %q, want %q\", uri, bodyStr, chlng.KeyAuthorization)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", privateKey)\n\trequire.NoError(t, err)\n\n\tsolver := NewChallenge(core, validate, providerServer)\n\n\tauthz := acme.Authorization{\n\t\tIdentifier: acme.Identifier{\n\t\t\tValue: \"localhost\",\n\t\t},\n\t\tChallenges: []acme.Challenge{\n\t\t\t{Type: challenge.HTTP01.String(), Token: \"http1\"},\n\t\t},\n\t}\n\n\terr = solver.Solve(authz)\n\trequire.NoError(t, err)\n}\n\nfunc TestChallengeInvalidPort(t *testing.T) {\n\tserver := tester.MockACMEServer().BuildHTTPS(t)\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", privateKey)\n\trequire.NoError(t, err)\n\n\tvalidate := func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }\n\n\tsolver := NewChallenge(core, validate, NewProviderServer(\"\", \"123456\"))\n\n\tauthz := acme.Authorization{\n\t\tIdentifier: acme.Identifier{\n\t\t\tValue: \"localhost:123456\",\n\t\t},\n\t\tChallenges: []acme.Challenge{\n\t\t\t{Type: challenge.HTTP01.String(), Token: \"http2\"},\n\t\t},\n\t}\n\n\terr = solver.Solve(authz)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"invalid port\")\n\tassert.Contains(t, err.Error(), \"123456\")\n}\n\ntype testProxyHeader struct {\n\tname   string\n\tvalues []string\n}\n\nfunc (h *testProxyHeader) update(r *http.Request) {\n\tif h == nil || len(h.values) == 0 {\n\t\treturn\n\t}\n\n\tif h.name == \"Host\" {\n\t\tr.Host = h.values[0]\n\t} else if h.name != \"\" {\n\t\tr.Header[h.name] = h.values\n\t}\n}\n\nfunc TestChallengeWithProxy(t *testing.T) {\n\th := func(name string, values ...string) *testProxyHeader {\n\t\tname = textproto.CanonicalMIMEHeaderKey(name)\n\t\treturn &testProxyHeader{name, values}\n\t}\n\n\tconst (\n\t\tok   = \"localhost:23457\"\n\t\tnook = \"example.com\"\n\t)\n\n\ttestCases := []struct {\n\t\tname   string\n\t\theader *testProxyHeader\n\t\textra  *testProxyHeader\n\t\tisErr  bool\n\t}{\n\t\t// tests for hostMatcher\n\t\t{\n\t\t\tname: \"no proxy\",\n\t\t},\n\t\t{\n\t\t\tname:   \"empty string\",\n\t\t\theader: h(\"\"),\n\t\t},\n\t\t{\n\t\t\tname:   \"empty Host\",\n\t\t\theader: h(\"host\"),\n\t\t},\n\t\t{\n\t\t\tname:   \"matching Host\",\n\t\t\theader: h(\"host\", ok),\n\t\t},\n\t\t{\n\t\t\tname:   \"Host mismatch\",\n\t\t\theader: h(\"host\", nook),\n\t\t\tisErr:  true,\n\t\t},\n\t\t{\n\t\t\tname:   \"Host mismatch (ignoring forwarding header)\",\n\t\t\theader: h(\"host\", nook),\n\t\t\textra:  h(\"X-Forwarded-Host\", ok),\n\t\t\tisErr:  true,\n\t\t},\n\t\t// test for arbitraryMatcher\n\t\t{\n\t\t\tname:   \"matching X-Forwarded-Host\",\n\t\t\theader: h(\"X-Forwarded-Host\", ok),\n\t\t},\n\t\t{\n\t\t\tname:   \"matching X-Forwarded-Host (multiple fields)\",\n\t\t\theader: h(\"X-Forwarded-Host\", ok, nook),\n\t\t},\n\t\t{\n\t\t\tname:   \"matching X-Forwarded-Host (chain value)\",\n\t\t\theader: h(\"X-Forwarded-Host\", ok+\", \"+nook),\n\t\t},\n\t\t{\n\t\t\tname:   \"X-Forwarded-Host mismatch\",\n\t\t\theader: h(\"X-Forwarded-Host\", nook),\n\t\t\textra:  h(\"host\", ok),\n\t\t\tisErr:  true,\n\t\t},\n\t\t{\n\t\t\tname:   \"X-Forwarded-Host mismatch (multiple fields)\",\n\t\t\theader: h(\"X-Forwarded-Host\", nook, ok),\n\t\t\tisErr:  true,\n\t\t},\n\t\t{\n\t\t\tname:   \"matching X-Something-Else\",\n\t\t\theader: h(\"X-Something-Else\", ok),\n\t\t},\n\t\t{\n\t\t\tname:   \"matching X-Something-Else (multiple fields)\",\n\t\t\theader: h(\"X-Something-Else\", ok, nook),\n\t\t},\n\t\t{\n\t\t\tname:   \"matching X-Something-Else (chain value)\",\n\t\t\theader: h(\"X-Something-Else\", ok+\", \"+nook),\n\t\t},\n\t\t{\n\t\t\tname:   \"X-Something-Else mismatch\",\n\t\t\theader: h(\"X-Something-Else\", nook),\n\t\t\tisErr:  true,\n\t\t},\n\t\t{\n\t\t\tname:   \"X-Something-Else mismatch (multiple fields)\",\n\t\t\theader: h(\"X-Something-Else\", nook, ok),\n\t\t\tisErr:  true,\n\t\t},\n\t\t{\n\t\t\tname:   \"X-Something-Else mismatch (chain value)\",\n\t\t\theader: h(\"X-Something-Else\", nook+\", \"+ok),\n\t\t\tisErr:  true,\n\t\t},\n\t\t// tests for forwardedHeader\n\t\t{\n\t\t\tname:   \"matching Forwarded\",\n\t\t\theader: h(\"Forwarded\", fmt.Sprintf(\"host=%q;foo=bar\", ok)),\n\t\t},\n\t\t{\n\t\t\tname:   \"matching Forwarded (multiple fields)\",\n\t\t\theader: h(\"Forwarded\", fmt.Sprintf(\"host=%q\", ok), \"host=\"+nook),\n\t\t},\n\t\t{\n\t\t\tname:   \"matching Forwarded (chain value)\",\n\t\t\theader: h(\"Forwarded\", fmt.Sprintf(\"host=%q, host=%s\", ok, nook)),\n\t\t},\n\t\t{\n\t\t\tname:   \"Forwarded mismatch\",\n\t\t\theader: h(\"Forwarded\", \"host=\"+nook),\n\t\t\tisErr:  true,\n\t\t},\n\t\t{\n\t\t\tname:   \"Forwarded mismatch (missing information)\",\n\t\t\theader: h(\"Forwarded\", \"for=127.0.0.1\"),\n\t\t\tisErr:  true,\n\t\t},\n\t\t{\n\t\t\tname:   \"Forwarded mismatch (multiple fields)\",\n\t\t\theader: h(\"Forwarded\", \"host=\"+nook, fmt.Sprintf(\"host=%q\", ok)),\n\t\t\tisErr:  true,\n\t\t},\n\t\t{\n\t\t\tname:   \"Forwarded mismatch (chain value)\",\n\t\t\theader: h(\"Forwarded\", fmt.Sprintf(\"host=%s, host=%q\", nook, ok)),\n\t\t\tisErr:  true,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\ttestServeWithProxy(t, test.header, test.extra, test.isErr)\n\t\t})\n\t}\n}\n\nfunc testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectError bool) {\n\tt.Helper()\n\n\tserver := tester.MockACMEServer().BuildHTTPS(t)\n\n\tproviderServer := NewProviderServer(\"localhost\", \"23457\")\n\tif header != nil {\n\t\tproviderServer.SetProxyHeader(header.name)\n\t}\n\n\tvalidate := func(_ *api.Core, _ string, chlng acme.Challenge) error {\n\t\turi := \"http://\" + providerServer.GetAddress() + ChallengePath(chlng.Token)\n\n\t\treq, err := http.NewRequest(http.MethodGet, uri, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\theader.update(req)\n\t\textra.update(req)\n\n\t\tresp, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tif want := \"text/plain\"; resp.Header.Get(\"Content-Type\") != want {\n\t\t\treturn fmt.Errorf(\"Get(%q) Content-Type: got %q, want %q\", uri, resp.Header.Get(\"Content-Type\"), want)\n\t\t}\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tbodyStr := string(body)\n\n\t\tif bodyStr != chlng.KeyAuthorization {\n\t\t\treturn fmt.Errorf(\"Get(%q) Body: got %q, want %q\", uri, bodyStr, chlng.KeyAuthorization)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", privateKey)\n\trequire.NoError(t, err)\n\n\tsolver := NewChallenge(core, validate, providerServer)\n\n\tauthz := acme.Authorization{\n\t\tIdentifier: acme.Identifier{\n\t\t\tValue: \"localhost:23457\",\n\t\t},\n\t\tChallenges: []acme.Challenge{\n\t\t\t{Type: challenge.HTTP01.String(), Token: \"http1\"},\n\t\t},\n\t}\n\n\terr = solver.Solve(authz)\n\tif expectError {\n\t\trequire.Error(t, err)\n\t} else {\n\t\trequire.NoError(t, err)\n\t}\n}\n"
  },
  {
    "path": "challenge/provider.go",
    "content": "package challenge\n\nimport \"time\"\n\n// Provider enables implementing a custom challenge\n// provider. Present presents the solution to a challenge available to\n// be solved. CleanUp will be called by the challenge if Present ends\n// in a non-error state.\ntype Provider interface {\n\tPresent(domain, token, keyAuth string) error\n\tCleanUp(domain, token, keyAuth string) error\n}\n\n// ProviderTimeout allows for implementing a\n// Provider where an unusually long timeout is required when\n// waiting for an ACME challenge to be satisfied, such as when\n// checking for DNS record propagation. If an implementor of a\n// Provider provides a Timeout method, then the return values\n// of the Timeout method will be used when appropriate by the acme\n// package. The interval value is the time between checks.\n//\n// The default values used for timeout and interval are 60 seconds and\n// 2 seconds respectively. These are used when no Timeout method is\n// defined for the Provider.\ntype ProviderTimeout interface {\n\tProvider\n\tTimeout() (timeout, interval time.Duration)\n}\n"
  },
  {
    "path": "challenge/resolver/errors.go",
    "content": "package resolver\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"maps\"\n\t\"slices\"\n\t\"sort\"\n)\n\n// obtainError is returned when there are specific errors available per domain.\ntype obtainError map[string]error\n\nfunc (e obtainError) Error() string {\n\tbuffer := bytes.NewBufferString(\"error: one or more domains had a problem:\\n\")\n\n\tvar domains []string\n\tfor domain := range e {\n\t\tdomains = append(domains, domain)\n\t}\n\n\tsort.Strings(domains)\n\n\tfor _, domain := range domains {\n\t\t_, _ = fmt.Fprintf(buffer, \"[%s] %s\\n\", domain, e[domain])\n\t}\n\n\treturn buffer.String()\n}\n\nfunc (e obtainError) Unwrap() []error {\n\treturn slices.AppendSeq(make([]error, 0, len(e)), maps.Values(e))\n}\n"
  },
  {
    "path": "challenge/resolver/errors_test.go",
    "content": "package resolver\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_obtainError_Error(t *testing.T) {\n\terr := obtainError{\n\t\t\"a\": &acme.ProblemDetails{Type: \"001\"},\n\t\t\"b\": errors.New(\"oops\"),\n\t\t\"c\": errors.New(\"I did it again\"),\n\t}\n\n\trequire.EqualError(t, err, `error: one or more domains had a problem:\n[a] acme: error: 0 :: 001 :: \n[b] oops\n[c] I did it again\n`)\n}\n\nfunc Test_obtainError_Unwrap(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc   string\n\t\terr    obtainError\n\t\tassert assert.BoolAssertionFunc\n\t}{\n\t\t{\n\t\t\tdesc: \"one ok\",\n\t\t\terr: obtainError{\n\t\t\t\t\"a\": &acme.ProblemDetails{},\n\t\t\t\t\"b\": errors.New(\"oops\"),\n\t\t\t\t\"c\": errors.New(\"I did it again\"),\n\t\t\t},\n\t\t\tassert: assert.True,\n\t\t},\n\t\t{\n\t\t\tdesc: \"all ok\",\n\t\t\terr: obtainError{\n\t\t\t\t\"a\": &acme.ProblemDetails{Type: \"001\"},\n\t\t\t\t\"b\": &acme.ProblemDetails{Type: \"002\"},\n\t\t\t\t\"c\": &acme.ProblemDetails{Type: \"002\"},\n\t\t\t},\n\t\t\tassert: assert.True,\n\t\t},\n\t\t{\n\t\t\tdesc: \"nope\",\n\t\t\terr: obtainError{\n\t\t\t\t\"a\": errors.New(\"hello\"),\n\t\t\t\t\"b\": errors.New(\"oops\"),\n\t\t\t\t\"c\": errors.New(\"I did it again\"),\n\t\t\t},\n\t\t\tassert: assert.False,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar pd *acme.ProblemDetails\n\n\t\t\ttest.assert(t, errors.As(test.err, &pd))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "challenge/resolver/prober.go",
    "content": "package resolver\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/log\"\n)\n\n// Interface for all challenge solvers to implement.\ntype solver interface {\n\tSolve(authorization acme.Authorization) error\n}\n\n// Interface for challenges like dns, where we can set a record in advance for ALL challenges.\n// This saves quite a bit of time vs creating the records and solving them serially.\ntype preSolver interface {\n\tPreSolve(authorization acme.Authorization) error\n}\n\n// Interface for challenges like dns, where we can solve all the challenges before to delete them.\ntype cleanup interface {\n\tCleanUp(authorization acme.Authorization) error\n}\n\ntype sequential interface {\n\tSequential() (bool, time.Duration)\n}\n\n// an authz with the solver we have chosen and the index of the challenge associated with it.\ntype selectedAuthSolver struct {\n\tauthz  acme.Authorization\n\tsolver solver\n}\n\ntype Prober struct {\n\tsolverManager *SolverManager\n}\n\nfunc NewProber(solverManager *SolverManager) *Prober {\n\treturn &Prober{\n\t\tsolverManager: solverManager,\n\t}\n}\n\n// Solve Looks through the challenge combinations to find a solvable match.\n// Then solves the challenges in series and returns.\nfunc (p *Prober) Solve(authorizations []acme.Authorization) error {\n\tfailures := make(obtainError)\n\n\tvar (\n\t\tauthSolvers           []*selectedAuthSolver\n\t\tauthSolversSequential []*selectedAuthSolver\n\t)\n\n\t// Loop through the resources, basically through the domains.\n\t// First pass just selects a solver for each authz.\n\n\tfor _, authz := range authorizations {\n\t\tdomain := challenge.GetTargetedDomain(authz)\n\t\tif authz.Status == acme.StatusValid {\n\t\t\t// Boulder might recycle recent validated authz (see issue #267)\n\t\t\tlog.Infof(\"[%s] acme: authorization already valid; skipping challenge\", domain)\n\t\t\tcontinue\n\t\t}\n\n\t\tif solvr := p.solverManager.chooseSolver(authz); solvr != nil {\n\t\t\tauthSolver := &selectedAuthSolver{authz: authz, solver: solvr}\n\n\t\t\tswitch s := solvr.(type) {\n\t\t\tcase sequential:\n\t\t\t\tif ok, _ := s.Sequential(); ok {\n\t\t\t\t\tauthSolversSequential = append(authSolversSequential, authSolver)\n\t\t\t\t} else {\n\t\t\t\t\tauthSolvers = append(authSolvers, authSolver)\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tauthSolvers = append(authSolvers, authSolver)\n\t\t\t}\n\t\t} else {\n\t\t\tfailures[domain] = fmt.Errorf(\"[%s] acme: could not determine solvers\", domain)\n\t\t}\n\t}\n\n\tparallelSolve(authSolvers, failures)\n\n\tsequentialSolve(authSolversSequential, failures)\n\n\t// Be careful not to return an empty failures map,\n\t// for even an empty obtainError is a non-nil error value\n\tif len(failures) > 0 {\n\t\treturn failures\n\t}\n\n\treturn nil\n}\n\nfunc sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) {\n\t// Some CA are using the same token,\n\t// this can be a problem with the DNS01 challenge when the DNS provider doesn't support duplicate TXT records.\n\t// In the sequential mode, this is not a problem because we can solve the challenges in order.\n\t// But it can reduce the number of call the DNS provider APIs.\n\tuniq := make(map[string]struct{})\n\n\tfor i, authSolver := range authSolvers {\n\t\t// Submit the challenge\n\t\tdomain := challenge.GetTargetedDomain(authSolver.authz)\n\n\t\tchlg, _ := challenge.FindChallenge(challenge.DNS01, authSolver.authz)\n\n\t\tif solvr, ok := authSolver.solver.(preSolver); ok {\n\t\t\tif _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok && chlg.Token != \"\" {\n\t\t\t\tlog.Infof(\"acme: duplicate token for %q (DNS-01); skipping pre-solve.\", authSolver.authz.Identifier.Value)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\terr := solvr.PreSolve(authSolver.authz)\n\t\t\tif err != nil {\n\t\t\t\tfailures[domain] = err\n\n\t\t\t\tcleanUp(authSolver.solver, authSolver.authz)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tuniq[authSolver.authz.Identifier.Value+chlg.Token] = struct{}{}\n\t\t}\n\n\t\t// Solve challenge\n\t\terr := authSolver.solver.Solve(authSolver.authz)\n\t\tif err != nil {\n\t\t\tfailures[domain] = err\n\n\t\t\tcleanUp(authSolver.solver, authSolver.authz)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok || chlg.Token == \"\" {\n\t\t\t// Clean challenge\n\t\t\tcleanUp(authSolver.solver, authSolver.authz)\n\n\t\t\tif len(authSolvers)-1 > i {\n\t\t\t\tsolvr := authSolver.solver.(sequential)\n\t\t\t\t_, interval := solvr.Sequential()\n\t\t\t\tlog.Infof(\"sequence: wait for %s\", interval)\n\t\t\t\ttime.Sleep(interval)\n\t\t\t}\n\n\t\t\tdelete(uniq, authSolver.authz.Identifier.Value+chlg.Token)\n\t\t} else {\n\t\t\tlog.Infof(\"acme: duplicate token for %q (DNS-01); skipping cleanup.\", authSolver.authz.Identifier.Value)\n\t\t}\n\t}\n}\n\nfunc parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) {\n\t// Some CA are using the same token,\n\t// this can be a problem with the DNS01 challenge when the DNS provider doesn't support duplicate TXT records.\n\tuniq := make(map[string]struct{})\n\n\t// For all valid preSolvers, first submit the challenges, so they have max time to propagate\n\tfor _, authSolver := range authSolvers {\n\t\tauthz := authSolver.authz\n\n\t\tchlg, err := challenge.FindChallenge(challenge.DNS01, authz)\n\t\tif err == nil {\n\t\t\tif _, ok := uniq[authz.Identifier.Value+chlg.Token]; ok {\n\t\t\t\tlog.Infof(\"acme: duplicate token for %q (DNS-01); skipping pre-solve.\", authSolver.authz.Identifier.Value)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tuniq[authz.Identifier.Value+chlg.Token] = struct{}{}\n\t\t}\n\n\t\tif solvr, ok := authSolver.solver.(preSolver); ok {\n\t\t\terr := solvr.PreSolve(authz)\n\t\t\tif err != nil {\n\t\t\t\tfailures[challenge.GetTargetedDomain(authz)] = err\n\t\t\t}\n\t\t}\n\t}\n\n\tdefer func() {\n\t\t// Clean all created TXT records\n\t\tfor _, authSolver := range authSolvers {\n\t\t\tchlg, err := challenge.FindChallenge(challenge.DNS01, authSolver.authz)\n\t\t\tif err == nil {\n\t\t\t\tif _, ok := uniq[authSolver.authz.Identifier.Value+chlg.Token]; ok {\n\t\t\t\t\tdelete(uniq, authSolver.authz.Identifier.Value+chlg.Token)\n\t\t\t\t} else {\n\t\t\t\t\tlog.Infof(\"acme: duplicate token for %q (DNS-01); skipping cleanup.\", authSolver.authz.Identifier.Value)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcleanUp(authSolver.solver, authSolver.authz)\n\t\t}\n\t}()\n\n\t// Finally solve all challenges for real\n\tfor _, authSolver := range authSolvers {\n\t\tauthz := authSolver.authz\n\n\t\tdomain := challenge.GetTargetedDomain(authz)\n\t\tif failures[domain] != nil {\n\t\t\t// already failed in previous loop\n\t\t\tcontinue\n\t\t}\n\n\t\terr := authSolver.solver.Solve(authz)\n\t\tif err != nil {\n\t\t\tfailures[domain] = err\n\t\t}\n\t}\n}\n\nfunc cleanUp(solvr solver, authz acme.Authorization) {\n\tif solvr, ok := solvr.(cleanup); ok {\n\t\tdomain := challenge.GetTargetedDomain(authz)\n\n\t\terr := solvr.CleanUp(authz)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"[%s] acme: cleaning up failed: %v \", domain, err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "challenge/resolver/prober_mock_test.go",
    "content": "package resolver\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n)\n\ntype preSolverMock struct {\n\tpreSolve map[string]error\n\tsolve    map[string]error\n\tcleanUp  map[string]error\n\n\tpreSolveCounter int\n\tsolveCounter    int\n\tcleanUpCounter  int\n}\n\nfunc (s *preSolverMock) PreSolve(authorization acme.Authorization) error {\n\ts.preSolveCounter++\n\n\treturn s.preSolve[authorization.Identifier.Value]\n}\n\nfunc (s *preSolverMock) Solve(authorization acme.Authorization) error {\n\ts.solveCounter++\n\n\treturn s.solve[authorization.Identifier.Value]\n}\n\nfunc (s *preSolverMock) CleanUp(authorization acme.Authorization) error {\n\ts.cleanUpCounter++\n\n\treturn s.cleanUp[authorization.Identifier.Value]\n}\n\nfunc (s *preSolverMock) String() string {\n\treturn fmt.Sprintf(\"PreSolve: %d, Solve: %d, CleanUp: %d\", s.preSolveCounter, s.solveCounter, s.cleanUpCounter)\n}\n\nfunc createStubAuthorizationHTTP01(domain, status string) acme.Authorization {\n\treturn createStubAuthorization(domain, status, false, acme.Challenge{\n\t\tType:      challenge.HTTP01.String(),\n\t\tValidated: time.Now(),\n\t})\n}\n\nfunc createStubAuthorizationDNS01(domain string, wildcard bool) acme.Authorization {\n\tvar chlgs []acme.Challenge\n\n\tif wildcard {\n\t\tchlgs = append(chlgs, acme.Challenge{\n\t\t\tType:      challenge.HTTP01.String(),\n\t\t\tValidated: time.Now(),\n\t\t})\n\t}\n\n\tchlgs = append(chlgs, acme.Challenge{\n\t\tType:      challenge.DNS01.String(),\n\t\tValidated: time.Now(),\n\t})\n\n\treturn createStubAuthorization(domain, acme.StatusProcessing, wildcard, chlgs...)\n}\n\nfunc createStubAuthorization(domain, status string, wildcard bool, chlgs ...acme.Challenge) acme.Authorization {\n\treturn acme.Authorization{\n\t\tWildcard: wildcard,\n\t\tStatus:   status,\n\t\tExpires:  time.Now(),\n\t\tIdentifier: acme.Identifier{\n\t\t\tType:  \"dns\",\n\t\t\tValue: domain,\n\t\t},\n\t\tChallenges: chlgs,\n\t}\n}\n"
  },
  {
    "path": "challenge/resolver/prober_test.go",
    "content": "package resolver\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestProber_Solve(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc             string\n\t\tsolvers          map[challenge.Type]solver\n\t\tauthz            []acme.Authorization\n\t\texpectedError    string\n\t\texpectedCounters map[challenge.Type]string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tsolvers: map[challenge.Type]solver{\n\t\t\t\tchallenge.HTTP01: &preSolverMock{\n\t\t\t\t\tpreSolve: map[string]error{},\n\t\t\t\t\tsolve:    map[string]error{},\n\t\t\t\t\tcleanUp:  map[string]error{},\n\t\t\t\t},\n\t\t\t},\n\t\t\tauthz: []acme.Authorization{\n\t\t\t\tcreateStubAuthorizationHTTP01(\"example.com\", acme.StatusProcessing),\n\t\t\t\tcreateStubAuthorizationHTTP01(\"example.org\", acme.StatusProcessing),\n\t\t\t\tcreateStubAuthorizationHTTP01(\"example.net\", acme.StatusProcessing),\n\t\t\t},\n\t\t\texpectedCounters: map[challenge.Type]string{\n\t\t\t\tchallenge.HTTP01: \"PreSolve: 3, Solve: 3, CleanUp: 3\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"DNS-01 deduplicate\",\n\t\t\tsolvers: map[challenge.Type]solver{\n\t\t\t\tchallenge.DNS01: &preSolverMock{\n\t\t\t\t\tpreSolve: map[string]error{},\n\t\t\t\t\tsolve:    map[string]error{},\n\t\t\t\t\tcleanUp:  map[string]error{},\n\t\t\t\t},\n\t\t\t},\n\t\t\tauthz: []acme.Authorization{\n\t\t\t\tcreateStubAuthorizationDNS01(\"a.example\", false),\n\t\t\t\tcreateStubAuthorizationDNS01(\"a.example\", true),\n\t\t\t\tcreateStubAuthorizationDNS01(\"b.example\", false),\n\t\t\t\tcreateStubAuthorizationDNS01(\"b.example\", true),\n\t\t\t\tcreateStubAuthorizationDNS01(\"c.example\", true),\n\t\t\t\tcreateStubAuthorizationDNS01(\"d.example\", false),\n\t\t\t},\n\t\t\texpectedCounters: map[challenge.Type]string{\n\t\t\t\tchallenge.DNS01: \"PreSolve: 4, Solve: 6, CleanUp: 4\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"already valid\",\n\t\t\tsolvers: map[challenge.Type]solver{\n\t\t\t\tchallenge.HTTP01: &preSolverMock{\n\t\t\t\t\tpreSolve: map[string]error{},\n\t\t\t\t\tsolve:    map[string]error{},\n\t\t\t\t\tcleanUp:  map[string]error{},\n\t\t\t\t},\n\t\t\t},\n\t\t\tauthz: []acme.Authorization{\n\t\t\t\tcreateStubAuthorizationHTTP01(\"example.com\", acme.StatusValid),\n\t\t\t\tcreateStubAuthorizationHTTP01(\"example.org\", acme.StatusValid),\n\t\t\t\tcreateStubAuthorizationHTTP01(\"example.net\", acme.StatusValid),\n\t\t\t},\n\t\t\texpectedCounters: map[challenge.Type]string{\n\t\t\t\tchallenge.HTTP01: \"PreSolve: 0, Solve: 0, CleanUp: 0\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"when preSolve fail, auth is flagged as error and skipped\",\n\t\t\tsolvers: map[challenge.Type]solver{\n\t\t\t\tchallenge.HTTP01: &preSolverMock{\n\t\t\t\t\tpreSolve: map[string]error{\n\t\t\t\t\t\t\"example.com\": errors.New(\"preSolve error example.com\"),\n\t\t\t\t\t},\n\t\t\t\t\tsolve: map[string]error{\n\t\t\t\t\t\t\"example.com\": errors.New(\"solve error example.com\"),\n\t\t\t\t\t},\n\t\t\t\t\tcleanUp: map[string]error{\n\t\t\t\t\t\t\"example.com\": errors.New(\"clean error example.com\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tauthz: []acme.Authorization{\n\t\t\t\tcreateStubAuthorizationHTTP01(\"example.com\", acme.StatusProcessing),\n\t\t\t\tcreateStubAuthorizationHTTP01(\"example.org\", acme.StatusProcessing),\n\t\t\t\tcreateStubAuthorizationHTTP01(\"example.net\", acme.StatusProcessing),\n\t\t\t},\n\t\t\texpectedError: `error: one or more domains had a problem:\n[example.com] preSolve error example.com\n`,\n\t\t\texpectedCounters: map[challenge.Type]string{\n\t\t\t\tchallenge.HTTP01: \"PreSolve: 3, Solve: 2, CleanUp: 3\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"errors at different stages\",\n\t\t\tsolvers: map[challenge.Type]solver{\n\t\t\t\tchallenge.HTTP01: &preSolverMock{\n\t\t\t\t\tpreSolve: map[string]error{\n\t\t\t\t\t\t\"example.com\": errors.New(\"preSolve error example.com\"),\n\t\t\t\t\t},\n\t\t\t\t\tsolve: map[string]error{\n\t\t\t\t\t\t\"example.com\": errors.New(\"solve error example.com\"),\n\t\t\t\t\t\t\"example.org\": errors.New(\"solve error example.org\"),\n\t\t\t\t\t},\n\t\t\t\t\tcleanUp: map[string]error{\n\t\t\t\t\t\t\"example.net\": errors.New(\"clean error example.net\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tauthz: []acme.Authorization{\n\t\t\t\tcreateStubAuthorizationHTTP01(\"example.com\", acme.StatusProcessing),\n\t\t\t\tcreateStubAuthorizationHTTP01(\"example.org\", acme.StatusProcessing),\n\t\t\t\tcreateStubAuthorizationHTTP01(\"example.net\", acme.StatusProcessing),\n\t\t\t},\n\t\t\texpectedError: `error: one or more domains had a problem:\n[example.com] preSolve error example.com\n[example.org] solve error example.org\n`,\n\t\t\texpectedCounters: map[challenge.Type]string{\n\t\t\t\tchallenge.HTTP01: \"PreSolve: 3, Solve: 2, CleanUp: 3\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tprober := &Prober{\n\t\t\t\tsolverManager: &SolverManager{solvers: test.solvers},\n\t\t\t}\n\n\t\t\terr := prober.Solve(test.authz)\n\t\t\tif test.expectedError != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expectedError)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tfor n, s := range test.solvers {\n\t\t\t\tassert.Equal(t, test.expectedCounters[n], fmt.Sprintf(\"%s\", s))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "challenge/resolver/solver_manager.go",
    "content": "package resolver\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/challenge/http01\"\n\t\"github.com/go-acme/lego/v4/challenge/tlsalpn01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n)\n\ntype byType []acme.Challenge\n\nfunc (a byType) Len() int           { return len(a) }\nfunc (a byType) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }\nfunc (a byType) Less(i, j int) bool { return a[i].Type > a[j].Type }\n\ntype SolverManager struct {\n\tcore    *api.Core\n\tsolvers map[challenge.Type]solver\n}\n\nfunc NewSolversManager(core *api.Core) *SolverManager {\n\treturn &SolverManager{\n\t\tsolvers: map[challenge.Type]solver{},\n\t\tcore:    core,\n\t}\n}\n\n// SetHTTP01Provider specifies a custom provider p that can solve the given HTTP-01 challenge.\nfunc (c *SolverManager) SetHTTP01Provider(p challenge.Provider, opts ...http01.ChallengeOption) error {\n\tc.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p, opts...)\n\treturn nil\n}\n\n// SetTLSALPN01Provider specifies a custom provider p that can solve the given TLS-ALPN-01 challenge.\nfunc (c *SolverManager) SetTLSALPN01Provider(p challenge.Provider, opts ...tlsalpn01.ChallengeOption) error {\n\tc.solvers[challenge.TLSALPN01] = tlsalpn01.NewChallenge(c.core, validate, p, opts...)\n\treturn nil\n}\n\n// SetDNS01Provider specifies a custom provider p that can solve the given DNS-01 challenge.\nfunc (c *SolverManager) SetDNS01Provider(p challenge.Provider, opts ...dns01.ChallengeOption) error {\n\tc.solvers[challenge.DNS01] = dns01.NewChallenge(c.core, validate, p, opts...)\n\treturn nil\n}\n\n// Remove removes a challenge type from the available solvers.\nfunc (c *SolverManager) Remove(chlgType challenge.Type) {\n\tdelete(c.solvers, chlgType)\n}\n\n// Checks all challenges from the server in order and returns the first matching solver.\nfunc (c *SolverManager) chooseSolver(authz acme.Authorization) solver {\n\t// Allow to have a deterministic challenge order\n\tsort.Sort(byType(authz.Challenges))\n\n\tdomain := challenge.GetTargetedDomain(authz)\n\tfor _, chlg := range authz.Challenges {\n\t\tif solvr, ok := c.solvers[challenge.Type(chlg.Type)]; ok {\n\t\t\tlog.Infof(\"[%s] acme: use %s solver\", domain, chlg.Type)\n\t\t\treturn solvr\n\t\t}\n\n\t\tlog.Infof(\"[%s] acme: Could not find solver for: %s\", domain, chlg.Type)\n\t}\n\n\treturn nil\n}\n\nfunc validate(core *api.Core, domain string, chlg acme.Challenge) error {\n\tchlng, err := core.Challenges.New(chlg.URL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to initiate challenge: %w\", err)\n\t}\n\n\tvalid, err := checkChallengeStatus(chlng)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif valid {\n\t\tlog.Infof(\"[%s] The server validated our request\", domain)\n\t\treturn nil\n\t}\n\n\tretryAfter, err := api.ParseRetryAfter(chlng.RetryAfter)\n\tif err != nil || retryAfter == 0 {\n\t\t// The ACME server MUST return a Retry-After.\n\t\t// If it doesn't, or if it's invalid, we'll just poll hard.\n\t\t// Boulder does not implement the ability to retry challenges or the Retry-After header.\n\t\t// https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md#section-82\n\t\tretryAfter = 5 * time.Second\n\t}\n\n\tctx := context.Background()\n\n\tbo := backoff.NewExponentialBackOff()\n\tbo.InitialInterval = retryAfter\n\tbo.MaxInterval = 10 * retryAfter\n\n\t// After the path is sent, the ACME server will access our server.\n\t// Repeatedly check the server for an updated status on our request.\n\toperation := func() error {\n\t\tauthz, err := core.Authorizations.Get(chlng.AuthorizationURL)\n\t\tif err != nil {\n\t\t\treturn backoff.Permanent(err)\n\t\t}\n\n\t\tvalid, err := checkAuthorizationStatus(authz)\n\t\tif err != nil {\n\t\t\treturn backoff.Permanent(err)\n\t\t}\n\n\t\tif valid {\n\t\t\tlog.Infof(\"[%s] The server validated our request\", domain)\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"the server didn't respond to our request (status=%s)\", authz.Status)\n\t}\n\n\treturn wait.Retry(ctx, operation,\n\t\tbackoff.WithBackOff(bo),\n\t\tbackoff.WithMaxElapsedTime(100*retryAfter))\n}\n\nfunc checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) {\n\tswitch chlng.Status {\n\tcase acme.StatusValid:\n\t\treturn true, nil\n\tcase acme.StatusPending, acme.StatusProcessing:\n\t\treturn false, nil\n\tcase acme.StatusInvalid:\n\t\treturn false, fmt.Errorf(\"invalid challenge: %w\", chlng.Err())\n\tdefault:\n\t\treturn false, fmt.Errorf(\"the server returned an unexpected challenge status: %s\", chlng.Status)\n\t}\n}\n\nfunc checkAuthorizationStatus(authz acme.Authorization) (bool, error) {\n\tswitch authz.Status {\n\tcase acme.StatusValid:\n\t\treturn true, nil\n\tcase acme.StatusPending, acme.StatusProcessing:\n\t\treturn false, nil\n\tcase acme.StatusDeactivated, acme.StatusExpired, acme.StatusRevoked:\n\t\treturn false, fmt.Errorf(\"the authorization state %s\", authz.Status)\n\tcase acme.StatusInvalid:\n\t\tfor _, chlg := range authz.Challenges {\n\t\t\tif chlg.Status == acme.StatusInvalid && chlg.Error != nil {\n\t\t\t\treturn false, fmt.Errorf(\"invalid authorization: %w\", chlg.Err())\n\t\t\t}\n\t\t}\n\n\t\treturn false, errors.New(\"invalid authorization\")\n\tdefault:\n\t\treturn false, fmt.Errorf(\"the server returned an unexpected authorization status: %s\", authz.Status)\n\t}\n}\n"
  },
  {
    "path": "challenge/resolver/solver_manager_test.go",
    "content": "package resolver\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-jose/go-jose/v4\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestByType(t *testing.T) {\n\tchallenges := []acme.Challenge{\n\t\t{Type: \"dns-01\"}, {Type: \"tlsalpn-01\"}, {Type: \"http-01\"},\n\t}\n\n\tsort.Sort(byType(challenges))\n\n\texpected := []acme.Challenge{\n\t\t{Type: \"tlsalpn-01\"}, {Type: \"http-01\"}, {Type: \"dns-01\"},\n\t}\n\n\tassert.Equal(t, expected, challenges)\n}\n\nfunc TestValidate(t *testing.T) {\n\tvar statuses []string\n\n\tprivateKey, _ := rsa.GenerateKey(rand.Reader, 1024)\n\n\tserver := tester.MockACMEServer().\n\t\tRoute(\"POST /chlg\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\tif err := validateNoBody(privateKey, req); err != nil {\n\t\t\t\t\thttp.Error(rw, err.Error(), http.StatusBadRequest)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\trw.Header().Set(\"Link\",\n\t\t\t\t\tfmt.Sprintf(`<https://%s/my-authz>; rel=\"up\"`, req.Context().Value(http.LocalAddrContextKey)))\n\n\t\t\t\tst := statuses[0]\n\t\t\t\tstatuses = statuses[1:]\n\n\t\t\t\tchlg := &acme.Challenge{Type: \"http-01\", Status: st, URL: \"http://example.com/\", Token: \"token\"}\n\n\t\t\t\tservermock.JSONEncode(chlg).ServeHTTP(rw, req)\n\t\t\t})).\n\t\tRoute(\"POST /my-authz\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\tst := statuses[0]\n\t\t\t\tstatuses = statuses[1:]\n\n\t\t\t\tauthorization := acme.Authorization{\n\t\t\t\t\tStatus:     st,\n\t\t\t\t\tChallenges: []acme.Challenge{},\n\t\t\t\t}\n\n\t\t\t\tif st == acme.StatusInvalid {\n\t\t\t\t\tchlg := acme.Challenge{\n\t\t\t\t\t\tStatus: acme.StatusInvalid,\n\t\t\t\t\t}\n\t\t\t\t\tauthorization.Challenges = append(authorization.Challenges, chlg)\n\t\t\t\t}\n\n\t\t\t\tservermock.JSONEncode(authorization).ServeHTTP(rw, req)\n\t\t\t})).\n\t\tBuildHTTPS(t)\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", privateKey)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tstatuses []string\n\t\twant     string\n\t}{\n\t\t{\n\t\t\tname:     \"POST-unexpected\",\n\t\t\tstatuses: []string{\"weird\"},\n\t\t\twant:     \"the server returned an unexpected challenge status: weird\",\n\t\t},\n\t\t{\n\t\t\tname:     \"POST-valid\",\n\t\t\tstatuses: []string{acme.StatusValid},\n\t\t},\n\t\t{\n\t\t\tname:     \"POST-invalid\",\n\t\t\tstatuses: []string{acme.StatusInvalid},\n\t\t\twant:     \"invalid challenge:\",\n\t\t},\n\t\t{\n\t\t\tname:     \"POST-pending-unexpected\",\n\t\t\tstatuses: []string{acme.StatusPending, \"weird\"},\n\t\t\twant:     \"the server returned an unexpected authorization status: weird\",\n\t\t},\n\t\t{\n\t\t\tname:     \"POST-pending-valid\",\n\t\t\tstatuses: []string{acme.StatusPending, acme.StatusValid},\n\t\t},\n\t\t{\n\t\t\tname:     \"POST-pending-invalid\",\n\t\t\tstatuses: []string{acme.StatusPending, acme.StatusInvalid},\n\t\t\twant:     \"invalid authorization\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tstatuses = test.statuses\n\n\t\t\terr := validate(core, \"example.com\", acme.Challenge{Type: \"http-01\", Token: \"token\", URL: server.URL + \"/chlg\"})\n\t\t\tif test.want == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), test.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_checkChallengeStatus(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc       string\n\t\tchallenge  acme.Challenge\n\t\trequireErr require.ErrorAssertionFunc\n\t\texpected   bool\n\t}{\n\t\t{\n\t\t\tdesc:       \"status valid\",\n\t\t\tchallenge:  acme.Challenge{Status: acme.StatusValid},\n\t\t\trequireErr: require.NoError,\n\t\t\texpected:   true,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"status invalid\",\n\t\t\tchallenge:  acme.Challenge{Status: acme.StatusInvalid},\n\t\t\trequireErr: require.Error,\n\t\t\texpected:   false,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"status invalid with error\",\n\t\t\tchallenge:  acme.Challenge{Status: acme.StatusInvalid, Error: &acme.ProblemDetails{}},\n\t\t\trequireErr: require.Error,\n\t\t\texpected:   false,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"status pending\",\n\t\t\tchallenge:  acme.Challenge{Status: acme.StatusPending},\n\t\t\trequireErr: require.NoError,\n\t\t\texpected:   false,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"status processing\",\n\t\t\tchallenge:  acme.Challenge{Status: acme.StatusProcessing},\n\t\t\trequireErr: require.NoError,\n\t\t\texpected:   false,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tstatus, err := checkChallengeStatus(acme.ExtendedChallenge{Challenge: test.challenge})\n\t\t\ttest.requireErr(t, err)\n\n\t\t\tassert.Equal(t, test.expected, status)\n\t\t})\n\t}\n}\n\nfunc Test_checkAuthorizationStatus(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tauthorization acme.Authorization\n\t\trequireErr    require.ErrorAssertionFunc\n\t\texpected      bool\n\t}{\n\t\t{\n\t\t\tdesc:          \"status valid\",\n\t\t\tauthorization: acme.Authorization{Status: acme.StatusValid},\n\t\t\trequireErr:    require.NoError,\n\t\t\texpected:      true,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"status invalid\",\n\t\t\tauthorization: acme.Authorization{Status: acme.StatusInvalid},\n\t\t\trequireErr:    require.Error,\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"status invalid with error\",\n\t\t\tauthorization: acme.Authorization{Status: acme.StatusInvalid, Challenges: []acme.Challenge{{Error: &acme.ProblemDetails{}}}},\n\t\t\trequireErr:    require.Error,\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"status pending\",\n\t\t\tauthorization: acme.Authorization{Status: acme.StatusPending},\n\t\t\trequireErr:    require.NoError,\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"status processing\",\n\t\t\tauthorization: acme.Authorization{Status: acme.StatusProcessing},\n\t\t\trequireErr:    require.NoError,\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"status deactivated\",\n\t\t\tauthorization: acme.Authorization{Status: acme.StatusDeactivated},\n\t\t\trequireErr:    require.Error,\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"status expired\",\n\t\t\tauthorization: acme.Authorization{Status: acme.StatusExpired},\n\t\t\trequireErr:    require.Error,\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"status revoked\",\n\t\t\tauthorization: acme.Authorization{Status: acme.StatusRevoked},\n\t\t\trequireErr:    require.Error,\n\t\t\texpected:      false,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tstatus, err := checkAuthorizationStatus(test.authorization)\n\t\t\ttest.requireErr(t, err)\n\n\t\t\tassert.Equal(t, test.expected, status)\n\t\t})\n\t}\n}\n\n// validateNoBody reads the http.Request POST body, parses the JWS and validates it to read the body.\n// If there is an error doing this,\n// or if the JWS body is not the empty JSON payload \"{}\" or a POST-as-GET payload \"\" an error is returned.\n// We use this to verify challenge POSTs to the ts below do not send a JWS body.\nfunc validateNoBody(privateKey *rsa.PrivateKey, r *http.Request) error {\n\treqBody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsigAlgs := []jose.SignatureAlgorithm{jose.RS256}\n\n\tjws, err := jose.ParseSigned(string(reqBody), sigAlgs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tbody, err := jws.Verify(&jose.JSONWebKey{\n\t\tKey:       privateKey.Public(),\n\t\tAlgorithm: \"RSA\",\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif bodyStr := string(body); bodyStr != \"{}\" && bodyStr != \"\" {\n\t\treturn fmt.Errorf(`expected JWS POST body \"{}\" or \"\", got %q`, bodyStr)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "challenge/tlsalpn01/tls_alpn_challenge.go",
    "content": "package tlsalpn01\n\nimport (\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/log\"\n)\n\n// idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension.\n// Reference: https://www.rfc-editor.org/rfc/rfc8737.html#section-6.1\nvar idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}\n\ntype ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error\n\ntype ChallengeOption func(*Challenge) error\n\n// SetDelay sets a delay between the start of the TLS listener and the challenge validation.\nfunc SetDelay(delay time.Duration) ChallengeOption {\n\treturn func(chlg *Challenge) error {\n\t\tchlg.delay = delay\n\t\treturn nil\n\t}\n}\n\ntype Challenge struct {\n\tcore     *api.Core\n\tvalidate ValidateFunc\n\tprovider challenge.Provider\n\tdelay    time.Duration\n}\n\nfunc NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge {\n\tchlg := &Challenge{\n\t\tcore:     core,\n\t\tvalidate: validate,\n\t\tprovider: provider,\n\t}\n\n\tfor _, opt := range opts {\n\t\terr := opt(chlg)\n\t\tif err != nil {\n\t\t\tlog.Infof(\"challenge option error: %v\", err)\n\t\t}\n\t}\n\n\treturn chlg\n}\n\nfunc (c *Challenge) SetProvider(provider challenge.Provider) {\n\tc.provider = provider\n}\n\n// Solve manages the provider to validate and solve the challenge.\nfunc (c *Challenge) Solve(authz acme.Authorization) error {\n\tdomain := authz.Identifier.Value\n\tlog.Infof(\"[%s] acme: Trying to solve TLS-ALPN-01\", challenge.GetTargetedDomain(authz))\n\n\tchlng, err := challenge.FindChallenge(challenge.TLSALPN01, authz)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Generate the Key Authorization for the challenge\n\tkeyAuth, err := c.core.GetKeyAuthorization(chlng.Token)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.provider.Present(domain, chlng.Token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"[%s] acme: error presenting token: %w\", challenge.GetTargetedDomain(authz), err)\n\t}\n\n\tdefer func() {\n\t\terr := c.provider.CleanUp(domain, chlng.Token, keyAuth)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"[%s] acme: cleaning up failed: %v\", challenge.GetTargetedDomain(authz), err)\n\t\t}\n\t}()\n\n\tif c.delay > 0 {\n\t\ttime.Sleep(c.delay)\n\t}\n\n\tchlng.KeyAuthorization = keyAuth\n\n\treturn c.validate(c.core, domain, chlng)\n}\n\n// ChallengeBlocks returns PEM blocks (certPEMBlock, keyPEMBlock) with the acmeValidation-v1 extension\n// and domain name for the `tls-alpn-01` challenge.\nfunc ChallengeBlocks(domain, keyAuth string) ([]byte, []byte, error) {\n\t// Compute the SHA-256 digest of the key authorization.\n\tzBytes := sha256.Sum256([]byte(keyAuth))\n\n\tvalue, err := asn1.Marshal(zBytes[:sha256.Size])\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Add the keyAuth digest as the acmeValidation-v1 extension\n\t// (marked as critical such that it won't be used by non-ACME software).\n\t// Reference: https://www.rfc-editor.org/rfc/rfc8737.html#section-3\n\textensions := []pkix.Extension{\n\t\t{\n\t\t\tId:       idPeAcmeIdentifierV1,\n\t\t\tCritical: true,\n\t\t\tValue:    value,\n\t\t},\n\t}\n\n\t// Generate a new RSA key for the certificates.\n\ttempPrivateKey, err := certcrypto.GeneratePrivateKey(certcrypto.RSA2048)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\trsaPrivateKey := tempPrivateKey.(*rsa.PrivateKey)\n\n\t// Generate the PEM certificate using the provided private key, domain, and extra extensions.\n\ttempCertPEM, err := certcrypto.GeneratePemCert(rsaPrivateKey, domain, extensions)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Encode the private key into a PEM format. We'll need to use it to generate the x509 keypair.\n\trsaPrivatePEM := certcrypto.PEMEncode(rsaPrivateKey)\n\n\treturn tempCertPEM, rsaPrivatePEM, nil\n}\n\n// ChallengeCert returns a certificate with the acmeValidation-v1 extension\n// and domain name for the `tls-alpn-01` challenge.\nfunc ChallengeCert(domain, keyAuth string) (*tls.Certificate, error) {\n\ttempCertPEM, rsaPrivatePEM, err := ChallengeBlocks(domain, keyAuth)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcert, err := tls.X509KeyPair(tempCertPEM, rsaPrivatePEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &cert, nil\n}\n"
  },
  {
    "path": "challenge/tlsalpn01/tls_alpn_challenge_server.go",
    "content": "package tlsalpn01\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/go-acme/lego/v4/log\"\n)\n\nconst (\n\t// ACMETLS1Protocol is the ALPN Protocol ID for the ACME-TLS/1 Protocol.\n\tACMETLS1Protocol = \"acme-tls/1\"\n\n\t// defaultTLSPort is the port that the ProviderServer will default to\n\t// when no other port is provided.\n\tdefaultTLSPort = \"443\"\n)\n\n// ProviderServer implements ChallengeProvider for `TLS-ALPN-01` challenge.\n// It may be instantiated without using the NewProviderServer\n// if you want only to use the default values.\ntype ProviderServer struct {\n\tiface    string\n\tport     string\n\tlistener net.Listener\n}\n\n// NewProviderServer creates a new ProviderServer on the selected interface and port.\n// Setting iface and / or port to an empty string will make the server fall back to\n// the \"any\" interface and port 443 respectively.\nfunc NewProviderServer(iface, port string) *ProviderServer {\n\treturn &ProviderServer{iface: iface, port: port}\n}\n\nfunc (s *ProviderServer) GetAddress() string {\n\treturn net.JoinHostPort(s.iface, s.port)\n}\n\n// Present generates a certificate with an SHA-256 digest of the keyAuth provided\n// as the acmeValidation-v1 extension value to conform to the ACME-TLS-ALPN spec.\nfunc (s *ProviderServer) Present(domain, token, keyAuth string) error {\n\tif s.port == \"\" {\n\t\t// Fallback to port 443 if the port was not provided.\n\t\ts.port = defaultTLSPort\n\t}\n\n\t// Generate the challenge certificate using the provided keyAuth and domain.\n\tcert, err := ChallengeCert(domain, keyAuth)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Place the generated certificate with the extension into the TLS config\n\t// so that it can serve the correct details.\n\ttlsConf := new(tls.Config)\n\ttlsConf.Certificates = []tls.Certificate{*cert}\n\n\t// We must set that the `acme-tls/1` application level protocol is supported\n\t// so that the protocol negotiation can succeed. Reference:\n\t// https://www.rfc-editor.org/rfc/rfc8737.html#section-6.2\n\ttlsConf.NextProtos = []string{ACMETLS1Protocol}\n\n\t// Create the listener with the created tls.Config.\n\ts.listener, err = tls.Listen(\"tcp\", s.GetAddress(), tlsConf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not start HTTPS server for challenge: %w\", err)\n\t}\n\n\t// Shut the server down when we're finished.\n\tgo func() {\n\t\terr := http.Serve(s.listener, nil)\n\t\tif err != nil && !strings.Contains(err.Error(), \"use of closed network connection\") {\n\t\t\tlog.Println(err)\n\t\t}\n\t}()\n\n\treturn nil\n}\n\n// CleanUp closes the HTTPS server.\nfunc (s *ProviderServer) CleanUp(domain, token, keyAuth string) error {\n\tif s.listener == nil {\n\t\treturn nil\n\t}\n\n\t// Server was created, close it.\n\tif err := s.listener.Close(); err != nil && errors.Is(err, http.ErrServerClosed) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "challenge/tlsalpn01/tls_alpn_challenge_test.go",
    "content": "package tlsalpn01\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"crypto/subtle\"\n\t\"crypto/tls\"\n\t\"encoding/asn1\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestChallenge(t *testing.T) {\n\tserver := tester.MockACMEServer().BuildHTTPS(t)\n\n\tdomain := \"localhost\"\n\tport := \"24457\"\n\n\tmockValidate := func(_ *api.Core, _ string, chlng acme.Challenge) error {\n\t\tconn, err := tls.Dial(\"tcp\", net.JoinHostPort(domain, port), &tls.Config{\n\t\t\tServerName:         domain,\n\t\t\tInsecureSkipVerify: true,\n\t\t})\n\t\trequire.NoError(t, err, \"Expected to connect to challenge server without an error\")\n\n\t\t// Expect the server to only return one certificate\n\t\tconnState := conn.ConnectionState()\n\t\tassert.Len(t, connState.PeerCertificates, 1, \"Expected the challenge server to return exactly one certificate\")\n\n\t\tremoteCert := connState.PeerCertificates[0]\n\t\tassert.Len(t, remoteCert.DNSNames, 1, \"Expected the challenge certificate to have exactly one DNSNames entry\")\n\t\tassert.Equal(t, domain, remoteCert.DNSNames[0], \"challenge certificate DNSName \")\n\t\tassert.NotEmpty(t, remoteCert.Extensions, \"Expected the challenge certificate to contain extensions\")\n\n\t\tidx := -1\n\n\t\tfor i, ext := range remoteCert.Extensions {\n\t\t\tif idPeAcmeIdentifierV1.Equal(ext.Id) {\n\t\t\t\tidx = i\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\trequire.NotEqual(t, -1, idx, \"Expected the challenge certificate to contain an extension with the id-pe-acmeIdentifier id,\")\n\n\t\text := remoteCert.Extensions[idx]\n\t\tassert.True(t, ext.Critical, \"Expected the challenge certificate id-pe-acmeIdentifier extension to be marked as critical\")\n\n\t\tzBytes := sha256.Sum256([]byte(chlng.KeyAuthorization))\n\t\tvalue, err := asn1.Marshal(zBytes[:sha256.Size])\n\t\trequire.NoError(t, err, \"Expected marshaling of the keyAuth to return no error\")\n\n\t\tif subtle.ConstantTimeCompare(value, ext.Value) != 1 {\n\t\t\tt.Errorf(\"Expected the challenge certificate id-pe-acmeIdentifier extension to contain the SHA-256 digest of the keyAuth, %v, but was %v\", zBytes[:], ext.Value)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", privateKey)\n\trequire.NoError(t, err)\n\n\tsolver := NewChallenge(\n\t\tcore,\n\t\tmockValidate,\n\t\t&ProviderServer{port: port},\n\t)\n\n\tauthz := acme.Authorization{\n\t\tIdentifier: acme.Identifier{\n\t\t\tType:  \"dns\",\n\t\t\tValue: domain,\n\t\t},\n\t\tChallenges: []acme.Challenge{\n\t\t\t{Type: challenge.TLSALPN01.String(), Token: \"tlsalpn1\"},\n\t\t},\n\t}\n\n\terr = solver.Solve(authz)\n\trequire.NoError(t, err)\n}\n\nfunc TestChallengeInvalidPort(t *testing.T) {\n\tserver := tester.MockACMEServer().BuildHTTPS(t)\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", privateKey)\n\trequire.NoError(t, err)\n\n\tsolver := NewChallenge(\n\t\tcore,\n\t\tfunc(_ *api.Core, _ string, _ acme.Challenge) error { return nil },\n\t\t&ProviderServer{port: \"123456\"},\n\t)\n\n\tauthz := acme.Authorization{\n\t\tIdentifier: acme.Identifier{\n\t\t\tValue: \"localhost:123456\",\n\t\t},\n\t\tChallenges: []acme.Challenge{\n\t\t\t{Type: challenge.TLSALPN01.String(), Token: \"tlsalpn1\"},\n\t\t},\n\t}\n\n\terr = solver.Solve(authz)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"invalid port\")\n\tassert.Contains(t, err.Error(), \"123456\")\n}\n\nfunc TestChallengeIPaddress(t *testing.T) {\n\tserver := tester.MockACMEServer().BuildHTTPS(t)\n\n\tdomain := \"127.0.0.1\"\n\tport := \"24457\"\n\trd, _ := dns.ReverseAddr(domain)\n\n\tmockValidate := func(_ *api.Core, _ string, chlng acme.Challenge) error {\n\t\tconn, err := tls.Dial(\"tcp\", net.JoinHostPort(domain, port), &tls.Config{\n\t\t\tServerName:         rd,\n\t\t\tInsecureSkipVerify: true,\n\t\t})\n\t\trequire.NoError(t, err, \"Expected to connect to challenge server without an error\")\n\n\t\t// Expect the server to only return one certificate\n\t\tconnState := conn.ConnectionState()\n\t\tassert.Len(t, connState.PeerCertificates, 1, \"Expected the challenge server to return exactly one certificate\")\n\n\t\tremoteCert := connState.PeerCertificates[0]\n\t\tassert.Empty(t, remoteCert.DNSNames, \"Expected the challenge certificate to have no DNSNames entry in context of challenge for IP\")\n\t\tassert.Len(t, remoteCert.IPAddresses, 1, \"Expected the challenge certificate to have exactly one IPAddresses entry\")\n\t\tassert.True(t, net.ParseIP(\"127.0.0.1\").Equal(remoteCert.IPAddresses[0]), \"challenge certificate IPAddress \")\n\t\tassert.NotEmpty(t, remoteCert.Extensions, \"Expected the challenge certificate to contain extensions\")\n\n\t\tvar (\n\t\t\tfoundAcmeIdentifier bool\n\t\t\textValue            []byte\n\t\t)\n\n\t\tfor _, ext := range remoteCert.Extensions {\n\t\t\tif idPeAcmeIdentifierV1.Equal(ext.Id) {\n\t\t\t\tassert.True(t, ext.Critical, \"Expected the challenge certificate id-pe-acmeIdentifier extension to be marked as critical\")\n\n\t\t\t\tfoundAcmeIdentifier = true\n\t\t\t\textValue = ext.Value\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\trequire.True(t, foundAcmeIdentifier, \"Expected the challenge certificate to contain an extension with the id-pe-acmeIdentifier id,\")\n\n\t\tzBytes := sha256.Sum256([]byte(chlng.KeyAuthorization))\n\t\tvalue, err := asn1.Marshal(zBytes[:sha256.Size])\n\t\trequire.NoError(t, err, \"Expected marshaling of the keyAuth to return no error\")\n\n\t\trequire.Equal(t, value, extValue, \"Expected the challenge certificate id-pe-acmeIdentifier extension to contain the SHA-256 digest of the keyAuth\")\n\n\t\treturn nil\n\t}\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", privateKey)\n\trequire.NoError(t, err)\n\n\tsolver := NewChallenge(\n\t\tcore,\n\t\tmockValidate,\n\t\t&ProviderServer{port: port},\n\t)\n\n\tauthz := acme.Authorization{\n\t\tIdentifier: acme.Identifier{\n\t\t\tType:  \"ip\",\n\t\t\tValue: domain,\n\t\t},\n\t\tChallenges: []acme.Challenge{\n\t\t\t{Type: challenge.TLSALPN01.String(), Token: \"tlsalpn1\"},\n\t\t},\n\t}\n\n\trequire.NoError(t, solver.Solve(authz))\n}\n"
  },
  {
    "path": "cmd/account.go",
    "content": "package cmd\n\nimport (\n\t\"crypto\"\n\n\t\"github.com/go-acme/lego/v4/registration\"\n)\n\n// Account represents a users local saved credentials.\ntype Account struct {\n\tEmail        string                 `json:\"email\"`\n\tRegistration *registration.Resource `json:\"registration\"`\n\tkey          crypto.PrivateKey\n}\n\n/** Implementation of the registration.User interface **/\n\n// GetEmail returns the email address for the account.\nfunc (a *Account) GetEmail() string {\n\treturn a.Email\n}\n\n// GetPrivateKey returns the private RSA account key.\nfunc (a *Account) GetPrivateKey() crypto.PrivateKey {\n\treturn a.key\n}\n\n// GetRegistration returns the server registration.\nfunc (a *Account) GetRegistration() *registration.Resource {\n\treturn a.Registration\n}\n"
  },
  {
    "path": "cmd/accounts_storage.go",
    "content": "package cmd\n\nimport (\n\t\"crypto\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/go-acme/lego/v4/lego\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/registration\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nconst userIDPlaceholder = \"noemail@example.com\"\n\nconst (\n\tbaseAccountsRootFolderName = \"accounts\"\n\tbaseKeysFolderName         = \"keys\"\n\taccountFileName            = \"account.json\"\n)\n\n// AccountsStorage A storage for account data.\n//\n// rootPath:\n//\n//\t./.lego/accounts/\n//\t     │      └── root accounts directory\n//\t     └── \"path\" option\n//\n// rootUserPath:\n//\n//\t./.lego/accounts/localhost_14000/foo@example.com/\n//\t     │      │             │             └── userID (\"email\" option)\n//\t     │      │             └── CA server (\"server\" option)\n//\t     │      └── root accounts directory\n//\t     └── \"path\" option\n//\n// keysPath:\n//\n//\t./.lego/accounts/localhost_14000/foo@example.com/keys/\n//\t     │      │             │             │           └── root keys directory\n//\t     │      │             │             └── userID (\"email\" option)\n//\t     │      │             └── CA server (\"server\" option)\n//\t     │      └── root accounts directory\n//\t     └── \"path\" option\n//\n// accountFilePath:\n//\n//\t./.lego/accounts/localhost_14000/foo@example.com/account.json\n//\t     │      │             │             │             └── account file\n//\t     │      │             │             └── userID (\"email\" option)\n//\t     │      │             └── CA server (\"server\" option)\n//\t     │      └── root accounts directory\n//\t     └── \"path\" option\ntype AccountsStorage struct {\n\tuserID          string\n\temail           string\n\trootPath        string\n\trootUserPath    string\n\tkeysPath        string\n\taccountFilePath string\n\tctx             *cli.Context\n}\n\n// NewAccountsStorage Creates a new AccountsStorage.\nfunc NewAccountsStorage(ctx *cli.Context) *AccountsStorage {\n\t// TODO: move to account struct?\n\temail := ctx.String(flgEmail)\n\n\tuserID := email\n\tif userID == \"\" {\n\t\tuserID = userIDPlaceholder\n\t}\n\n\tserverURL, err := url.Parse(ctx.String(flgServer))\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\trootPath := filepath.Join(ctx.String(flgPath), baseAccountsRootFolderName)\n\tserverPath := strings.NewReplacer(\":\", \"_\", \"/\", string(os.PathSeparator)).Replace(serverURL.Host)\n\taccountsPath := filepath.Join(rootPath, serverPath)\n\trootUserPath := filepath.Join(accountsPath, userID)\n\n\treturn &AccountsStorage{\n\t\tuserID:          userID,\n\t\temail:           email,\n\t\trootPath:        rootPath,\n\t\trootUserPath:    rootUserPath,\n\t\tkeysPath:        filepath.Join(rootUserPath, baseKeysFolderName),\n\t\taccountFilePath: filepath.Join(rootUserPath, accountFileName),\n\t\tctx:             ctx,\n\t}\n}\n\nfunc (s *AccountsStorage) ExistsAccountFilePath() bool {\n\taccountFile := filepath.Join(s.rootUserPath, accountFileName)\n\tif _, err := os.Stat(accountFile); os.IsNotExist(err) {\n\t\treturn false\n\t} else if err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\treturn true\n}\n\nfunc (s *AccountsStorage) GetRootPath() string {\n\treturn s.rootPath\n}\n\nfunc (s *AccountsStorage) GetRootUserPath() string {\n\treturn s.rootUserPath\n}\n\nfunc (s *AccountsStorage) GetUserID() string {\n\treturn s.userID\n}\n\nfunc (s *AccountsStorage) GetEmail() string {\n\treturn s.email\n}\n\nfunc (s *AccountsStorage) Save(account *Account) error {\n\tjsonBytes, err := json.MarshalIndent(account, \"\", \"\\t\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn os.WriteFile(s.accountFilePath, jsonBytes, filePerm)\n}\n\nfunc (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account {\n\tfileBytes, err := os.ReadFile(s.accountFilePath)\n\tif err != nil {\n\t\tlog.Fatalf(\"Could not load file for account %s: %v\", s.GetUserID(), err)\n\t}\n\n\tvar account Account\n\n\terr = json.Unmarshal(fileBytes, &account)\n\tif err != nil {\n\t\tlog.Fatalf(\"Could not parse file for account %s: %v\", s.GetUserID(), err)\n\t}\n\n\taccount.key = privateKey\n\n\tif account.Registration == nil || account.Registration.Body.Status == \"\" {\n\t\treg, err := tryRecoverRegistration(s.ctx, privateKey)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Could not load account for %s. Registration is nil: %#v\", s.GetUserID(), err)\n\t\t}\n\n\t\taccount.Registration = reg\n\n\t\terr = s.Save(&account)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Could not save account for %s. Registration is nil: %#v\", s.GetUserID(), err)\n\t\t}\n\t}\n\n\treturn &account\n}\n\nfunc (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.PrivateKey {\n\taccKeyPath := filepath.Join(s.keysPath, s.GetUserID()+\".key\")\n\n\tif _, err := os.Stat(accKeyPath); os.IsNotExist(err) {\n\t\tlog.Printf(\"No key found for account %s. Generating a %s key.\", s.GetUserID(), keyType)\n\t\ts.createKeysFolder()\n\n\t\tprivateKey, err := generatePrivateKey(accKeyPath, keyType)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Could not generate RSA private account key for account %s: %v\", s.GetUserID(), err)\n\t\t}\n\n\t\tlog.Printf(\"Saved key to %s\", accKeyPath)\n\n\t\treturn privateKey\n\t}\n\n\tprivateKey, err := loadPrivateKey(accKeyPath)\n\tif err != nil {\n\t\tlog.Fatalf(\"Could not load RSA private key from file %s: %v\", accKeyPath, err)\n\t}\n\n\treturn privateKey\n}\n\nfunc (s *AccountsStorage) createKeysFolder() {\n\tif err := createNonExistingFolder(s.keysPath); err != nil {\n\t\tlog.Fatalf(\"Could not check/create directory for account %s: %v\", s.GetUserID(), err)\n\t}\n}\n\nfunc generatePrivateKey(file string, keyType certcrypto.KeyType) (crypto.PrivateKey, error) {\n\tprivateKey, err := certcrypto.GeneratePrivateKey(keyType)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcertOut, err := os.Create(file)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer certOut.Close()\n\n\tpemKey := certcrypto.PEMBlock(privateKey)\n\n\terr = pem.Encode(certOut, pemKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn privateKey, nil\n}\n\nfunc loadPrivateKey(file string) (crypto.PrivateKey, error) {\n\tkeyBytes, err := os.ReadFile(file)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprivateKey, err := certcrypto.ParsePEMPrivateKey(keyBytes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn privateKey, nil\n}\n\nfunc tryRecoverRegistration(ctx *cli.Context, privateKey crypto.PrivateKey) (*registration.Resource, error) {\n\t// couldn't load account but got a key. Try to look the account up.\n\tconfig := lego.NewConfig(&Account{key: privateKey})\n\tconfig.CADirURL = ctx.String(flgServer)\n\tconfig.UserAgent = getUserAgent(ctx)\n\n\tclient, err := lego.NewClient(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treg, err := client.Registration.ResolveAccountByKey()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn reg, nil\n}\n"
  },
  {
    "path": "cmd/certs_storage.go",
    "content": "package cmd\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/go-acme/lego/v4/certificate\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/urfave/cli/v2\"\n\t\"golang.org/x/net/idna\"\n\t\"software.sslmate.com/src/go-pkcs12\"\n)\n\nconst (\n\tbaseCertificatesFolderName = \"certificates\"\n\tbaseArchivesFolderName     = \"archives\"\n)\n\nconst (\n\tissuerExt   = \".issuer.crt\"\n\tcertExt     = \".crt\"\n\tkeyExt      = \".key\"\n\tpemExt      = \".pem\"\n\tpfxExt      = \".pfx\"\n\tresourceExt = \".json\"\n)\n\n// CertificatesStorage a certificates' storage.\n//\n// rootPath:\n//\n//\t./.lego/certificates/\n//\t     │      └── root certificates directory\n//\t     └── \"path\" option\n//\n// archivePath:\n//\n//\t./.lego/archives/\n//\t     │      └── archived certificates directory\n//\t     └── \"path\" option\ntype CertificatesStorage struct {\n\trootPath    string\n\tarchivePath string\n\tpem         bool\n\tpfx         bool\n\tpfxPassword string\n\tpfxFormat   string\n\tfilename    string // Deprecated\n}\n\n// NewCertificatesStorage create a new certificates storage.\nfunc NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage {\n\tpfxFormat := ctx.String(flgPFXFormat)\n\n\tswitch pfxFormat {\n\tcase \"DES\", \"RC2\", \"SHA256\":\n\tdefault:\n\t\tlog.Fatalf(\"Invalid PFX format: %s\", pfxFormat)\n\t}\n\n\treturn &CertificatesStorage{\n\t\trootPath:    filepath.Join(ctx.String(flgPath), baseCertificatesFolderName),\n\t\tarchivePath: filepath.Join(ctx.String(flgPath), baseArchivesFolderName),\n\t\tpem:         ctx.Bool(flgPEM),\n\t\tpfx:         ctx.Bool(flgPFX),\n\t\tpfxPassword: ctx.String(flgPFXPass),\n\t\tpfxFormat:   pfxFormat,\n\t\tfilename:    ctx.String(flgFilename),\n\t}\n}\n\nfunc (s *CertificatesStorage) CreateRootFolder() {\n\terr := createNonExistingFolder(s.rootPath)\n\tif err != nil {\n\t\tlog.Fatalf(\"Could not check/create path: %v\", err)\n\t}\n}\n\nfunc (s *CertificatesStorage) CreateArchiveFolder() {\n\terr := createNonExistingFolder(s.archivePath)\n\tif err != nil {\n\t\tlog.Fatalf(\"Could not check/create path: %v\", err)\n\t}\n}\n\nfunc (s *CertificatesStorage) GetRootPath() string {\n\treturn s.rootPath\n}\n\nfunc (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {\n\tdomain := certRes.Domain\n\n\t// We store the certificate, private key and metadata in different files\n\t// as web servers would not be able to work with a combined file.\n\terr := s.WriteFile(domain, certExt, certRes.Certificate)\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to save Certificate for domain %s\\n\\t%v\", domain, err)\n\t}\n\n\tif certRes.IssuerCertificate != nil {\n\t\terr = s.WriteFile(domain, issuerExt, certRes.IssuerCertificate)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Unable to save IssuerCertificate for domain %s\\n\\t%v\", domain, err)\n\t\t}\n\t}\n\n\t// if we were given a CSR, we don't know the private key\n\tif certRes.PrivateKey != nil {\n\t\terr = s.WriteCertificateFiles(domain, certRes)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Unable to save PrivateKey for domain %s\\n\\t%v\", domain, err)\n\t\t}\n\t} else if s.pem || s.pfx {\n\t\t// we don't have the private key; can't write the .pem or .pfx file\n\t\tlog.Fatalf(\"Unable to save PEM or PFX without private key for domain %s. Are you using a CSR?\", domain)\n\t}\n\n\tjsonBytes, err := json.MarshalIndent(certRes, \"\", \"\\t\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to marshal CertResource for domain %s\\n\\t%v\", domain, err)\n\t}\n\n\terr = s.WriteFile(domain, resourceExt, jsonBytes)\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to save CertResource for domain %s\\n\\t%v\", domain, err)\n\t}\n}\n\nfunc (s *CertificatesStorage) ReadResource(domain string) certificate.Resource {\n\traw, err := s.ReadFile(domain, resourceExt)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error while loading the meta data for domain %s\\n\\t%v\", domain, err)\n\t}\n\n\tvar resource certificate.Resource\n\tif err = json.Unmarshal(raw, &resource); err != nil {\n\t\tlog.Fatalf(\"Error while marshaling the meta data for domain %s\\n\\t%v\", domain, err)\n\t}\n\n\treturn resource\n}\n\nfunc (s *CertificatesStorage) ExistsFile(domain, extension string) bool {\n\tfilePath := s.GetFileName(domain, extension)\n\n\tif _, err := os.Stat(filePath); os.IsNotExist(err) {\n\t\treturn false\n\t} else if err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\treturn true\n}\n\nfunc (s *CertificatesStorage) ReadFile(domain, extension string) ([]byte, error) {\n\treturn os.ReadFile(s.GetFileName(domain, extension))\n}\n\nfunc (s *CertificatesStorage) GetFileName(domain, extension string) string {\n\tfilename := sanitizedDomain(domain) + extension\n\treturn filepath.Join(s.rootPath, filename)\n}\n\nfunc (s *CertificatesStorage) ReadCertificate(domain, extension string) ([]*x509.Certificate, error) {\n\tcontent, err := s.ReadFile(domain, extension)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// The input may be a bundle or a single certificate.\n\treturn certcrypto.ParsePEMBundle(content)\n}\n\nfunc (s *CertificatesStorage) WriteFile(domain, extension string, data []byte) error {\n\tvar baseFileName string\n\tif s.filename != \"\" {\n\t\tbaseFileName = s.filename\n\t} else {\n\t\tbaseFileName = sanitizedDomain(domain)\n\t}\n\n\tfilePath := filepath.Join(s.rootPath, baseFileName+extension)\n\n\treturn os.WriteFile(filePath, data, filePerm)\n}\n\nfunc (s *CertificatesStorage) WriteCertificateFiles(domain string, certRes *certificate.Resource) error {\n\terr := s.WriteFile(domain, keyExt, certRes.PrivateKey)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to save key file: %w\", err)\n\t}\n\n\tif s.pem {\n\t\terr = s.WriteFile(domain, pemExt, bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to save PEM file: %w\", err)\n\t\t}\n\t}\n\n\tif s.pfx {\n\t\terr = s.WritePFXFile(domain, certRes)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to save PFX file: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.Resource) error {\n\tcertPemBlock, _ := pem.Decode(certRes.Certificate)\n\tif certPemBlock == nil {\n\t\treturn fmt.Errorf(\"unable to parse Certificate for domain %s\", domain)\n\t}\n\n\tcert, err := x509.ParseCertificate(certPemBlock.Bytes)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to load Certificate for domain %s: %w\", domain, err)\n\t}\n\n\tcertChain, err := getCertificateChain(certRes)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get certificate chain for domain %s: %w\", domain, err)\n\t}\n\n\tprivateKey, err := certcrypto.ParsePEMPrivateKey(certRes.PrivateKey)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to parse PrivateKey for domain %s: %w\", domain, err)\n\t}\n\n\tencoder, err := getPFXEncoder(s.pfxFormat)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"PFX encoder: %w\", err)\n\t}\n\n\tpfxBytes, err := encoder.Encode(privateKey, cert, certChain, s.pfxPassword)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to encode PFX data for domain %s: %w\", domain, err)\n\t}\n\n\treturn s.WriteFile(domain, pfxExt, pfxBytes)\n}\n\nfunc (s *CertificatesStorage) MoveToArchive(domain string) error {\n\tbaseFilename := filepath.Join(s.rootPath, sanitizedDomain(domain))\n\n\tmatches, err := filepath.Glob(baseFilename + \".*\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, oldFile := range matches {\n\t\tif strings.TrimSuffix(oldFile, filepath.Ext(oldFile)) != baseFilename && oldFile != baseFilename+issuerExt {\n\t\t\tcontinue\n\t\t}\n\n\t\tdate := strconv.FormatInt(time.Now().Unix(), 10)\n\t\tfilename := date + \".\" + filepath.Base(oldFile)\n\t\tnewFile := filepath.Join(s.archivePath, filename)\n\n\t\terr = os.Rename(oldFile, newFile)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc getCertificateChain(certRes *certificate.Resource) ([]*x509.Certificate, error) {\n\tchainCertPemBlock, rest := pem.Decode(certRes.IssuerCertificate)\n\tif chainCertPemBlock == nil {\n\t\treturn nil, errors.New(\"unable to parse Issuer Certificate\")\n\t}\n\n\tvar certChain []*x509.Certificate\n\n\tfor chainCertPemBlock != nil {\n\t\tchainCert, err := x509.ParseCertificate(chainCertPemBlock.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to parse Chain Certificate: %w\", err)\n\t\t}\n\n\t\tcertChain = append(certChain, chainCert)\n\t\tchainCertPemBlock, rest = pem.Decode(rest) // Try decoding the next pem block\n\t}\n\n\treturn certChain, nil\n}\n\nfunc getPFXEncoder(pfxFormat string) (*pkcs12.Encoder, error) {\n\tvar encoder *pkcs12.Encoder\n\n\tswitch pfxFormat {\n\tcase \"SHA256\":\n\t\tencoder = pkcs12.Modern2023\n\tcase \"DES\":\n\t\tencoder = pkcs12.LegacyDES\n\tcase \"RC2\":\n\t\tencoder = pkcs12.LegacyRC2\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"invalid PFX format: %s\", pfxFormat)\n\t}\n\n\treturn encoder, nil\n}\n\n// sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)).\nfunc sanitizedDomain(domain string) string {\n\tsafe, err := idna.ToASCII(strings.NewReplacer(\":\", \"-\", \"*\", \"_\").Replace(domain))\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\treturn safe\n}\n"
  },
  {
    "path": "cmd/certs_storage_test.go",
    "content": "package cmd\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCertificatesStorage_MoveToArchive(t *testing.T) {\n\tdomain := \"example.com\"\n\n\tstorage := CertificatesStorage{\n\t\trootPath:    t.TempDir(),\n\t\tarchivePath: t.TempDir(),\n\t}\n\n\tdomainFiles := generateTestFiles(t, storage.rootPath, domain)\n\n\terr := storage.MoveToArchive(domain)\n\trequire.NoError(t, err)\n\n\tfor _, file := range domainFiles {\n\t\tassert.NoFileExists(t, file)\n\t}\n\n\troot, err := os.ReadDir(storage.rootPath)\n\trequire.NoError(t, err)\n\trequire.Empty(t, root)\n\n\tarchive, err := os.ReadDir(storage.archivePath)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, archive, len(domainFiles))\n\tassert.Regexp(t, `\\d+\\.`+regexp.QuoteMeta(domain), archive[0].Name())\n}\n\nfunc TestCertificatesStorage_MoveToArchive_noFileRelatedToDomain(t *testing.T) {\n\tdomain := \"example.com\"\n\n\tstorage := CertificatesStorage{\n\t\trootPath:    t.TempDir(),\n\t\tarchivePath: t.TempDir(),\n\t}\n\n\tdomainFiles := generateTestFiles(t, storage.rootPath, \"example.org\")\n\n\terr := storage.MoveToArchive(domain)\n\trequire.NoError(t, err)\n\n\tfor _, file := range domainFiles {\n\t\tassert.FileExists(t, file)\n\t}\n\n\troot, err := os.ReadDir(storage.rootPath)\n\trequire.NoError(t, err)\n\tassert.Len(t, root, len(domainFiles))\n\n\tarchive, err := os.ReadDir(storage.archivePath)\n\trequire.NoError(t, err)\n\n\tassert.Empty(t, archive)\n}\n\nfunc TestCertificatesStorage_MoveToArchive_ambiguousDomain(t *testing.T) {\n\tdomain := \"example.com\"\n\n\tstorage := CertificatesStorage{\n\t\trootPath:    t.TempDir(),\n\t\tarchivePath: t.TempDir(),\n\t}\n\n\tdomainFiles := generateTestFiles(t, storage.rootPath, domain)\n\totherDomainFiles := generateTestFiles(t, storage.rootPath, domain+\".example.org\")\n\n\terr := storage.MoveToArchive(domain)\n\trequire.NoError(t, err)\n\n\tfor _, file := range domainFiles {\n\t\tassert.NoFileExists(t, file)\n\t}\n\n\tfor _, file := range otherDomainFiles {\n\t\tassert.FileExists(t, file)\n\t}\n\n\troot, err := os.ReadDir(storage.rootPath)\n\trequire.NoError(t, err)\n\trequire.Len(t, root, len(otherDomainFiles))\n\n\tarchive, err := os.ReadDir(storage.archivePath)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, archive, len(domainFiles))\n\tassert.Regexp(t, `\\d+\\.`+regexp.QuoteMeta(domain), archive[0].Name())\n}\n\nfunc generateTestFiles(t *testing.T, dir, domain string) []string {\n\tt.Helper()\n\n\tvar filenames []string\n\n\tfor _, ext := range []string{issuerExt, certExt, keyExt, pemExt, pfxExt, resourceExt} {\n\t\tfilename := filepath.Join(dir, domain+ext)\n\t\terr := os.WriteFile(filename, []byte(\"test\"), 0o666)\n\t\trequire.NoError(t, err)\n\n\t\tfilenames = append(filenames, filename)\n\t}\n\n\treturn filenames\n}\n"
  },
  {
    "path": "cmd/cmd.go",
    "content": "package cmd\n\nimport \"github.com/urfave/cli/v2\"\n\n// CreateCommands Creates all CLI commands.\nfunc CreateCommands() []*cli.Command {\n\treturn []*cli.Command{\n\t\tcreateRun(),\n\t\tcreateRevoke(),\n\t\tcreateRenew(),\n\t\tcreateDNSHelp(),\n\t\tcreateList(),\n\t}\n}\n"
  },
  {
    "path": "cmd/cmd_before.go",
    "content": "package cmd\n\nimport (\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc Before(ctx *cli.Context) error {\n\tif ctx.String(flgPath) == \"\" {\n\t\tlog.Fatalf(\"Could not determine current working directory. Please pass --%s.\", flgPath)\n\t}\n\n\terr := createNonExistingFolder(ctx.String(flgPath))\n\tif err != nil {\n\t\tlog.Fatalf(\"Could not check/create path: %v\", err)\n\t}\n\n\tif ctx.String(flgServer) == \"\" {\n\t\tlog.Fatalf(\"Could not determine current working server. Please pass --%s.\", flgServer)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/cmd_dnshelp.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\n\t\"github.com/urfave/cli/v2\"\n)\n\nconst flgCode = \"code\"\n\nfunc createDNSHelp() *cli.Command {\n\treturn &cli.Command{\n\t\tName:   \"dnshelp\",\n\t\tUsage:  \"Shows additional help for the '--dns' global option\",\n\t\tAction: dnsHelp,\n\t\tFlags: []cli.Flag{\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:    flgCode,\n\t\t\t\tAliases: []string{\"c\"},\n\t\t\t\tUsage:   fmt.Sprintf(\"DNS code: %s\", allDNSCodes()),\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc dnsHelp(ctx *cli.Context) error {\n\tcode := ctx.String(flgCode)\n\tif code == \"\" {\n\t\tw := tabwriter.NewWriter(ctx.App.Writer, 0, 0, 2, ' ', 0)\n\t\tew := &errWriter{w: w}\n\n\t\tew.writeln(`Credentials for DNS providers must be passed through environment variables.`)\n\t\tew.writeln()\n\t\tew.writeln(`To display the documentation for a specific DNS provider, run:`)\n\t\tew.writeln()\n\t\tew.writeln(\"\\t$ lego dnshelp -c code\")\n\t\tew.writeln()\n\t\tew.writeln(\"Supported DNS providers:\")\n\t\tew.writef(\"\\t%s\\n\", allDNSCodes())\n\t\tew.writeln()\n\t\tew.writeln(\"More information: https://go-acme.github.io/lego/dns\")\n\n\t\tif ew.err != nil {\n\t\t\treturn ew.err\n\t\t}\n\n\t\treturn w.Flush()\n\t}\n\n\treturn displayDNSHelp(ctx.App.Writer, strings.ToLower(code))\n}\n\ntype errWriter struct {\n\tw   io.Writer\n\terr error\n}\n\nfunc (ew *errWriter) writeln(a ...any) {\n\tif ew.err != nil {\n\t\treturn\n\t}\n\n\t_, ew.err = fmt.Fprintln(ew.w, a...)\n}\n\nfunc (ew *errWriter) writef(format string, a ...any) {\n\tif ew.err != nil {\n\t\treturn\n\t}\n\n\t_, ew.err = fmt.Fprintf(ew.w, format, a...)\n}\n"
  },
  {
    "path": "cmd/cmd_list.go",
    "content": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nconst (\n\tflgAccounts = \"accounts\"\n\tflgNames    = \"names\"\n)\n\nfunc createList() *cli.Command {\n\treturn &cli.Command{\n\t\tName:   \"list\",\n\t\tUsage:  \"Display certificates and accounts information.\",\n\t\tAction: list,\n\t\tFlags: []cli.Flag{\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:    flgAccounts,\n\t\t\t\tAliases: []string{\"a\"},\n\t\t\t\tUsage:   \"Display accounts.\",\n\t\t\t},\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:    flgNames,\n\t\t\t\tAliases: []string{\"n\"},\n\t\t\t\tUsage:   \"Display certificate common names only.\",\n\t\t\t},\n\t\t\t// fake email, needed by NewAccountsStorage\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:   flgEmail,\n\t\t\t\tValue:  \"\",\n\t\t\t\tHidden: true,\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc list(ctx *cli.Context) error {\n\tif ctx.Bool(flgAccounts) && !ctx.Bool(flgNames) {\n\t\tif err := listAccount(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn listCertificates(ctx)\n}\n\nfunc listCertificates(ctx *cli.Context) error {\n\tcertsStorage := NewCertificatesStorage(ctx)\n\n\tmatches, err := filepath.Glob(filepath.Join(certsStorage.GetRootPath(), \"*.crt\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnames := ctx.Bool(flgNames)\n\n\tif len(matches) == 0 {\n\t\tif !names {\n\t\t\tfmt.Println(\"No certificates found.\")\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif !names {\n\t\tfmt.Println(\"Found the following certs:\")\n\t}\n\n\tfor _, filename := range matches {\n\t\tif strings.HasSuffix(filename, issuerExt) {\n\t\t\tcontinue\n\t\t}\n\n\t\tdata, err := os.ReadFile(filename)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tpCert, err := certcrypto.ParsePEMCertificate(data)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tname, err := certcrypto.GetCertificateMainDomain(pCert)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif names {\n\t\t\tfmt.Println(name)\n\t\t} else {\n\t\t\tfmt.Println(\"  Certificate Name:\", name)\n\t\t\tfmt.Println(\"    Domains:\", strings.Join(pCert.DNSNames, \", \"))\n\n\t\t\tif len(pCert.IPAddresses) > 0 {\n\t\t\t\tfmt.Println(\"    IPs:\", formatIPAddresses(pCert.IPAddresses))\n\t\t\t}\n\n\t\t\tfmt.Println(\"    Expiry Date:\", pCert.NotAfter)\n\t\t\tfmt.Println(\"    Certificate Path:\", filename)\n\t\t\tfmt.Println()\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc listAccount(ctx *cli.Context) error {\n\taccountsStorage := NewAccountsStorage(ctx)\n\n\tmatches, err := filepath.Glob(filepath.Join(accountsStorage.GetRootPath(), \"*\", \"*\", \"*.json\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(matches) == 0 {\n\t\tfmt.Println(\"No accounts found.\")\n\t\treturn nil\n\t}\n\n\tfmt.Println(\"Found the following accounts:\")\n\n\tfor _, filename := range matches {\n\t\tdata, err := os.ReadFile(filename)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar account Account\n\n\t\terr = json.Unmarshal(data, &account)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\turi, err := url.Parse(account.Registration.URI)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfmt.Println(\"  Email:\", account.Email)\n\t\tfmt.Println(\"  Server:\", uri.Host)\n\t\tfmt.Println(\"  Path:\", filepath.Dir(filename))\n\t\tfmt.Println()\n\t}\n\n\treturn nil\n}\n\nfunc formatIPAddresses(ipAddresses []net.IP) string {\n\tvar ips []string\n\tfor _, ip := range ipAddresses {\n\t\tips = append(ips, ip.String())\n\t}\n\n\treturn strings.Join(ips, \", \")\n}\n"
  },
  {
    "path": "cmd/cmd_renew.go",
    "content": "package cmd\n\nimport (\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"math/rand\"\n\t\"os\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/go-acme/lego/v4/certificate\"\n\t\"github.com/go-acme/lego/v4/lego\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/mattn/go-isatty\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Flag names.\nconst (\n\tflgRenewDays              = \"days\"\n\tflgRenewDynamic           = \"dynamic\"\n\tflgARIDisable             = \"ari-disable\"\n\tflgARIWaitToRenewDuration = \"ari-wait-to-renew-duration\"\n\tflgReuseKey               = \"reuse-key\"\n\tflgRenewHook              = \"renew-hook\"\n\tflgRenewHookTimeout       = \"renew-hook-timeout\"\n\tflgNoRandomSleep          = \"no-random-sleep\"\n\tflgForceCertDomains       = \"force-cert-domains\"\n)\n\nfunc createRenew() *cli.Command {\n\treturn &cli.Command{\n\t\tName:   \"renew\",\n\t\tUsage:  \"Renew a certificate\",\n\t\tAction: renew,\n\t\tBefore: func(ctx *cli.Context) error {\n\t\t\t// we require either domains or csr, but not both\n\t\t\thasDomains := len(ctx.StringSlice(flgDomains)) > 0\n\n\t\t\thasCsr := ctx.String(flgCSR) != \"\"\n\t\t\tif hasDomains && hasCsr {\n\t\t\t\tlog.Fatalf(\"Please specify either --%s/-d or --%s/-c, but not both\", flgDomains, flgCSR)\n\t\t\t}\n\n\t\t\tif !hasDomains && !hasCsr {\n\t\t\t\tlog.Fatalf(\"Please specify --%s/-d (or --%s/-c if you already have a CSR)\", flgDomains, flgCSR)\n\t\t\t}\n\n\t\t\tif ctx.Bool(flgForceCertDomains) && hasCsr {\n\t\t\t\tlog.Fatalf(\"--%s only works with --%s/-d, --%s/-c doesn't support this option.\", flgForceCertDomains, flgDomains, flgCSR)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t\tFlags: []cli.Flag{\n\t\t\t&cli.IntFlag{\n\t\t\t\tName:  flgRenewDays,\n\t\t\t\tValue: 30,\n\t\t\t\tUsage: \"The number of days left on a certificate to renew it.\",\n\t\t\t},\n\t\t\t// TODO(ldez): in v5, remove this flag, use this behavior as default.\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:  flgRenewDynamic,\n\t\t\t\tValue: false,\n\t\t\t\tUsage: \"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.\",\n\t\t\t},\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:  flgARIDisable,\n\t\t\t\tUsage: \"Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed.\",\n\t\t\t},\n\t\t\t&cli.DurationFlag{\n\t\t\t\tName:  flgARIWaitToRenewDuration,\n\t\t\t\tUsage: \"The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint.\",\n\t\t\t},\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:  flgReuseKey,\n\t\t\t\tUsage: \"Used to indicate you want to reuse your current private key for the new certificate.\",\n\t\t\t},\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:  flgNoBundle,\n\t\t\t\tUsage: \"Do not create a certificate bundle by adding the issuers certificate to the new certificate.\",\n\t\t\t},\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName: flgMustStaple,\n\t\t\t\tUsage: \"Include the OCSP must staple TLS extension in the CSR and generated certificate.\" +\n\t\t\t\t\t\" Only works if the CSR is generated by lego.\",\n\t\t\t},\n\t\t\t&cli.TimestampFlag{\n\t\t\t\tName:   flgNotBefore,\n\t\t\t\tUsage:  \"Set the notBefore field in the certificate (RFC3339 format)\",\n\t\t\t\tLayout: time.RFC3339,\n\t\t\t},\n\t\t\t&cli.TimestampFlag{\n\t\t\t\tName:   flgNotAfter,\n\t\t\t\tUsage:  \"Set the notAfter field in the certificate (RFC3339 format)\",\n\t\t\t\tLayout: time.RFC3339,\n\t\t\t},\n\t\t\t&cli.StringFlag{\n\t\t\t\tName: flgPreferredChain,\n\t\t\t\tUsage: \"If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name.\" +\n\t\t\t\t\t\" If no match, the default offered chain will be used.\",\n\t\t\t},\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:  flgProfile,\n\t\t\t\tUsage: \"If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.\",\n\t\t\t},\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:  flgAlwaysDeactivateAuthorizations,\n\t\t\t\tUsage: \"Force the authorizations to be relinquished even if the certificate request was successful.\",\n\t\t\t},\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:  flgRenewHook,\n\t\t\t\tUsage: \"Define a hook. The hook is executed only when the certificates are effectively renewed.\",\n\t\t\t},\n\t\t\t&cli.DurationFlag{\n\t\t\t\tName:  flgRenewHookTimeout,\n\t\t\t\tUsage: \"Define the timeout for the hook execution.\",\n\t\t\t\tValue: 2 * time.Minute,\n\t\t\t},\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName: flgNoRandomSleep,\n\t\t\t\tUsage: \"Do not add a random sleep before the renewal.\" +\n\t\t\t\t\t\" We do not recommend using this flag if you are doing your renewals in an automated way.\",\n\t\t\t},\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:  flgForceCertDomains,\n\t\t\t\tUsage: \"Check and ensure that the cert's domain list matches those passed in the domains argument.\",\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc renew(ctx *cli.Context) error {\n\taccount, keyType := setupAccount(ctx, NewAccountsStorage(ctx))\n\n\tif account.Registration == nil {\n\t\tlog.Fatalf(\"Account %s is not registered. Use 'run' to register a new account.\\n\", account.Email)\n\t}\n\n\tcertsStorage := NewCertificatesStorage(ctx)\n\n\tbundle := !ctx.Bool(flgNoBundle)\n\n\tmeta := map[string]string{\n\t\thookEnvAccountEmail: account.Email,\n\t}\n\n\t// CSR\n\tif ctx.IsSet(flgCSR) {\n\t\treturn renewForCSR(ctx, account, keyType, certsStorage, bundle, meta)\n\t}\n\n\t// Domains\n\treturn renewForDomains(ctx, account, keyType, certsStorage, bundle, meta)\n}\n\nfunc renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {\n\tdomains := ctx.StringSlice(flgDomains)\n\tdomain := domains[0]\n\n\t// load the cert resource from files.\n\t// We store the certificate, private key and metadata in different files\n\t// as web servers would not be able to work with a combined file.\n\tcertificates, err := certsStorage.ReadCertificate(domain, certExt)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error while loading the certificate for domain %s\\n\\t%v\", domain, err)\n\t}\n\n\tcert := certificates[0]\n\n\tvar (\n\t\tariRenewalTime *time.Time\n\t\treplacesCertID string\n\t)\n\n\tvar client *lego.Client\n\n\tif !ctx.Bool(flgARIDisable) {\n\t\tclient = setupClient(ctx, account, keyType)\n\n\t\tariRenewalTime = getARIRenewalTime(ctx, cert, domain, client)\n\t\tif ariRenewalTime != nil {\n\t\t\tnow := time.Now().UTC()\n\n\t\t\t// Figure out if we need to sleep before renewing.\n\t\t\tif ariRenewalTime.After(now) {\n\t\t\t\tlog.Infof(\"[%s] Sleeping %s until renewal time %s\", domain, ariRenewalTime.Sub(now), ariRenewalTime)\n\t\t\t\ttime.Sleep(ariRenewalTime.Sub(now))\n\t\t\t}\n\t\t}\n\n\t\treplacesCertID, err = certificate.MakeARICertID(cert)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Error while construction the ARI CertID for domain %s\\n\\t%v\", domain, err)\n\t\t}\n\t}\n\n\tforceDomains := ctx.Bool(flgForceCertDomains)\n\n\tcertDomains := certcrypto.ExtractDomains(cert)\n\n\tif ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) &&\n\t\t(!forceDomains || slices.Equal(certDomains, domains)) {\n\t\treturn nil\n\t}\n\n\tif client == nil {\n\t\tclient = setupClient(ctx, account, keyType)\n\t}\n\n\t// This is just meant to be informal for the user.\n\ttimeLeft := cert.NotAfter.Sub(time.Now().UTC())\n\tlog.Infof(\"[%s] acme: Trying renewal with %d hours remaining\", domain, int(timeLeft.Hours()))\n\n\tvar privateKey crypto.PrivateKey\n\n\tif ctx.Bool(flgReuseKey) {\n\t\tkeyBytes, errR := certsStorage.ReadFile(domain, keyExt)\n\t\tif errR != nil {\n\t\t\tlog.Fatalf(\"Error while loading the private key for domain %s\\n\\t%v\", domain, errR)\n\t\t}\n\n\t\tprivateKey, errR = certcrypto.ParsePEMPrivateKey(keyBytes)\n\t\tif errR != nil {\n\t\t\treturn errR\n\t\t}\n\t}\n\n\t// https://github.com/go-acme/lego/issues/1656\n\t// https://github.com/certbot/certbot/blob/284023a1b7672be2bd4018dd7623b3b92197d4b0/certbot/certbot/_internal/renewal.py#L435-L440\n\tif !isatty.IsTerminal(os.Stdout.Fd()) && !ctx.Bool(flgNoRandomSleep) {\n\t\t// https://github.com/certbot/certbot/blob/284023a1b7672be2bd4018dd7623b3b92197d4b0/certbot/certbot/_internal/renewal.py#L472\n\t\tconst jitter = 8 * time.Minute\n\n\t\trnd := rand.New(rand.NewSource(time.Now().UnixNano()))\n\t\tsleepTime := time.Duration(rnd.Int63n(int64(jitter)))\n\n\t\tlog.Infof(\"renewal: random delay of %s\", sleepTime)\n\t\ttime.Sleep(sleepTime)\n\t}\n\n\trenewalDomains := slices.Clone(domains)\n\tif !forceDomains {\n\t\trenewalDomains = merge(certDomains, domains)\n\t}\n\n\trequest := certificate.ObtainRequest{\n\t\tDomains:                        renewalDomains,\n\t\tPrivateKey:                     privateKey,\n\t\tMustStaple:                     ctx.Bool(flgMustStaple),\n\t\tNotBefore:                      getTime(ctx, flgNotBefore),\n\t\tNotAfter:                       getTime(ctx, flgNotAfter),\n\t\tBundle:                         bundle,\n\t\tPreferredChain:                 ctx.String(flgPreferredChain),\n\t\tProfile:                        ctx.String(flgProfile),\n\t\tAlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),\n\t}\n\n\tif replacesCertID != \"\" {\n\t\trequest.ReplacesCertID = replacesCertID\n\t}\n\n\tcertRes, err := client.Certificate.Obtain(request)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tcertRes.Domain = domain\n\n\tcertsStorage.SaveResource(certRes)\n\n\taddPathToMetadata(meta, domain, certRes, certsStorage)\n\n\treturn launchHook(ctx.String(flgRenewHook), ctx.Duration(flgRenewHookTimeout), meta)\n}\n\nfunc renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {\n\tcsr, err := readCSRFile(ctx.String(flgCSR))\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tdomain, err := certcrypto.GetCSRMainDomain(csr)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error: %v\", err)\n\t}\n\n\t// load the cert resource from files.\n\t// We store the certificate, private key and metadata in different files\n\t// as web servers would not be able to work with a combined file.\n\tcertificates, err := certsStorage.ReadCertificate(domain, certExt)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error while loading the certificate for domain %s\\n\\t%v\", domain, err)\n\t}\n\n\tcert := certificates[0]\n\n\tvar (\n\t\tariRenewalTime *time.Time\n\t\treplacesCertID string\n\t)\n\n\tvar client *lego.Client\n\n\tif !ctx.Bool(flgARIDisable) {\n\t\tclient = setupClient(ctx, account, keyType)\n\n\t\tariRenewalTime = getARIRenewalTime(ctx, cert, domain, client)\n\t\tif ariRenewalTime != nil {\n\t\t\tnow := time.Now().UTC()\n\n\t\t\t// Figure out if we need to sleep before renewing.\n\t\t\tif ariRenewalTime.After(now) {\n\t\t\t\tlog.Infof(\"[%s] Sleeping %s until renewal time %s\", domain, ariRenewalTime.Sub(now), ariRenewalTime)\n\t\t\t\ttime.Sleep(ariRenewalTime.Sub(now))\n\t\t\t}\n\t\t}\n\n\t\treplacesCertID, err = certificate.MakeARICertID(cert)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Error while construction the ARI CertID for domain %s\\n\\t%v\", domain, err)\n\t\t}\n\t}\n\n\tif ariRenewalTime == nil && !needRenewal(cert, domain, ctx.Int(flgRenewDays), ctx.Bool(flgRenewDynamic)) {\n\t\treturn nil\n\t}\n\n\tif client == nil {\n\t\tclient = setupClient(ctx, account, keyType)\n\t}\n\n\t// This is just meant to be informal for the user.\n\ttimeLeft := cert.NotAfter.Sub(time.Now().UTC())\n\tlog.Infof(\"[%s] acme: Trying renewal with %d hours remaining\", domain, int(timeLeft.Hours()))\n\n\trequest := certificate.ObtainForCSRRequest{\n\t\tCSR:                            csr,\n\t\tNotBefore:                      getTime(ctx, flgNotBefore),\n\t\tNotAfter:                       getTime(ctx, flgNotAfter),\n\t\tBundle:                         bundle,\n\t\tPreferredChain:                 ctx.String(flgPreferredChain),\n\t\tProfile:                        ctx.String(flgProfile),\n\t\tAlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),\n\t}\n\n\tif replacesCertID != \"\" {\n\t\trequest.ReplacesCertID = replacesCertID\n\t}\n\n\tcertRes, err := client.Certificate.ObtainForCSR(request)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tcertsStorage.SaveResource(certRes)\n\n\taddPathToMetadata(meta, domain, certRes, certsStorage)\n\n\treturn launchHook(ctx.String(flgRenewHook), ctx.Duration(flgRenewHookTimeout), meta)\n}\n\nfunc needRenewal(x509Cert *x509.Certificate, domain string, days int, dynamic bool) bool {\n\tif x509Cert.IsCA {\n\t\tlog.Fatalf(\"[%s] Certificate bundle starts with a CA certificate\", domain)\n\t}\n\n\tif dynamic {\n\t\treturn needRenewalDynamic(x509Cert, domain, time.Now())\n\t}\n\n\tif days < 0 {\n\t\treturn true\n\t}\n\n\tnotAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0)\n\tif notAfter <= days {\n\t\treturn true\n\t}\n\n\tlog.Printf(\"[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal.\",\n\t\tdomain, notAfter, days)\n\n\treturn false\n}\n\nfunc needRenewalDynamic(x509Cert *x509.Certificate, domain string, now time.Time) bool {\n\tlifetime := x509Cert.NotAfter.Sub(x509Cert.NotBefore)\n\n\tvar divisor int64 = 3\n\tif lifetime.Round(24*time.Hour).Hours()/24.0 <= 10 {\n\t\tdivisor = 2\n\t}\n\n\tdueDate := x509Cert.NotAfter.Add(-1 * time.Duration(lifetime.Nanoseconds()/divisor))\n\n\tif dueDate.Before(now) {\n\t\treturn true\n\t}\n\n\tlog.Infof(\"[%s] The certificate expires at %s, the renewal can be performed in %s: no renewal.\",\n\t\tdomain, x509Cert.NotAfter.Format(time.RFC3339), dueDate.Sub(now))\n\n\treturn false\n}\n\n// getARIRenewalTime checks if the certificate needs to be renewed using the renewalInfo endpoint.\nfunc getARIRenewalTime(ctx *cli.Context, cert *x509.Certificate, domain string, client *lego.Client) *time.Time {\n\tif cert.IsCA {\n\t\tlog.Fatalf(\"[%s] Certificate bundle starts with a CA certificate\", domain)\n\t}\n\n\trenewalInfo, err := client.Certificate.GetRenewalInfo(certificate.RenewalInfoRequest{Cert: cert})\n\tif err != nil {\n\t\tif errors.Is(err, api.ErrNoARI) {\n\t\t\t// The server does not advertise a renewal info endpoint.\n\t\t\tlog.Warnf(\"[%s] acme: %v\", domain, err)\n\t\t\treturn nil\n\t\t}\n\n\t\tlog.Warnf(\"[%s] acme: calling renewal info endpoint: %v\", domain, err)\n\n\t\treturn nil\n\t}\n\n\tnow := time.Now().UTC()\n\n\trenewalTime := renewalInfo.ShouldRenewAt(now, ctx.Duration(flgARIWaitToRenewDuration))\n\tif renewalTime == nil {\n\t\tlog.Infof(\"[%s] acme: renewalInfo endpoint indicates that renewal is not needed\", domain)\n\t\treturn nil\n\t}\n\n\tlog.Infof(\"[%s] acme: renewalInfo endpoint indicates that renewal is needed\", domain)\n\n\tif renewalInfo.ExplanationURL != \"\" {\n\t\tlog.Infof(\"[%s] acme: renewalInfo endpoint provided an explanation: %s\", domain, renewalInfo.ExplanationURL)\n\t}\n\n\treturn renewalTime\n}\n\nfunc merge(prevDomains, nextDomains []string) []string {\n\tfor _, next := range nextDomains {\n\t\tif slices.Contains(prevDomains, next) {\n\t\t\tcontinue\n\t\t}\n\n\t\tprevDomains = append(prevDomains, next)\n\t}\n\n\treturn prevDomains\n}\n"
  },
  {
    "path": "cmd/cmd_renew_test.go",
    "content": "package cmd\n\nimport (\n\t\"crypto/x509\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_merge(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tprevDomains []string\n\t\tnextDomains []string\n\t\texpected    []string\n\t}{\n\t\t{\n\t\t\tdesc:        \"all empty\",\n\t\t\tprevDomains: []string{},\n\t\t\tnextDomains: []string{},\n\t\t\texpected:    []string{},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"next empty\",\n\t\t\tprevDomains: []string{\"a\", \"b\", \"c\"},\n\t\t\tnextDomains: []string{},\n\t\t\texpected:    []string{\"a\", \"b\", \"c\"},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"prev empty\",\n\t\t\tprevDomains: []string{},\n\t\t\tnextDomains: []string{\"a\", \"b\", \"c\"},\n\t\t\texpected:    []string{\"a\", \"b\", \"c\"},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"merge append\",\n\t\t\tprevDomains: []string{\"a\", \"b\", \"c\"},\n\t\t\tnextDomains: []string{\"a\", \"c\", \"d\"},\n\t\t\texpected:    []string{\"a\", \"b\", \"c\", \"d\"},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"merge same\",\n\t\t\tprevDomains: []string{\"a\", \"b\", \"c\"},\n\t\t\tnextDomains: []string{\"a\", \"b\", \"c\"},\n\t\t\texpected:    []string{\"a\", \"b\", \"c\"},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := merge(test.prevDomains, test.nextDomains)\n\t\t\tassert.Equal(t, test.expected, actual)\n\t\t})\n\t}\n}\n\nfunc Test_needRenewal(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tx509Cert *x509.Certificate\n\t\tdays     int\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tdesc: \"30 days, NotAfter now\",\n\t\t\tx509Cert: &x509.Certificate{\n\t\t\t\tNotAfter: time.Now(),\n\t\t\t},\n\t\t\tdays:     30,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"30 days, NotAfter 31 days\",\n\t\t\tx509Cert: &x509.Certificate{\n\t\t\t\tNotAfter: time.Now().Add(31*24*time.Hour + 1*time.Second),\n\t\t\t},\n\t\t\tdays:     30,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"30 days, NotAfter 30 days\",\n\t\t\tx509Cert: &x509.Certificate{\n\t\t\t\tNotAfter: time.Now().Add(30 * 24 * time.Hour),\n\t\t\t},\n\t\t\tdays:     30,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"0 days, NotAfter 30 days: only the day of the expiration\",\n\t\t\tx509Cert: &x509.Certificate{\n\t\t\t\tNotAfter: time.Now().Add(30 * 24 * time.Hour),\n\t\t\t},\n\t\t\tdays:     0,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"-1 days, NotAfter 30 days: always renew\",\n\t\t\tx509Cert: &x509.Certificate{\n\t\t\t\tNotAfter: time.Now().Add(30 * 24 * time.Hour),\n\t\t\t},\n\t\t\tdays:     -1,\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tactual := needRenewal(test.x509Cert, \"foo.com\", test.days, false)\n\n\t\t\tassert.Equal(t, test.expected, actual)\n\t\t})\n\t}\n}\n\nfunc Test_needRenewalDynamic(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc                string\n\t\tnow                 time.Time\n\t\tnotBefore, notAfter time.Time\n\t\texpected            assert.BoolAssertionFunc\n\t}{\n\t\t{\n\t\t\tdesc:      \"higher than 1/3 of the certificate lifetime left (lifetime > 10 days)\",\n\t\t\tnow:       time.Date(2025, 1, 19, 1, 1, 1, 1, time.UTC),\n\t\t\tnotBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),\n\t\t\tnotAfter:  time.Date(2025, 1, 30, 1, 1, 1, 1, time.UTC),\n\t\t\texpected:  assert.False,\n\t\t},\n\t\t{\n\t\t\tdesc:      \"lower than 1/3 of the certificate lifetime left(lifetime > 10 days)\",\n\t\t\tnow:       time.Date(2025, 1, 21, 1, 1, 1, 1, time.UTC),\n\t\t\tnotBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),\n\t\t\tnotAfter:  time.Date(2025, 1, 30, 1, 1, 1, 1, time.UTC),\n\t\t\texpected:  assert.True,\n\t\t},\n\t\t{\n\t\t\tdesc:      \"higher than 1/2 of the certificate lifetime left (lifetime < 10 days)\",\n\t\t\tnow:       time.Date(2025, 1, 4, 1, 1, 1, 1, time.UTC),\n\t\t\tnotBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),\n\t\t\tnotAfter:  time.Date(2025, 1, 10, 1, 1, 1, 1, time.UTC),\n\t\t\texpected:  assert.False,\n\t\t},\n\t\t{\n\t\t\tdesc:      \"lower than 1/2 of the certificate lifetime left (lifetime < 10 days)\",\n\t\t\tnow:       time.Date(2025, 1, 6, 1, 1, 1, 1, time.UTC),\n\t\t\tnotBefore: time.Date(2025, 1, 1, 1, 1, 1, 1, time.UTC),\n\t\t\tnotAfter:  time.Date(2025, 1, 10, 1, 1, 1, 1, time.UTC),\n\t\t\texpected:  assert.True,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tx509Cert := &x509.Certificate{\n\t\t\t\tNotBefore: test.notBefore,\n\t\t\t\tNotAfter:  test.notAfter,\n\t\t\t}\n\n\t\t\tok := needRenewalDynamic(x509Cert, \"example.com\", test.now)\n\n\t\t\ttest.expected(t, ok)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/cmd_revoke.go",
    "content": "package cmd\n\nimport (\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Flag names.\nconst (\n\tflgKeep   = \"keep\"\n\tflgReason = \"reason\"\n)\n\nfunc createRevoke() *cli.Command {\n\treturn &cli.Command{\n\t\tName:   \"revoke\",\n\t\tUsage:  \"Revoke a certificate\",\n\t\tAction: revoke,\n\t\tFlags: []cli.Flag{\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:    flgKeep,\n\t\t\t\tAliases: []string{\"k\"},\n\t\t\t\tUsage:   \"Keep the certificates after the revocation instead of archiving them.\",\n\t\t\t},\n\t\t\t&cli.UintFlag{\n\t\t\t\tName: flgReason,\n\t\t\t\tUsage: \"Identifies the reason for the certificate revocation.\" +\n\t\t\t\t\t\" See https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1.\" +\n\t\t\t\t\t\" Valid values are:\" +\n\t\t\t\t\t\" 0 (unspecified), 1 (keyCompromise), 2 (cACompromise), 3 (affiliationChanged),\" +\n\t\t\t\t\t\" 4 (superseded), 5 (cessationOfOperation), 6 (certificateHold), 8 (removeFromCRL),\" +\n\t\t\t\t\t\" 9 (privilegeWithdrawn), or 10 (aACompromise).\",\n\t\t\t\tValue: acme.CRLReasonUnspecified,\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc revoke(ctx *cli.Context) error {\n\taccount, keyType := setupAccount(ctx, NewAccountsStorage(ctx))\n\n\tif account.Registration == nil {\n\t\tlog.Fatalf(\"Account %s is not registered. Use 'run' to register a new account.\\n\", account.Email)\n\t}\n\n\tclient := newClient(ctx, account, keyType)\n\n\tcertsStorage := NewCertificatesStorage(ctx)\n\tcertsStorage.CreateRootFolder()\n\n\tfor _, domain := range ctx.StringSlice(flgDomains) {\n\t\tlog.Printf(\"Trying to revoke certificate for domain %s\", domain)\n\n\t\tcertBytes, err := certsStorage.ReadFile(domain, certExt)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Error while revoking the certificate for domain %s\\n\\t%v\", domain, err)\n\t\t}\n\n\t\treason := ctx.Uint(flgReason)\n\n\t\terr = client.Certificate.RevokeWithReason(certBytes, &reason)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Error while revoking the certificate for domain %s\\n\\t%v\", domain, err)\n\t\t}\n\n\t\tlog.Println(\"Certificate was revoked.\")\n\n\t\tif ctx.Bool(flgKeep) {\n\t\t\treturn nil\n\t\t}\n\n\t\tcertsStorage.CreateArchiveFolder()\n\n\t\terr = certsStorage.MoveToArchive(domain)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlog.Println(\"Certificate was archived for domain:\", domain)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/cmd_run.go",
    "content": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/certificate\"\n\t\"github.com/go-acme/lego/v4/lego\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/registration\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// Flag names.\nconst (\n\tflgNoBundle                       = \"no-bundle\"\n\tflgMustStaple                     = \"must-staple\"\n\tflgNotBefore                      = \"not-before\"\n\tflgNotAfter                       = \"not-after\"\n\tflgPrivateKey                     = \"private-key\"\n\tflgPreferredChain                 = \"preferred-chain\"\n\tflgProfile                        = \"profile\"\n\tflgAlwaysDeactivateAuthorizations = \"always-deactivate-authorizations\"\n\tflgRunHook                        = \"run-hook\"\n\tflgRunHookTimeout                 = \"run-hook-timeout\"\n)\n\nfunc createRun() *cli.Command {\n\treturn &cli.Command{\n\t\tName:  \"run\",\n\t\tUsage: \"Register an account, then create and install a certificate\",\n\t\tBefore: func(ctx *cli.Context) error {\n\t\t\t// we require either domains or csr, but not both\n\t\t\thasDomains := len(ctx.StringSlice(flgDomains)) > 0\n\n\t\t\thasCsr := ctx.String(flgCSR) != \"\"\n\t\t\tif hasDomains && hasCsr {\n\t\t\t\tlog.Fatal(\"Please specify either --domains/-d or --csr/-c, but not both\")\n\t\t\t}\n\n\t\t\tif !hasDomains && !hasCsr {\n\t\t\t\tlog.Fatal(\"Please specify --domains/-d (or --csr/-c if you already have a CSR)\")\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t\tAction: run,\n\t\tFlags: []cli.Flag{\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:  flgNoBundle,\n\t\t\t\tUsage: \"Do not create a certificate bundle by adding the issuers certificate to the new certificate.\",\n\t\t\t},\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName: flgMustStaple,\n\t\t\t\tUsage: \"Include the OCSP must staple TLS extension in the CSR and generated certificate.\" +\n\t\t\t\t\t\" Only works if the CSR is generated by lego.\",\n\t\t\t},\n\t\t\t&cli.TimestampFlag{\n\t\t\t\tName:   flgNotBefore,\n\t\t\t\tUsage:  \"Set the notBefore field in the certificate (RFC3339 format)\",\n\t\t\t\tLayout: time.RFC3339,\n\t\t\t},\n\t\t\t&cli.TimestampFlag{\n\t\t\t\tName:   flgNotAfter,\n\t\t\t\tUsage:  \"Set the notAfter field in the certificate (RFC3339 format)\",\n\t\t\t\tLayout: time.RFC3339,\n\t\t\t},\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:  flgPrivateKey,\n\t\t\t\tUsage: \"Path to private key (in PEM encoding) for the certificate. By default, the private key is generated.\",\n\t\t\t},\n\t\t\t&cli.StringFlag{\n\t\t\t\tName: flgPreferredChain,\n\t\t\t\tUsage: \"If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name.\" +\n\t\t\t\t\t\" If no match, the default offered chain will be used.\",\n\t\t\t},\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:  flgProfile,\n\t\t\t\tUsage: \"If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.\",\n\t\t\t},\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:  flgAlwaysDeactivateAuthorizations,\n\t\t\t\tUsage: \"Force the authorizations to be relinquished even if the certificate request was successful.\",\n\t\t\t},\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:  flgRunHook,\n\t\t\t\tUsage: \"Define a hook. The hook is executed when the certificates are effectively created.\",\n\t\t\t},\n\t\t\t&cli.DurationFlag{\n\t\t\t\tName:  flgRunHookTimeout,\n\t\t\t\tUsage: \"Define the timeout for the hook execution.\",\n\t\t\t\tValue: 2 * time.Minute,\n\t\t\t},\n\t\t},\n\t}\n}\n\nconst rootPathWarningMessage = `!!!! HEADS UP !!!!\n\nYour account credentials have been saved in your\nconfiguration directory at \"%s\".\n\nYou should make a secure backup of this folder now. This\nconfiguration directory will also contain private keys\ngenerated by lego and certificates obtained from the ACME\nserver. Making regular backups of this folder is ideal.\n`\n\nfunc run(ctx *cli.Context) error {\n\taccountsStorage := NewAccountsStorage(ctx)\n\n\taccount, keyType := setupAccount(ctx, accountsStorage)\n\n\tclient := setupClient(ctx, account, keyType)\n\n\tif account.Registration == nil {\n\t\treg, err := register(ctx, client)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Could not complete registration\\n\\t%v\", err)\n\t\t}\n\n\t\taccount.Registration = reg\n\t\tif err = accountsStorage.Save(account); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\tfmt.Printf(rootPathWarningMessage, accountsStorage.GetRootPath())\n\t}\n\n\tcertsStorage := NewCertificatesStorage(ctx)\n\tcertsStorage.CreateRootFolder()\n\n\tcert, err := obtainCertificate(ctx, client)\n\tif err != nil {\n\t\t// Make sure to return a non-zero exit code if ObtainSANCertificate returned at least one error.\n\t\t// Due to us not returning partial certificate we can just exit here instead of at the end.\n\t\tlog.Fatalf(\"Could not obtain certificates:\\n\\t%v\", err)\n\t}\n\n\tcertsStorage.SaveResource(cert)\n\n\tmeta := map[string]string{\n\t\thookEnvAccountEmail: account.Email,\n\t}\n\n\taddPathToMetadata(meta, cert.Domain, cert, certsStorage)\n\n\treturn launchHook(ctx.String(flgRunHook), ctx.Duration(flgRunHookTimeout), meta)\n}\n\nfunc handleTOS(ctx *cli.Context, client *lego.Client) bool {\n\t// Check for a global accept override\n\tif ctx.Bool(flgAcceptTOS) {\n\t\treturn true\n\t}\n\n\treader := bufio.NewReader(os.Stdin)\n\n\tlog.Printf(\"Please review the TOS at %s\", client.GetToSURL())\n\n\tfor {\n\t\tfmt.Println(\"Do you accept the TOS? Y/n\")\n\n\t\ttext, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Could not read from console: %v\", err)\n\t\t}\n\n\t\ttext = strings.Trim(text, \"\\r\\n\")\n\t\tswitch text {\n\t\tcase \"\", \"y\", \"Y\":\n\t\t\treturn true\n\t\tcase \"n\", \"N\":\n\t\t\treturn false\n\t\tdefault:\n\t\t\tfmt.Println(\"Your input was invalid. Please answer with one of Y/y, n/N or by pressing enter.\")\n\t\t}\n\t}\n}\n\nfunc register(ctx *cli.Context, client *lego.Client) (*registration.Resource, error) {\n\taccepted := handleTOS(ctx, client)\n\tif !accepted {\n\t\tlog.Fatal(\"You did not accept the TOS. Unable to proceed.\")\n\t}\n\n\tif ctx.Bool(flgEAB) {\n\t\tkid := ctx.String(flgKID)\n\t\thmacEncoded := ctx.String(flgHMAC)\n\n\t\tif kid == \"\" || hmacEncoded == \"\" {\n\t\t\tlog.Fatalf(\"Requires arguments --%s and --%s.\", flgKID, flgHMAC)\n\t\t}\n\n\t\treturn client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{\n\t\t\tTermsOfServiceAgreed: accepted,\n\t\t\tKid:                  kid,\n\t\t\tHmacEncoded:          hmacEncoded,\n\t\t})\n\t}\n\n\treturn client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})\n}\n\nfunc obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Resource, error) {\n\tbundle := !ctx.Bool(flgNoBundle)\n\n\tdomains := ctx.StringSlice(flgDomains)\n\tif len(domains) > 0 {\n\t\t// obtain a certificate, generating a new private key\n\t\trequest := certificate.ObtainRequest{\n\t\t\tDomains:                        domains,\n\t\t\tMustStaple:                     ctx.Bool(flgMustStaple),\n\t\t\tNotBefore:                      getTime(ctx, flgNotBefore),\n\t\t\tNotAfter:                       getTime(ctx, flgNotAfter),\n\t\t\tBundle:                         bundle,\n\t\t\tPreferredChain:                 ctx.String(flgPreferredChain),\n\t\t\tProfile:                        ctx.String(flgProfile),\n\t\t\tAlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),\n\t\t}\n\n\t\tif ctx.IsSet(flgPrivateKey) {\n\t\t\tvar err error\n\n\t\t\trequest.PrivateKey, err = loadPrivateKey(ctx.String(flgPrivateKey))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"load private key: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\treturn client.Certificate.Obtain(request)\n\t}\n\n\t// read the CSR\n\tcsr, err := readCSRFile(ctx.String(flgCSR))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// obtain a certificate for this CSR\n\trequest := certificate.ObtainForCSRRequest{\n\t\tCSR:                            csr,\n\t\tNotBefore:                      getTime(ctx, flgNotBefore),\n\t\tNotAfter:                       getTime(ctx, flgNotAfter),\n\t\tBundle:                         bundle,\n\t\tPreferredChain:                 ctx.String(flgPreferredChain),\n\t\tProfile:                        ctx.String(flgProfile),\n\t\tAlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),\n\t}\n\n\tif ctx.IsSet(flgPrivateKey) {\n\t\tvar err error\n\n\t\trequest.PrivateKey, err = loadPrivateKey(ctx.String(flgPrivateKey))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"load private key: %w\", err)\n\t\t}\n\t}\n\n\treturn client.Certificate.ObtainForCSR(request)\n}\n"
  },
  {
    "path": "cmd/flags.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/certificate\"\n\t\"github.com/go-acme/lego/v4/lego\"\n\t\"github.com/urfave/cli/v2\"\n\t\"software.sslmate.com/src/go-pkcs12\"\n)\n\n// Flag names.\nconst (\n\tflgDomains                  = \"domains\"\n\tflgServer                   = \"server\"\n\tflgAcceptTOS                = \"accept-tos\"\n\tflgEmail                    = \"email\"\n\tflgDisableCommonName        = \"disable-cn\"\n\tflgCSR                      = \"csr\"\n\tflgEAB                      = \"eab\"\n\tflgKID                      = \"kid\"\n\tflgHMAC                     = \"hmac\"\n\tflgKeyType                  = \"key-type\"\n\tflgFilename                 = \"filename\"\n\tflgPath                     = \"path\"\n\tflgHTTP                     = \"http\"\n\tflgHTTPPort                 = \"http.port\"\n\tflgHTTPDelay                = \"http.delay\"\n\tflgHTTPProxyHeader          = \"http.proxy-header\"\n\tflgHTTPWebroot              = \"http.webroot\"\n\tflgHTTPMemcachedHost        = \"http.memcached-host\"\n\tflgHTTPS3Bucket             = \"http.s3-bucket\"\n\tflgTLS                      = \"tls\"\n\tflgTLSPort                  = \"tls.port\"\n\tflgTLSDelay                 = \"tls.delay\"\n\tflgDNS                      = \"dns\"\n\tflgDNSDisableCP             = \"dns.disable-cp\"\n\tflgDNSPropagationWait       = \"dns.propagation-wait\"\n\tflgDNSPropagationDisableANS = \"dns.propagation-disable-ans\"\n\tflgDNSPropagationRNS        = \"dns.propagation-rns\"\n\tflgDNSResolvers             = \"dns.resolvers\"\n\tflgHTTPTimeout              = \"http-timeout\"\n\tflgTLSSkipVerify            = \"tls-skip-verify\"\n\tflgDNSTimeout               = \"dns-timeout\"\n\tflgPEM                      = \"pem\"\n\tflgPFX                      = \"pfx\"\n\tflgPFXPass                  = \"pfx.pass\"\n\tflgPFXFormat                = \"pfx.format\"\n\tflgCertTimeout              = \"cert.timeout\"\n\tflgOverallRequestLimit      = \"overall-request-limit\"\n\tflgUserAgent                = \"user-agent\"\n)\n\nconst (\n\tenvEAB         = \"LEGO_EAB\"\n\tenvEABHMAC     = \"LEGO_EAB_HMAC\"\n\tenvEABKID      = \"LEGO_EAB_KID\"\n\tenvEmail       = \"LEGO_EMAIL\"\n\tenvPath        = \"LEGO_PATH\"\n\tenvPFX         = \"LEGO_PFX\"\n\tenvPFXFormat   = \"LEGO_PFX_FORMAT\"\n\tenvPFXPassword = \"LEGO_PFX_PASSWORD\"\n\tenvServer      = \"LEGO_SERVER\"\n)\n\nfunc CreateFlags(defaultPath string) []cli.Flag {\n\treturn []cli.Flag{\n\t\t&cli.StringSliceFlag{\n\t\t\tName:    flgDomains,\n\t\t\tAliases: []string{\"d\"},\n\t\t\tUsage:   \"Add a domain to the process. Can be specified multiple times.\",\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:    flgServer,\n\t\t\tAliases: []string{\"s\"},\n\t\t\tEnvVars: []string{envServer},\n\t\t\tUsage:   \"CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.\",\n\t\t\tValue:   lego.LEDirectoryProduction,\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:    flgAcceptTOS,\n\t\t\tAliases: []string{\"a\"},\n\t\t\tUsage:   \"By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.\",\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:    flgEmail,\n\t\t\tAliases: []string{\"m\"},\n\t\t\tEnvVars: []string{envEmail},\n\t\t\tUsage:   \"Email used for registration and recovery contact.\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:  flgDisableCommonName,\n\t\t\tUsage: \"Disable the use of the common name in the CSR.\",\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:    flgCSR,\n\t\t\tAliases: []string{\"c\"},\n\t\t\tUsage:   \"Certificate signing request filename, if an external CSR is to be used.\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:    flgEAB,\n\t\t\tEnvVars: []string{envEAB},\n\t\t\tUsage:   \"Use External Account Binding for account registration. Requires --kid and --hmac.\",\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:    flgKID,\n\t\t\tEnvVars: []string{envEABKID},\n\t\t\tUsage:   \"Key identifier from External CA. Used for External Account Binding.\",\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:    flgHMAC,\n\t\t\tEnvVars: []string{envEABHMAC},\n\t\t\tUsage:   \"MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.\",\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:    flgKeyType,\n\t\t\tAliases: []string{\"k\"},\n\t\t\tValue:   \"ec256\",\n\t\t\tUsage:   \"Key type to use for private keys. Supported: rsa2048, rsa3072, rsa4096, rsa8192, ec256, ec384.\",\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:  flgFilename,\n\t\t\tUsage: \"(deprecated) Filename of the generated certificate.\",\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:    flgPath,\n\t\t\tEnvVars: []string{envPath},\n\t\t\tUsage:   \"Directory to use for storing the data.\",\n\t\t\tValue:   defaultPath,\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:  flgHTTP,\n\t\t\tUsage: \"Use the HTTP-01 challenge to solve challenges. Can be mixed with other types of challenges.\",\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:  flgHTTPPort,\n\t\t\tUsage: \"Set the port and interface to use for HTTP-01 based challenges to listen on. Supported: interface:port or :port.\",\n\t\t\tValue: \":80\",\n\t\t},\n\t\t&cli.DurationFlag{\n\t\t\tName:  flgHTTPDelay,\n\t\t\tUsage: \"Delay between the starts of the HTTP server (use for HTTP-01 based challenges) and the validation of the challenge.\",\n\t\t\tValue: 0,\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:  flgHTTPProxyHeader,\n\t\t\tUsage: \"Validate against this HTTP header when solving HTTP-01 based challenges behind a reverse proxy.\",\n\t\t\tValue: \"Host\",\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName: flgHTTPWebroot,\n\t\t\tUsage: \"Set the webroot folder to use for HTTP-01 based challenges to write directly to the .well-known/acme-challenge file.\" +\n\t\t\t\t\" This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge\",\n\t\t},\n\t\t&cli.StringSliceFlag{\n\t\t\tName:  flgHTTPMemcachedHost,\n\t\t\tUsage: \"Set the memcached host(s) to use for HTTP-01 based challenges. Challenges will be written to all specified hosts.\",\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:  flgHTTPS3Bucket,\n\t\t\tUsage: \"Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:  flgTLS,\n\t\t\tUsage: \"Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges.\",\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:  flgTLSPort,\n\t\t\tUsage: \"Set the port and interface to use for TLS-ALPN-01 based challenges to listen on. Supported: interface:port or :port.\",\n\t\t\tValue: \":443\",\n\t\t},\n\t\t&cli.DurationFlag{\n\t\t\tName:  flgTLSDelay,\n\t\t\tUsage: \"Delay between the start of the TLS listener (use for TLSALPN-01 based challenges) and the validation of the challenge.\",\n\t\t\tValue: 0,\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:  flgDNS,\n\t\t\tUsage: \"Solve a DNS-01 challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:  flgDNSDisableCP,\n\t\t\tUsage: fmt.Sprintf(\"(deprecated) use %s instead.\", flgDNSPropagationDisableANS),\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:  flgDNSPropagationDisableANS,\n\t\t\tUsage: \"By setting this flag to true, disables the need to await propagation of the TXT record to all authoritative name servers.\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:  flgDNSPropagationRNS,\n\t\t\tUsage: \"By setting this flag to true, use all the recursive nameservers to check the propagation of the TXT record.\",\n\t\t},\n\t\t&cli.DurationFlag{\n\t\t\tName:  flgDNSPropagationWait,\n\t\t\tUsage: \"By setting this flag, disables all the propagation checks of the TXT record and uses a wait duration instead.\",\n\t\t},\n\t\t&cli.StringSliceFlag{\n\t\t\tName: flgDNSResolvers,\n\t\t\tUsage: \"Set the resolvers to use for performing (recursive) CNAME resolving and apex domain determination.\" +\n\t\t\t\t\" For DNS-01 challenge verification, the authoritative DNS server is queried directly.\" +\n\t\t\t\t\" Supported: host:port.\" +\n\t\t\t\t\" The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.\",\n\t\t},\n\t\t&cli.IntFlag{\n\t\t\tName:  flgHTTPTimeout,\n\t\t\tUsage: \"Set the HTTP timeout value to a specific value in seconds.\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:  flgTLSSkipVerify,\n\t\t\tUsage: \"Skip the TLS verification of the ACME server.\",\n\t\t},\n\t\t&cli.IntFlag{\n\t\t\tName:  flgDNSTimeout,\n\t\t\tUsage: \"Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name server queries.\",\n\t\t\tValue: 10,\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:  flgPEM,\n\t\t\tUsage: \"Generate an additional .pem (base64) file by concatenating the .key and .crt files together.\",\n\t\t},\n\t\t&cli.BoolFlag{\n\t\t\tName:    flgPFX,\n\t\t\tUsage:   \"Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together.\",\n\t\t\tEnvVars: []string{envPFX},\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:    flgPFXPass,\n\t\t\tUsage:   \"The password used to encrypt the .pfx (PCKS#12) file.\",\n\t\t\tValue:   pkcs12.DefaultPassword,\n\t\t\tEnvVars: []string{envPFXPassword},\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:    flgPFXFormat,\n\t\t\tUsage:   \"The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256.\",\n\t\t\tValue:   \"RC2\",\n\t\t\tEnvVars: []string{envPFXFormat},\n\t\t},\n\t\t&cli.IntFlag{\n\t\t\tName:  flgCertTimeout,\n\t\t\tUsage: \"Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates.\",\n\t\t\tValue: 30,\n\t\t},\n\t\t&cli.IntFlag{\n\t\t\tName:  flgOverallRequestLimit,\n\t\t\tUsage: \"ACME overall requests limit.\",\n\t\t\tValue: certificate.DefaultOverallRequestLimit,\n\t\t},\n\t\t&cli.StringFlag{\n\t\t\tName:  flgUserAgent,\n\t\t\tUsage: \"Add to the user-agent sent to the CA to identify an application embedding lego-cli\",\n\t\t},\n\t}\n}\n\nfunc getTime(ctx *cli.Context, name string) time.Time {\n\tvalue := ctx.Timestamp(name)\n\tif value == nil {\n\t\treturn time.Time{}\n\t}\n\n\treturn *value\n}\n"
  },
  {
    "path": "cmd/hook.go",
    "content": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/certificate\"\n)\n\nconst (\n\thookEnvAccountEmail      = \"LEGO_ACCOUNT_EMAIL\"\n\thookEnvCertDomain        = \"LEGO_CERT_DOMAIN\"\n\thookEnvCertPath          = \"LEGO_CERT_PATH\"\n\thookEnvCertKeyPath       = \"LEGO_CERT_KEY_PATH\"\n\thookEnvIssuerCertKeyPath = \"LEGO_ISSUER_CERT_PATH\"\n\thookEnvCertPEMPath       = \"LEGO_CERT_PEM_PATH\"\n\thookEnvCertPFXPath       = \"LEGO_CERT_PFX_PATH\"\n)\n\nfunc launchHook(hook string, timeout time.Duration, meta map[string]string) error {\n\tif hook == \"\" {\n\t\treturn nil\n\t}\n\n\tctxCmd, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\tparts := strings.Fields(hook)\n\n\tcmd := exec.CommandContext(ctxCmd, parts[0], parts[1:]...)\n\n\tcmd.Env = append(os.Environ(), metaToEnv(meta)...)\n\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create pipe: %w\", err)\n\t}\n\n\tcmd.Stderr = cmd.Stdout\n\n\terr = cmd.Start()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"start command: %w\", err)\n\t}\n\n\tgo func() {\n\t\t<-ctxCmd.Done()\n\n\t\tif ctxCmd.Err() != nil {\n\t\t\t_ = cmd.Process.Kill()\n\t\t\t_ = stdout.Close()\n\t\t}\n\t}()\n\n\tscanner := bufio.NewScanner(stdout)\n\tfor scanner.Scan() {\n\t\tfmt.Println(scanner.Text())\n\t}\n\n\terr = cmd.Wait()\n\tif err != nil {\n\t\tif errors.Is(ctxCmd.Err(), context.DeadlineExceeded) {\n\t\t\treturn errors.New(\"hook timed out\")\n\t\t}\n\n\t\treturn fmt.Errorf(\"wait command: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc metaToEnv(meta map[string]string) []string {\n\tvar envs []string\n\n\tfor k, v := range meta {\n\t\tenvs = append(envs, k+\"=\"+v)\n\t}\n\n\treturn envs\n}\n\nfunc addPathToMetadata(meta map[string]string, domain string, certRes *certificate.Resource, certsStorage *CertificatesStorage) {\n\tmeta[hookEnvCertDomain] = domain\n\tmeta[hookEnvCertPath] = certsStorage.GetFileName(domain, certExt)\n\tmeta[hookEnvCertKeyPath] = certsStorage.GetFileName(domain, keyExt)\n\n\tif certRes.IssuerCertificate != nil {\n\t\tmeta[hookEnvIssuerCertKeyPath] = certsStorage.GetFileName(domain, issuerExt)\n\t}\n\n\tif certsStorage.pem {\n\t\tmeta[hookEnvCertPEMPath] = certsStorage.GetFileName(domain, pemExt)\n\t}\n\n\tif certsStorage.pfx {\n\t\tmeta[hookEnvCertPFXPath] = certsStorage.GetFileName(domain, pfxExt)\n\t}\n}\n"
  },
  {
    "path": "cmd/hook_test.go",
    "content": "package cmd\n\nimport (\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_launchHook(t *testing.T) {\n\terr := launchHook(\"echo foo\", 1*time.Second, map[string]string{})\n\trequire.NoError(t, err)\n}\n\nfunc Test_launchHook_errors(t *testing.T) {\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"skipping test on Windows\")\n\t}\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\thook     string\n\t\ttimeout  time.Duration\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"kill the hook\",\n\t\t\thook:     \"sleep 5\",\n\t\t\ttimeout:  1 * time.Second,\n\t\t\texpected: \"hook timed out\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"context timeout on Start\",\n\t\t\thook:     \"echo foo\",\n\t\t\ttimeout:  1 * time.Nanosecond,\n\t\t\texpected: \"start command: context deadline exceeded\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"multiple short sleeps\",\n\t\t\thook:     \"./testdata/sleepy.sh\",\n\t\t\ttimeout:  1 * time.Second,\n\t\t\texpected: \"hook timed out\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"long sleep\",\n\t\t\thook:     \"./testdata/sleeping_beauty.sh\",\n\t\t\ttimeout:  1 * time.Second,\n\t\t\texpected: \"hook timed out\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\terr := launchHook(test.hook, test.timeout, map[string]string{})\n\t\t\trequire.EqualError(t, err, test.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/lego/main.go",
    "content": "// Let's Encrypt client to go!\n// CLI application for generating Let's Encrypt certificates using the ACME package.\npackage main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/go-acme/lego/v4/cmd\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc main() {\n\tapp := cli.NewApp()\n\tapp.Name = \"lego\"\n\tapp.HelpName = \"lego\"\n\tapp.Usage = \"Let's Encrypt client written in Go\"\n\tapp.EnableBashCompletion = true\n\n\tapp.Version = getVersion()\n\tcli.VersionPrinter = func(c *cli.Context) {\n\t\tfmt.Printf(\"lego version %s %s/%s\\n\", c.App.Version, runtime.GOOS, runtime.GOARCH)\n\t}\n\n\tvar defaultPath string\n\n\tcwd, err := os.Getwd()\n\tif err == nil {\n\t\tdefaultPath = filepath.Join(cwd, \".lego\")\n\t}\n\n\tapp.Flags = cmd.CreateFlags(defaultPath)\n\n\tapp.Before = cmd.Before\n\n\tapp.Commands = cmd.CreateCommands()\n\n\terr = app.Run(os.Args)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/lego/zz_gen_version.go",
    "content": "// Code generated by 'internal/releaser'; DO NOT EDIT.\n\npackage main\n\nconst defaultVersion = \"v4.33.0+dev-detach\"\n\nvar version = \"\"\n\nfunc getVersion() string {\n\tif version == \"\" {\n\t\treturn defaultVersion\n\t}\n\n\treturn version\n}\n"
  },
  {
    "path": "cmd/setup.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/go-acme/lego/v4/lego\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/registration\"\n\t\"github.com/hashicorp/go-retryablehttp\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nconst filePerm os.FileMode = 0o600\n\n// setupClient creates a new client with challenge settings.\nfunc setupClient(ctx *cli.Context, account *Account, keyType certcrypto.KeyType) *lego.Client {\n\tclient := newClient(ctx, account, keyType)\n\n\tsetupChallenges(ctx, client)\n\n\treturn client\n}\n\nfunc setupAccount(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, certcrypto.KeyType) {\n\tkeyType := getKeyType(ctx)\n\tprivateKey := accountsStorage.GetPrivateKey(keyType)\n\n\tvar account *Account\n\tif accountsStorage.ExistsAccountFilePath() {\n\t\taccount = accountsStorage.LoadAccount(privateKey)\n\t} else {\n\t\taccount = &Account{Email: accountsStorage.GetEmail(), key: privateKey}\n\t}\n\n\treturn account, keyType\n}\n\nfunc newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyType) *lego.Client {\n\tconfig := lego.NewConfig(acc)\n\tconfig.CADirURL = ctx.String(flgServer)\n\n\tconfig.Certificate = lego.CertificateConfig{\n\t\tKeyType:             keyType,\n\t\tTimeout:             time.Duration(ctx.Int(flgCertTimeout)) * time.Second,\n\t\tOverallRequestLimit: ctx.Int(flgOverallRequestLimit),\n\t\tDisableCommonName:   ctx.Bool(flgDisableCommonName),\n\t}\n\tconfig.UserAgent = getUserAgent(ctx)\n\n\tif ctx.IsSet(flgHTTPTimeout) {\n\t\tconfig.HTTPClient.Timeout = time.Duration(ctx.Int(flgHTTPTimeout)) * time.Second\n\t}\n\n\tif ctx.Bool(flgTLSSkipVerify) {\n\t\tdefaultTransport, ok := config.HTTPClient.Transport.(*http.Transport)\n\t\tif ok { // This is always true because the default client used by the CLI defined the transport.\n\t\t\ttr := defaultTransport.Clone()\n\t\t\ttr.TLSClientConfig.InsecureSkipVerify = true\n\t\t\tconfig.HTTPClient.Transport = tr\n\t\t}\n\t}\n\n\tretryClient := retryablehttp.NewClient()\n\tretryClient.RetryMax = 5\n\tretryClient.HTTPClient = config.HTTPClient\n\tretryClient.CheckRetry = checkRetry\n\tretryClient.Logger = nil\n\n\tif _, v := os.LookupEnv(\"LEGO_DEBUG_ACME_HTTP_CLIENT\"); v {\n\t\tretryClient.Logger = log.Logger\n\t}\n\n\tconfig.HTTPClient = retryClient.StandardClient()\n\n\tclient, err := lego.NewClient(config)\n\tif err != nil {\n\t\tlog.Fatalf(\"Could not create client: %v\", err)\n\t}\n\n\tif client.GetExternalAccountRequired() && !ctx.IsSet(flgEAB) {\n\t\tlog.Fatalf(\"Server requires External Account Binding. Use --%s with --%s and --%s.\", flgEAB, flgKID, flgHMAC)\n\t}\n\n\treturn client\n}\n\n// getKeyType the type from which private keys should be generated.\nfunc getKeyType(ctx *cli.Context) certcrypto.KeyType {\n\tkeyType := ctx.String(flgKeyType)\n\tswitch strings.ToUpper(keyType) {\n\tcase \"RSA2048\":\n\t\treturn certcrypto.RSA2048\n\tcase \"RSA3072\":\n\t\treturn certcrypto.RSA3072\n\tcase \"RSA4096\":\n\t\treturn certcrypto.RSA4096\n\tcase \"RSA8192\":\n\t\treturn certcrypto.RSA8192\n\tcase \"EC256\":\n\t\treturn certcrypto.EC256\n\tcase \"EC384\":\n\t\treturn certcrypto.EC384\n\t}\n\n\tlog.Fatalf(\"Unsupported KeyType: %s\", keyType)\n\n\treturn \"\"\n}\n\nfunc getUserAgent(ctx *cli.Context) string {\n\treturn strings.TrimSpace(fmt.Sprintf(\"%s lego-cli/%s\", ctx.String(flgUserAgent), ctx.App.Version))\n}\n\nfunc createNonExistingFolder(path string) error {\n\tif _, err := os.Stat(path); os.IsNotExist(err) {\n\t\treturn os.MkdirAll(path, 0o700)\n\t} else if err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc readCSRFile(filename string) (*x509.CertificateRequest, error) {\n\tbytes, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\traw := bytes\n\n\t// see if we can find a PEM-encoded CSR\n\tvar p *pem.Block\n\n\trest := bytes\n\tfor {\n\t\t// decode a PEM block\n\t\tp, rest = pem.Decode(rest)\n\n\t\t// did we fail?\n\t\tif p == nil {\n\t\t\tbreak\n\t\t}\n\n\t\t// did we get a CSR?\n\t\tif p.Type == \"CERTIFICATE REQUEST\" || p.Type == \"NEW CERTIFICATE REQUEST\" {\n\t\t\traw = p.Bytes\n\t\t}\n\t}\n\n\t// no PEM-encoded CSR\n\t// assume we were given a DER-encoded ASN.1 CSR\n\t// (if this assumption is wrong, parsing these bytes will fail)\n\treturn x509.ParseCertificateRequest(raw)\n}\n\nfunc checkRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {\n\trt, err := retryablehttp.ErrorPropagatedRetryPolicy(ctx, resp, err)\n\tif err != nil {\n\t\treturn rt, err\n\t}\n\n\tif resp == nil {\n\t\treturn rt, nil\n\t}\n\n\tif resp.StatusCode/100 == 2 {\n\t\treturn rt, nil\n\t}\n\n\tall, err := io.ReadAll(resp.Body)\n\tif err == nil {\n\t\tvar errorDetails *acme.ProblemDetails\n\n\t\terr = json.Unmarshal(all, &errorDetails)\n\t\tif err != nil {\n\t\t\treturn rt, fmt.Errorf(\"%s %s: %s\", resp.Request.Method, resp.Request.URL.Redacted(), string(all))\n\t\t}\n\n\t\tswitch errorDetails.Type {\n\t\tcase acme.BadNonceErr:\n\t\t\treturn false, &acme.NonceError{\n\t\t\t\tProblemDetails: errorDetails,\n\t\t\t}\n\n\t\tcase acme.AlreadyReplacedErr:\n\t\t\tif errorDetails.HTTPStatus == http.StatusConflict {\n\t\t\t\treturn false, &acme.AlreadyReplacedError{\n\t\t\t\t\tProblemDetails: errorDetails,\n\t\t\t\t}\n\t\t\t}\n\n\t\tdefault:\n\t\t\tlog.Warnf(\"retry: %v\", errorDetails)\n\n\t\t\treturn rt, errorDetails\n\t\t}\n\t}\n\n\treturn rt, nil\n}\n"
  },
  {
    "path": "cmd/setup_challenges.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/challenge/http01\"\n\t\"github.com/go-acme/lego/v4/challenge/tlsalpn01\"\n\t\"github.com/go-acme/lego/v4/lego\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/providers/dns\"\n\t\"github.com/go-acme/lego/v4/providers/http/memcached\"\n\t\"github.com/go-acme/lego/v4/providers/http/s3\"\n\t\"github.com/go-acme/lego/v4/providers/http/webroot\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nfunc setupChallenges(ctx *cli.Context, client *lego.Client) {\n\tif !ctx.Bool(flgHTTP) && !ctx.Bool(flgTLS) && !ctx.IsSet(flgDNS) {\n\t\tlog.Fatalf(\"No challenge selected. You must specify at least one challenge: `--%s`, `--%s`, `--%s`.\", flgHTTP, flgTLS, flgDNS)\n\t}\n\n\tif ctx.Bool(flgHTTP) {\n\t\terr := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx), http01.SetDelay(ctx.Duration(flgHTTPDelay)))\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t}\n\n\tif ctx.Bool(flgTLS) {\n\t\terr := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx), tlsalpn01.SetDelay(ctx.Duration(flgTLSDelay)))\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t}\n\n\tif ctx.IsSet(flgDNS) {\n\t\terr := setupDNS(ctx, client)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t}\n}\n\n//nolint:gocyclo // the complexity is expected.\nfunc setupHTTPProvider(ctx *cli.Context) challenge.Provider {\n\tswitch {\n\tcase ctx.IsSet(flgHTTPWebroot):\n\t\tps, err := webroot.NewHTTPProvider(ctx.String(flgHTTPWebroot))\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\treturn ps\n\tcase ctx.IsSet(flgHTTPMemcachedHost):\n\t\tps, err := memcached.NewMemcachedProvider(ctx.StringSlice(flgHTTPMemcachedHost))\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\treturn ps\n\tcase ctx.IsSet(flgHTTPS3Bucket):\n\t\tps, err := s3.NewHTTPProvider(ctx.String(flgHTTPS3Bucket))\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\treturn ps\n\tcase ctx.IsSet(flgHTTPPort):\n\t\tiface := ctx.String(flgHTTPPort)\n\t\tif !strings.Contains(iface, \":\") {\n\t\t\tlog.Fatalf(\"The --%s switch only accepts interface:port or :port for its argument.\", flgHTTPPort)\n\t\t}\n\n\t\thost, port, err := net.SplitHostPort(iface)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\tsrv := http01.NewProviderServer(host, port)\n\t\tif header := ctx.String(flgHTTPProxyHeader); header != \"\" {\n\t\t\tsrv.SetProxyHeader(header)\n\t\t}\n\n\t\treturn srv\n\tcase ctx.Bool(flgHTTP):\n\t\tsrv := http01.NewProviderServer(\"\", \"\")\n\t\tif header := ctx.String(flgHTTPProxyHeader); header != \"\" {\n\t\t\tsrv.SetProxyHeader(header)\n\t\t}\n\n\t\treturn srv\n\tdefault:\n\t\tlog.Fatal(\"Invalid HTTP challenge options.\")\n\t\treturn nil\n\t}\n}\n\nfunc setupTLSProvider(ctx *cli.Context) challenge.Provider {\n\tswitch {\n\tcase ctx.IsSet(flgTLSPort):\n\t\tiface := ctx.String(flgTLSPort)\n\t\tif !strings.Contains(iface, \":\") {\n\t\t\tlog.Fatalf(\"The --%s switch only accepts interface:port or :port for its argument.\", flgTLSPort)\n\t\t}\n\n\t\thost, port, err := net.SplitHostPort(iface)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\treturn tlsalpn01.NewProviderServer(host, port)\n\tcase ctx.Bool(flgTLS):\n\t\treturn tlsalpn01.NewProviderServer(\"\", \"\")\n\tdefault:\n\t\tlog.Fatal(\"Invalid HTTP challenge options.\")\n\t\treturn nil\n\t}\n}\n\nfunc setupDNS(ctx *cli.Context, client *lego.Client) error {\n\terr := checkPropagationExclusiveOptions(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\twait := ctx.Duration(flgDNSPropagationWait)\n\tif wait < 0 {\n\t\treturn fmt.Errorf(\"'%s' cannot be negative\", flgDNSPropagationWait)\n\t}\n\n\tprovider, err := dns.NewDNSChallengeProviderByName(ctx.String(flgDNS))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tservers := ctx.StringSlice(flgDNSResolvers)\n\n\terr = client.Challenge.SetDNS01Provider(provider,\n\t\tdns01.CondOption(len(servers) > 0,\n\t\t\tdns01.AddRecursiveNameservers(dns01.ParseNameservers(ctx.StringSlice(flgDNSResolvers)))),\n\n\t\tdns01.CondOption(ctx.Bool(flgDNSDisableCP) || ctx.Bool(flgDNSPropagationDisableANS),\n\t\t\tdns01.DisableAuthoritativeNssPropagationRequirement()),\n\n\t\tdns01.CondOption(ctx.Duration(flgDNSPropagationWait) > 0,\n\t\t\t// TODO(ldez): inside the next major version we will use flgDNSDisableCP here.\n\t\t\t// This will change the meaning of this flag to really disable all propagation checks.\n\t\t\tdns01.PropagationWait(wait, true)),\n\n\t\tdns01.CondOption(ctx.Bool(flgDNSPropagationRNS),\n\t\t\tdns01.RecursiveNSsPropagationRequirement()),\n\n\t\tdns01.CondOption(ctx.IsSet(flgDNSTimeout),\n\t\t\tdns01.AddDNSTimeout(time.Duration(ctx.Int(flgDNSTimeout))*time.Second)),\n\t)\n\n\treturn err\n}\n\nfunc checkPropagationExclusiveOptions(ctx *cli.Context) error {\n\tif ctx.IsSet(flgDNSDisableCP) {\n\t\tlog.Printf(\"The flag '%s' is deprecated use '%s' instead.\", flgDNSDisableCP, flgDNSPropagationDisableANS)\n\t}\n\n\tif (isSetBool(ctx, flgDNSDisableCP) || isSetBool(ctx, flgDNSPropagationDisableANS)) && ctx.IsSet(flgDNSPropagationWait) {\n\t\treturn fmt.Errorf(\"'%s' and '%s' are mutually exclusive\", flgDNSPropagationDisableANS, flgDNSPropagationWait)\n\t}\n\n\tif isSetBool(ctx, flgDNSPropagationRNS) && ctx.IsSet(flgDNSPropagationWait) {\n\t\treturn fmt.Errorf(\"'%s' and '%s' are mutually exclusive\", flgDNSPropagationRNS, flgDNSPropagationWait)\n\t}\n\n\treturn nil\n}\n\nfunc isSetBool(ctx *cli.Context, name string) bool {\n\treturn ctx.IsSet(name) && ctx.Bool(name)\n}\n"
  },
  {
    "path": "cmd/testdata/sleeping_beauty.sh",
    "content": "#!/bin/bash -e\n\nsleep 50\n"
  },
  {
    "path": "cmd/testdata/sleepy.sh",
    "content": "#!/bin/bash -e\n\nfor i in `seq 1 10`\ndo\n  echo $i\n  sleep 0.2\ndone\n"
  },
  {
    "path": "cmd/zz_gen_cmd_dnshelp.go",
    "content": "// Code generated by 'make generate-dns'; DO NOT EDIT.\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/tabwriter\"\n)\n\nfunc allDNSCodes() string {\n\tproviders := []string{\n\t\t\"acme-dns\",\n\t\t\"active24\",\n\t\t\"alidns\",\n\t\t\"aliesa\",\n\t\t\"allinkl\",\n\t\t\"alwaysdata\",\n\t\t\"anexia\",\n\t\t\"artfiles\",\n\t\t\"arvancloud\",\n\t\t\"auroradns\",\n\t\t\"autodns\",\n\t\t\"axelname\",\n\t\t\"azion\",\n\t\t\"azure\",\n\t\t\"azuredns\",\n\t\t\"baiducloud\",\n\t\t\"beget\",\n\t\t\"binarylane\",\n\t\t\"bindman\",\n\t\t\"bluecat\",\n\t\t\"bluecatv2\",\n\t\t\"bookmyname\",\n\t\t\"brandit\",\n\t\t\"bunny\",\n\t\t\"checkdomain\",\n\t\t\"civo\",\n\t\t\"clouddns\",\n\t\t\"cloudflare\",\n\t\t\"cloudns\",\n\t\t\"cloudru\",\n\t\t\"cloudxns\",\n\t\t\"com35\",\n\t\t\"conoha\",\n\t\t\"conohav3\",\n\t\t\"constellix\",\n\t\t\"corenetworks\",\n\t\t\"cpanel\",\n\t\t\"czechia\",\n\t\t\"ddnss\",\n\t\t\"derak\",\n\t\t\"desec\",\n\t\t\"designate\",\n\t\t\"digitalocean\",\n\t\t\"directadmin\",\n\t\t\"dnsexit\",\n\t\t\"dnshomede\",\n\t\t\"dnsimple\",\n\t\t\"dnsmadeeasy\",\n\t\t\"dnspod\",\n\t\t\"dode\",\n\t\t\"domeneshop\",\n\t\t\"dreamhost\",\n\t\t\"duckdns\",\n\t\t\"dyn\",\n\t\t\"dyndnsfree\",\n\t\t\"dynu\",\n\t\t\"easydns\",\n\t\t\"edgecenter\",\n\t\t\"edgedns\",\n\t\t\"edgeone\",\n\t\t\"efficientip\",\n\t\t\"epik\",\n\t\t\"eurodns\",\n\t\t\"excedo\",\n\t\t\"exec\",\n\t\t\"exoscale\",\n\t\t\"f5xc\",\n\t\t\"freemyip\",\n\t\t\"gandi\",\n\t\t\"gandiv5\",\n\t\t\"gcloud\",\n\t\t\"gcore\",\n\t\t\"gigahostno\",\n\t\t\"glesys\",\n\t\t\"godaddy\",\n\t\t\"googledomains\",\n\t\t\"gravity\",\n\t\t\"hetzner\",\n\t\t\"hostingde\",\n\t\t\"hostinger\",\n\t\t\"hostingnl\",\n\t\t\"hosttech\",\n\t\t\"httpnet\",\n\t\t\"httpreq\",\n\t\t\"huaweicloud\",\n\t\t\"hurricane\",\n\t\t\"hyperone\",\n\t\t\"ibmcloud\",\n\t\t\"iij\",\n\t\t\"iijdpf\",\n\t\t\"infoblox\",\n\t\t\"infomaniak\",\n\t\t\"internetbs\",\n\t\t\"inwx\",\n\t\t\"ionos\",\n\t\t\"ionoscloud\",\n\t\t\"ipv64\",\n\t\t\"ispconfig\",\n\t\t\"ispconfigddns\",\n\t\t\"iwantmyname\",\n\t\t\"jdcloud\",\n\t\t\"joker\",\n\t\t\"keyhelp\",\n\t\t\"leaseweb\",\n\t\t\"liara\",\n\t\t\"lightsail\",\n\t\t\"limacity\",\n\t\t\"linode\",\n\t\t\"liquidweb\",\n\t\t\"loopia\",\n\t\t\"luadns\",\n\t\t\"mailinabox\",\n\t\t\"manageengine\",\n\t\t\"manual\",\n\t\t\"metaname\",\n\t\t\"metaregistrar\",\n\t\t\"mijnhost\",\n\t\t\"mittwald\",\n\t\t\"myaddr\",\n\t\t\"mydnsjp\",\n\t\t\"mythicbeasts\",\n\t\t\"namecheap\",\n\t\t\"namedotcom\",\n\t\t\"namesilo\",\n\t\t\"namesurfer\",\n\t\t\"nearlyfreespeech\",\n\t\t\"neodigit\",\n\t\t\"netcup\",\n\t\t\"netlify\",\n\t\t\"nicmanager\",\n\t\t\"nicru\",\n\t\t\"nifcloud\",\n\t\t\"njalla\",\n\t\t\"nodion\",\n\t\t\"ns1\",\n\t\t\"octenium\",\n\t\t\"oraclecloud\",\n\t\t\"otc\",\n\t\t\"ovh\",\n\t\t\"pdns\",\n\t\t\"plesk\",\n\t\t\"porkbun\",\n\t\t\"rackspace\",\n\t\t\"rainyun\",\n\t\t\"rcodezero\",\n\t\t\"regfish\",\n\t\t\"regru\",\n\t\t\"rfc2136\",\n\t\t\"rimuhosting\",\n\t\t\"route53\",\n\t\t\"safedns\",\n\t\t\"sakuracloud\",\n\t\t\"scaleway\",\n\t\t\"selectel\",\n\t\t\"selectelv2\",\n\t\t\"selfhostde\",\n\t\t\"servercow\",\n\t\t\"shellrent\",\n\t\t\"simply\",\n\t\t\"sonic\",\n\t\t\"spaceship\",\n\t\t\"stackpath\",\n\t\t\"syse\",\n\t\t\"technitium\",\n\t\t\"tencentcloud\",\n\t\t\"timewebcloud\",\n\t\t\"todaynic\",\n\t\t\"transip\",\n\t\t\"ultradns\",\n\t\t\"uniteddomains\",\n\t\t\"variomedia\",\n\t\t\"vegadns\",\n\t\t\"vercel\",\n\t\t\"versio\",\n\t\t\"vinyldns\",\n\t\t\"virtualname\",\n\t\t\"vkcloud\",\n\t\t\"volcengine\",\n\t\t\"vscale\",\n\t\t\"vultr\",\n\t\t\"webnames\",\n\t\t\"webnamesca\",\n\t\t\"websupport\",\n\t\t\"wedos\",\n\t\t\"westcn\",\n\t\t\"yandex\",\n\t\t\"yandex360\",\n\t\t\"yandexcloud\",\n\t\t\"zoneedit\",\n\t\t\"zoneee\",\n\t\t\"zonomi\",\n\t}\n\tsort.Strings(providers)\n\treturn strings.Join(providers, \", \")\n}\n\nfunc displayDNSHelp(w io.Writer, name string) error {\n\tw = tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)\n\tew := &errWriter{w: w}\n\n\tswitch name {\n\tcase \"acme-dns\":\n\t\t// generated from: providers/dns/acmedns/acmedns.toml\n\t\tew.writeln(`Configuration for Joohoi's ACME-DNS.`)\n\t\tew.writeln(`Code:\t'acme-dns'`)\n\t\tew.writeln(`Since:\t'v1.1.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ACME_DNS_API_BASE\":\tThe ACME-DNS API address`)\n\t\tew.writeln(`\t- \"ACME_DNS_STORAGE_BASE_URL\":\tThe ACME-DNS JSON account data server.`)\n\t\tew.writeln(`\t- \"ACME_DNS_STORAGE_PATH\":\tThe ACME-DNS JSON account data file. A per-domain account will be registered/persisted to this file and used for TXT updates.`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ACME_DNS_ALLOWLIST\":\tSource networks using CIDR notation (multiple values should be separated with a comma).`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/acme-dns`)\n\n\tcase \"active24\":\n\t\t// generated from: providers/dns/active24/active24.toml\n\t\tew.writeln(`Configuration for Active24.`)\n\t\tew.writeln(`Code:\t'active24'`)\n\t\tew.writeln(`Since:\t'v4.23.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ACTIVE24_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"ACTIVE24_SECRET\":\tSecret`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ACTIVE24_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"ACTIVE24_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"ACTIVE24_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"ACTIVE24_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/active24`)\n\n\tcase \"alidns\":\n\t\t// generated from: providers/dns/alidns/alidns.toml\n\t\tew.writeln(`Configuration for Alibaba Cloud DNS.`)\n\t\tew.writeln(`Code:\t'alidns'`)\n\t\tew.writeln(`Since:\t'v1.1.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ALICLOUD_ACCESS_KEY\":\tAccess key ID`)\n\t\tew.writeln(`\t- \"ALICLOUD_RAM_ROLE\":\tYour instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance)`)\n\t\tew.writeln(`\t- \"ALICLOUD_SECRET_KEY\":\tAccess Key secret`)\n\t\tew.writeln(`\t- \"ALICLOUD_SECURITY_TOKEN\":\tSTS Security Token (optional)`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ALICLOUD_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"ALICLOUD_LINE\":\tLine (Default: default)`)\n\t\tew.writeln(`\t- \"ALICLOUD_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"ALICLOUD_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"ALICLOUD_REGION_ID\":\tRegion ID (Default: cn-hangzhou)`)\n\t\tew.writeln(`\t- \"ALICLOUD_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/alidns`)\n\n\tcase \"aliesa\":\n\t\t// generated from: providers/dns/aliesa/aliesa.toml\n\t\tew.writeln(`Configuration for AlibabaCloud ESA.`)\n\t\tew.writeln(`Code:\t'aliesa'`)\n\t\tew.writeln(`Since:\t'v4.29.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ALIESA_ACCESS_KEY\":\tAccess key ID`)\n\t\tew.writeln(`\t- \"ALIESA_RAM_ROLE\":\tYour instance RAM role (https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance)`)\n\t\tew.writeln(`\t- \"ALIESA_SECRET_KEY\":\tAccess Key secret`)\n\t\tew.writeln(`\t- \"ALIESA_SECURITY_TOKEN\":\tSTS Security Token (optional)`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ALIESA_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"ALIESA_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"ALIESA_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"ALIESA_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/aliesa`)\n\n\tcase \"allinkl\":\n\t\t// generated from: providers/dns/allinkl/allinkl.toml\n\t\tew.writeln(`Configuration for all-inkl.`)\n\t\tew.writeln(`Code:\t'allinkl'`)\n\t\tew.writeln(`Since:\t'v4.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ALL_INKL_LOGIN\":\tKAS login`)\n\t\tew.writeln(`\t- \"ALL_INKL_PASSWORD\":\tKAS password`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ALL_INKL_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"ALL_INKL_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"ALL_INKL_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/allinkl`)\n\n\tcase \"alwaysdata\":\n\t\t// generated from: providers/dns/alwaysdata/alwaysdata.toml\n\t\tew.writeln(`Configuration for Alwaysdata.`)\n\t\tew.writeln(`Code:\t'alwaysdata'`)\n\t\tew.writeln(`Since:\t'v4.31.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ALWAYSDATA_API_KEY\":\tAPI Key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ALWAYSDATA_ACCOUNT\":\tAccount name`)\n\t\tew.writeln(`\t- \"ALWAYSDATA_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"ALWAYSDATA_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"ALWAYSDATA_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"ALWAYSDATA_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/alwaysdata`)\n\n\tcase \"anexia\":\n\t\t// generated from: providers/dns/anexia/anexia.toml\n\t\tew.writeln(`Configuration for Anexia CloudDNS.`)\n\t\tew.writeln(`Code:\t'anexia'`)\n\t\tew.writeln(`Since:\t'v4.28.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ANEXIA_TOKEN\":\tAPI token for Anexia Engine`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ANEXIA_API_URL\":\tAPI endpoint URL (default: https://engine.anexia-it.com)`)\n\t\tew.writeln(`\t- \"ANEXIA_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"ANEXIA_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"ANEXIA_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 300)`)\n\t\tew.writeln(`\t- \"ANEXIA_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/anexia`)\n\n\tcase \"artfiles\":\n\t\t// generated from: providers/dns/artfiles/artfiles.toml\n\t\tew.writeln(`Configuration for ArtFiles.`)\n\t\tew.writeln(`Code:\t'artfiles'`)\n\t\tew.writeln(`Since:\t'v4.32.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ARTFILES_PASSWORD\":\tAPI password`)\n\t\tew.writeln(`\t- \"ARTFILES_USERNAME\":\tAPI username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ARTFILES_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"ARTFILES_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"ARTFILES_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 360)`)\n\t\tew.writeln(`\t- \"ARTFILES_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/artfiles`)\n\n\tcase \"arvancloud\":\n\t\t// generated from: providers/dns/arvancloud/arvancloud.toml\n\t\tew.writeln(`Configuration for ArvanCloud.`)\n\t\tew.writeln(`Code:\t'arvancloud'`)\n\t\tew.writeln(`Since:\t'v3.8.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ARVANCLOUD_API_KEY\":\tAPI key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ARVANCLOUD_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"ARVANCLOUD_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"ARVANCLOUD_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"ARVANCLOUD_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/arvancloud`)\n\n\tcase \"auroradns\":\n\t\t// generated from: providers/dns/auroradns/auroradns.toml\n\t\tew.writeln(`Configuration for Aurora DNS.`)\n\t\tew.writeln(`Code:\t'auroradns'`)\n\t\tew.writeln(`Since:\t'v0.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"AURORA_API_KEY\":\tAPI key or username to used`)\n\t\tew.writeln(`\t- \"AURORA_SECRET\":\tSecret password to be used`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"AURORA_ENDPOINT\":\tAPI endpoint URL`)\n\t\tew.writeln(`\t- \"AURORA_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"AURORA_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"AURORA_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/auroradns`)\n\n\tcase \"autodns\":\n\t\t// generated from: providers/dns/autodns/autodns.toml\n\t\tew.writeln(`Configuration for Autodns.`)\n\t\tew.writeln(`Code:\t'autodns'`)\n\t\tew.writeln(`Since:\t'v3.2.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"AUTODNS_API_PASSWORD\":\tUser Password`)\n\t\tew.writeln(`\t- \"AUTODNS_API_USER\":\tUsername`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"AUTODNS_CONTEXT\":\tAPI context (4 for production, 1 for testing. Defaults to 4)`)\n\t\tew.writeln(`\t- \"AUTODNS_ENDPOINT\":\tAPI endpoint URL, defaults to https://api.autodns.com/v1/`)\n\t\tew.writeln(`\t- \"AUTODNS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"AUTODNS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"AUTODNS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"AUTODNS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/autodns`)\n\n\tcase \"axelname\":\n\t\t// generated from: providers/dns/axelname/axelname.toml\n\t\tew.writeln(`Configuration for Axelname.`)\n\t\tew.writeln(`Code:\t'axelname'`)\n\t\tew.writeln(`Since:\t'v4.23.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"AXELNAME_NICKNAME\":\tAccount nickname`)\n\t\tew.writeln(`\t- \"AXELNAME_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"AXELNAME_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"AXELNAME_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"AXELNAME_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"AXELNAME_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/axelname`)\n\n\tcase \"azion\":\n\t\t// generated from: providers/dns/azion/azion.toml\n\t\tew.writeln(`Configuration for Azion.`)\n\t\tew.writeln(`Code:\t'azion'`)\n\t\tew.writeln(`Since:\t'v4.24.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"AZION_PERSONAL_TOKEN\":\tYour Azion personal token.`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"AZION_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"AZION_PAGE_SIZE\":\tThe page size for the API request (Default: 50)`)\n\t\tew.writeln(`\t- \"AZION_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"AZION_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"AZION_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/azion`)\n\n\tcase \"azure\":\n\t\t// generated from: providers/dns/azure/azure.toml\n\t\tew.writeln(`Configuration for Azure (deprecated).`)\n\t\tew.writeln(`Code:\t'azure'`)\n\t\tew.writeln(`Since:\t'v0.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"AZURE_CLIENT_ID\":\tClient ID`)\n\t\tew.writeln(`\t- \"AZURE_CLIENT_SECRET\":\tClient secret`)\n\t\tew.writeln(`\t- \"AZURE_ENVIRONMENT\":\tAzure environment, one of: public, usgovernment, german, and china`)\n\t\tew.writeln(`\t- \"AZURE_RESOURCE_GROUP\":\tResource group`)\n\t\tew.writeln(`\t- \"AZURE_SUBSCRIPTION_ID\":\tSubscription ID`)\n\t\tew.writeln(`\t- \"AZURE_TENANT_ID\":\tTenant ID`)\n\t\tew.writeln(`\t- \"instance metadata service\":\tIf the credentials are **not** set via the environment, then it will attempt to get a bearer token via the [instance metadata service](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service).`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"AZURE_METADATA_ENDPOINT\":\tMetadata Service endpoint URL`)\n\t\tew.writeln(`\t- \"AZURE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"AZURE_PRIVATE_ZONE\":\tSet to true to use Azure Private DNS Zones and not public`)\n\t\tew.writeln(`\t- \"AZURE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"AZURE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"AZURE_ZONE_NAME\":\tZone name to use inside Azure DNS service to add the TXT record in`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/azure`)\n\n\tcase \"azuredns\":\n\t\t// generated from: providers/dns/azuredns/azuredns.toml\n\t\tew.writeln(`Configuration for Azure DNS.`)\n\t\tew.writeln(`Code:\t'azuredns'`)\n\t\tew.writeln(`Since:\t'v4.13.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"AZURE_CLIENT_CERTIFICATE_PATH\":\tClient certificate path`)\n\t\tew.writeln(`\t- \"AZURE_CLIENT_ID\":\tClient ID`)\n\t\tew.writeln(`\t- \"AZURE_CLIENT_SECRET\":\tClient secret`)\n\t\tew.writeln(`\t- \"AZURE_TENANT_ID\":\tTenant ID`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"AZURE_AUTH_METHOD\":\tSpecify which authentication method to use`)\n\t\tew.writeln(`\t- \"AZURE_AUTH_MSI_TIMEOUT\":\tManaged Identity timeout duration`)\n\t\tew.writeln(`\t- \"AZURE_ENVIRONMENT\":\tAzure environment, one of: public, usgovernment, and china`)\n\t\tew.writeln(`\t- \"AZURE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"AZURE_PRIVATE_ZONE\":\tSet to true to use Azure Private DNS Zones and not public`)\n\t\tew.writeln(`\t- \"AZURE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"AZURE_RESOURCE_GROUP\":\tDNS zone resource group`)\n\t\tew.writeln(`\t- \"AZURE_SERVICEDISCOVERY_FILTER\":\tAdvanced ServiceDiscovery filter using Kusto query condition`)\n\t\tew.writeln(`\t- \"AZURE_SUBSCRIPTION_ID\":\tDNS zone subscription ID`)\n\t\tew.writeln(`\t- \"AZURE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"AZURE_ZONE_NAME\":\tZone name to use inside Azure DNS service to add the TXT record in`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/azuredns`)\n\n\tcase \"baiducloud\":\n\t\t// generated from: providers/dns/baiducloud/baiducloud.toml\n\t\tew.writeln(`Configuration for Baidu Cloud.`)\n\t\tew.writeln(`Code:\t'baiducloud'`)\n\t\tew.writeln(`Since:\t'v4.23.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"BAIDUCLOUD_ACCESS_KEY_ID\":\tAccess key`)\n\t\tew.writeln(`\t- \"BAIDUCLOUD_SECRET_ACCESS_KEY\":\tSecret access key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"BAIDUCLOUD_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"BAIDUCLOUD_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"BAIDUCLOUD_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/baiducloud`)\n\n\tcase \"beget\":\n\t\t// generated from: providers/dns/beget/beget.toml\n\t\tew.writeln(`Configuration for Beget.com.`)\n\t\tew.writeln(`Code:\t'beget'`)\n\t\tew.writeln(`Since:\t'v4.27.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"BEGET_PASSWORD\":\tAPI password`)\n\t\tew.writeln(`\t- \"BEGET_USERNAME\":\tAPI username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"BEGET_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"BEGET_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"BEGET_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 300)`)\n\t\tew.writeln(`\t- \"BEGET_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/beget`)\n\n\tcase \"binarylane\":\n\t\t// generated from: providers/dns/binarylane/binarylane.toml\n\t\tew.writeln(`Configuration for Binary Lane.`)\n\t\tew.writeln(`Code:\t'binarylane'`)\n\t\tew.writeln(`Since:\t'v4.26.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"BINARYLANE_API_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"BINARYLANE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"BINARYLANE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"BINARYLANE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"BINARYLANE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/binarylane`)\n\n\tcase \"bindman\":\n\t\t// generated from: providers/dns/bindman/bindman.toml\n\t\tew.writeln(`Configuration for Bindman.`)\n\t\tew.writeln(`Code:\t'bindman'`)\n\t\tew.writeln(`Since:\t'v2.6.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"BINDMAN_MANAGER_ADDRESS\":\tThe server URL, should have scheme, hostname, and port (if required) of the Bindman-DNS Manager server`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"BINDMAN_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"BINDMAN_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"BINDMAN_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/bindman`)\n\n\tcase \"bluecat\":\n\t\t// generated from: providers/dns/bluecat/bluecat.toml\n\t\tew.writeln(`Configuration for Bluecat.`)\n\t\tew.writeln(`Code:\t'bluecat'`)\n\t\tew.writeln(`Since:\t'v0.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"BLUECAT_CONFIG_NAME\":\tConfiguration name`)\n\t\tew.writeln(`\t- \"BLUECAT_DNS_VIEW\":\tExternal DNS View Name`)\n\t\tew.writeln(`\t- \"BLUECAT_PASSWORD\":\tAPI password`)\n\t\tew.writeln(`\t- \"BLUECAT_SERVER_URL\":\tThe server URL, should have scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve`)\n\t\tew.writeln(`\t- \"BLUECAT_USER_NAME\":\tAPI username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"BLUECAT_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"BLUECAT_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"BLUECAT_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"BLUECAT_SKIP_DEPLOY\":\tSkip deployements`)\n\t\tew.writeln(`\t- \"BLUECAT_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/bluecat`)\n\n\tcase \"bluecatv2\":\n\t\t// generated from: providers/dns/bluecatv2/bluecatv2.toml\n\t\tew.writeln(`Configuration for Bluecat v2.`)\n\t\tew.writeln(`Code:\t'bluecatv2'`)\n\t\tew.writeln(`Since:\t'v4.32.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"BLUECATV2_CONFIG_NAME\":\tConfiguration name`)\n\t\tew.writeln(`\t- \"BLUECATV2_PASSWORD\":\tAPI password`)\n\t\tew.writeln(`\t- \"BLUECATV2_USERNAME\":\tAPI username`)\n\t\tew.writeln(`\t- \"BLUECATV2_VIEW_NAME\":\tDNS View Name`)\n\t\tew.writeln(`\t- \"BLUECAT_SERVER_URL\":\tThe server URL: it should have a scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"BLUECATV2_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"BLUECATV2_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"BLUECATV2_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"BLUECATV2_SKIP_DEPLOY\":\tSkip quick deployements`)\n\t\tew.writeln(`\t- \"BLUECATV2_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/bluecatv2`)\n\n\tcase \"bookmyname\":\n\t\t// generated from: providers/dns/bookmyname/bookmyname.toml\n\t\tew.writeln(`Configuration for BookMyName.`)\n\t\tew.writeln(`Code:\t'bookmyname'`)\n\t\tew.writeln(`Since:\t'v4.23.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"BOOKMYNAME_PASSWORD\":\tPassword`)\n\t\tew.writeln(`\t- \"BOOKMYNAME_USERNAME\":\tUsername`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"BOOKMYNAME_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"BOOKMYNAME_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"BOOKMYNAME_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"BOOKMYNAME_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/bookmyname`)\n\n\tcase \"brandit\":\n\t\t// generated from: providers/dns/brandit/brandit.toml\n\t\tew.writeln(`Configuration for Brandit (deprecated).`)\n\t\tew.writeln(`Code:\t'brandit'`)\n\t\tew.writeln(`Since:\t'v4.11.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"BRANDIT_API_KEY\":\tThe API key`)\n\t\tew.writeln(`\t- \"BRANDIT_API_USERNAME\":\tThe API username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"BRANDIT_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"BRANDIT_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"BRANDIT_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 600)`)\n\t\tew.writeln(`\t- \"BRANDIT_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/brandit`)\n\n\tcase \"bunny\":\n\t\t// generated from: providers/dns/bunny/bunny.toml\n\t\tew.writeln(`Configuration for Bunny.`)\n\t\tew.writeln(`Code:\t'bunny'`)\n\t\tew.writeln(`Since:\t'v4.11.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"BUNNY_API_KEY\":\tAPI key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"BUNNY_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"BUNNY_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"BUNNY_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"BUNNY_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/bunny`)\n\n\tcase \"checkdomain\":\n\t\t// generated from: providers/dns/checkdomain/checkdomain.toml\n\t\tew.writeln(`Configuration for Checkdomain.`)\n\t\tew.writeln(`Code:\t'checkdomain'`)\n\t\tew.writeln(`Since:\t'v3.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"CHECKDOMAIN_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"CHECKDOMAIN_ENDPOINT\":\tAPI endpoint URL, defaults to https://api.checkdomain.de`)\n\t\tew.writeln(`\t- \"CHECKDOMAIN_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"CHECKDOMAIN_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 300)`)\n\t\tew.writeln(`\t- \"CHECKDOMAIN_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 7)`)\n\t\tew.writeln(`\t- \"CHECKDOMAIN_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/checkdomain`)\n\n\tcase \"civo\":\n\t\t// generated from: providers/dns/civo/civo.toml\n\t\tew.writeln(`Configuration for Civo.`)\n\t\tew.writeln(`Code:\t'civo'`)\n\t\tew.writeln(`Since:\t'v4.9.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"CIVO_TOKEN\":\tAuthentication token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"CIVO_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"CIVO_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 300)`)\n\t\tew.writeln(`\t- \"CIVO_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/civo`)\n\n\tcase \"clouddns\":\n\t\t// generated from: providers/dns/clouddns/clouddns.toml\n\t\tew.writeln(`Configuration for CloudDNS.`)\n\t\tew.writeln(`Code:\t'clouddns'`)\n\t\tew.writeln(`Since:\t'v3.6.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"CLOUDDNS_CLIENT_ID\":\tClient ID`)\n\t\tew.writeln(`\t- \"CLOUDDNS_EMAIL\":\tAccount email`)\n\t\tew.writeln(`\t- \"CLOUDDNS_PASSWORD\":\tAccount password`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"CLOUDDNS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"CLOUDDNS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 5)`)\n\t\tew.writeln(`\t- \"CLOUDDNS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"CLOUDDNS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/clouddns`)\n\n\tcase \"cloudflare\":\n\t\t// generated from: providers/dns/cloudflare/cloudflare.toml\n\t\tew.writeln(`Configuration for Cloudflare.`)\n\t\tew.writeln(`Code:\t'cloudflare'`)\n\t\tew.writeln(`Since:\t'v0.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"CF_API_EMAIL\":\tAccount email`)\n\t\tew.writeln(`\t- \"CF_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"CF_DNS_API_TOKEN\":\tAPI token with DNS:Edit permission (since v3.1.0)`)\n\t\tew.writeln(`\t- \"CF_ZONE_API_TOKEN\":\tAPI token with Zone:Read permission (since v3.1.0)`)\n\t\tew.writeln(`\t- \"CLOUDFLARE_API_KEY\":\tAlias to CF_API_KEY`)\n\t\tew.writeln(`\t- \"CLOUDFLARE_DNS_API_TOKEN\":\tAlias to CF_DNS_API_TOKEN`)\n\t\tew.writeln(`\t- \"CLOUDFLARE_EMAIL\":\tAlias to CF_API_EMAIL`)\n\t\tew.writeln(`\t- \"CLOUDFLARE_ZONE_API_TOKEN\":\tAlias to CF_ZONE_API_TOKEN`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"CLOUDFLARE_BASE_URL\":\tAPI base URL (Default: https://api.cloudflare.com/client/v4)`)\n\t\tew.writeln(`\t- \"CLOUDFLARE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: )`)\n\t\tew.writeln(`\t- \"CLOUDFLARE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"CLOUDFLARE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"CLOUDFLARE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudflare`)\n\n\tcase \"cloudns\":\n\t\t// generated from: providers/dns/cloudns/cloudns.toml\n\t\tew.writeln(`Configuration for ClouDNS.`)\n\t\tew.writeln(`Code:\t'cloudns'`)\n\t\tew.writeln(`Since:\t'v2.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"CLOUDNS_AUTH_ID\":\tThe API user ID`)\n\t\tew.writeln(`\t- \"CLOUDNS_AUTH_PASSWORD\":\tThe password for API user ID`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"CLOUDNS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"CLOUDNS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"CLOUDNS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 180)`)\n\t\tew.writeln(`\t- \"CLOUDNS_SUB_AUTH_ID\":\tThe API sub user ID`)\n\t\tew.writeln(`\t- \"CLOUDNS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudns`)\n\n\tcase \"cloudru\":\n\t\t// generated from: providers/dns/cloudru/cloudru.toml\n\t\tew.writeln(`Configuration for Cloud.ru.`)\n\t\tew.writeln(`Code:\t'cloudru'`)\n\t\tew.writeln(`Since:\t'v4.14.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"CLOUDRU_KEY_ID\":\tKey ID (login)`)\n\t\tew.writeln(`\t- \"CLOUDRU_SECRET\":\tKey Secret`)\n\t\tew.writeln(`\t- \"CLOUDRU_SERVICE_INSTANCE_ID\":\tService Instance ID (parentId)`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"CLOUDRU_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"CLOUDRU_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 5)`)\n\t\tew.writeln(`\t- \"CLOUDRU_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 300)`)\n\t\tew.writeln(`\t- \"CLOUDRU_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"CLOUDRU_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudru`)\n\n\tcase \"cloudxns\":\n\t\t// generated from: providers/dns/cloudxns/cloudxns.toml\n\t\tew.writeln(`Configuration for CloudXNS (Deprecated).`)\n\t\tew.writeln(`Code:\t'cloudxns'`)\n\t\tew.writeln(`Since:\t'v0.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"CLOUDXNS_API_KEY\":\tThe API key`)\n\t\tew.writeln(`\t- \"CLOUDXNS_SECRET_KEY\":\tThe API secret key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"CLOUDXNS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: )`)\n\t\tew.writeln(`\t- \"CLOUDXNS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: )`)\n\t\tew.writeln(`\t- \"CLOUDXNS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: )`)\n\t\tew.writeln(`\t- \"CLOUDXNS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: )`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudxns`)\n\n\tcase \"com35\":\n\t\t// generated from: providers/dns/com35/com35.toml\n\t\tew.writeln(`Configuration for 35.com/三五互联.`)\n\t\tew.writeln(`Code:\t'com35'`)\n\t\tew.writeln(`Since:\t'v4.31.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"COM35_PASSWORD\":\tAPI password`)\n\t\tew.writeln(`\t- \"COM35_USERNAME\":\tUsername`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"COM35_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"COM35_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"COM35_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"COM35_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/com35`)\n\n\tcase \"conoha\":\n\t\t// generated from: providers/dns/conoha/conoha.toml\n\t\tew.writeln(`Configuration for ConoHa v2.`)\n\t\tew.writeln(`Code:\t'conoha'`)\n\t\tew.writeln(`Since:\t'v1.2.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"CONOHA_API_PASSWORD\":\tThe API password`)\n\t\tew.writeln(`\t- \"CONOHA_API_USERNAME\":\tThe API username`)\n\t\tew.writeln(`\t- \"CONOHA_TENANT_ID\":\tTenant ID`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"CONOHA_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"CONOHA_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"CONOHA_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"CONOHA_REGION\":\tThe region (Default: tyo1)`)\n\t\tew.writeln(`\t- \"CONOHA_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/conoha`)\n\n\tcase \"conohav3\":\n\t\t// generated from: providers/dns/conohav3/conohav3.toml\n\t\tew.writeln(`Configuration for ConoHa v3.`)\n\t\tew.writeln(`Code:\t'conohav3'`)\n\t\tew.writeln(`Since:\t'v4.24.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"CONOHAV3_API_PASSWORD\":\tThe API password`)\n\t\tew.writeln(`\t- \"CONOHAV3_API_USER_ID\":\tThe API user ID`)\n\t\tew.writeln(`\t- \"CONOHAV3_TENANT_ID\":\tTenant ID`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"CONOHAV3_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"CONOHAV3_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"CONOHAV3_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"CONOHAV3_REGION\":\tThe region (Default: c3j1)`)\n\t\tew.writeln(`\t- \"CONOHAV3_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/conohav3`)\n\n\tcase \"constellix\":\n\t\t// generated from: providers/dns/constellix/constellix.toml\n\t\tew.writeln(`Configuration for Constellix.`)\n\t\tew.writeln(`Code:\t'constellix'`)\n\t\tew.writeln(`Since:\t'v3.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"CONSTELLIX_API_KEY\":\tUser API key`)\n\t\tew.writeln(`\t- \"CONSTELLIX_SECRET_KEY\":\tUser secret key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"CONSTELLIX_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"CONSTELLIX_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"CONSTELLIX_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"CONSTELLIX_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/constellix`)\n\n\tcase \"corenetworks\":\n\t\t// generated from: providers/dns/corenetworks/corenetworks.toml\n\t\tew.writeln(`Configuration for Core-Networks.`)\n\t\tew.writeln(`Code:\t'corenetworks'`)\n\t\tew.writeln(`Since:\t'v4.20.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"CORENETWORKS_LOGIN\":\tThe username of the API account`)\n\t\tew.writeln(`\t- \"CORENETWORKS_PASSWORD\":\tThe password`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"CORENETWORKS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"CORENETWORKS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"CORENETWORKS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"CORENETWORKS_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"CORENETWORKS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/corenetworks`)\n\n\tcase \"cpanel\":\n\t\t// generated from: providers/dns/cpanel/cpanel.toml\n\t\tew.writeln(`Configuration for CPanel/WHM.`)\n\t\tew.writeln(`Code:\t'cpanel'`)\n\t\tew.writeln(`Since:\t'v4.16.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"CPANEL_BASE_URL\":\tAPI server URL`)\n\t\tew.writeln(`\t- \"CPANEL_TOKEN\":\tAPI token`)\n\t\tew.writeln(`\t- \"CPANEL_USERNAME\":\tusername`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"CPANEL_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"CPANEL_MODE\":\tuse cpanel API or WHM API (Default: cpanel)`)\n\t\tew.writeln(`\t- \"CPANEL_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"CPANEL_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"CPANEL_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/cpanel`)\n\n\tcase \"czechia\":\n\t\t// generated from: providers/dns/czechia/czechia.toml\n\t\tew.writeln(`Configuration for Czechia.`)\n\t\tew.writeln(`Code:\t'czechia'`)\n\t\tew.writeln(`Since:\t'v4.33.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"CZECHIA_TOKEN\":\tAuthorization token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"CZECHIA_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"CZECHIA_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"CZECHIA_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"CZECHIA_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/czechia`)\n\n\tcase \"ddnss\":\n\t\t// generated from: providers/dns/ddnss/ddnss.toml\n\t\tew.writeln(`Configuration for DDnss (DynDNS Service).`)\n\t\tew.writeln(`Code:\t'ddnss'`)\n\t\tew.writeln(`Since:\t'v4.32.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DDNSS_KEY\":\tUpdate key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DDNSS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DDNSS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"DDNSS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"DDNSS_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"DDNSS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/ddnss`)\n\n\tcase \"derak\":\n\t\t// generated from: providers/dns/derak/derak.toml\n\t\tew.writeln(`Configuration for Derak Cloud.`)\n\t\tew.writeln(`Code:\t'derak'`)\n\t\tew.writeln(`Since:\t'v4.12.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DERAK_API_KEY\":\tThe API key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DERAK_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DERAK_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 5)`)\n\t\tew.writeln(`\t- \"DERAK_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"DERAK_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"DERAK_WEBSITE_ID\":\tForce the zone/website ID`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/derak`)\n\n\tcase \"desec\":\n\t\t// generated from: providers/dns/desec/desec.toml\n\t\tew.writeln(`Configuration for deSEC.io.`)\n\t\tew.writeln(`Code:\t'desec'`)\n\t\tew.writeln(`Since:\t'v3.7.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DESEC_TOKEN\":\tDomain token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DESEC_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DESEC_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 4)`)\n\t\tew.writeln(`\t- \"DESEC_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"DESEC_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/desec`)\n\n\tcase \"designate\":\n\t\t// generated from: providers/dns/designate/designate.toml\n\t\tew.writeln(`Configuration for Designate DNSaaS for Openstack.`)\n\t\tew.writeln(`Code:\t'designate'`)\n\t\tew.writeln(`Since:\t'v2.2.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"OS_APPLICATION_CREDENTIAL_ID\":\tApplication credential ID`)\n\t\tew.writeln(`\t- \"OS_APPLICATION_CREDENTIAL_NAME\":\tApplication credential name`)\n\t\tew.writeln(`\t- \"OS_APPLICATION_CREDENTIAL_SECRET\":\tApplication credential secret`)\n\t\tew.writeln(`\t- \"OS_AUTH_URL\":\tIdentity endpoint URL`)\n\t\tew.writeln(`\t- \"OS_PASSWORD\":\tPassword`)\n\t\tew.writeln(`\t- \"OS_PROJECT_NAME\":\tProject name`)\n\t\tew.writeln(`\t- \"OS_REGION_NAME\":\tRegion name`)\n\t\tew.writeln(`\t- \"OS_USERNAME\":\tUsername`)\n\t\tew.writeln(`\t- \"OS_USER_ID\":\tUser ID`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DESIGNATE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"DESIGNATE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 600)`)\n\t\tew.writeln(`\t- \"DESIGNATE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"DESIGNATE_ZONE_NAME\":\tThe zone name to use in the OpenStack Project to manage TXT records.`)\n\t\tew.writeln(`\t- \"OS_PROJECT_ID\":\tProject ID`)\n\t\tew.writeln(`\t- \"OS_TENANT_NAME\":\tTenant name (deprecated see OS_PROJECT_NAME and OS_PROJECT_ID)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/designate`)\n\n\tcase \"digitalocean\":\n\t\t// generated from: providers/dns/digitalocean/digitalocean.toml\n\t\tew.writeln(`Configuration for Digital Ocean.`)\n\t\tew.writeln(`Code:\t'digitalocean'`)\n\t\tew.writeln(`Since:\t'v0.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DO_AUTH_TOKEN\":\tAuthentication token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DO_API_URL\":\tThe URL of the API`)\n\t\tew.writeln(`\t- \"DO_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DO_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 5)`)\n\t\tew.writeln(`\t- \"DO_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"DO_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 30)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/digitalocean`)\n\n\tcase \"directadmin\":\n\t\t// generated from: providers/dns/directadmin/directadmin.toml\n\t\tew.writeln(`Configuration for DirectAdmin.`)\n\t\tew.writeln(`Code:\t'directadmin'`)\n\t\tew.writeln(`Since:\t'v4.18.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DIRECTADMIN_API_URL\":\tURL of the API`)\n\t\tew.writeln(`\t- \"DIRECTADMIN_PASSWORD\":\tAPI password`)\n\t\tew.writeln(`\t- \"DIRECTADMIN_USERNAME\":\tAPI username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DIRECTADMIN_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DIRECTADMIN_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 5)`)\n\t\tew.writeln(`\t- \"DIRECTADMIN_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"DIRECTADMIN_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DIRECTADMIN_ZONE_NAME\":\tZone name used to add the TXT record`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/directadmin`)\n\n\tcase \"dnsexit\":\n\t\t// generated from: providers/dns/dnsexit/dnsexit.toml\n\t\tew.writeln(`Configuration for DNSExit.`)\n\t\tew.writeln(`Code:\t'dnsexit'`)\n\t\tew.writeln(`Since:\t'v4.32.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DNSEXIT_API_KEY\":\tAPI key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DNSEXIT_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DNSEXIT_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"DNSEXIT_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 300)`)\n\t\tew.writeln(`\t- \"DNSEXIT_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/dnsexit`)\n\n\tcase \"dnshomede\":\n\t\t// generated from: providers/dns/dnshomede/dnshomede.toml\n\t\tew.writeln(`Configuration for dnsHome.de.`)\n\t\tew.writeln(`Code:\t'dnshomede'`)\n\t\tew.writeln(`Since:\t'v4.10.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DNSHOMEDE_CREDENTIALS\":\tComma-separated list of domain:password credential pairs`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DNSHOMEDE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DNSHOMEDE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 1200)`)\n\t\tew.writeln(`\t- \"DNSHOMEDE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"DNSHOMEDE_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/dnshomede`)\n\n\tcase \"dnsimple\":\n\t\t// generated from: providers/dns/dnsimple/dnsimple.toml\n\t\tew.writeln(`Configuration for DNSimple.`)\n\t\tew.writeln(`Code:\t'dnsimple'`)\n\t\tew.writeln(`Since:\t'v0.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DNSIMPLE_OAUTH_TOKEN\":\tOAuth token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DNSIMPLE_BASE_URL\":\tAPI endpoint URL`)\n\t\tew.writeln(`\t- \"DNSIMPLE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"DNSIMPLE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"DNSIMPLE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/dnsimple`)\n\n\tcase \"dnsmadeeasy\":\n\t\t// generated from: providers/dns/dnsmadeeasy/dnsmadeeasy.toml\n\t\tew.writeln(`Configuration for DNS Made Easy.`)\n\t\tew.writeln(`Code:\t'dnsmadeeasy'`)\n\t\tew.writeln(`Since:\t'v0.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DNSMADEEASY_API_KEY\":\tThe API key`)\n\t\tew.writeln(`\t- \"DNSMADEEASY_API_SECRET\":\tThe API Secret key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DNSMADEEASY_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"DNSMADEEASY_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"DNSMADEEASY_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"DNSMADEEASY_SANDBOX\":\tActivate the sandbox (boolean)`)\n\t\tew.writeln(`\t- \"DNSMADEEASY_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/dnsmadeeasy`)\n\n\tcase \"dnspod\":\n\t\t// generated from: providers/dns/dnspod/dnspod.toml\n\t\tew.writeln(`Configuration for DNSPod (deprecated).`)\n\t\tew.writeln(`Code:\t'dnspod'`)\n\t\tew.writeln(`Since:\t'v0.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DNSPOD_API_KEY\":\tThe user token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DNSPOD_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DNSPOD_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"DNSPOD_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"DNSPOD_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/dnspod`)\n\n\tcase \"dode\":\n\t\t// generated from: providers/dns/dode/dode.toml\n\t\tew.writeln(`Configuration for Domain Offensive (do.de).`)\n\t\tew.writeln(`Code:\t'dode'`)\n\t\tew.writeln(`Since:\t'v2.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DODE_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DODE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DODE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"DODE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"DODE_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/dode`)\n\n\tcase \"domeneshop\":\n\t\t// generated from: providers/dns/domeneshop/domeneshop.toml\n\t\tew.writeln(`Configuration for Domeneshop.`)\n\t\tew.writeln(`Code:\t'domeneshop'`)\n\t\tew.writeln(`Since:\t'v4.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DOMENESHOP_API_SECRET\":\tAPI secret`)\n\t\tew.writeln(`\t- \"DOMENESHOP_API_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DOMENESHOP_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DOMENESHOP_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 20)`)\n\t\tew.writeln(`\t- \"DOMENESHOP_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/domeneshop`)\n\n\tcase \"dreamhost\":\n\t\t// generated from: providers/dns/dreamhost/dreamhost.toml\n\t\tew.writeln(`Configuration for DreamHost.`)\n\t\tew.writeln(`Code:\t'dreamhost'`)\n\t\tew.writeln(`Since:\t'v1.1.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DREAMHOST_API_KEY\":\tThe API key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DREAMHOST_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DREAMHOST_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"DREAMHOST_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 3600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/dreamhost`)\n\n\tcase \"duckdns\":\n\t\t// generated from: providers/dns/duckdns/duckdns.toml\n\t\tew.writeln(`Configuration for Duck DNS.`)\n\t\tew.writeln(`Code:\t'duckdns'`)\n\t\tew.writeln(`Since:\t'v0.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DUCKDNS_TOKEN\":\tAccount token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DUCKDNS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DUCKDNS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"DUCKDNS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"DUCKDNS_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/duckdns`)\n\n\tcase \"dyn\":\n\t\t// generated from: providers/dns/dyn/dyn.toml\n\t\tew.writeln(`Configuration for Dyn.`)\n\t\tew.writeln(`Code:\t'dyn'`)\n\t\tew.writeln(`Since:\t'v0.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DYN_CUSTOMER_NAME\":\tCustomer name`)\n\t\tew.writeln(`\t- \"DYN_PASSWORD\":\tPassword`)\n\t\tew.writeln(`\t- \"DYN_USER_NAME\":\tUser name`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DYN_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"DYN_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"DYN_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"DYN_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/dyn`)\n\n\tcase \"dyndnsfree\":\n\t\t// generated from: providers/dns/dyndnsfree/dyndnsfree.toml\n\t\tew.writeln(`Configuration for DynDnsFree.de.`)\n\t\tew.writeln(`Code:\t'dyndnsfree'`)\n\t\tew.writeln(`Since:\t'v4.23.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DYNDNSFREE_PASSWORD\":\tPassword`)\n\t\tew.writeln(`\t- \"DYNDNSFREE_USERNAME\":\tUsername`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DYNDNSFREE_HTTP_TIMEOUT\":\tRequest timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DYNDNSFREE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"DYNDNSFREE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/dyndnsfree`)\n\n\tcase \"dynu\":\n\t\t// generated from: providers/dns/dynu/dynu.toml\n\t\tew.writeln(`Configuration for Dynu.`)\n\t\tew.writeln(`Code:\t'dynu'`)\n\t\tew.writeln(`Since:\t'v3.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"DYNU_API_KEY\":\tAPI key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"DYNU_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"DYNU_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"DYNU_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 180)`)\n\t\tew.writeln(`\t- \"DYNU_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/dynu`)\n\n\tcase \"easydns\":\n\t\t// generated from: providers/dns/easydns/easydns.toml\n\t\tew.writeln(`Configuration for EasyDNS.`)\n\t\tew.writeln(`Code:\t'easydns'`)\n\t\tew.writeln(`Since:\t'v2.6.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"EASYDNS_KEY\":\tAPI Key`)\n\t\tew.writeln(`\t- \"EASYDNS_TOKEN\":\tAPI Token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"EASYDNS_ENDPOINT\":\tThe endpoint URL of the API Server`)\n\t\tew.writeln(`\t- \"EASYDNS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"EASYDNS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"EASYDNS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"EASYDNS_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"EASYDNS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/easydns`)\n\n\tcase \"edgecenter\":\n\t\t// generated from: providers/dns/edgecenter/edgecenter.toml\n\t\tew.writeln(`Configuration for EdgeCenter.`)\n\t\tew.writeln(`Code:\t'edgecenter'`)\n\t\tew.writeln(`Since:\t'v4.29.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"EDGECENTER_PERMANENT_API_TOKEN\":\tPermanent API token (https://edgecenter.ru/blog/permanent-api-token-explained/)`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"EDGECENTER_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"EDGECENTER_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 20)`)\n\t\tew.writeln(`\t- \"EDGECENTER_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 360)`)\n\t\tew.writeln(`\t- \"EDGECENTER_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/edgecenter`)\n\n\tcase \"edgedns\":\n\t\t// generated from: providers/dns/edgedns/edgedns.toml\n\t\tew.writeln(`Configuration for Akamai EdgeDNS.`)\n\t\tew.writeln(`Code:\t'edgedns'`)\n\t\tew.writeln(`Since:\t'v3.9.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"AKAMAI_ACCESS_TOKEN\":\tAccess token, managed by the Akamai EdgeGrid client`)\n\t\tew.writeln(`\t- \"AKAMAI_CLIENT_SECRET\":\tClient secret, managed by the Akamai EdgeGrid client`)\n\t\tew.writeln(`\t- \"AKAMAI_CLIENT_TOKEN\":\tClient token, managed by the Akamai EdgeGrid client`)\n\t\tew.writeln(`\t- \"AKAMAI_EDGERC\":\tPath to the .edgerc file, managed by the Akamai EdgeGrid client`)\n\t\tew.writeln(`\t- \"AKAMAI_EDGERC_SECTION\":\tConfiguration section, managed by the Akamai EdgeGrid client`)\n\t\tew.writeln(`\t- \"AKAMAI_HOST\":\tAPI host, managed by the Akamai EdgeGrid client`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"AKAMAI_ACCOUNT_SWITCH_KEY\":\tTarget account ID when the DNS zone and credentials belong to different accounts`)\n\t\tew.writeln(`\t- \"AKAMAI_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 15)`)\n\t\tew.writeln(`\t- \"AKAMAI_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 180)`)\n\t\tew.writeln(`\t- \"AKAMAI_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/edgedns`)\n\n\tcase \"edgeone\":\n\t\t// generated from: providers/dns/edgeone/edgeone.toml\n\t\tew.writeln(`Configuration for Tencent EdgeOne.`)\n\t\tew.writeln(`Code:\t'edgeone'`)\n\t\tew.writeln(`Since:\t'v4.26.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"EDGEONE_SECRET_ID\":\tAccess key ID`)\n\t\tew.writeln(`\t- \"EDGEONE_SECRET_KEY\":\tAccess Key secret`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"EDGEONE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"EDGEONE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"EDGEONE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 1200)`)\n\t\tew.writeln(`\t- \"EDGEONE_REGION\":\tRegion`)\n\t\tew.writeln(`\t- \"EDGEONE_SESSION_TOKEN\":\tAccess Key token`)\n\t\tew.writeln(`\t- \"EDGEONE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"EDGEONE_ZONES_MAPPING\":\tMapping between DNS zones and site IDs. (ex: 'example.org:id1,example.com:id2')`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/edgeone`)\n\n\tcase \"efficientip\":\n\t\t// generated from: providers/dns/efficientip/efficientip.toml\n\t\tew.writeln(`Configuration for Efficient IP.`)\n\t\tew.writeln(`Code:\t'efficientip'`)\n\t\tew.writeln(`Since:\t'v4.13.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"EFFICIENTIP_DNS_NAME\":\tDNS name (ex: dns.smart)`)\n\t\tew.writeln(`\t- \"EFFICIENTIP_HOSTNAME\":\tHostname (ex: foo.example.com)`)\n\t\tew.writeln(`\t- \"EFFICIENTIP_PASSWORD\":\tPassword`)\n\t\tew.writeln(`\t- \"EFFICIENTIP_USERNAME\":\tUsername`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"EFFICIENTIP_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"EFFICIENTIP_INSECURE_SKIP_VERIFY\":\tWhether or not to verify EfficientIP API certificate`)\n\t\tew.writeln(`\t- \"EFFICIENTIP_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"EFFICIENTIP_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"EFFICIENTIP_VIEW_NAME\":\tView name (ex: external)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/efficientip`)\n\n\tcase \"epik\":\n\t\t// generated from: providers/dns/epik/epik.toml\n\t\tew.writeln(`Configuration for Epik.`)\n\t\tew.writeln(`Code:\t'epik'`)\n\t\tew.writeln(`Since:\t'v4.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"EPIK_SIGNATURE\":\tEpik API signature (https://registrar.epik.com/account/api-settings/)`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"EPIK_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"EPIK_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"EPIK_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"EPIK_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/epik`)\n\n\tcase \"eurodns\":\n\t\t// generated from: providers/dns/eurodns/eurodns.toml\n\t\tew.writeln(`Configuration for EuroDNS.`)\n\t\tew.writeln(`Code:\t'eurodns'`)\n\t\tew.writeln(`Since:\t'v4.33.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"EURODNS_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"EURODNS_APP_ID\":\tApplication ID`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"EURODNS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"EURODNS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"EURODNS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"EURODNS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/eurodns`)\n\n\tcase \"excedo\":\n\t\t// generated from: providers/dns/excedo/excedo.toml\n\t\tew.writeln(`Configuration for Excedo.`)\n\t\tew.writeln(`Code:\t'excedo'`)\n\t\tew.writeln(`Since:\t'v4.33.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"EXCEDO_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"EXCEDO_API_URL\":\tAPI base URL`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"EXCEDO_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"EXCEDO_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"EXCEDO_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 300)`)\n\t\tew.writeln(`\t- \"EXCEDO_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/excedo`)\n\n\tcase \"exec\":\n\t\t// generated from: providers/dns/exec/exec.toml\n\t\tew.writeln(`Configuration for External program.`)\n\t\tew.writeln(`Code:\t'exec'`)\n\t\tew.writeln(`Since:\t'v0.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/exec`)\n\n\tcase \"exoscale\":\n\t\t// generated from: providers/dns/exoscale/exoscale.toml\n\t\tew.writeln(`Configuration for Exoscale.`)\n\t\tew.writeln(`Code:\t'exoscale'`)\n\t\tew.writeln(`Since:\t'v0.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"EXOSCALE_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"EXOSCALE_API_SECRET\":\tAPI secret`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"EXOSCALE_ENDPOINT\":\tAPI endpoint URL`)\n\t\tew.writeln(`\t- \"EXOSCALE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"EXOSCALE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"EXOSCALE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"EXOSCALE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/exoscale`)\n\n\tcase \"f5xc\":\n\t\t// generated from: providers/dns/f5xc/f5xc.toml\n\t\tew.writeln(`Configuration for F5 XC.`)\n\t\tew.writeln(`Code:\t'f5xc'`)\n\t\tew.writeln(`Since:\t'v4.23.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"F5XC_API_TOKEN\":\tAPI token`)\n\t\tew.writeln(`\t- \"F5XC_GROUP_NAME\":\tGroup name`)\n\t\tew.writeln(`\t- \"F5XC_TENANT_NAME\":\tXC Tenant shortname`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"F5XC_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"F5XC_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"F5XC_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"F5XC_SERVER\":\tServer domain (Default: console.ves.volterra.io)`)\n\t\tew.writeln(`\t- \"F5XC_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/f5xc`)\n\n\tcase \"freemyip\":\n\t\t// generated from: providers/dns/freemyip/freemyip.toml\n\t\tew.writeln(`Configuration for freemyip.com.`)\n\t\tew.writeln(`Code:\t'freemyip'`)\n\t\tew.writeln(`Since:\t'v4.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"FREEMYIP_TOKEN\":\tAccount token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"FREEMYIP_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"FREEMYIP_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"FREEMYIP_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"FREEMYIP_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"FREEMYIP_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/freemyip`)\n\n\tcase \"gandi\":\n\t\t// generated from: providers/dns/gandi/gandi.toml\n\t\tew.writeln(`Configuration for Gandi.`)\n\t\tew.writeln(`Code:\t'gandi'`)\n\t\tew.writeln(`Since:\t'v0.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"GANDI_API_KEY\":\tAPI key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"GANDI_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"GANDI_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"GANDI_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 2400)`)\n\t\tew.writeln(`\t- \"GANDI_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/gandi`)\n\n\tcase \"gandiv5\":\n\t\t// generated from: providers/dns/gandiv5/gandiv5.toml\n\t\tew.writeln(`Configuration for Gandi Live DNS (v5).`)\n\t\tew.writeln(`Code:\t'gandiv5'`)\n\t\tew.writeln(`Since:\t'v0.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"GANDIV5_API_KEY\":\tAPI key (Deprecated)`)\n\t\tew.writeln(`\t- \"GANDIV5_PERSONAL_ACCESS_TOKEN\":\tPersonal Access Token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"GANDIV5_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"GANDIV5_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 20)`)\n\t\tew.writeln(`\t- \"GANDIV5_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 1200)`)\n\t\tew.writeln(`\t- \"GANDIV5_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/gandiv5`)\n\n\tcase \"gcloud\":\n\t\t// generated from: providers/dns/gcloud/gcloud.toml\n\t\tew.writeln(`Configuration for Google Cloud.`)\n\t\tew.writeln(`Code:\t'gcloud'`)\n\t\tew.writeln(`Since:\t'v0.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"Application Default Credentials\":\t[Documentation](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application)`)\n\t\tew.writeln(`\t- \"GCE_PROJECT\":\tProject name (by default, the project name is auto-detected by using the metadata service)`)\n\t\tew.writeln(`\t- \"GCE_SERVICE_ACCOUNT\":\tAccount`)\n\t\tew.writeln(`\t- \"GCE_SERVICE_ACCOUNT_FILE\":\tAccount file path`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"GCE_ALLOW_PRIVATE_ZONE\":\tAllows requested domain to be in private DNS zone, works only with a private ACME server (by default: false)`)\n\t\tew.writeln(`\t- \"GCE_IMPERSONATE_SERVICE_ACCOUNT\":\tService account email to impersonate`)\n\t\tew.writeln(`\t- \"GCE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 5)`)\n\t\tew.writeln(`\t- \"GCE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 180)`)\n\t\tew.writeln(`\t- \"GCE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"GCE_ZONE_ID\":\tAllows to skip the automatic detection of the zone`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/gcloud`)\n\n\tcase \"gcore\":\n\t\t// generated from: providers/dns/gcore/gcore.toml\n\t\tew.writeln(`Configuration for G-Core.`)\n\t\tew.writeln(`Code:\t'gcore'`)\n\t\tew.writeln(`Since:\t'v4.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"GCORE_PERMANENT_API_TOKEN\":\tPermanent API token (https://gcore.com/blog/permanent-api-token-explained/)`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"GCORE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"GCORE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 20)`)\n\t\tew.writeln(`\t- \"GCORE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 360)`)\n\t\tew.writeln(`\t- \"GCORE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/gcore`)\n\n\tcase \"gigahostno\":\n\t\t// generated from: providers/dns/gigahostno/gigahostno.toml\n\t\tew.writeln(`Configuration for Gigahost.no.`)\n\t\tew.writeln(`Code:\t'gigahostno'`)\n\t\tew.writeln(`Since:\t'v4.29.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"GIGAHOSTNO_PASSWORD\":\tPassword`)\n\t\tew.writeln(`\t- \"GIGAHOSTNO_USERNAME\":\tUsername`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"GIGAHOSTNO_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"GIGAHOSTNO_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"GIGAHOSTNO_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"GIGAHOSTNO_SECRET\":\tTOTP secret`)\n\t\tew.writeln(`\t- \"GIGAHOSTNO_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/gigahostno`)\n\n\tcase \"glesys\":\n\t\t// generated from: providers/dns/glesys/glesys.toml\n\t\tew.writeln(`Configuration for Glesys.`)\n\t\tew.writeln(`Code:\t'glesys'`)\n\t\tew.writeln(`Since:\t'v0.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"GLESYS_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"GLESYS_API_USER\":\tAPI user`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"GLESYS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"GLESYS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 20)`)\n\t\tew.writeln(`\t- \"GLESYS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 1200)`)\n\t\tew.writeln(`\t- \"GLESYS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/glesys`)\n\n\tcase \"godaddy\":\n\t\t// generated from: providers/dns/godaddy/godaddy.toml\n\t\tew.writeln(`Configuration for Go Daddy.`)\n\t\tew.writeln(`Code:\t'godaddy'`)\n\t\tew.writeln(`Since:\t'v0.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"GODADDY_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"GODADDY_API_SECRET\":\tAPI secret`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"GODADDY_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"GODADDY_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"GODADDY_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"GODADDY_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/godaddy`)\n\n\tcase \"googledomains\":\n\t\t// generated from: providers/dns/googledomains/googledomains.toml\n\t\tew.writeln(`Configuration for Google Domains.`)\n\t\tew.writeln(`Code:\t'googledomains'`)\n\t\tew.writeln(`Since:\t'v4.11.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"GOOGLE_DOMAINS_ACCESS_TOKEN\":\tAccess token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"GOOGLE_DOMAINS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"GOOGLE_DOMAINS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"GOOGLE_DOMAINS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/googledomains`)\n\n\tcase \"gravity\":\n\t\t// generated from: providers/dns/gravity/gravity.toml\n\t\tew.writeln(`Configuration for Gravity.`)\n\t\tew.writeln(`Code:\t'gravity'`)\n\t\tew.writeln(`Since:\t'v4.30.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"GRAVITY_PASSWORD\":\tPassword`)\n\t\tew.writeln(`\t- \"GRAVITY_SERVER_URL\":\tURL of the server`)\n\t\tew.writeln(`\t- \"GRAVITY_USERNAME\":\tUsername`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"GRAVITY_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"GRAVITY_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"GRAVITY_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"GRAVITY_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 1)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/gravity`)\n\n\tcase \"hetzner\":\n\t\t// generated from: providers/dns/hetzner/hetzner.toml\n\t\tew.writeln(`Configuration for Hetzner.`)\n\t\tew.writeln(`Code:\t'hetzner'`)\n\t\tew.writeln(`Since:\t'v3.7.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"HETZNER_API_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"HETZNER_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"HETZNER_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"HETZNER_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"HETZNER_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/hetzner`)\n\n\tcase \"hostingde\":\n\t\t// generated from: providers/dns/hostingde/hostingde.toml\n\t\tew.writeln(`Configuration for Hosting.de.`)\n\t\tew.writeln(`Code:\t'hostingde'`)\n\t\tew.writeln(`Since:\t'v1.1.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"HOSTINGDE_API_KEY\":\tAPI key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"HOSTINGDE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"HOSTINGDE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"HOSTINGDE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"HOSTINGDE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"HOSTINGDE_ZONE_NAME\":\tZone name in ACE format`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/hostingde`)\n\n\tcase \"hostinger\":\n\t\t// generated from: providers/dns/hostinger/hostinger.toml\n\t\tew.writeln(`Configuration for Hostinger.`)\n\t\tew.writeln(`Code:\t'hostinger'`)\n\t\tew.writeln(`Since:\t'v4.27.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"HOSTINGER_API_TOKEN\":\tAPI Token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"HOSTINGER_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"HOSTINGER_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"HOSTINGER_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"HOSTINGER_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/hostinger`)\n\n\tcase \"hostingnl\":\n\t\t// generated from: providers/dns/hostingnl/hostingnl.toml\n\t\tew.writeln(`Configuration for Hosting.nl.`)\n\t\tew.writeln(`Code:\t'hostingnl'`)\n\t\tew.writeln(`Since:\t'v4.30.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"HOSTINGNL_API_KEY\":\tThe API key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"HOSTINGNL_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"HOSTINGNL_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"HOSTINGNL_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"HOSTINGNL_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/hostingnl`)\n\n\tcase \"hosttech\":\n\t\t// generated from: providers/dns/hosttech/hosttech.toml\n\t\tew.writeln(`Configuration for Hosttech.`)\n\t\tew.writeln(`Code:\t'hosttech'`)\n\t\tew.writeln(`Since:\t'v4.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"HOSTTECH_API_KEY\":\tAPI login`)\n\t\tew.writeln(`\t- \"HOSTTECH_PASSWORD\":\tAPI password`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"HOSTTECH_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"HOSTTECH_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"HOSTTECH_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"HOSTTECH_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/hosttech`)\n\n\tcase \"httpnet\":\n\t\t// generated from: providers/dns/httpnet/httpnet.toml\n\t\tew.writeln(`Configuration for http.net.`)\n\t\tew.writeln(`Code:\t'httpnet'`)\n\t\tew.writeln(`Since:\t'v4.15.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"HTTPNET_API_KEY\":\tAPI key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"HTTPNET_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"HTTPNET_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"HTTPNET_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"HTTPNET_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"HTTPNET_ZONE_NAME\":\tZone name in ACE format`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/httpnet`)\n\n\tcase \"httpreq\":\n\t\t// generated from: providers/dns/httpreq/httpreq.toml\n\t\tew.writeln(`Configuration for HTTP request.`)\n\t\tew.writeln(`Code:\t'httpreq'`)\n\t\tew.writeln(`Since:\t'v2.0.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"HTTPREQ_ENDPOINT\":\tThe URL of the server`)\n\t\tew.writeln(`\t- \"HTTPREQ_MODE\":\t'RAW', none`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"HTTPREQ_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"HTTPREQ_PASSWORD\":\tBasic authentication password`)\n\t\tew.writeln(`\t- \"HTTPREQ_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"HTTPREQ_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"HTTPREQ_USERNAME\":\tBasic authentication username`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/httpreq`)\n\n\tcase \"huaweicloud\":\n\t\t// generated from: providers/dns/huaweicloud/huaweicloud.toml\n\t\tew.writeln(`Configuration for Huawei Cloud.`)\n\t\tew.writeln(`Code:\t'huaweicloud'`)\n\t\tew.writeln(`Since:\t'v4.19'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"HUAWEICLOUD_ACCESS_KEY_ID\":\tAccess key ID`)\n\t\tew.writeln(`\t- \"HUAWEICLOUD_REGION\":\tRegion`)\n\t\tew.writeln(`\t- \"HUAWEICLOUD_SECRET_ACCESS_KEY\":\tAccess Key secret`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"HUAWEICLOUD_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"HUAWEICLOUD_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"HUAWEICLOUD_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"HUAWEICLOUD_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/huaweicloud`)\n\n\tcase \"hurricane\":\n\t\t// generated from: providers/dns/hurricane/hurricane.toml\n\t\tew.writeln(`Configuration for Hurricane Electric DNS.`)\n\t\tew.writeln(`Code:\t'hurricane'`)\n\t\tew.writeln(`Since:\t'v4.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"HURRICANE_TOKENS\":\tTXT record names and tokens`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"HURRICANE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"HURRICANE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"HURRICANE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation (Default: 300)`)\n\t\tew.writeln(`\t- \"HURRICANE_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/hurricane`)\n\n\tcase \"hyperone\":\n\t\t// generated from: providers/dns/hyperone/hyperone.toml\n\t\tew.writeln(`Configuration for HyperOne.`)\n\t\tew.writeln(`Code:\t'hyperone'`)\n\t\tew.writeln(`Since:\t'v3.9.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"HYPERONE_API_URL\":\tAllows to pass custom API Endpoint to be used in the challenge (default https://api.hyperone.com/v2)`)\n\t\tew.writeln(`\t- \"HYPERONE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"HYPERONE_LOCATION_ID\":\tSpecifies location (region) to be used in API calls. (default pl-waw-1)`)\n\t\tew.writeln(`\t- \"HYPERONE_PASSPORT_LOCATION\":\tAllows to pass custom passport file location (default ~/.h1/passport.json)`)\n\t\tew.writeln(`\t- \"HYPERONE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"HYPERONE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"HYPERONE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/hyperone`)\n\n\tcase \"ibmcloud\":\n\t\t// generated from: providers/dns/ibmcloud/ibmcloud.toml\n\t\tew.writeln(`Configuration for IBM Cloud (SoftLayer).`)\n\t\tew.writeln(`Code:\t'ibmcloud'`)\n\t\tew.writeln(`Since:\t'v4.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"SOFTLAYER_API_KEY\":\tClassic Infrastructure API key`)\n\t\tew.writeln(`\t- \"SOFTLAYER_USERNAME\":\tUsername (IBM Cloud is {accountID}_{emailAddress})`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"SOFTLAYER_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"SOFTLAYER_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"SOFTLAYER_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"SOFTLAYER_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/ibmcloud`)\n\n\tcase \"iij\":\n\t\t// generated from: providers/dns/iij/iij.toml\n\t\tew.writeln(`Configuration for Internet Initiative Japan.`)\n\t\tew.writeln(`Code:\t'iij'`)\n\t\tew.writeln(`Since:\t'v1.1.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"IIJ_API_ACCESS_KEY\":\tAPI access key`)\n\t\tew.writeln(`\t- \"IIJ_API_SECRET_KEY\":\tAPI secret key`)\n\t\tew.writeln(`\t- \"IIJ_DO_SERVICE_CODE\":\tDO service code`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"IIJ_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 4)`)\n\t\tew.writeln(`\t- \"IIJ_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 240)`)\n\t\tew.writeln(`\t- \"IIJ_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/iij`)\n\n\tcase \"iijdpf\":\n\t\t// generated from: providers/dns/iijdpf/iijdpf.toml\n\t\tew.writeln(`Configuration for IIJ DNS Platform Service.`)\n\t\tew.writeln(`Code:\t'iijdpf'`)\n\t\tew.writeln(`Since:\t'v4.7.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"IIJ_DPF_API_TOKEN\":\tAPI token`)\n\t\tew.writeln(`\t- \"IIJ_DPF_DPM_SERVICE_CODE\":\tIIJ Managed DNS Service's service code`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"IIJ_DPF_API_ENDPOINT\":\tAPI endpoint URL, defaults to https://api.dns-platform.jp/dpf/v1`)\n\t\tew.writeln(`\t- \"IIJ_DPF_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 5)`)\n\t\tew.writeln(`\t- \"IIJ_DPF_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 660)`)\n\t\tew.writeln(`\t- \"IIJ_DPF_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/iijdpf`)\n\n\tcase \"infoblox\":\n\t\t// generated from: providers/dns/infoblox/infoblox.toml\n\t\tew.writeln(`Configuration for Infoblox.`)\n\t\tew.writeln(`Code:\t'infoblox'`)\n\t\tew.writeln(`Since:\t'v4.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"INFOBLOX_HOST\":\tHost URI`)\n\t\tew.writeln(`\t- \"INFOBLOX_PASSWORD\":\tAccount Password`)\n\t\tew.writeln(`\t- \"INFOBLOX_USERNAME\":\tAccount Username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"INFOBLOX_CA_CERTIFICATE\":\tThe path to the CA certificate (PEM encoded)`)\n\t\tew.writeln(`\t- \"INFOBLOX_DNS_VIEW\":\tThe view for the TXT records (Default: External)`)\n\t\tew.writeln(`\t- \"INFOBLOX_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"INFOBLOX_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"INFOBLOX_PORT\":\tThe port for the infoblox grid manager  (Default: 443)`)\n\t\tew.writeln(`\t- \"INFOBLOX_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"INFOBLOX_SSL_VERIFY\":\tWhether or not to verify the TLS certificate  (Default: true)`)\n\t\tew.writeln(`\t- \"INFOBLOX_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"INFOBLOX_WAPI_VERSION\":\tThe version of WAPI being used  (Default: 2.11)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/infoblox`)\n\n\tcase \"infomaniak\":\n\t\t// generated from: providers/dns/infomaniak/infomaniak.toml\n\t\tew.writeln(`Configuration for Infomaniak.`)\n\t\tew.writeln(`Code:\t'infomaniak'`)\n\t\tew.writeln(`Since:\t'v4.1.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"INFOMANIAK_ACCESS_TOKEN\":\tAccess token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"INFOMANIAK_ENDPOINT\":\thttps://api.infomaniak.com`)\n\t\tew.writeln(`\t- \"INFOMANIAK_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"INFOMANIAK_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"INFOMANIAK_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"INFOMANIAK_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/infomaniak`)\n\n\tcase \"internetbs\":\n\t\t// generated from: providers/dns/internetbs/internetbs.toml\n\t\tew.writeln(`Configuration for Internet.bs.`)\n\t\tew.writeln(`Code:\t'internetbs'`)\n\t\tew.writeln(`Since:\t'v4.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"INTERNET_BS_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"INTERNET_BS_PASSWORD\":\tAPI password`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"INTERNET_BS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"INTERNET_BS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"INTERNET_BS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"INTERNET_BS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/internetbs`)\n\n\tcase \"inwx\":\n\t\t// generated from: providers/dns/inwx/inwx.toml\n\t\tew.writeln(`Configuration for INWX.`)\n\t\tew.writeln(`Code:\t'inwx'`)\n\t\tew.writeln(`Since:\t'v2.0.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"INWX_PASSWORD\":\tPassword`)\n\t\tew.writeln(`\t- \"INWX_USERNAME\":\tUsername`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"INWX_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"INWX_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 360)`)\n\t\tew.writeln(`\t- \"INWX_SANDBOX\":\tActivate the sandbox (boolean)`)\n\t\tew.writeln(`\t- \"INWX_SHARED_SECRET\":\tshared secret related to 2FA`)\n\t\tew.writeln(`\t- \"INWX_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/inwx`)\n\n\tcase \"ionos\":\n\t\t// generated from: providers/dns/ionos/ionos.toml\n\t\tew.writeln(`Configuration for Ionos.`)\n\t\tew.writeln(`Code:\t'ionos'`)\n\t\tew.writeln(`Since:\t'v4.2.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"IONOS_API_KEY\":\tAPI key '<prefix>.<secret>' https://developer.hosting.ionos.com/docs/getstarted`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"IONOS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"IONOS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"IONOS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 900)`)\n\t\tew.writeln(`\t- \"IONOS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/ionos`)\n\n\tcase \"ionoscloud\":\n\t\t// generated from: providers/dns/ionoscloud/ionoscloud.toml\n\t\tew.writeln(`Configuration for Ionos Cloud.`)\n\t\tew.writeln(`Code:\t'ionoscloud'`)\n\t\tew.writeln(`Since:\t'v4.30.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"IONOSCLOUD_API_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"IONOSCLOUD_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"IONOSCLOUD_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"IONOSCLOUD_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"IONOSCLOUD_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/ionoscloud`)\n\n\tcase \"ipv64\":\n\t\t// generated from: providers/dns/ipv64/ipv64.toml\n\t\tew.writeln(`Configuration for IPv64.`)\n\t\tew.writeln(`Code:\t'ipv64'`)\n\t\tew.writeln(`Since:\t'v4.13.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"IPV64_API_KEY\":\tAccount API Key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"IPV64_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"IPV64_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"IPV64_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/ipv64`)\n\n\tcase \"ispconfig\":\n\t\t// generated from: providers/dns/ispconfig/ispconfig.toml\n\t\tew.writeln(`Configuration for ISPConfig 3.`)\n\t\tew.writeln(`Code:\t'ispconfig'`)\n\t\tew.writeln(`Since:\t'v4.31.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ISPCONFIG_PASSWORD\":\tPassword`)\n\t\tew.writeln(`\t- \"ISPCONFIG_SERVER_URL\":\tServer URL`)\n\t\tew.writeln(`\t- \"ISPCONFIG_USERNAME\":\tUsername`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ISPCONFIG_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"ISPCONFIG_INSECURE_SKIP_VERIFY\":\tWhether to verify the API certificate`)\n\t\tew.writeln(`\t- \"ISPCONFIG_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"ISPCONFIG_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"ISPCONFIG_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/ispconfig`)\n\n\tcase \"ispconfigddns\":\n\t\t// generated from: providers/dns/ispconfigddns/ispconfigddns.toml\n\t\tew.writeln(`Configuration for ISPConfig 3 - Dynamic DNS (DDNS) Module.`)\n\t\tew.writeln(`Code:\t'ispconfigddns'`)\n\t\tew.writeln(`Since:\t'v4.31.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ISPCONFIG_DDNS_SERVER_URL\":\tAPI server URL (ex: https://panel.example.com:8080)`)\n\t\tew.writeln(`\t- \"ISPCONFIG_DDNS_TOKEN\":\tDDNS API token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ISPCONFIG_DDNS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"ISPCONFIG_DDNS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"ISPCONFIG_DDNS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"ISPCONFIG_DDNS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/ispconfigddns`)\n\n\tcase \"iwantmyname\":\n\t\t// generated from: providers/dns/iwantmyname/iwantmyname.toml\n\t\tew.writeln(`Configuration for iwantmyname (Deprecated).`)\n\t\tew.writeln(`Code:\t'iwantmyname'`)\n\t\tew.writeln(`Since:\t'v4.7.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"IWANTMYNAME_PASSWORD\":\tAPI password`)\n\t\tew.writeln(`\t- \"IWANTMYNAME_USERNAME\":\tAPI username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"IWANTMYNAME_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"IWANTMYNAME_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"IWANTMYNAME_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"IWANTMYNAME_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/iwantmyname`)\n\n\tcase \"jdcloud\":\n\t\t// generated from: providers/dns/jdcloud/jdcloud.toml\n\t\tew.writeln(`Configuration for JD Cloud.`)\n\t\tew.writeln(`Code:\t'jdcloud'`)\n\t\tew.writeln(`Since:\t'v4.31.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"JDCLOUD_ACCESS_KEY_ID\":\tAccess key ID`)\n\t\tew.writeln(`\t- \"JDCLOUD_ACCESS_KEY_SECRET\":\tAccess key secret`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"JDCLOUD_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"JDCLOUD_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"JDCLOUD_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"JDCLOUD_REGION_ID\":\tRegion ID (Default: cn-north-1)`)\n\t\tew.writeln(`\t- \"JDCLOUD_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/jdcloud`)\n\n\tcase \"joker\":\n\t\t// generated from: providers/dns/joker/joker.toml\n\t\tew.writeln(`Configuration for Joker.`)\n\t\tew.writeln(`Code:\t'joker'`)\n\t\tew.writeln(`Since:\t'v2.6.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"JOKER_API_KEY\":\tAPI key (only with DMAPI mode)`)\n\t\tew.writeln(`\t- \"JOKER_API_MODE\":\t'DMAPI' or 'SVC'. DMAPI is for resellers accounts. (Default: DMAPI)`)\n\t\tew.writeln(`\t- \"JOKER_PASSWORD\":\tJoker.com password`)\n\t\tew.writeln(`\t- \"JOKER_USERNAME\":\tJoker.com username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"JOKER_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"JOKER_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"JOKER_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"JOKER_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60), only with 'SVC' mode`)\n\t\tew.writeln(`\t- \"JOKER_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/joker`)\n\n\tcase \"keyhelp\":\n\t\t// generated from: providers/dns/keyhelp/keyhelp.toml\n\t\tew.writeln(`Configuration for KeyHelp.`)\n\t\tew.writeln(`Code:\t'keyhelp'`)\n\t\tew.writeln(`Since:\t'v4.26.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"KEYHELP_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"KEYHELP_BASE_URL\":\tServer URL`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"KEYHELP_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"KEYHELP_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"KEYHELP_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"KEYHELP_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/keyhelp`)\n\n\tcase \"leaseweb\":\n\t\t// generated from: providers/dns/leaseweb/leaseweb.toml\n\t\tew.writeln(`Configuration for Leaseweb.`)\n\t\tew.writeln(`Code:\t'leaseweb'`)\n\t\tew.writeln(`Since:\t'v4.32.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"LEASEWEB_API_KEY\":\tAPI key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"LEASEWEB_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"LEASEWEB_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"LEASEWEB_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"LEASEWEB_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/leaseweb`)\n\n\tcase \"liara\":\n\t\t// generated from: providers/dns/liara/liara.toml\n\t\tew.writeln(`Configuration for Liara.`)\n\t\tew.writeln(`Code:\t'liara'`)\n\t\tew.writeln(`Since:\t'v4.10.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"LIARA_API_KEY\":\tThe API key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"LIARA_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"LIARA_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"LIARA_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"LIARA_TEAM_ID\":\tThe team ID to access services in a team`)\n\t\tew.writeln(`\t- \"LIARA_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/liara`)\n\n\tcase \"lightsail\":\n\t\t// generated from: providers/dns/lightsail/lightsail.toml\n\t\tew.writeln(`Configuration for Amazon Lightsail.`)\n\t\tew.writeln(`Code:\t'lightsail'`)\n\t\tew.writeln(`Since:\t'v0.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"AWS_ACCESS_KEY_ID\":\tManaged by the AWS client. Access key ID ('AWS_ACCESS_KEY_ID_FILE' is not supported, use 'AWS_SHARED_CREDENTIALS_FILE' instead)`)\n\t\tew.writeln(`\t- \"AWS_SECRET_ACCESS_KEY\":\tManaged by the AWS client. Secret access key ('AWS_SECRET_ACCESS_KEY_FILE' is not supported, use 'AWS_SHARED_CREDENTIALS_FILE' instead)`)\n\t\tew.writeln(`\t- \"DNS_ZONE\":\tDomain name of the DNS zone`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"AWS_SHARED_CREDENTIALS_FILE\":\tManaged by the AWS client. Shared credentials file.`)\n\t\tew.writeln(`\t- \"LIGHTSAIL_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"LIGHTSAIL_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/lightsail`)\n\n\tcase \"limacity\":\n\t\t// generated from: providers/dns/limacity/limacity.toml\n\t\tew.writeln(`Configuration for Lima-City.`)\n\t\tew.writeln(`Code:\t'limacity'`)\n\t\tew.writeln(`Since:\t'v4.18.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"LIMACITY_API_KEY\":\tThe API key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"LIMACITY_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"LIMACITY_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 80)`)\n\t\tew.writeln(`\t- \"LIMACITY_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 480)`)\n\t\tew.writeln(`\t- \"LIMACITY_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 90)`)\n\t\tew.writeln(`\t- \"LIMACITY_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/limacity`)\n\n\tcase \"linode\":\n\t\t// generated from: providers/dns/linode/linode.toml\n\t\tew.writeln(`Configuration for Linode (v4).`)\n\t\tew.writeln(`Code:\t'linode'`)\n\t\tew.writeln(`Since:\t'v1.1.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"LINODE_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"LINODE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"LINODE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 15)`)\n\t\tew.writeln(`\t- \"LINODE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"LINODE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/linode`)\n\n\tcase \"liquidweb\":\n\t\t// generated from: providers/dns/liquidweb/liquidweb.toml\n\t\tew.writeln(`Configuration for Liquid Web.`)\n\t\tew.writeln(`Code:\t'liquidweb'`)\n\t\tew.writeln(`Since:\t'v3.1.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"LWAPI_PASSWORD\":\tLiquid Web API Password`)\n\t\tew.writeln(`\t- \"LWAPI_USERNAME\":\tLiquid Web API Username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"LWAPI_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"LWAPI_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"LWAPI_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"LWAPI_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\t\tew.writeln(`\t- \"LWAPI_URL\":\tLiquid Web API endpoint`)\n\t\tew.writeln(`\t- \"LWAPI_ZONE\":\tDNS Zone`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/liquidweb`)\n\n\tcase \"loopia\":\n\t\t// generated from: providers/dns/loopia/loopia.toml\n\t\tew.writeln(`Configuration for Loopia.`)\n\t\tew.writeln(`Code:\t'loopia'`)\n\t\tew.writeln(`Since:\t'v4.2.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"LOOPIA_API_PASSWORD\":\tAPI password`)\n\t\tew.writeln(`\t- \"LOOPIA_API_USER\":\tAPI username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"LOOPIA_API_URL\":\tAPI endpoint. Ex: https://api.loopia.se/RPCSERV or https://api.loopia.rs/RPCSERV`)\n\t\tew.writeln(`\t- \"LOOPIA_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"LOOPIA_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2400)`)\n\t\tew.writeln(`\t- \"LOOPIA_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"LOOPIA_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/loopia`)\n\n\tcase \"luadns\":\n\t\t// generated from: providers/dns/luadns/luadns.toml\n\t\tew.writeln(`Configuration for LuaDNS.`)\n\t\tew.writeln(`Code:\t'luadns'`)\n\t\tew.writeln(`Since:\t'v3.7.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"LUADNS_API_TOKEN\":\tAPI token`)\n\t\tew.writeln(`\t- \"LUADNS_API_USERNAME\":\tUsername (your email)`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"LUADNS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"LUADNS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"LUADNS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"LUADNS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/luadns`)\n\n\tcase \"mailinabox\":\n\t\t// generated from: providers/dns/mailinabox/mailinabox.toml\n\t\tew.writeln(`Configuration for Mail-in-a-Box.`)\n\t\tew.writeln(`Code:\t'mailinabox'`)\n\t\tew.writeln(`Since:\t'v4.16.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"MAILINABOX_BASE_URL\":\tBase API URL (ex: https://box.example.com)`)\n\t\tew.writeln(`\t- \"MAILINABOX_EMAIL\":\tUser email`)\n\t\tew.writeln(`\t- \"MAILINABOX_PASSWORD\":\tUser password`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"MAILINABOX_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"MAILINABOX_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 4)`)\n\t\tew.writeln(`\t- \"MAILINABOX_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/mailinabox`)\n\n\tcase \"manageengine\":\n\t\t// generated from: providers/dns/manageengine/manageengine.toml\n\t\tew.writeln(`Configuration for ManageEngine CloudDNS.`)\n\t\tew.writeln(`Code:\t'manageengine'`)\n\t\tew.writeln(`Since:\t'v4.21.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"MANAGEENGINE_CLIENT_ID\":\tClient ID`)\n\t\tew.writeln(`\t- \"MANAGEENGINE_CLIENT_SECRET\":\tClient Secret`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"MANAGEENGINE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"MANAGEENGINE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"MANAGEENGINE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/manageengine`)\n\n\tcase \"manual\":\n\t\t// generated from: providers/dns/manual/manual.toml\n\t\tew.writeln(`Configuration for Manual.`)\n\t\tew.writeln(`Code:\t'manual'`)\n\t\tew.writeln(`Since:\t'v0.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/manual`)\n\n\tcase \"metaname\":\n\t\t// generated from: providers/dns/metaname/metaname.toml\n\t\tew.writeln(`Configuration for Metaname.`)\n\t\tew.writeln(`Code:\t'metaname'`)\n\t\tew.writeln(`Since:\t'v4.13.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"METANAME_ACCOUNT_REFERENCE\":\tThe four-digit reference of a Metaname account`)\n\t\tew.writeln(`\t- \"METANAME_API_KEY\":\tAPI Key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"METANAME_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"METANAME_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"METANAME_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/metaname`)\n\n\tcase \"metaregistrar\":\n\t\t// generated from: providers/dns/metaregistrar/metaregistrar.toml\n\t\tew.writeln(`Configuration for Metaregistrar.`)\n\t\tew.writeln(`Code:\t'metaregistrar'`)\n\t\tew.writeln(`Since:\t'v4.23.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"METAREGISTRAR_API_TOKEN\":\tThe API token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"METAREGISTRAR_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"METAREGISTRAR_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"METAREGISTRAR_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"METAREGISTRAR_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/metaregistrar`)\n\n\tcase \"mijnhost\":\n\t\t// generated from: providers/dns/mijnhost/mijnhost.toml\n\t\tew.writeln(`Configuration for mijn.host.`)\n\t\tew.writeln(`Code:\t'mijnhost'`)\n\t\tew.writeln(`Since:\t'v4.18.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"MIJNHOST_API_KEY\":\tThe API key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"MIJNHOST_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"MIJNHOST_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"MIJNHOST_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"MIJNHOST_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"MIJNHOST_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/mijnhost`)\n\n\tcase \"mittwald\":\n\t\t// generated from: providers/dns/mittwald/mittwald.toml\n\t\tew.writeln(`Configuration for Mittwald.`)\n\t\tew.writeln(`Code:\t'mittwald'`)\n\t\tew.writeln(`Since:\t'v1.48.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"MITTWALD_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"MITTWALD_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"MITTWALD_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"MITTWALD_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"MITTWALD_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"MITTWALD_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/mittwald`)\n\n\tcase \"myaddr\":\n\t\t// generated from: providers/dns/myaddr/myaddr.toml\n\t\tew.writeln(`Configuration for myaddr.{tools,dev,io}.`)\n\t\tew.writeln(`Code:\t'myaddr'`)\n\t\tew.writeln(`Since:\t'v4.22.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"MYADDR_PRIVATE_KEYS_MAPPING\":\tMapping between subdomains and private keys. The format is: '<subdomain1>:<private_key1>,<subdomain2>:<private_key2>,<subdomain3>:<private_key3>'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"MYADDR_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"MYADDR_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"MYADDR_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"MYADDR_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"MYADDR_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/myaddr`)\n\n\tcase \"mydnsjp\":\n\t\t// generated from: providers/dns/mydnsjp/mydnsjp.toml\n\t\tew.writeln(`Configuration for MyDNS.jp.`)\n\t\tew.writeln(`Code:\t'mydnsjp'`)\n\t\tew.writeln(`Since:\t'v1.2.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"MYDNSJP_MASTER_ID\":\tMaster ID`)\n\t\tew.writeln(`\t- \"MYDNSJP_PASSWORD\":\tPassword`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"MYDNSJP_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"MYDNSJP_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"MYDNSJP_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/mydnsjp`)\n\n\tcase \"mythicbeasts\":\n\t\t// generated from: providers/dns/mythicbeasts/mythicbeasts.toml\n\t\tew.writeln(`Configuration for MythicBeasts.`)\n\t\tew.writeln(`Code:\t'mythicbeasts'`)\n\t\tew.writeln(`Since:\t'v0.3.7'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"MYTHICBEASTS_PASSWORD\":\tPassword`)\n\t\tew.writeln(`\t- \"MYTHICBEASTS_USERNAME\":\tUser name`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"MYTHICBEASTS_API_ENDPOINT\":\tThe endpoint for the API (must implement v2)`)\n\t\tew.writeln(`\t- \"MYTHICBEASTS_AUTH_API_ENDPOINT\":\tThe endpoint for Mythic Beasts' Authentication`)\n\t\tew.writeln(`\t- \"MYTHICBEASTS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"MYTHICBEASTS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"MYTHICBEASTS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"MYTHICBEASTS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/mythicbeasts`)\n\n\tcase \"namecheap\":\n\t\t// generated from: providers/dns/namecheap/namecheap.toml\n\t\tew.writeln(`Configuration for Namecheap.`)\n\t\tew.writeln(`Code:\t'namecheap'`)\n\t\tew.writeln(`Since:\t'v0.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"NAMECHEAP_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"NAMECHEAP_API_USER\":\tAPI user`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"NAMECHEAP_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"NAMECHEAP_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 15)`)\n\t\tew.writeln(`\t- \"NAMECHEAP_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 3600)`)\n\t\tew.writeln(`\t- \"NAMECHEAP_SANDBOX\":\tActivate the sandbox (boolean)`)\n\t\tew.writeln(`\t- \"NAMECHEAP_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/namecheap`)\n\n\tcase \"namedotcom\":\n\t\t// generated from: providers/dns/namedotcom/namedotcom.toml\n\t\tew.writeln(`Configuration for Name.com.`)\n\t\tew.writeln(`Code:\t'namedotcom'`)\n\t\tew.writeln(`Since:\t'v0.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"NAMECOM_API_TOKEN\":\tAPI token`)\n\t\tew.writeln(`\t- \"NAMECOM_USERNAME\":\tUsername`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"NAMECOM_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"NAMECOM_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 20)`)\n\t\tew.writeln(`\t- \"NAMECOM_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 900)`)\n\t\tew.writeln(`\t- \"NAMECOM_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/namedotcom`)\n\n\tcase \"namesilo\":\n\t\t// generated from: providers/dns/namesilo/namesilo.toml\n\t\tew.writeln(`Configuration for Namesilo.`)\n\t\tew.writeln(`Code:\t'namesilo'`)\n\t\tew.writeln(`Since:\t'v2.7.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"NAMESILO_API_KEY\":\tClient ID`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"NAMESILO_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"NAMESILO_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60), it is better to set larger than 15 minutes`)\n\t\tew.writeln(`\t- \"NAMESILO_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 3600), should be in [3600, 2592000]`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/namesilo`)\n\n\tcase \"namesurfer\":\n\t\t// generated from: providers/dns/namesurfer/namesurfer.toml\n\t\tew.writeln(`Configuration for FusionLayer NameSurfer.`)\n\t\tew.writeln(`Code:\t'namesurfer'`)\n\t\tew.writeln(`Since:\t'v4.32.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"NAMESURFER_API_KEY\":\tAPI key name`)\n\t\tew.writeln(`\t- \"NAMESURFER_API_SECRET\":\tAPI secret`)\n\t\tew.writeln(`\t- \"NAMESURFER_BASE_URL\":\tThe base URL of NameSurfer API (jsonrpc10) endpoint URL (e.g., https://foo.example.com:8443/API/NSService_10)`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"NAMESURFER_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"NAMESURFER_INSECURE_SKIP_VERIFY\":\tWhether to verify the API certificate`)\n\t\tew.writeln(`\t- \"NAMESURFER_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"NAMESURFER_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"NAMESURFER_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\t\tew.writeln(`\t- \"NAMESURFER_VIEW\":\tDNS view name (optional, default: empty string)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/namesurfer`)\n\n\tcase \"nearlyfreespeech\":\n\t\t// generated from: providers/dns/nearlyfreespeech/nearlyfreespeech.toml\n\t\tew.writeln(`Configuration for NearlyFreeSpeech.NET.`)\n\t\tew.writeln(`Code:\t'nearlyfreespeech'`)\n\t\tew.writeln(`Since:\t'v4.8.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"NEARLYFREESPEECH_API_KEY\":\tAPI Key for API requests`)\n\t\tew.writeln(`\t- \"NEARLYFREESPEECH_LOGIN\":\tUsername for API requests`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"NEARLYFREESPEECH_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"NEARLYFREESPEECH_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"NEARLYFREESPEECH_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"NEARLYFREESPEECH_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"NEARLYFREESPEECH_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/nearlyfreespeech`)\n\n\tcase \"neodigit\":\n\t\t// generated from: providers/dns/neodigit/neodigit.toml\n\t\tew.writeln(`Configuration for Neodigit.`)\n\t\tew.writeln(`Code:\t'neodigit'`)\n\t\tew.writeln(`Since:\t'v4.30.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"NEODIGIT_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"NEODIGIT_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"NEODIGIT_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"NEODIGIT_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 300)`)\n\t\tew.writeln(`\t- \"NEODIGIT_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/neodigit`)\n\n\tcase \"netcup\":\n\t\t// generated from: providers/dns/netcup/netcup.toml\n\t\tew.writeln(`Configuration for Netcup.`)\n\t\tew.writeln(`Code:\t'netcup'`)\n\t\tew.writeln(`Since:\t'v1.1.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"NETCUP_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"NETCUP_API_PASSWORD\":\tAPI password`)\n\t\tew.writeln(`\t- \"NETCUP_CUSTOMER_NUMBER\":\tCustomer number`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"NETCUP_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"NETCUP_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"NETCUP_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 900)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/netcup`)\n\n\tcase \"netlify\":\n\t\t// generated from: providers/dns/netlify/netlify.toml\n\t\tew.writeln(`Configuration for Netlify.`)\n\t\tew.writeln(`Code:\t'netlify'`)\n\t\tew.writeln(`Since:\t'v3.7.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"NETLIFY_TOKEN\":\tToken`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"NETLIFY_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"NETLIFY_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"NETLIFY_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"NETLIFY_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/netlify`)\n\n\tcase \"nicmanager\":\n\t\t// generated from: providers/dns/nicmanager/nicmanager.toml\n\t\tew.writeln(`Configuration for Nicmanager.`)\n\t\tew.writeln(`Code:\t'nicmanager'`)\n\t\tew.writeln(`Since:\t'v4.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"NICMANAGER_API_EMAIL\":\tEmail-based login`)\n\t\tew.writeln(`\t- \"NICMANAGER_API_LOGIN\":\tLogin, used for Username-based login`)\n\t\tew.writeln(`\t- \"NICMANAGER_API_PASSWORD\":\tPassword, always required`)\n\t\tew.writeln(`\t- \"NICMANAGER_API_USERNAME\":\tUsername, used for Username-based login`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"NICMANAGER_API_MODE\":\tmode: 'anycast' or 'zones' (for FreeDNS) (default: 'anycast')`)\n\t\tew.writeln(`\t- \"NICMANAGER_API_OTP\":\tTOTP Secret (optional)`)\n\t\tew.writeln(`\t- \"NICMANAGER_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"NICMANAGER_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"NICMANAGER_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 300)`)\n\t\tew.writeln(`\t- \"NICMANAGER_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 900)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/nicmanager`)\n\n\tcase \"nicru\":\n\t\t// generated from: providers/dns/nicru/nicru.toml\n\t\tew.writeln(`Configuration for RU CENTER.`)\n\t\tew.writeln(`Code:\t'nicru'`)\n\t\tew.writeln(`Since:\t'v4.24.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"NICRU_PASSWORD\":\tPassword for an account in RU CENTER`)\n\t\tew.writeln(`\t- \"NICRU_SECRET\":\tSecret for application in DNS-hosting RU CENTER`)\n\t\tew.writeln(`\t- \"NICRU_SERVICE_ID\":\tService ID for application in DNS-hosting RU CENTER`)\n\t\tew.writeln(`\t- \"NICRU_SERVICE_NAME\":\tService Name for DNS-hosting RU CENTER`)\n\t\tew.writeln(`\t- \"NICRU_USER\":\tAgreement for an account in RU CENTER`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"NICRU_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"NICRU_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 600)`)\n\t\tew.writeln(`\t- \"NICRU_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 30)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/nicru`)\n\n\tcase \"nifcloud\":\n\t\t// generated from: providers/dns/nifcloud/nifcloud.toml\n\t\tew.writeln(`Configuration for NIFCloud.`)\n\t\tew.writeln(`Code:\t'nifcloud'`)\n\t\tew.writeln(`Since:\t'v1.1.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"NIFCLOUD_ACCESS_KEY_ID\":\tAccess key`)\n\t\tew.writeln(`\t- \"NIFCLOUD_SECRET_ACCESS_KEY\":\tSecret access key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"NIFCLOUD_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"NIFCLOUD_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"NIFCLOUD_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"NIFCLOUD_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/nifcloud`)\n\n\tcase \"njalla\":\n\t\t// generated from: providers/dns/njalla/njalla.toml\n\t\tew.writeln(`Configuration for Njalla.`)\n\t\tew.writeln(`Code:\t'njalla'`)\n\t\tew.writeln(`Since:\t'v4.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"NJALLA_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"NJALLA_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"NJALLA_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"NJALLA_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"NJALLA_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/njalla`)\n\n\tcase \"nodion\":\n\t\t// generated from: providers/dns/nodion/nodion.toml\n\t\tew.writeln(`Configuration for Nodion.`)\n\t\tew.writeln(`Code:\t'nodion'`)\n\t\tew.writeln(`Since:\t'v4.11.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"NODION_API_TOKEN\":\tThe API token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"NODION_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"NODION_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"NODION_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"NODION_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/nodion`)\n\n\tcase \"ns1\":\n\t\t// generated from: providers/dns/ns1/ns1.toml\n\t\tew.writeln(`Configuration for NS1.`)\n\t\tew.writeln(`Code:\t'ns1'`)\n\t\tew.writeln(`Since:\t'v0.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"NS1_API_KEY\":\tAPI key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"NS1_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"NS1_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"NS1_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"NS1_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/ns1`)\n\n\tcase \"octenium\":\n\t\t// generated from: providers/dns/octenium/octenium.toml\n\t\tew.writeln(`Configuration for Octenium.`)\n\t\tew.writeln(`Code:\t'octenium'`)\n\t\tew.writeln(`Since:\t'v4.27.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"OCTENIUM_API_KEY\":\tAPI key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"OCTENIUM_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"OCTENIUM_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"OCTENIUM_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"OCTENIUM_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/octenium`)\n\n\tcase \"oraclecloud\":\n\t\t// generated from: providers/dns/oraclecloud/oraclecloud.toml\n\t\tew.writeln(`Configuration for Oracle Cloud.`)\n\t\tew.writeln(`Code:\t'oraclecloud'`)\n\t\tew.writeln(`Since:\t'v2.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"OCI_COMPARTMENT_OCID\":\tCompartment OCID`)\n\t\tew.writeln(`\t- \"OCI_FINGERPRINT\":\tPublic key fingerprint (ignored if 'OCI_AUTH_TYPE=instance_principal')`)\n\t\tew.writeln(`\t- \"OCI_PRIVATE_KEY_PASSWORD\":\tPrivate key password (ignored if 'OCI_AUTH_TYPE=instance_principal')`)\n\t\tew.writeln(`\t- \"OCI_PRIVATE_KEY_PATH\":\tPrivate key file (ignored if 'OCI_AUTH_TYPE=instance_principal')`)\n\t\tew.writeln(`\t- \"OCI_REGION\":\tRegion (it can be empty if 'OCI_AUTH_TYPE=instance_principal').`)\n\t\tew.writeln(`\t- \"OCI_TENANCY_OCID\":\tTenancy OCID (ignored if 'OCI_AUTH_TYPE=instance_principal')`)\n\t\tew.writeln(`\t- \"OCI_USER_OCID\":\tUser OCID (ignored if 'OCI_AUTH_TYPE=instance_principal')`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"OCI_AUTH_TYPE\":\tAuthorization type. Possible values: 'instance_principal', ''  (Default: '')`)\n\t\tew.writeln(`\t- \"OCI_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"OCI_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"OCI_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"OCI_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"TF_VAR_fingerprint\":\tAlias on 'OCI_FINGERPRINT'`)\n\t\tew.writeln(`\t- \"TF_VAR_private_key_path\":\tAlias on 'OCI_PRIVATE_KEY_PATH'`)\n\t\tew.writeln(`\t- \"TF_VAR_region\":\tAlias on 'OCI_REGION'`)\n\t\tew.writeln(`\t- \"TF_VAR_tenancy_ocid\":\tAlias on 'OCI_TENANCY_OCID'`)\n\t\tew.writeln(`\t- \"TF_VAR_user_ocid\":\tAlias on 'OCI_USER_OCID'`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/oraclecloud`)\n\n\tcase \"otc\":\n\t\t// generated from: providers/dns/otc/otc.toml\n\t\tew.writeln(`Configuration for Open Telekom Cloud.`)\n\t\tew.writeln(`Code:\t'otc'`)\n\t\tew.writeln(`Since:\t'v0.4.1'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"OTC_DOMAIN_NAME\":\tDomain name`)\n\t\tew.writeln(`\t- \"OTC_PASSWORD\":\tPassword`)\n\t\tew.writeln(`\t- \"OTC_PROJECT_NAME\":\tProject name`)\n\t\tew.writeln(`\t- \"OTC_USER_NAME\":\tUser name`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"OTC_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"OTC_IDENTITY_ENDPOINT\":\tIdentity endpoint URL (default: https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens)`)\n\t\tew.writeln(`\t- \"OTC_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"OTC_PRIVATE_ZONE\":\tSet to true to use private zones only (default: use public zones only)`)\n\t\tew.writeln(`\t- \"OTC_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"OTC_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"OTC_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/otc`)\n\n\tcase \"ovh\":\n\t\t// generated from: providers/dns/ovh/ovh.toml\n\t\tew.writeln(`Configuration for OVH.`)\n\t\tew.writeln(`Code:\t'ovh'`)\n\t\tew.writeln(`Since:\t'v0.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"OVH_ACCESS_TOKEN\":\tAccess token`)\n\t\tew.writeln(`\t- \"OVH_APPLICATION_KEY\":\tApplication key (Application Key authentication)`)\n\t\tew.writeln(`\t- \"OVH_APPLICATION_SECRET\":\tApplication secret (Application Key authentication)`)\n\t\tew.writeln(`\t- \"OVH_CLIENT_ID\":\tClient ID (OAuth2)`)\n\t\tew.writeln(`\t- \"OVH_CLIENT_SECRET\":\tClient secret (OAuth2)`)\n\t\tew.writeln(`\t- \"OVH_CONSUMER_KEY\":\tConsumer key (Application Key authentication)`)\n\t\tew.writeln(`\t- \"OVH_ENDPOINT\":\tEndpoint URL (ovh-eu or ovh-ca)`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"OVH_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 180)`)\n\t\tew.writeln(`\t- \"OVH_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"OVH_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"OVH_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/ovh`)\n\n\tcase \"pdns\":\n\t\t// generated from: providers/dns/pdns/pdns.toml\n\t\tew.writeln(`Configuration for PowerDNS.`)\n\t\tew.writeln(`Code:\t'pdns'`)\n\t\tew.writeln(`Since:\t'v0.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"PDNS_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"PDNS_API_URL\":\tAPI URL`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"PDNS_API_VERSION\":\tSkip API version autodetection and use the provided version number.`)\n\t\tew.writeln(`\t- \"PDNS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"PDNS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"PDNS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"PDNS_SERVER_NAME\":\tName of the server in the URL, 'localhost' by default`)\n\t\tew.writeln(`\t- \"PDNS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/pdns`)\n\n\tcase \"plesk\":\n\t\t// generated from: providers/dns/plesk/plesk.toml\n\t\tew.writeln(`Configuration for plesk.com.`)\n\t\tew.writeln(`Code:\t'plesk'`)\n\t\tew.writeln(`Since:\t'v4.11.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"PLESK_PASSWORD\":\tAPI password`)\n\t\tew.writeln(`\t- \"PLESK_SERVER_BASE_URL\":\tBase URL of the server (ex: https://plesk.myserver.com:8443)`)\n\t\tew.writeln(`\t- \"PLESK_USERNAME\":\tAPI username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"PLESK_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"PLESK_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"PLESK_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"PLESK_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/plesk`)\n\n\tcase \"porkbun\":\n\t\t// generated from: providers/dns/porkbun/porkbun.toml\n\t\tew.writeln(`Configuration for Porkbun.`)\n\t\tew.writeln(`Code:\t'porkbun'`)\n\t\tew.writeln(`Since:\t'v4.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"PORKBUN_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"PORKBUN_SECRET_API_KEY\":\tsecret API key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"PORKBUN_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"PORKBUN_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"PORKBUN_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 600)`)\n\t\tew.writeln(`\t- \"PORKBUN_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/porkbun`)\n\n\tcase \"rackspace\":\n\t\t// generated from: providers/dns/rackspace/rackspace.toml\n\t\tew.writeln(`Configuration for Rackspace.`)\n\t\tew.writeln(`Code:\t'rackspace'`)\n\t\tew.writeln(`Since:\t'v0.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"RACKSPACE_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"RACKSPACE_USER\":\tAPI user`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"RACKSPACE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"RACKSPACE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 3)`)\n\t\tew.writeln(`\t- \"RACKSPACE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"RACKSPACE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/rackspace`)\n\n\tcase \"rainyun\":\n\t\t// generated from: providers/dns/rainyun/rainyun.toml\n\t\tew.writeln(`Configuration for Rain Yun/雨云.`)\n\t\tew.writeln(`Code:\t'rainyun'`)\n\t\tew.writeln(`Since:\t'v4.21.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"RAINYUN_API_KEY\":\tAPI key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"RAINYUN_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"RAINYUN_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"RAINYUN_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"RAINYUN_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/rainyun`)\n\n\tcase \"rcodezero\":\n\t\t// generated from: providers/dns/rcodezero/rcodezero.toml\n\t\tew.writeln(`Configuration for RcodeZero.`)\n\t\tew.writeln(`Code:\t'rcodezero'`)\n\t\tew.writeln(`Since:\t'v4.13'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"RCODEZERO_API_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"RCODEZERO_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"RCODEZERO_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"RCODEZERO_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 240)`)\n\t\tew.writeln(`\t- \"RCODEZERO_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/rcodezero`)\n\n\tcase \"regfish\":\n\t\t// generated from: providers/dns/regfish/regfish.toml\n\t\tew.writeln(`Configuration for Regfish.`)\n\t\tew.writeln(`Code:\t'regfish'`)\n\t\tew.writeln(`Since:\t'v4.20.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"REGFISH_API_KEY\":\tAPI key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"REGFISH_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"REGFISH_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"REGFISH_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"REGFISH_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/regfish`)\n\n\tcase \"regru\":\n\t\t// generated from: providers/dns/regru/regru.toml\n\t\tew.writeln(`Configuration for reg.ru.`)\n\t\tew.writeln(`Code:\t'regru'`)\n\t\tew.writeln(`Since:\t'v3.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"REGRU_PASSWORD\":\tAPI password`)\n\t\tew.writeln(`\t- \"REGRU_USERNAME\":\tAPI username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"REGRU_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"REGRU_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"REGRU_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"REGRU_TLS_CERT\":\tauthentication certificate`)\n\t\tew.writeln(`\t- \"REGRU_TLS_KEY\":\tauthentication private key`)\n\t\tew.writeln(`\t- \"REGRU_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/regru`)\n\n\tcase \"rfc2136\":\n\t\t// generated from: providers/dns/rfc2136/rfc2136.toml\n\t\tew.writeln(`Configuration for RFC2136.`)\n\t\tew.writeln(`Code:\t'rfc2136'`)\n\t\tew.writeln(`Since:\t'v0.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"RFC2136_NAMESERVER\":\tNetwork address in the form \"host\" or \"host:port\"`)\n\t\tew.writeln(`\t- \"RFC2136_TSIG_ALGORITHM\":\tTSIG algorithm. See [miekg/dns#tsig.go](https://github.com/miekg/dns/blob/master/tsig.go) for supported values. To disable TSIG authentication, leave the 'RFC2136_TSIG_KEY' or 'RFC2136_TSIG_SECRET' variables unset.`)\n\t\tew.writeln(`\t- \"RFC2136_TSIG_KEY\":\tName of the secret key as defined in DNS server configuration. To disable TSIG authentication, leave the 'RFC2136_TSIG_KEY' variable unset.`)\n\t\tew.writeln(`\t- \"RFC2136_TSIG_SECRET\":\tSecret key payload. To disable TSIG authentication, leave the 'RFC2136_TSIG_SECRET' variable unset.`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"RFC2136_DNS_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"RFC2136_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"RFC2136_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"RFC2136_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"RFC2136_TSIG_FILE\":\tPath to a key file generated by tsig-keygen`)\n\t\tew.writeln(`\t- \"RFC2136_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/rfc2136`)\n\n\tcase \"rimuhosting\":\n\t\t// generated from: providers/dns/rimuhosting/rimuhosting.toml\n\t\tew.writeln(`Configuration for RimuHosting.`)\n\t\tew.writeln(`Code:\t'rimuhosting'`)\n\t\tew.writeln(`Since:\t'v0.3.5'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"RIMUHOSTING_API_KEY\":\tUser API key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"RIMUHOSTING_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"RIMUHOSTING_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"RIMUHOSTING_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"RIMUHOSTING_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/rimuhosting`)\n\n\tcase \"route53\":\n\t\t// generated from: providers/dns/route53/route53.toml\n\t\tew.writeln(`Configuration for Amazon Route 53.`)\n\t\tew.writeln(`Code:\t'route53'`)\n\t\tew.writeln(`Since:\t'v0.3.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"AWS_ACCESS_KEY_ID\":\tManaged by the AWS client. Access key ID ('AWS_ACCESS_KEY_ID_FILE' is not supported, use 'AWS_SHARED_CREDENTIALS_FILE' instead)`)\n\t\tew.writeln(`\t- \"AWS_ASSUME_ROLE_ARN\":\tManaged by the AWS Role ARN ('AWS_ASSUME_ROLE_ARN_FILE' is not supported)`)\n\t\tew.writeln(`\t- \"AWS_EXTERNAL_ID\":\tManaged by STS AssumeRole API operation ('AWS_EXTERNAL_ID_FILE' is not supported)`)\n\t\tew.writeln(`\t- \"AWS_HOSTED_ZONE_ID\":\tOverride the hosted zone ID.`)\n\t\tew.writeln(`\t- \"AWS_PROFILE\":\tManaged by the AWS client ('AWS_PROFILE_FILE' is not supported)`)\n\t\tew.writeln(`\t- \"AWS_REGION\":\tManaged by the AWS client ('AWS_REGION_FILE' is not supported)`)\n\t\tew.writeln(`\t- \"AWS_SDK_LOAD_CONFIG\":\tManaged by the AWS client. Retrieve the region from the CLI config file ('AWS_SDK_LOAD_CONFIG_FILE' is not supported)`)\n\t\tew.writeln(`\t- \"AWS_SECRET_ACCESS_KEY\":\tManaged by the AWS client. Secret access key ('AWS_SECRET_ACCESS_KEY_FILE' is not supported, use 'AWS_SHARED_CREDENTIALS_FILE' instead)`)\n\t\tew.writeln(`\t- \"AWS_WAIT_FOR_RECORD_SETS_CHANGED\":\tWait for changes to be INSYNC (it can be unstable)`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"AWS_MAX_RETRIES\":\tThe number of maximum returns the service will use to make an individual API request`)\n\t\tew.writeln(`\t- \"AWS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 4)`)\n\t\tew.writeln(`\t- \"AWS_PRIVATE_ZONE\":\tSet to true to use private zones only (default: use public zones only)`)\n\t\tew.writeln(`\t- \"AWS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"AWS_SHARED_CREDENTIALS_FILE\":\tManaged by the AWS client. Shared credentials file.`)\n\t\tew.writeln(`\t- \"AWS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 10)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/route53`)\n\n\tcase \"safedns\":\n\t\t// generated from: providers/dns/safedns/safedns.toml\n\t\tew.writeln(`Configuration for ANS SafeDNS.`)\n\t\tew.writeln(`Code:\t'safedns'`)\n\t\tew.writeln(`Since:\t'v4.6.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"SAFEDNS_AUTH_TOKEN\":\tAuthentication token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"SAFEDNS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"SAFEDNS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"SAFEDNS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"SAFEDNS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/safedns`)\n\n\tcase \"sakuracloud\":\n\t\t// generated from: providers/dns/sakuracloud/sakuracloud.toml\n\t\tew.writeln(`Configuration for Sakura Cloud.`)\n\t\tew.writeln(`Code:\t'sakuracloud'`)\n\t\tew.writeln(`Since:\t'v1.1.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"SAKURACLOUD_ACCESS_TOKEN\":\tAccess token`)\n\t\tew.writeln(`\t- \"SAKURACLOUD_ACCESS_TOKEN_SECRET\":\tAccess token secret`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"SAKURACLOUD_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"SAKURACLOUD_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"SAKURACLOUD_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"SAKURACLOUD_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/sakuracloud`)\n\n\tcase \"scaleway\":\n\t\t// generated from: providers/dns/scaleway/scaleway.toml\n\t\tew.writeln(`Configuration for Scaleway.`)\n\t\tew.writeln(`Code:\t'scaleway'`)\n\t\tew.writeln(`Since:\t'v3.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"SCW_PROJECT_ID\":\tProject to use (optional)`)\n\t\tew.writeln(`\t- \"SCW_SECRET_KEY\":\tSecret key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"SCW_ACCESS_KEY\":\tAccess key`)\n\t\tew.writeln(`\t- \"SCW_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"SCW_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"SCW_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"SCW_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/scaleway`)\n\n\tcase \"selectel\":\n\t\t// generated from: providers/dns/selectel/selectel.toml\n\t\tew.writeln(`Configuration for Selectel.`)\n\t\tew.writeln(`Code:\t'selectel'`)\n\t\tew.writeln(`Since:\t'v1.2.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"SELECTEL_API_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"SELECTEL_BASE_URL\":\tAPI endpoint URL`)\n\t\tew.writeln(`\t- \"SELECTEL_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"SELECTEL_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"SELECTEL_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"SELECTEL_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/selectel`)\n\n\tcase \"selectelv2\":\n\t\t// generated from: providers/dns/selectelv2/selectelv2.toml\n\t\tew.writeln(`Configuration for Selectel v2.`)\n\t\tew.writeln(`Code:\t'selectelv2'`)\n\t\tew.writeln(`Since:\t'v4.17.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"SELECTELV2_ACCOUNT_ID\":\tSelectel account ID (INT)`)\n\t\tew.writeln(`\t- \"SELECTELV2_PASSWORD\":\tOpenstack username's password`)\n\t\tew.writeln(`\t- \"SELECTELV2_PROJECT_ID\":\tCloud project ID (UUID)`)\n\t\tew.writeln(`\t- \"SELECTELV2_USERNAME\":\tOpenstack username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"SELECTELV2_AUTH_REGION\":\tLocation for auth endpoint like ResellAPI or Keystone (default: 'ru-1')`)\n\t\tew.writeln(`\t- \"SELECTELV2_AUTH_URL\":\tIdentity endpoint (defaul: 'https://cloud.api.selcloud.ru/identity/v3/')`)\n\t\tew.writeln(`\t- \"SELECTELV2_BASE_URL\":\tAPI endpoint URL`)\n\t\tew.writeln(`\t- \"SELECTELV2_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"SELECTELV2_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 5)`)\n\t\tew.writeln(`\t- \"SELECTELV2_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"SELECTELV2_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"SELECTELV2_USER_DOMAIN_NAME\":\tTo specify the domain name (account ID) where the user is located. (default: SELECTELV2_ACCOUNT_ID)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/selectelv2`)\n\n\tcase \"selfhostde\":\n\t\t// generated from: providers/dns/selfhostde/selfhostde.toml\n\t\tew.writeln(`Configuration for SelfHost.(de|eu).`)\n\t\tew.writeln(`Code:\t'selfhostde'`)\n\t\tew.writeln(`Since:\t'v4.19.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"SELFHOSTDE_PASSWORD\":\tPassword`)\n\t\tew.writeln(`\t- \"SELFHOSTDE_RECORDS_MAPPING\":\tRecord IDs mapping with domains (ex: example.com:123:456,example.org:789,foo.example.com:147)`)\n\t\tew.writeln(`\t- \"SELFHOSTDE_USERNAME\":\tUsername`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"SELFHOSTDE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"SELFHOSTDE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"SELFHOSTDE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 240)`)\n\t\tew.writeln(`\t- \"SELFHOSTDE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/selfhostde`)\n\n\tcase \"servercow\":\n\t\t// generated from: providers/dns/servercow/servercow.toml\n\t\tew.writeln(`Configuration for Servercow.`)\n\t\tew.writeln(`Code:\t'servercow'`)\n\t\tew.writeln(`Since:\t'v3.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"SERVERCOW_PASSWORD\":\tAPI password`)\n\t\tew.writeln(`\t- \"SERVERCOW_USERNAME\":\tAPI username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"SERVERCOW_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"SERVERCOW_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"SERVERCOW_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"SERVERCOW_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/servercow`)\n\n\tcase \"shellrent\":\n\t\t// generated from: providers/dns/shellrent/shellrent.toml\n\t\tew.writeln(`Configuration for Shellrent.`)\n\t\tew.writeln(`Code:\t'shellrent'`)\n\t\tew.writeln(`Since:\t'v4.16.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"SHELLRENT_TOKEN\":\tToken`)\n\t\tew.writeln(`\t- \"SHELLRENT_USERNAME\":\tUsername`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"SHELLRENT_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"SHELLRENT_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"SHELLRENT_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 300)`)\n\t\tew.writeln(`\t- \"SHELLRENT_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/shellrent`)\n\n\tcase \"simply\":\n\t\t// generated from: providers/dns/simply/simply.toml\n\t\tew.writeln(`Configuration for Simply.com.`)\n\t\tew.writeln(`Code:\t'simply'`)\n\t\tew.writeln(`Since:\t'v4.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"SIMPLY_ACCOUNT_NAME\":\tAccount name`)\n\t\tew.writeln(`\t- \"SIMPLY_API_KEY\":\tAPI key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"SIMPLY_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"SIMPLY_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"SIMPLY_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 300)`)\n\t\tew.writeln(`\t- \"SIMPLY_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/simply`)\n\n\tcase \"sonic\":\n\t\t// generated from: providers/dns/sonic/sonic.toml\n\t\tew.writeln(`Configuration for Sonic.`)\n\t\tew.writeln(`Code:\t'sonic'`)\n\t\tew.writeln(`Since:\t'v4.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"SONIC_API_KEY\":\tAPI Key`)\n\t\tew.writeln(`\t- \"SONIC_USER_ID\":\tUser ID`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"SONIC_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"SONIC_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"SONIC_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"SONIC_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"SONIC_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/sonic`)\n\n\tcase \"spaceship\":\n\t\t// generated from: providers/dns/spaceship/spaceship.toml\n\t\tew.writeln(`Configuration for Spaceship.`)\n\t\tew.writeln(`Code:\t'spaceship'`)\n\t\tew.writeln(`Since:\t'v4.22.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"SPACESHIP_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"SPACESHIP_API_SECRET\":\tAPI secret`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"SPACESHIP_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"SPACESHIP_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"SPACESHIP_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"SPACESHIP_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/spaceship`)\n\n\tcase \"stackpath\":\n\t\t// generated from: providers/dns/stackpath/stackpath.toml\n\t\tew.writeln(`Configuration for Stackpath.`)\n\t\tew.writeln(`Code:\t'stackpath'`)\n\t\tew.writeln(`Since:\t'v1.1.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"STACKPATH_CLIENT_ID\":\tClient ID`)\n\t\tew.writeln(`\t- \"STACKPATH_CLIENT_SECRET\":\tClient secret`)\n\t\tew.writeln(`\t- \"STACKPATH_STACK_ID\":\tStack ID`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"STACKPATH_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"STACKPATH_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"STACKPATH_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/stackpath`)\n\n\tcase \"syse\":\n\t\t// generated from: providers/dns/syse/syse.toml\n\t\tew.writeln(`Configuration for Syse.`)\n\t\tew.writeln(`Code:\t'syse'`)\n\t\tew.writeln(`Since:\t'v4.30.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"SYSE_CREDENTIALS\":\tComma-separated list of 'zone:password' credential pairs`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"SYSE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"SYSE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"SYSE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 1200)`)\n\t\tew.writeln(`\t- \"SYSE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/syse`)\n\n\tcase \"technitium\":\n\t\t// generated from: providers/dns/technitium/technitium.toml\n\t\tew.writeln(`Configuration for Technitium.`)\n\t\tew.writeln(`Code:\t'technitium'`)\n\t\tew.writeln(`Since:\t'v4.20.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"TECHNITIUM_API_TOKEN\":\tAPI token`)\n\t\tew.writeln(`\t- \"TECHNITIUM_SERVER_BASE_URL\":\tServer base URL`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"TECHNITIUM_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"TECHNITIUM_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"TECHNITIUM_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"TECHNITIUM_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/technitium`)\n\n\tcase \"tencentcloud\":\n\t\t// generated from: providers/dns/tencentcloud/tencentcloud.toml\n\t\tew.writeln(`Configuration for Tencent Cloud DNS.`)\n\t\tew.writeln(`Code:\t'tencentcloud'`)\n\t\tew.writeln(`Since:\t'v4.6.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"TENCENTCLOUD_SECRET_ID\":\tAccess key ID`)\n\t\tew.writeln(`\t- \"TENCENTCLOUD_SECRET_KEY\":\tAccess Key secret`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"TENCENTCLOUD_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"TENCENTCLOUD_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"TENCENTCLOUD_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"TENCENTCLOUD_REGION\":\tRegion`)\n\t\tew.writeln(`\t- \"TENCENTCLOUD_SESSION_TOKEN\":\tAccess Key token`)\n\t\tew.writeln(`\t- \"TENCENTCLOUD_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/tencentcloud`)\n\n\tcase \"timewebcloud\":\n\t\t// generated from: providers/dns/timewebcloud/timewebcloud.toml\n\t\tew.writeln(`Configuration for Timeweb Cloud.`)\n\t\tew.writeln(`Code:\t'timewebcloud'`)\n\t\tew.writeln(`Since:\t'v4.20.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"TIMEWEBCLOUD_AUTH_TOKEN\":\tAuthentication token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"TIMEWEBCLOUD_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"TIMEWEBCLOUD_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"TIMEWEBCLOUD_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/timewebcloud`)\n\n\tcase \"todaynic\":\n\t\t// generated from: providers/dns/todaynic/todaynic.toml\n\t\tew.writeln(`Configuration for TodayNIC/时代互联.`)\n\t\tew.writeln(`Code:\t'todaynic'`)\n\t\tew.writeln(`Since:\t'v4.32.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"TODAYNIC_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"TODAYNIC_AUTH_USER_ID\":\taccount ID`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"TODAYNIC_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"TODAYNIC_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"TODAYNIC_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"TODAYNIC_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/todaynic`)\n\n\tcase \"transip\":\n\t\t// generated from: providers/dns/transip/transip.toml\n\t\tew.writeln(`Configuration for TransIP.`)\n\t\tew.writeln(`Code:\t'transip'`)\n\t\tew.writeln(`Since:\t'v2.0.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"TRANSIP_ACCOUNT_NAME\":\tAccount name`)\n\t\tew.writeln(`\t- \"TRANSIP_PRIVATE_KEY_PATH\":\tPrivate key path`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"TRANSIP_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"TRANSIP_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"TRANSIP_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 600)`)\n\t\tew.writeln(`\t- \"TRANSIP_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 10)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/transip`)\n\n\tcase \"ultradns\":\n\t\t// generated from: providers/dns/ultradns/ultradns.toml\n\t\tew.writeln(`Configuration for Ultradns.`)\n\t\tew.writeln(`Code:\t'ultradns'`)\n\t\tew.writeln(`Since:\t'v4.10.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ULTRADNS_PASSWORD\":\tAPI Password`)\n\t\tew.writeln(`\t- \"ULTRADNS_USERNAME\":\tAPI Username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ULTRADNS_ENDPOINT\":\tAPI endpoint URL, defaults to https://api.ultradns.com/`)\n\t\tew.writeln(`\t- \"ULTRADNS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 4)`)\n\t\tew.writeln(`\t- \"ULTRADNS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"ULTRADNS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/ultradns`)\n\n\tcase \"uniteddomains\":\n\t\t// generated from: providers/dns/uniteddomains/uniteddomains.toml\n\t\tew.writeln(`Configuration for United-Domains.`)\n\t\tew.writeln(`Code:\t'uniteddomains'`)\n\t\tew.writeln(`Since:\t'v4.29.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"UNITEDDOMAINS_API_KEY\":\tAPI key '<prefix>.<secret>' https://www.united-domains.de/help/faq-article/getting-started-with-the-united-domains-dns-api/`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"UNITEDDOMAINS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"UNITEDDOMAINS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"UNITEDDOMAINS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 900)`)\n\t\tew.writeln(`\t- \"UNITEDDOMAINS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/uniteddomains`)\n\n\tcase \"variomedia\":\n\t\t// generated from: providers/dns/variomedia/variomedia.toml\n\t\tew.writeln(`Configuration for Variomedia.`)\n\t\tew.writeln(`Code:\t'variomedia'`)\n\t\tew.writeln(`Since:\t'v4.8.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"VARIOMEDIA_API_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"VARIOMEDIA_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"VARIOMEDIA_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"VARIOMEDIA_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"VARIOMEDIA_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"VARIOMEDIA_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/variomedia`)\n\n\tcase \"vegadns\":\n\t\t// generated from: providers/dns/vegadns/vegadns.toml\n\t\tew.writeln(`Configuration for VegaDNS.`)\n\t\tew.writeln(`Code:\t'vegadns'`)\n\t\tew.writeln(`Since:\t'v1.1.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"SECRET_VEGADNS_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"SECRET_VEGADNS_SECRET\":\tAPI secret`)\n\t\tew.writeln(`\t- \"VEGADNS_URL\":\tAPI endpoint URL`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"VEGADNS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"VEGADNS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 720)`)\n\t\tew.writeln(`\t- \"VEGADNS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 10)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/vegadns`)\n\n\tcase \"vercel\":\n\t\t// generated from: providers/dns/vercel/vercel.toml\n\t\tew.writeln(`Configuration for Vercel.`)\n\t\tew.writeln(`Code:\t'vercel'`)\n\t\tew.writeln(`Since:\t'v4.7.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"VERCEL_API_TOKEN\":\tAuthentication token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"VERCEL_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"VERCEL_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 5)`)\n\t\tew.writeln(`\t- \"VERCEL_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"VERCEL_TEAM_ID\":\tTeam ID (ex: team_xxxxxxxxxxxxxxxxxxxxxxxx)`)\n\t\tew.writeln(`\t- \"VERCEL_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/vercel`)\n\n\tcase \"versio\":\n\t\t// generated from: providers/dns/versio/versio.toml\n\t\tew.writeln(`Configuration for Versio.[nl|eu|uk].`)\n\t\tew.writeln(`Code:\t'versio'`)\n\t\tew.writeln(`Since:\t'v2.7.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"VERSIO_PASSWORD\":\tBasic authentication password`)\n\t\tew.writeln(`\t- \"VERSIO_USERNAME\":\tBasic authentication username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"VERSIO_ENDPOINT\":\tThe endpoint URL of the API Server`)\n\t\tew.writeln(`\t- \"VERSIO_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"VERSIO_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 5)`)\n\t\tew.writeln(`\t- \"VERSIO_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"VERSIO_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"VERSIO_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/versio`)\n\n\tcase \"vinyldns\":\n\t\t// generated from: providers/dns/vinyldns/vinyldns.toml\n\t\tew.writeln(`Configuration for VinylDNS.`)\n\t\tew.writeln(`Code:\t'vinyldns'`)\n\t\tew.writeln(`Since:\t'v4.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"VINYLDNS_ACCESS_KEY\":\tThe VinylDNS API key`)\n\t\tew.writeln(`\t- \"VINYLDNS_HOST\":\tThe VinylDNS API URL`)\n\t\tew.writeln(`\t- \"VINYLDNS_SECRET_KEY\":\tThe VinylDNS API Secret key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"VINYLDNS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"VINYLDNS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 4)`)\n\t\tew.writeln(`\t- \"VINYLDNS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"VINYLDNS_QUOTE_VALUE\":\tAdds quotes around the TXT record value (Default: false)`)\n\t\tew.writeln(`\t- \"VINYLDNS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 30)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/vinyldns`)\n\n\tcase \"virtualname\":\n\t\t// generated from: providers/dns/virtualname/virtualname.toml\n\t\tew.writeln(`Configuration for Virtualname.`)\n\t\tew.writeln(`Code:\t'virtualname'`)\n\t\tew.writeln(`Since:\t'v4.30.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"VIRTUALNAME_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"VIRTUALNAME_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"VIRTUALNAME_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"VIRTUALNAME_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 300)`)\n\t\tew.writeln(`\t- \"VIRTUALNAME_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/virtualname`)\n\n\tcase \"vkcloud\":\n\t\t// generated from: providers/dns/vkcloud/vkcloud.toml\n\t\tew.writeln(`Configuration for VK Cloud.`)\n\t\tew.writeln(`Code:\t'vkcloud'`)\n\t\tew.writeln(`Since:\t'v4.9.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"VK_CLOUD_PASSWORD\":\tPassword for VK Cloud account`)\n\t\tew.writeln(`\t- \"VK_CLOUD_PROJECT_ID\":\tString ID of project in VK Cloud`)\n\t\tew.writeln(`\t- \"VK_CLOUD_USERNAME\":\tEmail of VK Cloud account`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"VK_CLOUD_DNS_ENDPOINT\":\tURL of DNS API. Defaults to https://mcs.mail.ru/public-dns but can be changed for usage with private clouds`)\n\t\tew.writeln(`\t- \"VK_CLOUD_DOMAIN_NAME\":\tOpenstack users domain name. Defaults to 'users' but can be changed for usage with private clouds`)\n\t\tew.writeln(`\t- \"VK_CLOUD_IDENTITY_ENDPOINT\":\tURL of OpenStack Auth API, Defaults to https://infra.mail.ru:35357/v3/ but can be changed for usage with private clouds`)\n\t\tew.writeln(`\t- \"VK_CLOUD_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"VK_CLOUD_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"VK_CLOUD_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/vkcloud`)\n\n\tcase \"volcengine\":\n\t\t// generated from: providers/dns/volcengine/volcengine.toml\n\t\tew.writeln(`Configuration for Volcano Engine/火山引擎.`)\n\t\tew.writeln(`Code:\t'volcengine'`)\n\t\tew.writeln(`Since:\t'v4.19.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"VOLC_ACCESSKEY\":\tAccess Key ID (AK)`)\n\t\tew.writeln(`\t- \"VOLC_SECRETKEY\":\tSecret Access Key (SK)`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"VOLC_HOST\":\tAPI host`)\n\t\tew.writeln(`\t- \"VOLC_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 15)`)\n\t\tew.writeln(`\t- \"VOLC_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"VOLC_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 240)`)\n\t\tew.writeln(`\t- \"VOLC_REGION\":\tRegion`)\n\t\tew.writeln(`\t- \"VOLC_SCHEME\":\tAPI scheme`)\n\t\tew.writeln(`\t- \"VOLC_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/volcengine`)\n\n\tcase \"vscale\":\n\t\t// generated from: providers/dns/vscale/vscale.toml\n\t\tew.writeln(`Configuration for Vscale.`)\n\t\tew.writeln(`Code:\t'vscale'`)\n\t\tew.writeln(`Since:\t'v2.0.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"VSCALE_API_TOKEN\":\tAPI token`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"VSCALE_BASE_URL\":\tAPI endpoint URL`)\n\t\tew.writeln(`\t- \"VSCALE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"VSCALE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"VSCALE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"VSCALE_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/vscale`)\n\n\tcase \"vultr\":\n\t\t// generated from: providers/dns/vultr/vultr.toml\n\t\tew.writeln(`Configuration for Vultr.`)\n\t\tew.writeln(`Code:\t'vultr'`)\n\t\tew.writeln(`Since:\t'v0.3.1'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"VULTR_API_KEY\":\tAPI key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"VULTR_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"VULTR_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"VULTR_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"VULTR_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/vultr`)\n\n\tcase \"webnames\":\n\t\t// generated from: providers/dns/webnames/webnames.toml\n\t\tew.writeln(`Configuration for webnames.ru.`)\n\t\tew.writeln(`Code:\t'webnames'`)\n\t\tew.writeln(`Since:\t'v4.15.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"WEBNAMESRU_API_KEY\":\tDomain API key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"WEBNAMESRU_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"WEBNAMESRU_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"WEBNAMESRU_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/webnames`)\n\n\tcase \"webnamesca\":\n\t\t// generated from: providers/dns/webnamesca/webnamesca.toml\n\t\tew.writeln(`Configuration for webnames.ca.`)\n\t\tew.writeln(`Code:\t'webnamesca'`)\n\t\tew.writeln(`Since:\t'v4.28.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"WEBNAMESCA_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"WEBNAMESCA_API_USER\":\tAPI username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"WEBNAMESCA_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"WEBNAMESCA_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"WEBNAMESCA_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"WEBNAMESCA_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 120)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/webnamesca`)\n\n\tcase \"websupport\":\n\t\t// generated from: providers/dns/websupport/websupport.toml\n\t\tew.writeln(`Configuration for Websupport.`)\n\t\tew.writeln(`Code:\t'websupport'`)\n\t\tew.writeln(`Since:\t'v4.10.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"WEBSUPPORT_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"WEBSUPPORT_SECRET\":\tAPI secret`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"WEBSUPPORT_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"WEBSUPPORT_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"WEBSUPPORT_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"WEBSUPPORT_SEQUENCE_INTERVAL\":\tTime between sequential requests in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"WEBSUPPORT_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/websupport`)\n\n\tcase \"wedos\":\n\t\t// generated from: providers/dns/wedos/wedos.toml\n\t\tew.writeln(`Configuration for WEDOS.`)\n\t\tew.writeln(`Code:\t'wedos'`)\n\t\tew.writeln(`Since:\t'v4.4.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"WEDOS_USERNAME\":\tUsername is the same as for the admin account`)\n\t\tew.writeln(`\t- \"WEDOS_WAPI_PASSWORD\":\tPassword needs to be generated and IP allowed in the admin interface`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"WEDOS_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"WEDOS_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"WEDOS_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 600)`)\n\t\tew.writeln(`\t- \"WEDOS_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/wedos`)\n\n\tcase \"westcn\":\n\t\t// generated from: providers/dns/westcn/westcn.toml\n\t\tew.writeln(`Configuration for West.cn/西部数码.`)\n\t\tew.writeln(`Code:\t'westcn'`)\n\t\tew.writeln(`Since:\t'v4.21.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"WESTCN_PASSWORD\":\tAPI password`)\n\t\tew.writeln(`\t- \"WESTCN_USERNAME\":\tUsername`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"WESTCN_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"WESTCN_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 10)`)\n\t\tew.writeln(`\t- \"WESTCN_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 120)`)\n\t\tew.writeln(`\t- \"WESTCN_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/westcn`)\n\n\tcase \"yandex\":\n\t\t// generated from: providers/dns/yandex/yandex.toml\n\t\tew.writeln(`Configuration for Yandex PDD.`)\n\t\tew.writeln(`Code:\t'yandex'`)\n\t\tew.writeln(`Since:\t'v3.7.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"YANDEX_PDD_TOKEN\":\tBasic authentication username`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"YANDEX_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"YANDEX_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"YANDEX_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"YANDEX_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 21600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/yandex`)\n\n\tcase \"yandex360\":\n\t\t// generated from: providers/dns/yandex360/yandex360.toml\n\t\tew.writeln(`Configuration for Yandex 360.`)\n\t\tew.writeln(`Code:\t'yandex360'`)\n\t\tew.writeln(`Since:\t'v4.14.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"YANDEX360_OAUTH_TOKEN\":\tThe OAuth Token`)\n\t\tew.writeln(`\t- \"YANDEX360_ORG_ID\":\tThe organization ID`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"YANDEX360_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"YANDEX360_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"YANDEX360_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"YANDEX360_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 21600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/yandex360`)\n\n\tcase \"yandexcloud\":\n\t\t// generated from: providers/dns/yandexcloud/yandexcloud.toml\n\t\tew.writeln(`Configuration for Yandex Cloud.`)\n\t\tew.writeln(`Code:\t'yandexcloud'`)\n\t\tew.writeln(`Since:\t'v4.9.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"YANDEX_CLOUD_FOLDER_ID\":\tThe string id of folder (aka project) in Yandex Cloud`)\n\t\tew.writeln(`\t- \"YANDEX_CLOUD_IAM_TOKEN\":\tThe base64 encoded json which contains information about iam token of service account with 'dns.admin' permissions`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"YANDEX_CLOUD_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"YANDEX_CLOUD_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"YANDEX_CLOUD_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/yandexcloud`)\n\n\tcase \"zoneedit\":\n\t\t// generated from: providers/dns/zoneedit/zoneedit.toml\n\t\tew.writeln(`Configuration for ZoneEdit.`)\n\t\tew.writeln(`Code:\t'zoneedit'`)\n\t\tew.writeln(`Since:\t'v4.25.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ZONEEDIT_AUTH_TOKEN\":\tAuthentication token`)\n\t\tew.writeln(`\t- \"ZONEEDIT_USER\":\tUser ID`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ZONEEDIT_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"ZONEEDIT_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"ZONEEDIT_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/zoneedit`)\n\n\tcase \"zoneee\":\n\t\t// generated from: providers/dns/zoneee/zoneee.toml\n\t\tew.writeln(`Configuration for Zone.ee.`)\n\t\tew.writeln(`Code:\t'zoneee'`)\n\t\tew.writeln(`Since:\t'v2.1.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ZONEEE_API_KEY\":\tAPI key`)\n\t\tew.writeln(`\t- \"ZONEEE_API_USER\":\tAPI user`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ZONEEE_ENDPOINT\":\tAPI endpoint URL`)\n\t\tew.writeln(`\t- \"ZONEEE_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"ZONEEE_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 5)`)\n\t\tew.writeln(`\t- \"ZONEEE_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 300)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/zoneee`)\n\n\tcase \"zonomi\":\n\t\t// generated from: providers/dns/zonomi/zonomi.toml\n\t\tew.writeln(`Configuration for Zonomi.`)\n\t\tew.writeln(`Code:\t'zonomi'`)\n\t\tew.writeln(`Since:\t'v3.5.0'`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Credentials:`)\n\t\tew.writeln(`\t- \"ZONOMI_API_KEY\":\tUser API key`)\n\t\tew.writeln()\n\n\t\tew.writeln(`Additional Configuration:`)\n\t\tew.writeln(`\t- \"ZONOMI_HTTP_TIMEOUT\":\tAPI request timeout in seconds (Default: 30)`)\n\t\tew.writeln(`\t- \"ZONOMI_POLLING_INTERVAL\":\tTime between DNS propagation check in seconds (Default: 2)`)\n\t\tew.writeln(`\t- \"ZONOMI_PROPAGATION_TIMEOUT\":\tMaximum waiting time for DNS propagation in seconds (Default: 60)`)\n\t\tew.writeln(`\t- \"ZONOMI_TTL\":\tThe TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)`)\n\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/zonomi`)\n\n\tdefault:\n\t\treturn fmt.Errorf(\"%q is not yet supported\", name)\n\t}\n\n\tif flusher, ok := w.(interface{ Flush() error }); ok {\n\t\treturn flusher.Flush()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "themes/\npublic/\n.hugo_build.lock\n"
  },
  {
    "path": "docs/Makefile",
    "content": ".PHONY: default clean serve build\n\ndefault: clean serve\n\nclean:\n\trm -rf public/\n\n\nbuild: clean\n\thugo --enableGitInfo --source .\n\nserve:\n\thugo server --disableFastRender --enableGitInfo --watch --source .\n\t# hugo server -D\n"
  },
  {
    "path": "docs/archetypes/default.md",
    "content": "---\ntitle: \"{{ replace .Name \"-\" \" \" | title }}\"\ndate: {{ .Date }}\ndraft: true\n---\n\n"
  },
  {
    "path": "docs/content/_index.md",
    "content": "---\ntitle: \"Lego\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nchapter: false\n---\n\nLet's Encrypt client and ACME library written in Go.\n\n{{% notice important %}}\nlego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️\n\nThis project is not owned by a company. I'm not an employee of a company.\n\nI don't have gifted domains/accounts from DNS companies.\n\nI've been maintaining it for about 10 years.\n{{% /notice %}}\n\n## Features\n\n- ACME v2 [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html)\n  - Support [RFC 8737](https://www.rfc-editor.org/rfc/rfc8737.html): TLS Application‑Layer Protocol Negotiation (ALPN) Challenge Extension\n  - Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): issues certificates for IP addresses\n  - Support [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html): Renewal Information (ARI) Extension\n  - Support [draft-ietf-acme-profiles-00](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/): Profiles Extension\n- Comes with about [180 DNS providers]({{% ref \"dns\" %}})\n- Register with CA\n- Obtain certificates, both from scratch or with an existing CSR\n- Renew certificates\n- Revoke certificates\n- Robust implementation of ACME challenges:\n  - HTTP (http-01)\n  - DNS (dns-01)\n  - TLS (tls-alpn-01)\n- SAN certificate support\n- [CNAME support](https://letsencrypt.org/2019/10/09/onboarding-your-customers-with-lets-encrypt-and-acme.html) by default\n- [Custom challenge solvers]({{% ref \"usage/library/Writing-a-Challenge-Solver\" %}})\n- Certificate bundling\n- OCSP helper function\n"
  },
  {
    "path": "docs/content/dns/_index.md",
    "content": "---\ntitle: \"DNS Providers\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nweight: 3\n---\n\n{{% notice important %}}\nlego is an independent, free, and open-source project, if you value it, consider [supporting it](https://donate.ldez.dev)! ❤️\n\nThis project is not owned by a company. I'm not an employee of a company.\n\nI don't have gifted domains/accounts from DNS companies.\n\nI've been maintaining it for about 10 years.\n{{% /notice %}}\n\n## Configuration and Credentials\n\nCredentials and DNS configuration for DNS providers must be passed through environment variables.\n\n### Environment Variables: Value\n\nThe environment variables can reference a value.\n\nHere is an example bash command using the Cloudflare DNS provider:\n\n```bash\n$ CLOUDFLARE_EMAIL=you@example.com \\\n  CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \\\n  lego --dns cloudflare --domains www.example.com --email you@example.com run\n```\n\n### Environment Variables: File\n\nThe environment variables can reference a path to file.\n\nIn this case the name of environment variable must be suffixed by `_FILE`.\n\n{{% notice note %}}\nThe file must contain only the value.\n{{% /notice %}}\n\nHere is an example bash command using the CloudFlare DNS provider:\n\n```bash\n$ cat /the/path/to/my/key\nb9841238feb177a84330febba8a83208921177bffe733\n\n$ cat /the/path/to/my/email\nyou@example.com\n\n$ CLOUDFLARE_EMAIL_FILE=/the/path/to/my/email \\\n  CLOUDFLARE_API_KEY_FILE=/the/path/to/my/key \\\n  lego --dns cloudflare --domains www.example.com --email you@example.com run\n```\n\n## DNS Providers\n\n{{% tableofdnsproviders %}}\n"
  },
  {
    "path": "docs/content/dns/zz_gen_acme-dns.md",
    "content": "---\ntitle: \"Joohoi's ACME-DNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: acme-dns\ndnsprovider:\n  since:    \"v1.1.0\"\n  code:     \"acme-dns\"\n  url:      \"https://github.com/joohoi/acme-dns\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/acmedns/acmedns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Joohoi's ACME-DNS](https://github.com/joohoi/acme-dns).\n\n\n<!--more-->\n\n- Code: `acme-dns`\n- Since: v1.1.0\n\n\nHere is an example bash command using the Joohoi's ACME-DNS provider:\n\n```bash\nACME_DNS_API_BASE=http://10.0.0.8:4443 \\\nACME_DNS_STORAGE_PATH=/root/.lego-acme-dns-accounts.json \\\nlego --dns \"acme-dns\" -d '*.example.com' -d example.com run\n\n# or\n\nACME_DNS_API_BASE=http://10.0.0.8:4443 \\\nACME_DNS_STORAGE_BASE_URL=http://10.10.10.10:80 \\\nlego --dns \"acme-dns\" -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ACME_DNS_API_BASE` | The ACME-DNS API address |\n| `ACME_DNS_STORAGE_BASE_URL` | The ACME-DNS JSON account data server. |\n| `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. |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ACME_DNS_ALLOWLIST` | Source networks using CIDR notation (multiple values should be separated with a comma). |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://github.com/joohoi/acme-dns#api)\n- [Go client](https://github.com/nrdcg/goacmedns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/acmedns/acmedns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_active24.md",
    "content": "---\ntitle: \"Active24\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: active24\ndnsprovider:\n  since:    \"v4.23.0\"\n  code:     \"active24\"\n  url:      \"https://www.active24.cz\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/active24/active24.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Active24](https://www.active24.cz).\n\n\n<!--more-->\n\n- Code: `active24`\n- Since: v4.23.0\n\n\nHere is an example bash command using the Active24 provider:\n\n```bash\nACTIVE24_API_KEY=\"xxx\" \\\nACTIVE24_SECRET=\"yyy\" \\\nlego --dns active24 -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ACTIVE24_API_KEY` | API key |\n| `ACTIVE24_SECRET` | Secret |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ACTIVE24_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `ACTIVE24_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `ACTIVE24_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `ACTIVE24_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://rest.active24.cz/v2/docs)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/active24/active24.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_alidns.md",
    "content": "---\ntitle: \"Alibaba Cloud DNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: alidns\ndnsprovider:\n  since:    \"v1.1.0\"\n  code:     \"alidns\"\n  url:      \"https://www.alibabacloud.com/product/dns\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/alidns/alidns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Alibaba Cloud DNS](https://www.alibabacloud.com/product/dns).\n\n\n<!--more-->\n\n- Code: `alidns`\n- Since: v1.1.0\n\n\nHere is an example bash command using the Alibaba Cloud DNS provider:\n\n```bash\n# Setup using instance RAM role\nALICLOUD_RAM_ROLE=lego \\\nlego --dns alidns -d '*.example.com' -d example.com run\n\n# Or, using credentials\nALICLOUD_ACCESS_KEY=abcdefghijklmnopqrstuvwx \\\nALICLOUD_SECRET_KEY=your-secret-key \\\nALICLOUD_SECURITY_TOKEN=your-sts-token \\\nlego --dns alidns - -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ALICLOUD_ACCESS_KEY` | Access key ID |\n| `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) |\n| `ALICLOUD_SECRET_KEY` | Access Key secret |\n| `ALICLOUD_SECURITY_TOKEN` | STS Security Token (optional) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ALICLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `ALICLOUD_LINE` | Line (Default: default) |\n| `ALICLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `ALICLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `ALICLOUD_REGION_ID` | Region ID (Default: cn-hangzhou) |\n| `ALICLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.alibabacloud.com/help/en/alibaba-cloud-dns/latest/api-alidns-2015-01-09-dir-parsing-records)\n- [Go client](https://github.com/alibabacloud-go/alidns-20150109)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/alidns/alidns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_aliesa.md",
    "content": "---\ntitle: \"AlibabaCloud ESA\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: aliesa\ndnsprovider:\n  since:    \"v4.29.0\"\n  code:     \"aliesa\"\n  url:      \"https://www.alibabacloud.com/en/product/esa\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/aliesa/aliesa.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [AlibabaCloud ESA](https://www.alibabacloud.com/en/product/esa).\n\n\n<!--more-->\n\n- Code: `aliesa`\n- Since: v4.29.0\n\n\nHere is an example bash command using the AlibabaCloud ESA provider:\n\n```bash\n# Setup using instance RAM role\nALIESA_RAM_ROLE=lego \\\nlego --dns aliesa -d '*.example.com' -d example.com run\n\n# Or, using credentials\nALIESA_ACCESS_KEY=abcdefghijklmnopqrstuvwx \\\nALIESA_SECRET_KEY=your-secret-key \\\nALIESA_SECURITY_TOKEN=your-sts-token \\\nlego --dns aliesa - -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ALIESA_ACCESS_KEY` | Access key ID |\n| `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) |\n| `ALIESA_SECRET_KEY` | Access Key secret |\n| `ALIESA_SECURITY_TOKEN` | STS Security Token (optional) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ALIESA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `ALIESA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `ALIESA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `ALIESA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [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)\n- [Go client](https://github.com/alibabacloud-go/esa-20240910)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/aliesa/aliesa.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_allinkl.md",
    "content": "---\ntitle: \"all-inkl\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: allinkl\ndnsprovider:\n  since:    \"v4.5.0\"\n  code:     \"allinkl\"\n  url:      \"https://all-inkl.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/allinkl/allinkl.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [all-inkl](https://all-inkl.com).\n\n\n<!--more-->\n\n- Code: `allinkl`\n- Since: v4.5.0\n\n\nHere is an example bash command using the all-inkl provider:\n\n```bash\nALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \\\nALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \\\nlego --dns allinkl -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ALL_INKL_LOGIN` | KAS login |\n| `ALL_INKL_PASSWORD` | KAS password |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ALL_INKL_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `ALL_INKL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `ALL_INKL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://kasapi.kasserver.com/dokumentation/phpdoc/index.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/allinkl/allinkl.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_alwaysdata.md",
    "content": "---\ntitle: \"Alwaysdata\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: alwaysdata\ndnsprovider:\n  since:    \"v4.31.0\"\n  code:     \"alwaysdata\"\n  url:      \"https://alwaysdata.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/alwaysdata/alwaysdata.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Alwaysdata](https://alwaysdata.com/).\n\n\n<!--more-->\n\n- Code: `alwaysdata`\n- Since: v4.31.0\n\n\nHere is an example bash command using the Alwaysdata provider:\n\n```bash\nALWAYSDATA_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns alwaysdata -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ALWAYSDATA_API_KEY` | API Key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ALWAYSDATA_ACCOUNT` | Account name |\n| `ALWAYSDATA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `ALWAYSDATA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `ALWAYSDATA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `ALWAYSDATA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://help.alwaysdata.com/en/api/resources/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/alwaysdata/alwaysdata.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_anexia.md",
    "content": "---\ntitle: \"Anexia CloudDNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: anexia\ndnsprovider:\n  since:    \"v4.28.0\"\n  code:     \"anexia\"\n  url:      \"https://www.anexia-it.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/anexia/anexia.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Anexia CloudDNS](https://www.anexia-it.com/).\n\n\n<!--more-->\n\n- Code: `anexia`\n- Since: v4.28.0\n\n\nHere is an example bash command using the Anexia CloudDNS provider:\n\n```bash\nANEXIA_TOKEN=xxx \\\nlego --dns anexia -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ANEXIA_TOKEN` | API token for Anexia Engine |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ANEXIA_API_URL` | API endpoint URL (default: https://engine.anexia-it.com) |\n| `ANEXIA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `ANEXIA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `ANEXIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |\n| `ANEXIA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Description\n\nYou need to create an API token in the [Anexia Engine](https://engine.anexia-it.com/).\n\nThe token must have permissions to manage DNS zones and records.\n\n\n\n## More information\n\n- [API documentation](https://engine.anexia-it.com/docs/en/module/clouddns/api)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/anexia/anexia.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_artfiles.md",
    "content": "---\ntitle: \"ArtFiles\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: artfiles\ndnsprovider:\n  since:    \"v4.32.0\"\n  code:     \"artfiles\"\n  url:      \"https://www.artfiles.de/extras/domains/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/artfiles/artfiles.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [ArtFiles](https://www.artfiles.de/extras/domains/).\n\n\n<!--more-->\n\n- Code: `artfiles`\n- Since: v4.32.0\n\n\nHere is an example bash command using the ArtFiles provider:\n\n```bash\nARTFILES_USERNAME=\"xxx\" \\\nARTFILES_PASSWORD=\"yyy\" \\\nlego --dns artfiles -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ARTFILES_PASSWORD` | API password |\n| `ARTFILES_USERNAME` | API username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ARTFILES_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `ARTFILES_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `ARTFILES_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 360) |\n| `ARTFILES_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://support.artfiles.de/DCP-API#dns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/artfiles/artfiles.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_arvancloud.md",
    "content": "---\ntitle: \"ArvanCloud\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: arvancloud\ndnsprovider:\n  since:    \"v3.8.0\"\n  code:     \"arvancloud\"\n  url:      \"https://arvancloud.ir\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/arvancloud/arvancloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [ArvanCloud](https://arvancloud.ir).\n\n\n<!--more-->\n\n- Code: `arvancloud`\n- Since: v3.8.0\n\n\nHere is an example bash command using the ArvanCloud provider:\n\n```bash\nARVANCLOUD_API_KEY=\"Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\" \\\nlego --dns arvancloud -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ARVANCLOUD_API_KEY` | API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ARVANCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `ARVANCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `ARVANCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `ARVANCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.arvancloud.ir/docs/api/cdn/4.0)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/arvancloud/arvancloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_auroradns.md",
    "content": "---\ntitle: \"Aurora DNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: auroradns\ndnsprovider:\n  since:    \"v0.4.0\"\n  code:     \"auroradns\"\n  url:      \"https://www.pcextreme.com/dns-health-checks\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/auroradns/auroradns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Aurora DNS](https://www.pcextreme.com/dns-health-checks).\n\n\n<!--more-->\n\n- Code: `auroradns`\n- Since: v0.4.0\n\n\nHere is an example bash command using the Aurora DNS provider:\n\n```bash\nAURORA_API_KEY=xxxxx \\\nAURORA_SECRET=yyyyyy \\\nlego --dns auroradns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `AURORA_API_KEY` | API key or username to used |\n| `AURORA_SECRET` | Secret password to be used |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `AURORA_ENDPOINT` | API endpoint URL |\n| `AURORA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `AURORA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `AURORA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://libcloud.readthedocs.io/en/latest/dns/drivers/auroradns.html#api-docs)\n- [Go client](https://github.com/nrdcg/auroradns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/auroradns/auroradns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_autodns.md",
    "content": "---\ntitle: \"Autodns\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: autodns\ndnsprovider:\n  since:    \"v3.2.0\"\n  code:     \"autodns\"\n  url:      \"https://www.internetx.com/domains/autodns/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/autodns/autodns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Autodns](https://www.internetx.com/domains/autodns/).\n\n\n<!--more-->\n\n- Code: `autodns`\n- Since: v3.2.0\n\n\nHere is an example bash command using the Autodns provider:\n\n```bash\nAUTODNS_API_USER=username \\\nAUTODNS_API_PASSWORD=supersecretpassword \\\nlego --dns autodns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `AUTODNS_API_PASSWORD` | User Password |\n| `AUTODNS_API_USER` | Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `AUTODNS_CONTEXT` | API context (4 for production, 1 for testing. Defaults to 4) |\n| `AUTODNS_ENDPOINT` | API endpoint URL, defaults to https://api.autodns.com/v1/ |\n| `AUTODNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `AUTODNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `AUTODNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `AUTODNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://help.internetx.com/display/APIJSONEN)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/autodns/autodns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_axelname.md",
    "content": "---\ntitle: \"Axelname\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: axelname\ndnsprovider:\n  since:    \"v4.23.0\"\n  code:     \"axelname\"\n  url:      \"https://axelname.ru\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/axelname/axelname.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Axelname](https://axelname.ru).\n\n\n<!--more-->\n\n- Code: `axelname`\n- Since: v4.23.0\n\n\nHere is an example bash command using the Axelname provider:\n\n```bash\nAXELNAME_NICKNAME=\"yyy\" \\\nAXELNAME_TOKEN=\"xxx\" \\\nlego --dns axelname -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `AXELNAME_NICKNAME` | Account nickname |\n| `AXELNAME_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `AXELNAME_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `AXELNAME_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `AXELNAME_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `AXELNAME_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://axelname.ru/static/content/files/axelname_api_rest_lite.pdf)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/axelname/axelname.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_azion.md",
    "content": "---\ntitle: \"Azion\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: azion\ndnsprovider:\n  since:    \"v4.24.0\"\n  code:     \"azion\"\n  url:      \"https://www.azion.com/en/products/edge-dns/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/azion/azion.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Azion](https://www.azion.com/en/products/edge-dns/).\n\n\n<!--more-->\n\n- Code: `azion`\n- Since: v4.24.0\n\n\nHere is an example bash command using the Azion provider:\n\n```bash\nAZION_PERSONAL_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns azion -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `AZION_PERSONAL_TOKEN` | Your Azion personal token. |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `AZION_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `AZION_PAGE_SIZE` | The page size for the API request (Default: 50) |\n| `AZION_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `AZION_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `AZION_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.azion.com/)\n- [Go client](https://github.com/aziontech/azionapi-go-sdk)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/azion/azion.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_azure.md",
    "content": "---\ntitle: \"Azure (deprecated)\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: azure\ndnsprovider:\n  since:    \"v0.4.0\"\n  code:     \"azure\"\n  url:      \"https://azure.microsoft.com/services/dns/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/azure/azure.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Azure (deprecated)](https://azure.microsoft.com/services/dns/).\n\n\n<!--more-->\n\n- Code: `azure`\n- Since: v0.4.0\n\n\n{{% notice note %}}\n_Please contribute by adding a CLI example._\n{{% /notice %}}\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `AZURE_CLIENT_ID` | Client ID |\n| `AZURE_CLIENT_SECRET` | Client secret |\n| `AZURE_ENVIRONMENT` | Azure environment, one of: public, usgovernment, german, and china |\n| `AZURE_RESOURCE_GROUP` | Resource group |\n| `AZURE_SUBSCRIPTION_ID` | Subscription ID |\n| `AZURE_TENANT_ID` | Tenant ID |\n| `instance metadata service` | If the credentials are **not** set via the environment, then it will attempt to get a bearer token via the [instance metadata service](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service). |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `AZURE_METADATA_ENDPOINT` | Metadata Service endpoint URL |\n| `AZURE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `AZURE_PRIVATE_ZONE` | Set to true to use Azure Private DNS Zones and not public |\n| `AZURE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `AZURE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n| `AZURE_ZONE_NAME` | Zone name to use inside Azure DNS service to add the TXT record in |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://docs.microsoft.com/en-us/go/azure/)\n- [Go client](https://github.com/Azure/azure-sdk-for-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/azure/azure.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_azuredns.md",
    "content": "---\ntitle: \"Azure DNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: azuredns\ndnsprovider:\n  since:    \"v4.13.0\"\n  code:     \"azuredns\"\n  url:      \"https://azure.microsoft.com/services/dns/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/azuredns/azuredns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Azure DNS](https://azure.microsoft.com/services/dns/).\n\n\n<!--more-->\n\n- Code: `azuredns`\n- Since: v4.13.0\n\n\nHere is an example bash command using the Azure DNS provider:\n\n```bash\n### Using client secret\n\nAZURE_CLIENT_ID=<your service principal client ID> \\\nAZURE_TENANT_ID=<your service principal tenant ID> \\\nAZURE_CLIENT_SECRET=<your service principal client secret> \\\nlego --dns azuredns -d '*.example.com' -d example.com run\n\n### Using client certificate\n\nAZURE_CLIENT_ID=<your service principal client ID> \\\nAZURE_TENANT_ID=<your service principal tenant ID> \\\nAZURE_CLIENT_CERTIFICATE_PATH=<your service principal certificate path> \\\nlego --dns azuredns -d '*.example.com' -d example.com run\n\n### Using Azure CLI\n\naz login \\\nlego --dns azuredns -d '*.example.com' -d example.com run\n\n### Using Managed Identity (Azure VM)\n\nAZURE_TENANT_ID=<your service principal tenant ID> \\\nAZURE_RESOURCE_GROUP=<your target zone resource group name> \\\nlego --dns azuredns -d '*.example.com' -d example.com run\n\n### Using Managed Identity (Azure Arc)\n\nAZURE_TENANT_ID=<your service principal tenant ID> \\\nIMDS_ENDPOINT=http://localhost:40342 \\\nIDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token \\\nlego --dns azuredns -d '*.example.com' -d example.com run\n\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `AZURE_CLIENT_CERTIFICATE_PATH` | Client certificate path |\n| `AZURE_CLIENT_ID` | Client ID |\n| `AZURE_CLIENT_SECRET` | Client secret |\n| `AZURE_TENANT_ID` | Tenant ID |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `AZURE_AUTH_METHOD` | Specify which authentication method to use |\n| `AZURE_AUTH_MSI_TIMEOUT` | Managed Identity timeout duration |\n| `AZURE_ENVIRONMENT` | Azure environment, one of: public, usgovernment, and china |\n| `AZURE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `AZURE_PRIVATE_ZONE` | Set to true to use Azure Private DNS Zones and not public |\n| `AZURE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `AZURE_RESOURCE_GROUP` | DNS zone resource group |\n| `AZURE_SERVICEDISCOVERY_FILTER` | Advanced ServiceDiscovery filter using Kusto query condition |\n| `AZURE_SUBSCRIPTION_ID` | DNS zone subscription ID |\n| `AZURE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n| `AZURE_ZONE_NAME` | Zone name to use inside Azure DNS service to add the TXT record in |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Description\n\nSeveral authentication methods can be used to authenticate against Azure DNS API.\n\n### Default Azure Credentials (default option)\n\nDefault Azure Credentials automatically detects in the following locations and prioritized in the following order:\n\n1. Environment variables for client secret: `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_CLIENT_SECRET`\n2. Environment variables for client certificate: `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_CLIENT_CERTIFICATE_PATH`\n3. Workload identity for resources hosted in Azure environment (see below)\n4. Shared credentials (defaults to `~/.azure` folder), used by Azure CLI\n\nLink:\n- [Azure Authentication](https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication)\n\n### Environment variables\n\n#### Service Discovery\n\nLego automatically finds all visible Azure (private) DNS zones using [Azure ResourceGraph query](https://learn.microsoft.com/en-us/azure/governance/resource-graph/).\nThis can be limited by specifying environment variable `AZURE_SUBSCRIPTION_ID` and/or `AZURE_RESOURCE_GROUP` which limits the\nDNS zones to only a subscription or to one resourceGroup.\n\nAdditionally environment variable `AZURE_SERVICEDISCOVERY_FILTER` can be used to filter DNS zones with an addition Kusto filter eg:\n\n```\nresources\n| where type =~ \"microsoft.network/dnszones\"\n| ${AZURE_SERVICEDISCOVERY_FILTER}\n| project subscriptionId, resourceGroup, name\n```\n\n\n#### Client secret\n\nThe Azure Credentials can be configured using the following environment variables:\n* AZURE_CLIENT_ID = \"Client ID\"\n* AZURE_CLIENT_SECRET = \"Client secret\"\n* AZURE_TENANT_ID = \"Tenant ID\"\n\nThis authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `env`.\n\n#### Client certificate\n\nThe Azure Credentials can be configured using the following environment variables:\n* AZURE_CLIENT_ID = \"Client ID\"\n* AZURE_CLIENT_CERTIFICATE_PATH = \"Client certificate path\"\n* AZURE_TENANT_ID = \"Tenant ID\"\n\nThis authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `env`.\n\n### Workload identity\n\nWorkload identity allows workloads running Azure Kubernetes Services (AKS) clusters to authenticate as an Azure AD application identity using federated credentials.\n\nThis must be configured in kubernetes workload deployment in one hand and on the Azure AD application registration in the other hand.\n\nHere is a summary of the steps to follow to use it :\n* create a `ServiceAccount` resource, add following annotations to reference the targeted Azure AD application registration : `azure.workload.identity/client-id` and `azure.workload.identity/tenant-id`.\n* on the `Deployment` resource you must reference the previous `ServiceAccount` and add the following label : `azure.workload.identity/use: \"true\"`.\n* create a federated credentials of type `Kubernetes accessing Azure resources`, add the cluster issuer URL  and add the namespace and name of your kubernetes service account.\n\nLink :\n- [Azure AD Workload identity](https://azure.github.io/azure-workload-identity/docs/topics/service-account-labels-and-annotations.html)\n\nThis authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `wli`.\n\n### Azure Managed Identity\n\n#### Azure Managed Identity (with Azure workload)\n\nThe Azure Managed Identity service allows linking Azure AD identities to Azure resources, without needing to manually manage client IDs and secrets.\n\nWorkloads with a Managed Identity can manage their own certificates, with permissions on specific domain names set using IAM assignments.\nFor this to work, the Managed Identity requires the **Reader** role on the target DNS Zone,\nand the **DNS Zone Contributor** on the relevant `_acme-challenge` TXT records.\n\nFor example, to allow a Managed Identity to create a certificate for \"fw01.lab.example.com\", using Azure CLI:\n\n```bash\nexport AZURE_SUBSCRIPTION_ID=\"00000000-0000-0000-0000-000000000000\"\nexport AZURE_RESOURCE_GROUP=\"rg1\"\nexport SERVICE_PRINCIPAL_ID=\"00000000-0000-0000-0000-000000000000\"\n\nexport AZURE_DNS_ZONE=\"lab.example.com\"\nexport AZ_HOSTNAME=\"fw01\"\nexport AZ_RECORD_SET=\"_acme-challenge.${AZ_HOSTNAME}\"\n\naz role assignment create \\\n--assignee \"${SERVICE_PRINCIPAL_ID}\" \\\n--role \"Reader\" \\\n--scope \"/subscriptions/${AZURE_SUBSCRIPTION_ID}/resourceGroups/${AZURE_RESOURCE_GROUP}/providers/Microsoft.Network/dnszones/${AZURE_DNS_ZONE}\"\n\naz role assignment create \\\n--assignee \"${SERVICE_PRINCIPAL_ID}\" \\\n--role \"DNS Zone Contributor\" \\\n--scope \"/subscriptions/${AZURE_SUBSCRIPTION_ID}/resourceGroups/${AZURE_RESOURCE_GROUP}/providers/Microsoft.Network/dnszones/${AZURE_DNS_ZONE}/TXT/${AZ_RECORD_SET}\"\n```\n\nA timeout wrapper is configured for this authentication method.\nThe duration can be configured by setting the `AZURE_AUTH_MSI_TIMEOUT`.\nThe default timeout is 2 seconds.\nThis authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `msi`.\n\n#### Azure Managed Identity (with Azure Arc)\n\nThe Azure Arc agent provides the ability to use a Managed Identity on resources hosted outside of Azure\n(such as on-prem virtual machines, or VMs in another cloud provider).\n\nWhile the upstream `azidentity` SDK will try to automatically identify and use the Azure Arc metadata service,\nif you get `azuredns: DefaultAzureCredential: failed to acquire a token.` error messages,\nyou may need to set the environment variables:\n* `IMDS_ENDPOINT=http://localhost:40342`\n* `IDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token`\n\nA timeout wrapper is configured for this authentication method.\nThe duration can be configured by setting the `AZURE_AUTH_MSI_TIMEOUT`.\nThe default timeout is 2 seconds.\nThis authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `msi`.\n\n### Azure CLI\n\nThe Azure CLI is a command-line tool provided by Microsoft to interact with Azure resources.\nIt provides an easy way to authenticate by simply running `az login` command.\nThe generated token will be cached by default in the `~/.azure` folder.\n\nThis authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `cli`.\n\n### Open ID Connect\n\nOpen ID Connect is a mechanism that establish a trust relationship between a running environment and the Azure AD identity provider.\nIt can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `oidc`.\n\n### Azure DevOps Pipelines\n\nIt can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `pipeline`.\n\n\n\n\n## More information\n\n- [API documentation](https://docs.microsoft.com/en-us/go/azure/)\n- [Go client](https://github.com/Azure/azure-sdk-for-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/azuredns/azuredns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_baiducloud.md",
    "content": "---\ntitle: \"Baidu Cloud\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: baiducloud\ndnsprovider:\n  since:    \"v4.23.0\"\n  code:     \"baiducloud\"\n  url:      \"https://cloud.baidu.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/baiducloud/baiducloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Baidu Cloud](https://cloud.baidu.com).\n\n\n<!--more-->\n\n- Code: `baiducloud`\n- Since: v4.23.0\n\n\nHere is an example bash command using the Baidu Cloud provider:\n\n```bash\nBAIDUCLOUD_ACCESS_KEY_ID=\"xxx\" \\\nBAIDUCLOUD_SECRET_ACCESS_KEY=\"yyy\" \\\nlego --dns baiducloud -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `BAIDUCLOUD_ACCESS_KEY_ID` | Access key |\n| `BAIDUCLOUD_SECRET_ACCESS_KEY` | Secret access key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `BAIDUCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `BAIDUCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `BAIDUCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://cloud.baidu.com/doc/DNS/s/El4s7lssr)\n- [Go client](https://github.com/baidubce/bce-sdk-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/baiducloud/baiducloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_beget.md",
    "content": "---\ntitle: \"Beget.com\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: beget\ndnsprovider:\n  since:    \"v4.27.0\"\n  code:     \"beget\"\n  url:      \"https://beget.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/beget/beget.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Beget.com](https://beget.com/).\n\n\n<!--more-->\n\n- Code: `beget`\n- Since: v4.27.0\n\n\nHere is an example bash command using the Beget.com provider:\n\n```bash\nBEGET_USERNAME=xxxxxx \\\nBEGET_PASSWORD=yyyyyy \\\nlego --dns beget -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `BEGET_PASSWORD` | API password |\n| `BEGET_USERNAME` | API username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `BEGET_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `BEGET_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 30) |\n| `BEGET_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |\n| `BEGET_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://beget.com/ru/kb/api/funkczii-upravleniya-dns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/beget/beget.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_binarylane.md",
    "content": "---\ntitle: \"Binary Lane\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: binarylane\ndnsprovider:\n  since:    \"v4.26.0\"\n  code:     \"binarylane\"\n  url:      \"https://www.binarylane.com.au/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/binarylane/binarylane.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Binary Lane](https://www.binarylane.com.au/).\n\n\n<!--more-->\n\n- Code: `binarylane`\n- Since: v4.26.0\n\n\nHere is an example bash command using the Binary Lane provider:\n\n```bash\nBINARYLANE_API_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns binarylane -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `BINARYLANE_API_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `BINARYLANE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `BINARYLANE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `BINARYLANE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `BINARYLANE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.binarylane.com.au/reference/#tag/Domains)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/binarylane/binarylane.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_bindman.md",
    "content": "---\ntitle: \"Bindman\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: bindman\ndnsprovider:\n  since:    \"v2.6.0\"\n  code:     \"bindman\"\n  url:      \"https://github.com/labbsr0x/bindman-dns-webhook\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/bindman/bindman.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Bindman](https://github.com/labbsr0x/bindman-dns-webhook).\n\n\n<!--more-->\n\n- Code: `bindman`\n- Since: v2.6.0\n\n\nHere is an example bash command using the Bindman provider:\n\n```bash\nBINDMAN_MANAGER_ADDRESS=<your bindman manager address> \\\nlego --dns bindman -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `BINDMAN_MANAGER_ADDRESS` | The server URL, should have scheme, hostname, and port (if required) of the Bindman-DNS Manager server |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `BINDMAN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) |\n| `BINDMAN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `BINDMAN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://gitlab.isc.org/isc-projects/bind9)\n- [Go client](https://github.com/labbsr0x/bindman-dns-webhook)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/bindman/bindman.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_bluecat.md",
    "content": "---\ntitle: \"Bluecat\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: bluecat\ndnsprovider:\n  since:    \"v0.5.0\"\n  code:     \"bluecat\"\n  url:      \"https://www.bluecatnetworks.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/bluecat/bluecat.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Bluecat](https://www.bluecatnetworks.com).\n\n\n<!--more-->\n\n- Code: `bluecat`\n- Since: v0.5.0\n\n\nHere is an example bash command using the Bluecat provider:\n\n```bash\nBLUECAT_PASSWORD=mypassword \\\nBLUECAT_DNS_VIEW=myview \\\nBLUECAT_USER_NAME=myusername \\\nBLUECAT_CONFIG_NAME=myconfig \\\nBLUECAT_SERVER_URL=https://bam.example.com \\\nBLUECAT_TTL=30 \\\nlego --dns bluecat -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `BLUECAT_CONFIG_NAME` | Configuration name |\n| `BLUECAT_DNS_VIEW` | External DNS View Name |\n| `BLUECAT_PASSWORD` | API password |\n| `BLUECAT_SERVER_URL` | The server URL, should have scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve |\n| `BLUECAT_USER_NAME` | API username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `BLUECAT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `BLUECAT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `BLUECAT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `BLUECAT_SKIP_DEPLOY` | Skip deployements |\n| `BLUECAT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/REST-API/9.1.0)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/bluecat/bluecat.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_bluecatv2.md",
    "content": "---\ntitle: \"Bluecat v2\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: bluecatv2\ndnsprovider:\n  since:    \"v4.32.0\"\n  code:     \"bluecatv2\"\n  url:      \"https://www.bluecatnetworks.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/bluecatv2/bluecatv2.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Bluecat v2](https://www.bluecatnetworks.com).\n\n\n<!--more-->\n\n- Code: `bluecatv2`\n- Since: v4.32.0\n\n\nHere is an example bash command using the Bluecat v2 provider:\n\n```bash\nBLUECATV2_SERVER_URL=\"https://example.com\" \\\nBLUECATV2_USERNAME=\"xxx\" \\\nBLUECATV2_PASSWORD=\"yyy\" \\\nBLUECATV2_CONFIG_NAME=\"myConfiguration\" \\\nBLUECATV2_VIEW_NAME=\"myView\" \\\nlego --dns bluecatv2 -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `BLUECATV2_CONFIG_NAME` | Configuration name |\n| `BLUECATV2_PASSWORD` | API password |\n| `BLUECATV2_USERNAME` | API username |\n| `BLUECATV2_VIEW_NAME` | DNS View Name |\n| `BLUECAT_SERVER_URL` | The server URL: it should have a scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `BLUECATV2_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `BLUECATV2_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `BLUECATV2_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `BLUECATV2_SKIP_DEPLOY` | Skip quick deployements |\n| `BLUECATV2_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Introduction/9.6.0)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/bluecatv2/bluecatv2.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_bookmyname.md",
    "content": "---\ntitle: \"BookMyName\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: bookmyname\ndnsprovider:\n  since:    \"v4.23.0\"\n  code:     \"bookmyname\"\n  url:      \"https://www.bookmyname.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/bookmyname/bookmyname.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [BookMyName](https://www.bookmyname.com/).\n\n\n<!--more-->\n\n- Code: `bookmyname`\n- Since: v4.23.0\n\n\nHere is an example bash command using the BookMyName provider:\n\n```bash\nBOOKMYNAME_USERNAME=\"xxx\" \\\nBOOKMYNAME_PASSWORD=\"yyy\" \\\nlego --dns bookmyname -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `BOOKMYNAME_PASSWORD` | Password |\n| `BOOKMYNAME_USERNAME` | Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `BOOKMYNAME_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `BOOKMYNAME_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `BOOKMYNAME_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `BOOKMYNAME_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://fr.faqs.bookmyname.com/frfaqs/dyndns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/bookmyname/bookmyname.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_brandit.md",
    "content": "---\ntitle: \"Brandit (deprecated)\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: brandit\ndnsprovider:\n  since:    \"v4.11.0\"\n  code:     \"brandit\"\n  url:      \"https://www.brandit.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/brandit/brandit.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\nBrandit has been acquired by Abion.\nAbion has a different API.\n\nIf you are a Brandit/Albion user, you can try the PR https://github.com/go-acme/lego/pull/2112.\n\n\n\n<!--more-->\n\n- Code: `brandit`\n- Since: v4.11.0\n\n\nHere is an example bash command using the Brandit (deprecated) provider:\n\n```bash\nBRANDIT_API_KEY=xxxxxxxxxxxxxxxxxxxxx \\\nBRANDIT_API_USERNAME=yyyyyyyyyyyyyyyyyyyy \\\nlego --dns brandit -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `BRANDIT_API_KEY` | The API key |\n| `BRANDIT_API_USERNAME` | The API username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `BRANDIT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `BRANDIT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `BRANDIT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 600) |\n| `BRANDIT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://portal.brandit.com/apidocv3)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/brandit/brandit.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_bunny.md",
    "content": "---\ntitle: \"Bunny\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: bunny\ndnsprovider:\n  since:    \"v4.11.0\"\n  code:     \"bunny\"\n  url:      \"https://bunny.net\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/bunny/bunny.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Bunny](https://bunny.net).\n\n\n<!--more-->\n\n- Code: `bunny`\n- Since: v4.11.0\n\n\nHere is an example bash command using the Bunny provider:\n\n```bash\nBUNNY_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \\\nlego --dns bunny -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `BUNNY_API_KEY` | API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `BUNNY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `BUNNY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `BUNNY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `BUNNY_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://docs.bunny.net/reference/dnszonepublic_index)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/bunny/bunny.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_checkdomain.md",
    "content": "---\ntitle: \"Checkdomain\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: checkdomain\ndnsprovider:\n  since:    \"v3.3.0\"\n  code:     \"checkdomain\"\n  url:      \"https://checkdomain.de/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/checkdomain/checkdomain.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Checkdomain](https://checkdomain.de/).\n\n\n<!--more-->\n\n- Code: `checkdomain`\n- Since: v3.3.0\n\n\nHere is an example bash command using the Checkdomain provider:\n\n```bash\nCHECKDOMAIN_TOKEN=yoursecrettoken \\\nlego --dns checkdomain -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `CHECKDOMAIN_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `CHECKDOMAIN_ENDPOINT` | API endpoint URL, defaults to https://api.checkdomain.de |\n| `CHECKDOMAIN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `CHECKDOMAIN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 300) |\n| `CHECKDOMAIN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 7) |\n| `CHECKDOMAIN_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developer.checkdomain.de/reference/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/checkdomain/checkdomain.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_civo.md",
    "content": "---\ntitle: \"Civo\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: civo\ndnsprovider:\n  since:    \"v4.9.0\"\n  code:     \"civo\"\n  url:      \"https://civo.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/civo/civo.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Civo](https://civo.com).\n\n\n<!--more-->\n\n- Code: `civo`\n- Since: v4.9.0\n\n\nHere is an example bash command using the Civo provider:\n\n```bash\nCIVO_TOKEN=xxxxxx \\\nlego --dns civo -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `CIVO_TOKEN` | Authentication token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `CIVO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 30) |\n| `CIVO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |\n| `CIVO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.civo.com/api/dns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/civo/civo.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_clouddns.md",
    "content": "---\ntitle: \"CloudDNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: clouddns\ndnsprovider:\n  since:    \"v3.6.0\"\n  code:     \"clouddns\"\n  url:      \"https://vshosting.eu/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/clouddns/clouddns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [CloudDNS](https://vshosting.eu/).\n\n\n<!--more-->\n\n- Code: `clouddns`\n- Since: v3.6.0\n\n\nHere is an example bash command using the CloudDNS provider:\n\n```bash\nCLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \\\nCLOUDDNS_EMAIL=you@example.com \\\nCLOUDDNS_PASSWORD=b9841238feb177a84330f \\\nlego --dns clouddns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `CLOUDDNS_CLIENT_ID` | Client ID |\n| `CLOUDDNS_EMAIL` | Account email |\n| `CLOUDDNS_PASSWORD` | Account password |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `CLOUDDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `CLOUDDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) |\n| `CLOUDDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `CLOUDDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://admin.vshosting.cloud/clouddns/swagger/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/clouddns/clouddns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_cloudflare.md",
    "content": "---\ntitle: \"Cloudflare\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: cloudflare\ndnsprovider:\n  since:    \"v0.3.0\"\n  code:     \"cloudflare\"\n  url:      \"https://www.cloudflare.com/dns/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/cloudflare/cloudflare.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Cloudflare](https://www.cloudflare.com/dns/).\n\n\n<!--more-->\n\n- Code: `cloudflare`\n- Since: v0.3.0\n\n\nHere is an example bash command using the Cloudflare provider:\n\n```bash\nCLOUDFLARE_EMAIL=you@example.com \\\nCLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \\\nlego --dns cloudflare -d '*.example.com' -d example.com run\n\n# or\n\nCLOUDFLARE_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \\\nlego --dns cloudflare -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `CF_API_EMAIL` | Account email |\n| `CF_API_KEY` | API key |\n| `CF_DNS_API_TOKEN` | API token with DNS:Edit permission (since v3.1.0) |\n| `CF_ZONE_API_TOKEN` | API token with Zone:Read permission (since v3.1.0) |\n| `CLOUDFLARE_API_KEY` | Alias to CF_API_KEY |\n| `CLOUDFLARE_DNS_API_TOKEN` | Alias to CF_DNS_API_TOKEN |\n| `CLOUDFLARE_EMAIL` | Alias to CF_API_EMAIL |\n| `CLOUDFLARE_ZONE_API_TOKEN` | Alias to CF_ZONE_API_TOKEN |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `CLOUDFLARE_BASE_URL` | API base URL (Default: https://api.cloudflare.com/client/v4) |\n| `CLOUDFLARE_HTTP_TIMEOUT` | API request timeout in seconds (Default: ) |\n| `CLOUDFLARE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `CLOUDFLARE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `CLOUDFLARE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Description\n\nYou may use `CF_API_EMAIL` and `CF_API_KEY` to authenticate, or `CF_DNS_API_TOKEN`, or `CF_DNS_API_TOKEN` and `CF_ZONE_API_TOKEN`.\n\n### API keys\n\nIf using API keys (`CF_API_EMAIL` and `CF_API_KEY`), the Global API Key needs to be used, not the Origin CA Key.\n\nPlease be aware, that this in principle allows Lego to read and change *everything* related to this account.\n\n### API tokens\n\nWith API tokens (`CF_DNS_API_TOKEN`, and optionally `CF_ZONE_API_TOKEN`),\nvery specific access can be granted to your resources at Cloudflare.\nSee this [Cloudflare announcement](https://blog.cloudflare.com/api-tokens-general-availability/) for details.\n\nThe main resources Lego cares for are the DNS entries for your Zones.\nIt also needs to resolve a domain name to an internal Zone ID in order to manipulate DNS entries.\n\nHence, you should create an API token with the following permissions:\n\n* Zone / Zone / Read\n* Zone / DNS / Edit\n\nYou also need to scope the access to all your domains for this to work.\nThen pass the API token as `CF_DNS_API_TOKEN` to Lego.\n\n**Alternatively,** if you prefer a more strict set of privileges,\nyou can split the access tokens:\n\n* Create one with *Zone / Zone / Read* permissions and scope it to all your zones or just the individual zone you need to edit.\n  This is needed to resolve domain names to Zone IDs and can be shared among multiple Lego installations.\n  Pass this API token as `CF_ZONE_API_TOKEN` to Lego.\n* Create another API token with *Zone / DNS / Edit* permissions and set the scope to the domains you want to manage with a single Lego installation.\n  Pass this token as `CF_DNS_API_TOKEN` to Lego.\n* Repeat the previous step for each host you want to run Lego on.\n* It is possible to use the same api token for both variables if it is given `Zone:Read` and `DNS:Edit` permission for the zone.\n\nThis \"paranoid\" setup is mainly interesting for users who manage many zones/domains with a single Cloudflare account.\nIt follows the principle of least privilege and limits the possible damage, should one of the hosts become compromised.\n\n\n\n## More information\n\n- [API documentation](https://api.cloudflare.com/)\n- [Go client](https://github.com/cloudflare/cloudflare-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/cloudflare/cloudflare.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_cloudns.md",
    "content": "---\ntitle: \"ClouDNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: cloudns\ndnsprovider:\n  since:    \"v2.3.0\"\n  code:     \"cloudns\"\n  url:      \"https://www.cloudns.net\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/cloudns/cloudns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [ClouDNS](https://www.cloudns.net).\n\n\n<!--more-->\n\n- Code: `cloudns`\n- Since: v2.3.0\n\n\nHere is an example bash command using the ClouDNS provider:\n\n```bash\nCLOUDNS_AUTH_ID=xxxx \\\nCLOUDNS_AUTH_PASSWORD=yyyy \\\nlego --dns cloudns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `CLOUDNS_AUTH_ID` | The API user ID |\n| `CLOUDNS_AUTH_PASSWORD` | The password for API user ID |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `CLOUDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `CLOUDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `CLOUDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 180) |\n| `CLOUDNS_SUB_AUTH_ID` | The API sub user ID |\n| `CLOUDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.cloudns.net/wiki/article/42/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/cloudns/cloudns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_cloudru.md",
    "content": "---\ntitle: \"Cloud.ru\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: cloudru\ndnsprovider:\n  since:    \"v4.14.0\"\n  code:     \"cloudru\"\n  url:      \"https://cloud.ru\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/cloudru/cloudru.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Cloud.ru](https://cloud.ru).\n\n\n<!--more-->\n\n- Code: `cloudru`\n- Since: v4.14.0\n\n\nHere is an example bash command using the Cloud.ru provider:\n\n```bash\nCLOUDRU_SERVICE_INSTANCE_ID=ppp \\\nCLOUDRU_KEY_ID=xxx \\\nCLOUDRU_SECRET=yyy \\\nlego --dns cloudru -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `CLOUDRU_KEY_ID` | Key ID (login) |\n| `CLOUDRU_SECRET` | Key Secret |\n| `CLOUDRU_SERVICE_INSTANCE_ID` | Service Instance ID (parentId) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `CLOUDRU_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `CLOUDRU_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) |\n| `CLOUDRU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |\n| `CLOUDRU_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 120) |\n| `CLOUDRU_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://cloud.ru/ru/docs/clouddns/ug/topics/api-ref.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/cloudru/cloudru.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_cloudxns.md",
    "content": "---\ntitle: \"CloudXNS (Deprecated)\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: cloudxns\ndnsprovider:\n  since:    \"v0.5.0\"\n  code:     \"cloudxns\"\n  url:      \"https://github.com/go-acme/lego/issues/2323\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/cloudxns/cloudxns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\nThe CloudXNS DNS provider has shut down.\n\n\n\n<!--more-->\n\n- Code: `cloudxns`\n- Since: v0.5.0\n\n\nHere is an example bash command using the CloudXNS (Deprecated) provider:\n\n```bash\nCLOUDXNS_API_KEY=xxxx \\\nCLOUDXNS_SECRET_KEY=yyyy \\\nlego --dns cloudxns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `CLOUDXNS_API_KEY` | The API key |\n| `CLOUDXNS_SECRET_KEY` | The API secret key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `CLOUDXNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: ) |\n| `CLOUDXNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: ) |\n| `CLOUDXNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: ) |\n| `CLOUDXNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: ) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/cloudxns/cloudxns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_com35.md",
    "content": "---\ntitle: \"35.com/三五互联\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: com35\ndnsprovider:\n  since:    \"v4.31.0\"\n  code:     \"com35\"\n  url:      \"https://www.35.cn/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/com35/com35.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [35.com/三五互联](https://www.35.cn/).\n\n\n<!--more-->\n\n- Code: `com35`\n- Since: v4.31.0\n\n\nHere is an example bash command using the 35.com/三五互联 provider:\n\n```bash\nCOM35_USERNAME=\"xxx\" \\\nCOM35_PASSWORD=\"yyy\" \\\nlego --dns com35 -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `COM35_PASSWORD` | API password |\n| `COM35_USERNAME` | Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `COM35_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `COM35_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `COM35_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `COM35_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.35.cn/CustomerCenter/doc/domain_v2.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/com35/com35.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_conoha.md",
    "content": "---\ntitle: \"ConoHa v2\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: conoha\ndnsprovider:\n  since:    \"v1.2.0\"\n  code:     \"conoha\"\n  url:      \"https://www.conoha.jp/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/conoha/conoha.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [ConoHa v2](https://www.conoha.jp/).\n\n\n<!--more-->\n\n- Code: `conoha`\n- Since: v1.2.0\n\n\nHere is an example bash command using the ConoHa v2 provider:\n\n```bash\nCONOHA_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \\\nCONOHA_API_USERNAME=xxxx \\\nCONOHA_API_PASSWORD=yyyy \\\nlego --dns conoha -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `CONOHA_API_PASSWORD` | The API password |\n| `CONOHA_API_USERNAME` | The API username |\n| `CONOHA_TENANT_ID` | Tenant ID |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `CONOHA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `CONOHA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `CONOHA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `CONOHA_REGION` | The region (Default: tyo1) |\n| `CONOHA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://doc.conoha.jp/reference/api-vps2/api-dns-vps2)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/conoha/conoha.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_conohav3.md",
    "content": "---\ntitle: \"ConoHa v3\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: conohav3\ndnsprovider:\n  since:    \"v4.24.0\"\n  code:     \"conohav3\"\n  url:      \"https://www.conoha.jp/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/conohav3/conohav3.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [ConoHa v3](https://www.conoha.jp/).\n\n\n<!--more-->\n\n- Code: `conohav3`\n- Since: v4.24.0\n\n\nHere is an example bash command using the ConoHa v3 provider:\n\n```bash\nCONOHAV3_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \\\nCONOHAV3_API_USER_ID=xxxx \\\nCONOHAV3_API_PASSWORD=yyyy \\\nlego --dns conohav3 -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `CONOHAV3_API_PASSWORD` | The API password |\n| `CONOHAV3_API_USER_ID` | The API user ID |\n| `CONOHAV3_TENANT_ID` | Tenant ID |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `CONOHAV3_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `CONOHAV3_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `CONOHAV3_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `CONOHAV3_REGION` | The region (Default: c3j1) |\n| `CONOHAV3_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://doc.conoha.jp/reference/api-vps3/api-dns-vps3/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/conohav3/conohav3.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_constellix.md",
    "content": "---\ntitle: \"Constellix\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: constellix\ndnsprovider:\n  since:    \"v3.4.0\"\n  code:     \"constellix\"\n  url:      \"https://constellix.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/constellix/constellix.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Constellix](https://constellix.com).\n\n\n<!--more-->\n\n- Code: `constellix`\n- Since: v3.4.0\n\n\nHere is an example bash command using the Constellix provider:\n\n```bash\nCONSTELLIX_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \\\nCONSTELLIX_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \\\nlego --dns constellix -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `CONSTELLIX_API_KEY` | User API key |\n| `CONSTELLIX_SECRET_KEY` | User secret key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `CONSTELLIX_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `CONSTELLIX_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `CONSTELLIX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `CONSTELLIX_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api-docs.constellix.com)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/constellix/constellix.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_corenetworks.md",
    "content": "---\ntitle: \"Core-Networks\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: corenetworks\ndnsprovider:\n  since:    \"v4.20.0\"\n  code:     \"corenetworks\"\n  url:      \"https://www.core-networks.de/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/corenetworks/corenetworks.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Core-Networks](https://www.core-networks.de/).\n\n\n<!--more-->\n\n- Code: `corenetworks`\n- Since: v4.20.0\n\n\nHere is an example bash command using the Core-Networks provider:\n\n```bash\nCORENETWORKS_LOGIN=\"xxxx\" \\\nCORENETWORKS_PASSWORD=\"yyyy\" \\\nlego --dns corenetworks -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `CORENETWORKS_LOGIN` | The username of the API account |\n| `CORENETWORKS_PASSWORD` | The password |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `CORENETWORKS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `CORENETWORKS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `CORENETWORKS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `CORENETWORKS_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n| `CORENETWORKS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://beta.api.core-networks.de/doc/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/corenetworks/corenetworks.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_cpanel.md",
    "content": "---\ntitle: \"CPanel/WHM\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: cpanel\ndnsprovider:\n  since:    \"v4.16.0\"\n  code:     \"cpanel\"\n  url:      \"https://cpanel.net/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/cpanel/cpanel.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [CPanel/WHM](https://cpanel.net/).\n\n\n<!--more-->\n\n- Code: `cpanel`\n- Since: v4.16.0\n\n\nHere is an example bash command using the CPanel/WHM provider:\n\n```bash\n### CPANEL (default)\n\nCPANEL_USERNAME=\"yyyy\" \\\nCPANEL_TOKEN=\"xxxx\" \\\nCPANEL_BASE_URL=\"https://example.com:2083\" \\\nlego --dns cpanel -d '*.example.com' -d example.com run\n\n## WHM\n\nCPANEL_MODE=whm \\\nCPANEL_USERNAME=\"yyyy\" \\\nCPANEL_TOKEN=\"xxxx\" \\\nCPANEL_BASE_URL=\"https://example.com:2087\" \\\nlego --dns cpanel -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `CPANEL_BASE_URL` | API server URL |\n| `CPANEL_TOKEN` | API token |\n| `CPANEL_USERNAME` | username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `CPANEL_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `CPANEL_MODE` | use cpanel API or WHM API (Default: cpanel) |\n| `CPANEL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `CPANEL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `CPANEL_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/cpanel/cpanel.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_czechia.md",
    "content": "---\ntitle: \"Czechia\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: czechia\ndnsprovider:\n  since:    \"v4.33.0\"\n  code:     \"czechia\"\n  url:      \"https://www.czechia.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/czechia/czechia.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Czechia](https://www.czechia.com/).\n\n\n<!--more-->\n\n- Code: `czechia`\n- Since: v4.33.0\n\n\nHere is an example bash command using the Czechia provider:\n\n```bash\nCZECHIA_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns czechia -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `CZECHIA_TOKEN` | Authorization token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `CZECHIA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `CZECHIA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `CZECHIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `CZECHIA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.czechia.com/swagger/index.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/czechia/czechia.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_ddnss.md",
    "content": "---\ntitle: \"DDnss (DynDNS Service)\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: ddnss\ndnsprovider:\n  since:    \"v4.32.0\"\n  code:     \"ddnss\"\n  url:      \"https://ddnss.de/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ddnss/ddnss.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [DDnss (DynDNS Service)](https://ddnss.de/).\n\n\n<!--more-->\n\n- Code: `ddnss`\n- Since: v4.32.0\n\n\nHere is an example bash command using the DDnss (DynDNS Service) provider:\n\n```bash\nDDNSS_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns ddnss -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DDNSS_KEY` | Update key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DDNSS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `DDNSS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `DDNSS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `DDNSS_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n| `DDNSS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://ddnss.de/info.php)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ddnss/ddnss.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_derak.md",
    "content": "---\ntitle: \"Derak Cloud\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: derak\ndnsprovider:\n  since:    \"v4.12.0\"\n  code:     \"derak\"\n  url:      \"https://derak.cloud/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/derak/derak.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Derak Cloud](https://derak.cloud/).\n\n\n<!--more-->\n\n- Code: `derak`\n- Since: v4.12.0\n\n\nHere is an example bash command using the Derak Cloud provider:\n\n```bash\nDERAK_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns derak -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DERAK_API_KEY` | The API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DERAK_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `DERAK_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) |\n| `DERAK_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `DERAK_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n| `DERAK_WEBSITE_ID` | Force the zone/website ID |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/derak/derak.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_desec.md",
    "content": "---\ntitle: \"deSEC.io\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: desec\ndnsprovider:\n  since:    \"v3.7.0\"\n  code:     \"desec\"\n  url:      \"https://desec.io\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/desec/desec.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [deSEC.io](https://desec.io).\n\n\n<!--more-->\n\n- Code: `desec`\n- Since: v3.7.0\n\n\nHere is an example bash command using the deSEC.io provider:\n\n```bash\nDESEC_TOKEN=x-xxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns desec -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DESEC_TOKEN` | Domain token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DESEC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `DESEC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 4) |\n| `DESEC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `DESEC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://desec.readthedocs.io/en/latest/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/desec/desec.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_designate.md",
    "content": "---\ntitle: \"Designate DNSaaS for Openstack\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: designate\ndnsprovider:\n  since:    \"v2.2.0\"\n  code:     \"designate\"\n  url:      \"https://docs.openstack.org/designate/latest/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/designate/designate.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Designate DNSaaS for Openstack](https://docs.openstack.org/designate/latest/).\n\n\n<!--more-->\n\n- Code: `designate`\n- Since: v2.2.0\n\n\nHere is an example bash command using the Designate DNSaaS for Openstack provider:\n\n```bash\n# With a `clouds.yaml`\nOS_CLOUD=my_openstack \\\nlego --dns designate -d '*.example.com' -d example.com run\n\n# or\n\nOS_AUTH_URL=https://openstack.example.org \\\nOS_REGION_NAME=RegionOne \\\nOS_PROJECT_ID=23d4522a987d4ab529f722a007c27846\nOS_USERNAME=myuser \\\nOS_PASSWORD=passw0rd \\\nlego --dns designate -d '*.example.com' -d example.com run\n\n# or\n\nOS_AUTH_URL=https://openstack.example.org \\\nOS_REGION_NAME=RegionOne \\\nOS_AUTH_TYPE=v3applicationcredential \\\nOS_APPLICATION_CREDENTIAL_ID=imn74uq0or7dyzz20dwo1ytls4me8dry \\\nOS_APPLICATION_CREDENTIAL_SECRET=68FuSPSdQqkFQYH5X1OoriEIJOwyLtQ8QSqXZOc9XxFK1A9tzZT6He2PfPw0OMja \\\nlego --dns designate -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `OS_APPLICATION_CREDENTIAL_ID` | Application credential ID |\n| `OS_APPLICATION_CREDENTIAL_NAME` | Application credential name |\n| `OS_APPLICATION_CREDENTIAL_SECRET` | Application credential secret |\n| `OS_AUTH_URL` | Identity endpoint URL |\n| `OS_PASSWORD` | Password |\n| `OS_PROJECT_NAME` | Project name |\n| `OS_REGION_NAME` | Region name |\n| `OS_USERNAME` | Username |\n| `OS_USER_ID` | User ID |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DESIGNATE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `DESIGNATE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 600) |\n| `DESIGNATE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 10) |\n| `DESIGNATE_ZONE_NAME` | The zone name to use in the OpenStack Project to manage TXT records. |\n| `OS_PROJECT_ID` | Project ID |\n| `OS_TENANT_NAME` | Tenant name (deprecated see OS_PROJECT_NAME and OS_PROJECT_ID) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Description\n\nThere are three main ways of authenticating with Designate:\n\n1. The first one is by using the `OS_CLOUD` environment variable and a `clouds.yaml` file.\n2. The second one is using your username and password, via the `OS_USERNAME`, `OS_PASSWORD` and `OS_PROJECT_NAME` environment variables.\n3. The third one is by using an application credential, via the `OS_APPLICATION_CREDENTIAL_*` and `OS_USER_ID` environment variables.\n\nFor the username/password and application methods, the `OS_AUTH_URL` and `OS_REGION_NAME` environment variables are required.\n\nFor more information, you can read about the different methods of authentication with OpenStack in the Keystone's documentation and the gophercloud documentation:\n\n- [Keystone username/password](https://docs.openstack.org/keystone/latest/user/supported_clients.html)\n- [Keystone application credentials](https://docs.openstack.org/keystone/latest/user/application_credentials.html)\n\nPublic cloud providers with support for Designate:\n\n- [Fuga Cloud](https://fuga.cloud/)\n\n\n\n## More information\n\n- [API documentation](https://docs.openstack.org/designate/latest/)\n- [Go client](https://pkg.go.dev/github.com/gophercloud/gophercloud/openstack/dns/v2)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/designate/designate.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_digitalocean.md",
    "content": "---\ntitle: \"Digital Ocean\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: digitalocean\ndnsprovider:\n  since:    \"v0.3.0\"\n  code:     \"digitalocean\"\n  url:      \"https://www.digitalocean.com/docs/networking/dns/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/digitalocean/digitalocean.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Digital Ocean](https://www.digitalocean.com/docs/networking/dns/).\n\n\n<!--more-->\n\n- Code: `digitalocean`\n- Since: v0.3.0\n\n\nHere is an example bash command using the Digital Ocean provider:\n\n```bash\nDO_AUTH_TOKEN=xxxxxx \\\nlego --dns digitalocean -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DO_AUTH_TOKEN` | Authentication token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DO_API_URL` | The URL of the API |\n| `DO_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `DO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) |\n| `DO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `DO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 30) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developers.digitalocean.com/documentation/v2/#domain-records)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/digitalocean/digitalocean.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_directadmin.md",
    "content": "---\ntitle: \"DirectAdmin\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: directadmin\ndnsprovider:\n  since:    \"v4.18.0\"\n  code:     \"directadmin\"\n  url:      \"https://www.directadmin.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/directadmin/directadmin.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [DirectAdmin](https://www.directadmin.com).\n\n\n<!--more-->\n\n- Code: `directadmin`\n- Since: v4.18.0\n\n\nHere is an example bash command using the DirectAdmin provider:\n\n```bash\nDIRECTADMIN_API_URL=\"http://example.com:2222\" \\\nDIRECTADMIN_USERNAME=xxxx \\\nDIRECTADMIN_PASSWORD=yyy \\\nlego --dns directadmin -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DIRECTADMIN_API_URL` | URL of the API |\n| `DIRECTADMIN_PASSWORD` | API password |\n| `DIRECTADMIN_USERNAME` | API username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DIRECTADMIN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `DIRECTADMIN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) |\n| `DIRECTADMIN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `DIRECTADMIN_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 30) |\n| `DIRECTADMIN_ZONE_NAME` | Zone name used to add the TXT record |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.directadmin.com/api.php)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/directadmin/directadmin.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_dnsexit.md",
    "content": "---\ntitle: \"DNSExit\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: dnsexit\ndnsprovider:\n  since:    \"v4.32.0\"\n  code:     \"dnsexit\"\n  url:      \"https://dnsexit.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dnsexit/dnsexit.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [DNSExit](https://dnsexit.com).\n\n\n<!--more-->\n\n- Code: `dnsexit`\n- Since: v4.32.0\n\n\nHere is an example bash command using the DNSExit provider:\n\n```bash\nDNSEXIT_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns dnsexit -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DNSEXIT_API_KEY` | API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DNSEXIT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `DNSEXIT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `DNSEXIT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |\n| `DNSEXIT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://dnsexit.com/dns/dns-api/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dnsexit/dnsexit.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_dnshomede.md",
    "content": "---\ntitle: \"dnsHome.de\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: dnshomede\ndnsprovider:\n  since:    \"v4.10.0\"\n  code:     \"dnshomede\"\n  url:      \"https://www.dnshome.de\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dnshomede/dnshomede.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [dnsHome.de](https://www.dnshome.de).\n\n\n<!--more-->\n\n- Code: `dnshomede`\n- Since: v4.10.0\n\n\nHere is an example bash command using the dnsHome.de provider:\n\n```bash\nDNSHOMEDE_CREDENTIALS=example.org:password \\\nlego --dns dnshomede -d '*.example.com' -d example.com run\n\nDNSHOMEDE_CREDENTIALS=my.example.org:password1,demo.example.org:password2 \\\nlego --dns dnshomede -d my.example.org -d demo.example.org\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DNSHOMEDE_CREDENTIALS` | Comma-separated list of domain:password credential pairs |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DNSHOMEDE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `DNSHOMEDE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 1200) |\n| `DNSHOMEDE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 2) |\n| `DNSHOMEDE_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dnshomede/dnshomede.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_dnsimple.md",
    "content": "---\ntitle: \"DNSimple\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: dnsimple\ndnsprovider:\n  since:    \"v0.3.0\"\n  code:     \"dnsimple\"\n  url:      \"https://dnsimple.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dnsimple/dnsimple.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [DNSimple](https://dnsimple.com/).\n\n\n<!--more-->\n\n- Code: `dnsimple`\n- Since: v0.3.0\n\n\nHere is an example bash command using the DNSimple provider:\n\n```bash\nDNSIMPLE_OAUTH_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \\\nlego --dns dnsimple -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DNSIMPLE_OAUTH_TOKEN` | OAuth token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DNSIMPLE_BASE_URL` | API endpoint URL |\n| `DNSIMPLE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `DNSIMPLE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `DNSIMPLE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Description\n\n`DNSIMPLE_BASE_URL` is optional and must be set to production (https://api.dnsimple.com).\nif `DNSIMPLE_BASE_URL` is not defined or empty, the production URL is used by default.\n\nWhile you can manage DNS records in the [DNSimple Sandbox environment](https://developer.dnsimple.com/sandbox/),\nDNS records will not resolve, and you will not be able to satisfy the ACME DNS challenge.\n\nTo authenticate you need to provide a valid API token.\nHTTP Basic Authentication is intentionally not supported.\n\n### API tokens\n\nYou can [generate a new API token](https://support.dnsimple.com/articles/api-access-token/) from your account page.\nOnly Account API tokens are supported, if you try to use a User API token you will receive an error message.\n\n\n\n## More information\n\n- [API documentation](https://developer.dnsimple.com/v2/)\n- [Go client](https://github.com/dnsimple/dnsimple-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dnsimple/dnsimple.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_dnsmadeeasy.md",
    "content": "---\ntitle: \"DNS Made Easy\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: dnsmadeeasy\ndnsprovider:\n  since:    \"v0.4.0\"\n  code:     \"dnsmadeeasy\"\n  url:      \"https://dnsmadeeasy.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dnsmadeeasy/dnsmadeeasy.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [DNS Made Easy](https://dnsmadeeasy.com/).\n\n\n<!--more-->\n\n- Code: `dnsmadeeasy`\n- Since: v0.4.0\n\n\nHere is an example bash command using the DNS Made Easy provider:\n\n```bash\nDNSMADEEASY_API_KEY=xxxxxx \\\nDNSMADEEASY_API_SECRET=yyyyy \\\nlego --dns dnsmadeeasy -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DNSMADEEASY_API_KEY` | The API key |\n| `DNSMADEEASY_API_SECRET` | The API Secret key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DNSMADEEASY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `DNSMADEEASY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `DNSMADEEASY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `DNSMADEEASY_SANDBOX` | Activate the sandbox (boolean) |\n| `DNSMADEEASY_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api-docs.dnsmadeeasy.com/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dnsmadeeasy/dnsmadeeasy.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_dnspod.md",
    "content": "---\ntitle: \"DNSPod (deprecated)\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: dnspod\ndnsprovider:\n  since:    \"v0.4.0\"\n  code:     \"dnspod\"\n  url:      \"https://www.dnspod.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dnspod/dnspod.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\nUse the Tencent Cloud provider instead.\n\n\n\n<!--more-->\n\n- Code: `dnspod`\n- Since: v0.4.0\n\n\nHere is an example bash command using the DNSPod (deprecated) provider:\n\n```bash\nDNSPOD_API_KEY=xxxxxx \\\nlego --dns dnspod -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DNSPOD_API_KEY` | The user token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DNSPOD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `DNSPOD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `DNSPOD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `DNSPOD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://docs.dnspod.com/api/)\n- [Go client](https://github.com/nrdcg/dnspod-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dnspod/dnspod.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_dode.md",
    "content": "---\ntitle: \"Domain Offensive (do.de)\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: dode\ndnsprovider:\n  since:    \"v2.4.0\"\n  code:     \"dode\"\n  url:      \"https://www.do.de/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dode/dode.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Domain Offensive (do.de)](https://www.do.de/).\n\n\n<!--more-->\n\n- Code: `dode`\n- Since: v2.4.0\n\n\nHere is an example bash command using the Domain Offensive (do.de) provider:\n\n```bash\nDODE_TOKEN=xxxxxx \\\nlego --dns dode -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DODE_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DODE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `DODE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `DODE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `DODE_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.do.de/wiki/freie-ssl-tls-zertifikate-ueber-acme/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dode/dode.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_domeneshop.md",
    "content": "---\ntitle: \"Domeneshop\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: domeneshop\ndnsprovider:\n  since:    \"v4.3.0\"\n  code:     \"domeneshop\"\n  url:      \"https://domene.shop\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/domeneshop/domeneshop.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Domeneshop](https://domene.shop).\n\n\n<!--more-->\n\n- Code: `domeneshop`\n- Since: v4.3.0\n\n\nHere is an example bash command using the Domeneshop provider:\n\n```bash\nDOMENESHOP_API_TOKEN=<token> \\\nDOMENESHOP_API_SECRET=<secret> \\\nlego --dns domeneshop -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DOMENESHOP_API_SECRET` | API secret |\n| `DOMENESHOP_API_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DOMENESHOP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `DOMENESHOP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 20) |\n| `DOMENESHOP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n### API credentials\n\nVisit the following page for information on how to create API credentials with Domeneshop:\n\n  https://api.domeneshop.no/docs/#section/Authentication\n\n\n\n## More information\n\n- [API documentation](https://api.domeneshop.no/docs)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/domeneshop/domeneshop.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_dreamhost.md",
    "content": "---\ntitle: \"DreamHost\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: dreamhost\ndnsprovider:\n  since:    \"v1.1.0\"\n  code:     \"dreamhost\"\n  url:      \"https://www.dreamhost.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dreamhost/dreamhost.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [DreamHost](https://www.dreamhost.com).\n\n\n<!--more-->\n\n- Code: `dreamhost`\n- Since: v1.1.0\n\n\nHere is an example bash command using the DreamHost provider:\n\n```bash\nDREAMHOST_API_KEY=\"YOURAPIKEY\" \\\nlego --dns dreamhost -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DREAMHOST_API_KEY` | The API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DREAMHOST_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `DREAMHOST_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 60) |\n| `DREAMHOST_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 3600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://help.dreamhost.com/hc/en-us/articles/217560167-API_overview)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dreamhost/dreamhost.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_duckdns.md",
    "content": "---\ntitle: \"Duck DNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: duckdns\ndnsprovider:\n  since:    \"v0.5.0\"\n  code:     \"duckdns\"\n  url:      \"https://www.duckdns.org/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/duckdns/duckdns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Duck DNS](https://www.duckdns.org/).\n\n\n<!--more-->\n\n- Code: `duckdns`\n- Since: v0.5.0\n\n\nHere is an example bash command using the Duck DNS provider:\n\n```bash\nDUCKDNS_TOKEN=xxxxxx \\\nlego --dns duckdns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DUCKDNS_TOKEN` | Account token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DUCKDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `DUCKDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `DUCKDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `DUCKDNS_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.duckdns.org/spec.jsp)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/duckdns/duckdns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_dyn.md",
    "content": "---\ntitle: \"Dyn\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: dyn\ndnsprovider:\n  since:    \"v0.3.0\"\n  code:     \"dyn\"\n  url:      \"https://dyn.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dyn/dyn.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Dyn](https://dyn.com/).\n\n\n<!--more-->\n\n- Code: `dyn`\n- Since: v0.3.0\n\n\nHere is an example bash command using the Dyn provider:\n\n```bash\nDYN_CUSTOMER_NAME=xxxxxx \\\nDYN_USER_NAME=yyyyy \\\nDYN_PASSWORD=zzzz \\\nlego --dns dyn -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DYN_CUSTOMER_NAME` | Customer name |\n| `DYN_PASSWORD` | Password |\n| `DYN_USER_NAME` | User name |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DYN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `DYN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `DYN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `DYN_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://help.dyn.com/rest/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dyn/dyn.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_dyndnsfree.md",
    "content": "---\ntitle: \"DynDnsFree.de\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: dyndnsfree\ndnsprovider:\n  since:    \"v4.23.0\"\n  code:     \"dyndnsfree\"\n  url:      \"https://www.dyndnsfree.de\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dyndnsfree/dyndnsfree.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [DynDnsFree.de](https://www.dyndnsfree.de).\n\n\n<!--more-->\n\n- Code: `dyndnsfree`\n- Since: v4.23.0\n\n\nHere is an example bash command using the DynDnsFree.de provider:\n\n```bash\nDYNDNSFREE_USERNAME=\"xxx\" \\\nDYNDNSFREE_PASSWORD=\"yyy\" \\\nlego --dns dyndnsfree -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DYNDNSFREE_PASSWORD` | Password |\n| `DYNDNSFREE_USERNAME` | Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DYNDNSFREE_HTTP_TIMEOUT` | Request timeout in seconds (Default: 30) |\n| `DYNDNSFREE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `DYNDNSFREE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.dyndnsfree.de/user/hilfe.php?hsm=2)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dyndnsfree/dyndnsfree.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_dynu.md",
    "content": "---\ntitle: \"Dynu\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: dynu\ndnsprovider:\n  since:    \"v3.5.0\"\n  code:     \"dynu\"\n  url:      \"https://www.dynu.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dynu/dynu.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Dynu](https://www.dynu.com/).\n\n\n<!--more-->\n\n- Code: `dynu`\n- Since: v3.5.0\n\n\nHere is an example bash command using the Dynu provider:\n\n```bash\nDYNU_API_KEY=1234567890abcdefghijklmnopqrstuvwxyz \\\nlego --dns dynu -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `DYNU_API_KEY` | API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `DYNU_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `DYNU_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `DYNU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 180) |\n| `DYNU_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.dynu.com/en-US/Support/API)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/dynu/dynu.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_easydns.md",
    "content": "---\ntitle: \"EasyDNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: easydns\ndnsprovider:\n  since:    \"v2.6.0\"\n  code:     \"easydns\"\n  url:      \"https://easydns.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/easydns/easydns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [EasyDNS](https://easydns.com/).\n\n\n<!--more-->\n\n- Code: `easydns`\n- Since: v2.6.0\n\n\nHere is an example bash command using the EasyDNS provider:\n\n```bash\nEASYDNS_TOKEN=xxx \\\nEASYDNS_KEY=yyy \\\nlego --dns easydns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `EASYDNS_KEY` | API Key |\n| `EASYDNS_TOKEN` | API Token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `EASYDNS_ENDPOINT` | The endpoint URL of the API Server |\n| `EASYDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `EASYDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `EASYDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `EASYDNS_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n| `EASYDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\nTo test with the sandbox environment set ```EASYDNS_ENDPOINT=https://sandbox.rest.easydns.net```\n\n\n\n## More information\n\n- [API documentation](https://docs.sandbox.rest.easydns.net)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/easydns/easydns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_edgecenter.md",
    "content": "---\ntitle: \"EdgeCenter\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: edgecenter\ndnsprovider:\n  since:    \"v4.29.0\"\n  code:     \"edgecenter\"\n  url:      \"https://edgecenter.ru/dns\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/edgecenter/edgecenter.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [EdgeCenter](https://edgecenter.ru/dns).\n\n\n<!--more-->\n\n- Code: `edgecenter`\n- Since: v4.29.0\n\n\nHere is an example bash command using the EdgeCenter provider:\n\n```bash\nEDGECENTER_PERMANENT_API_TOKEN=xxxxx \\\nlego --dns edgecenter -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `EDGECENTER_PERMANENT_API_TOKEN` | Permanent API token (https://edgecenter.ru/blog/permanent-api-token-explained/) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `EDGECENTER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `EDGECENTER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 20) |\n| `EDGECENTER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 360) |\n| `EDGECENTER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://apidocs.edgecenter.ru/dns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/edgecenter/edgecenter.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_edgedns.md",
    "content": "---\ntitle: \"Akamai EdgeDNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: edgedns\ndnsprovider:\n  since:    \"v3.9.0\"\n  code:     \"edgedns\"\n  url:      \"https://www.akamai.com/us/en/products/security/edge-dns.jsp\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/edgedns/edgedns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\nAkamai edgedns supersedes FastDNS; implementing a DNS provider for solving the DNS-01 challenge using Akamai EdgeDNS\n\n\n\n<!--more-->\n\n- Code: `edgedns`\n- Since: v3.9.0\n\n\nHere is an example bash command using the Akamai EdgeDNS provider:\n\n```bash\nAKAMAI_CLIENT_SECRET=abcdefghijklmnopqrstuvwxyz1234567890ABCDEFG= \\\nAKAMAI_CLIENT_TOKEN=akab-mnbvcxzlkjhgfdsapoiuytrewq1234567 \\\nAKAMAI_HOST=akab-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.luna.akamaiapis.net \\\nAKAMAI_ACCESS_TOKEN=akab-1234567890qwerty-asdfghjklzxcvtnu \\\nlego --dns edgedns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `AKAMAI_ACCESS_TOKEN` | Access token, managed by the Akamai EdgeGrid client |\n| `AKAMAI_CLIENT_SECRET` | Client secret, managed by the Akamai EdgeGrid client |\n| `AKAMAI_CLIENT_TOKEN` | Client token, managed by the Akamai EdgeGrid client |\n| `AKAMAI_EDGERC` | Path to the .edgerc file, managed by the Akamai EdgeGrid client |\n| `AKAMAI_EDGERC_SECTION` | Configuration section, managed by the Akamai EdgeGrid client |\n| `AKAMAI_HOST` | API host, managed by the Akamai EdgeGrid client |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `AKAMAI_ACCOUNT_SWITCH_KEY` | Target account ID when the DNS zone and credentials belong to different accounts |\n| `AKAMAI_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 15) |\n| `AKAMAI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 180) |\n| `AKAMAI_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\nAkamai's credentials are automatically detected in the following locations and prioritized in the following order:\n\n1. Section-specific environment variables (where `{SECTION}` is specified using `AKAMAI_EDGERC_SECTION`):\n  - `AKAMAI_{SECTION}_HOST`\n  - `AKAMAI_{SECTION}_ACCESS_TOKEN`\n  - `AKAMAI_{SECTION}_CLIENT_TOKEN`\n  - `AKAMAI_{SECTION}_CLIENT_SECRET`\n2. If `AKAMAI_EDGERC_SECTION` is not defined or is set to `default`, environment variables:\n  - `AKAMAI_HOST`\n  - `AKAMAI_ACCESS_TOKEN`\n  - `AKAMAI_CLIENT_TOKEN`\n  - `AKAMAI_CLIENT_SECRET`\n3. `.edgerc` file located at `AKAMAI_EDGERC`\n  - defaults to `~/.edgerc`, sections can be specified using `AKAMAI_EDGERC_SECTION`\n4. Default environment variables:\n  - `AKAMAI_HOST`\n  - `AKAMAI_ACCESS_TOKEN`\n  - `AKAMAI_CLIENT_TOKEN`\n  - `AKAMAI_CLIENT_SECRET`\n\nSee also:\n\n- [Setting up Akamai credentials](https://developer.akamai.com/api/getting-started)\n- [.edgerc Format](https://developer.akamai.com/legacy/introduction/Conf_Client.html#edgercformat)\n- [API Client Authentication](https://developer.akamai.com/legacy/introduction/Client_Auth.html)\n- [Config from Env](https://github.com/akamai/AkamaiOPEN-edgegrid-golang/blob/master/pkg/edgegrid/config.go#L118)\n- [Manage many accounts](https://techdocs.akamai.com/developer/docs/manage-many-accounts-with-one-api-client)\n\n\n\n## More information\n\n- [API documentation](https://developer.akamai.com/api/cloud_security/edge_dns_zone_management/v2.html)\n- [Go client](https://github.com/akamai/AkamaiOPEN-edgegrid-golang)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/edgedns/edgedns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_edgeone.md",
    "content": "---\ntitle: \"Tencent EdgeOne\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: edgeone\ndnsprovider:\n  since:    \"v4.26.0\"\n  code:     \"edgeone\"\n  url:      \"https://edgeone.ai\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/edgeone/edgeone.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Tencent EdgeOne](https://edgeone.ai).\n\n\n<!--more-->\n\n- Code: `edgeone`\n- Since: v4.26.0\n\n\nHere is an example bash command using the Tencent EdgeOne provider:\n\n```bash\nEDGEONE_SECRET_ID=abcdefghijklmnopqrstuvwx \\\nEDGEONE_SECRET_KEY=your-secret-key \\\nlego --dns edgeone -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `EDGEONE_SECRET_ID` | Access key ID |\n| `EDGEONE_SECRET_KEY` | Access Key secret |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `EDGEONE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `EDGEONE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 30) |\n| `EDGEONE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 1200) |\n| `EDGEONE_REGION` | Region |\n| `EDGEONE_SESSION_TOKEN` | Access Key token |\n| `EDGEONE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n| `EDGEONE_ZONES_MAPPING` | Mapping between DNS zones and site IDs. (ex: 'example.org:id1,example.com:id2') |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://edgeone.ai/document/50454#dns-record-apis)\n- [Go client](https://github.com/tencentcloud/tencentcloud-sdk-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/edgeone/edgeone.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_efficientip.md",
    "content": "---\ntitle: \"Efficient IP\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: efficientip\ndnsprovider:\n  since:    \"v4.13.0\"\n  code:     \"efficientip\"\n  url:      \"https://efficientip.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/efficientip/efficientip.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Efficient IP](https://efficientip.com/).\n\n\n<!--more-->\n\n- Code: `efficientip`\n- Since: v4.13.0\n\n\nHere is an example bash command using the Efficient IP provider:\n\n```bash\nEFFICIENTIP_USERNAME=\"user\" \\\nEFFICIENTIP_PASSWORD=\"secret\" \\\nEFFICIENTIP_HOSTNAME=\"ipam.example.org\" \\\nEFFICIENTIP_DNS_NAME=\"dns.smart\" \\\nlego --dns efficientip -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `EFFICIENTIP_DNS_NAME` | DNS name (ex: dns.smart) |\n| `EFFICIENTIP_HOSTNAME` | Hostname (ex: foo.example.com) |\n| `EFFICIENTIP_PASSWORD` | Password |\n| `EFFICIENTIP_USERNAME` | Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `EFFICIENTIP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `EFFICIENTIP_INSECURE_SKIP_VERIFY` | Whether or not to verify EfficientIP API certificate |\n| `EFFICIENTIP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `EFFICIENTIP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `EFFICIENTIP_VIEW_NAME` | View name (ex: external) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/efficientip/efficientip.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_epik.md",
    "content": "---\ntitle: \"Epik\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: epik\ndnsprovider:\n  since:    \"v4.5.0\"\n  code:     \"epik\"\n  url:      \"https://www.epik.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/epik/epik.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Epik](https://www.epik.com/).\n\n\n<!--more-->\n\n- Code: `epik`\n- Since: v4.5.0\n\n\nHere is an example bash command using the Epik provider:\n\n```bash\nEPIK_SIGNATURE=xxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns epik -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `EPIK_SIGNATURE` | Epik API signature (https://registrar.epik.com/account/api-settings/) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `EPIK_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `EPIK_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `EPIK_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `EPIK_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://docs-userapi.epik.com/v2/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/epik/epik.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_eurodns.md",
    "content": "---\ntitle: \"EuroDNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: eurodns\ndnsprovider:\n  since:    \"v4.33.0\"\n  code:     \"eurodns\"\n  url:      \"https://www.eurodns.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/eurodns/eurodns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [EuroDNS](https://www.eurodns.com/).\n\n\n<!--more-->\n\n- Code: `eurodns`\n- Since: v4.33.0\n\n\nHere is an example bash command using the EuroDNS provider:\n\n```bash\nEURODNS_APP_ID=\"xxx\" \\\nEURODNS_API_KEY=\"yyy\" \\\nlego --dns eurodns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `EURODNS_API_KEY` | API key |\n| `EURODNS_APP_ID` | Application ID |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `EURODNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `EURODNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `EURODNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `EURODNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://docapi.eurodns.com/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/eurodns/eurodns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_excedo.md",
    "content": "---\ntitle: \"Excedo\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: excedo\ndnsprovider:\n  since:    \"v4.33.0\"\n  code:     \"excedo\"\n  url:      \"https://excedo.se/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/excedo/excedo.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Excedo](https://excedo.se/).\n\n\n<!--more-->\n\n- Code: `excedo`\n- Since: v4.33.0\n\n\nHere is an example bash command using the Excedo provider:\n\n```bash\nEXCEDO_API_KEY=your-api-key \\\nEXCEDO_API_URL=your-base-url \\\nlego --dns excedo -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `EXCEDO_API_KEY` | API key |\n| `EXCEDO_API_URL` | API base URL |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `EXCEDO_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `EXCEDO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `EXCEDO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |\n| `EXCEDO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](none)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/excedo/excedo.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_exec.md",
    "content": "---\ntitle: \"External program\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: exec\ndnsprovider:\n  since:    \"v0.5.0\"\n  code:     \"exec\"\n  url:      \"/dns/exec\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/exec/exec.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\nSolving the DNS-01 challenge using an external program.\n\n\n<!--more-->\n\n- Code: `exec`\n- Since: v0.5.0\n\n\nHere is an example bash command using the External program provider:\n\n```bash\nEXEC_PATH=/the/path/to/myscript.sh \\\nlego --dns exec -d '*.example.com' -d example.com run\n```\n\n\n\n\n\n## Base Configuration\n\n| Environment Variable Name | Description                           |\n|---------------------------|---------------------------------------|\n| `EXEC_MODE`               | `RAW`, none                           |\n| `EXEC_PATH`               | The path of the the external program. |\n\n\n## Additional Configuration\n\n| Environment Variable Name  | Description                                                        |\n|----------------------------|--------------------------------------------------------------------|\n| `EXEC_POLLING_INTERVAL`    | Time between DNS propagation check in seconds (Default: 3).        |\n| `EXEC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60). |\n| `EXEC_SEQUENCE_INTERVAL`   | Time between sequential requests in seconds (Default: 60).         |\n\n\n## Description\n\nThe file name of the external program is specified in the environment variable `EXEC_PATH`.\n\nWhen it is run by lego, three command-line parameters are passed to it:\nThe action (\"present\" or \"cleanup\"), the fully-qualified domain name and the value for the record.\n\nFor example, requesting a certificate for the domain 'my.example.org' can be achieved by calling lego as follows:\n\n```bash\nEXEC_PATH=./update-dns.sh \\\nlego --dns exec --d my.example.org run\n```\n\nIt will then call the program './update-dns.sh' with like this:\n\n```bash\n./update-dns.sh \"present\" \"_acme-challenge.my.example.org.\" \"MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI\"\n```\n\nThe program then needs to make sure the record is inserted.\nWhen it returns an error via a non-zero exit code, lego aborts.\n\nWhen the record is to be removed again,\nthe program is called with the first command-line parameter set to `cleanup` instead of `present`.\n\nIf you want to use the raw domain, token, and keyAuth values with your program, you can set `EXEC_MODE=RAW`:\n\n```bash\nEXEC_MODE=RAW \\\nEXEC_PATH=./update-dns.sh \\\nlego --dns exec -d my.example.org run\n```\n\nIt will then call the program `./update-dns.sh` like this:\n\n```bash\n./update-dns.sh \"present\" \"--\" \"my.example.org.\" \"some-token\" \"KxAy-J3NwUmg9ZQuM-gP_Mq1nStaYSaP9tYQs5_-YsE.ksT-qywTd8058G-SHHWA3RAN72Pr0yWtPYmmY5UBpQ8\"\n```\n\n## Commands\n\n{{% notice note %}}\nThe `--` is because the token MAY start with a `-`, and the called program may try and interpret a `-` as indicating a flag.\nIn the case of urfave, which is commonly used,\nyou can use the `--` delimiter to specify the start of positional arguments, and handle such a string safely.\n{{% /notice %}}\n\n### Present\n\n| Mode    | Command                                            |\n|---------|----------------------------------------------------|\n| default | `myprogram present <FQDN> <record>`                |\n| `RAW`   | `myprogram present -- <domain> <token> <key_auth>` |\n\n### Cleanup\n\n| Mode    | Command                                            |\n|---------|----------------------------------------------------|\n| default | `myprogram cleanup <FQDN> <record>`                |\n| `RAW`   | `myprogram cleanup -- <domain> <token> <key_auth>` |\n\n\n\n\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/exec/exec.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_exoscale.md",
    "content": "---\ntitle: \"Exoscale\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: exoscale\ndnsprovider:\n  since:    \"v0.4.0\"\n  code:     \"exoscale\"\n  url:      \"https://www.exoscale.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/exoscale/exoscale.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Exoscale](https://www.exoscale.com/).\n\n\n<!--more-->\n\n- Code: `exoscale`\n- Since: v0.4.0\n\n\nHere is an example bash command using the Exoscale provider:\n\n```bash\nEXOSCALE_API_KEY=abcdefghijklmnopqrstuvwx \\\nEXOSCALE_API_SECRET=xxxxxxx \\\nlego --dns exoscale -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `EXOSCALE_API_KEY` | API key |\n| `EXOSCALE_API_SECRET` | API secret |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `EXOSCALE_ENDPOINT` | API endpoint URL |\n| `EXOSCALE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) |\n| `EXOSCALE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `EXOSCALE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `EXOSCALE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://openapi-v2.exoscale.com/#endpoint-dns)\n- [Go client](https://github.com/exoscale/egoscale)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/exoscale/exoscale.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_f5xc.md",
    "content": "---\ntitle: \"F5 XC\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: f5xc\ndnsprovider:\n  since:    \"v4.23.0\"\n  code:     \"f5xc\"\n  url:      \"https://www.f5.com/products/distributed-cloud-services\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/f5xc/f5xc.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [F5 XC](https://www.f5.com/products/distributed-cloud-services).\n\n\n<!--more-->\n\n- Code: `f5xc`\n- Since: v4.23.0\n\n\nHere is an example bash command using the F5 XC provider:\n\n```bash\nF5XC_API_TOKEN=\"xxx\" \\\nF5XC_TENANT_NAME=\"yyy\" \\\nF5XC_GROUP_NAME=\"zzz\" \\\nlego --dns f5xc -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `F5XC_API_TOKEN` | API token |\n| `F5XC_GROUP_NAME` | Group name |\n| `F5XC_TENANT_NAME` | XC Tenant shortname |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `F5XC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `F5XC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `F5XC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `F5XC_SERVER` | Server domain (Default: console.ves.volterra.io) |\n| `F5XC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/f5xc/f5xc.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_freemyip.md",
    "content": "---\ntitle: \"freemyip.com\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: freemyip\ndnsprovider:\n  since:    \"v4.5.0\"\n  code:     \"freemyip\"\n  url:      \"https://freemyip.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/freemyip/freemyip.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [freemyip.com](https://freemyip.com/).\n\n\n<!--more-->\n\n- Code: `freemyip`\n- Since: v4.5.0\n\n\nHere is an example bash command using the freemyip.com provider:\n\n```bash\nFREEMYIP_TOKEN=xxxxxx \\\nlego --dns freemyip -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `FREEMYIP_TOKEN` | Account token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `FREEMYIP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `FREEMYIP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `FREEMYIP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `FREEMYIP_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n| `FREEMYIP_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://freemyip.com/help)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/freemyip/freemyip.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_gandi.md",
    "content": "---\ntitle: \"Gandi\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: gandi\ndnsprovider:\n  since:    \"v0.3.0\"\n  code:     \"gandi\"\n  url:      \"https://www.gandi.net\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/gandi/gandi.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Gandi](https://www.gandi.net).\n\n\n<!--more-->\n\n- Code: `gandi`\n- Since: v0.3.0\n\n\nHere is an example bash command using the Gandi provider:\n\n```bash\nGANDI_API_KEY=abcdefghijklmnopqrstuvwx \\\nlego --dns gandi -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `GANDI_API_KEY` | API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `GANDI_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) |\n| `GANDI_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 60) |\n| `GANDI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 2400) |\n| `GANDI_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://doc.rpc.gandi.net/index.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/gandi/gandi.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_gandiv5.md",
    "content": "---\ntitle: \"Gandi Live DNS (v5)\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: gandiv5\ndnsprovider:\n  since:    \"v0.5.0\"\n  code:     \"gandiv5\"\n  url:      \"https://www.gandi.net\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/gandiv5/gandiv5.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Gandi Live DNS (v5)](https://www.gandi.net).\n\n\n<!--more-->\n\n- Code: `gandiv5`\n- Since: v0.5.0\n\n\nHere is an example bash command using the Gandi Live DNS (v5) provider:\n\n```bash\nGANDIV5_PERSONAL_ACCESS_TOKEN=abcdefghijklmnopqrstuvwx \\\nlego --dns gandiv5 -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `GANDIV5_API_KEY` | API key (Deprecated) |\n| `GANDIV5_PERSONAL_ACCESS_TOKEN` | Personal Access Token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `GANDIV5_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `GANDIV5_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 20) |\n| `GANDIV5_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 1200) |\n| `GANDIV5_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.gandi.net/docs/livedns/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/gandiv5/gandiv5.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_gcloud.md",
    "content": "---\ntitle: \"Google Cloud\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: gcloud\ndnsprovider:\n  since:    \"v0.3.0\"\n  code:     \"gcloud\"\n  url:      \"https://cloud.google.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/gcloud/gcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Google Cloud](https://cloud.google.com).\n\n\n<!--more-->\n\n- Code: `gcloud`\n- Since: v0.3.0\n\n\nHere is an example bash command using the Google Cloud provider:\n\n```bash\n# Using a service account file\nGCE_PROJECT=\"gc-project-id\" \\\nGCE_SERVICE_ACCOUNT_FILE=\"/path/to/svc/account/file.json\" \\\nlego --dns gcloud -d '*.example.com' -d example.com run\n\n# Using default credentials with impersonation\nGCE_PROJECT=\"gc-project-id\" \\\nGCE_IMPERSONATE_SERVICE_ACCOUNT=\"target-sa@gc-project-id.iam.gserviceaccount.com\" \\\nlego --dns gcloud -d '*.example.com' -d example.com run\n\n# Using service account key with impersonation\nGCE_PROJECT=\"gc-project-id\" \\\nGCE_SERVICE_ACCOUNT_FILE=\"/path/to/svc/account/file.json\" \\\nGCE_IMPERSONATE_SERVICE_ACCOUNT=\"target-sa@gc-project-id.iam.gserviceaccount.com\" \\\nlego --dns gcloud -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `Application Default Credentials` | [Documentation](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application) |\n| `GCE_PROJECT` | Project name (by default, the project name is auto-detected by using the metadata service) |\n| `GCE_SERVICE_ACCOUNT` | Account |\n| `GCE_SERVICE_ACCOUNT_FILE` | Account file path |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `GCE_ALLOW_PRIVATE_ZONE` | Allows requested domain to be in private DNS zone, works only with a private ACME server (by default: false) |\n| `GCE_IMPERSONATE_SERVICE_ACCOUNT` | Service account email to impersonate |\n| `GCE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) |\n| `GCE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 180) |\n| `GCE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n| `GCE_ZONE_ID` | Allows to skip the automatic detection of the zone |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\nSupports service account impersonation to access Google Cloud DNS resources across different projects or with restricted permissions.\n\nWhen using impersonation, the source service account must have:\n1. The \"Service Account Token Creator\" role on the source service account\n2. The \"https://www.googleapis.com/auth/cloud-platform\" scope\n\n\n\n## More information\n\n- [API documentation](https://cloud.google.com/dns/api/v1/)\n- [Go client](https://github.com/googleapis/google-api-go-client)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/gcloud/gcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_gcore.md",
    "content": "---\ntitle: \"G-Core\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: gcore\ndnsprovider:\n  since:    \"v4.5.0\"\n  code:     \"gcore\"\n  url:      \"https://gcore.com/dns/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/gcore/gcore.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [G-Core](https://gcore.com/dns/).\n\n\n<!--more-->\n\n- Code: `gcore`\n- Since: v4.5.0\n\n\nHere is an example bash command using the G-Core provider:\n\n```bash\nGCORE_PERMANENT_API_TOKEN=xxxxx \\\nlego --dns gcore -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `GCORE_PERMANENT_API_TOKEN` | Permanent API token (https://gcore.com/blog/permanent-api-token-explained/) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `GCORE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `GCORE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 20) |\n| `GCORE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 360) |\n| `GCORE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.gcore.com/docs/dns#tag/zones)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/gcore/gcore.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_gigahostno.md",
    "content": "---\ntitle: \"Gigahost.no\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: gigahostno\ndnsprovider:\n  since:    \"v4.29.0\"\n  code:     \"gigahostno\"\n  url:      \"https://gigahost.no/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/gigahostno/gigahostno.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Gigahost.no](https://gigahost.no/).\n\n\n<!--more-->\n\n- Code: `gigahostno`\n- Since: v4.29.0\n\n\nHere is an example bash command using the Gigahost.no provider:\n\n```bash\nGIGAHOSTNO_USERNAME=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nGIGAHOSTNO_PASSWORD=\"yyyyyyyyyyyyyyyyyyyyy\" \\\nlego --dns gigahostno -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `GIGAHOSTNO_PASSWORD` | Password |\n| `GIGAHOSTNO_USERNAME` | Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `GIGAHOSTNO_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `GIGAHOSTNO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `GIGAHOSTNO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `GIGAHOSTNO_SECRET` | TOTP secret |\n| `GIGAHOSTNO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://gigahost.no/api-dokumentasjon)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/gigahostno/gigahostno.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_glesys.md",
    "content": "---\ntitle: \"Glesys\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: glesys\ndnsprovider:\n  since:    \"v0.5.0\"\n  code:     \"glesys\"\n  url:      \"https://glesys.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/glesys/glesys.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Glesys](https://glesys.com/).\n\n\n<!--more-->\n\n- Code: `glesys`\n- Since: v0.5.0\n\n\nHere is an example bash command using the Glesys provider:\n\n```bash\nGLESYS_API_USER=xxxxx \\\nGLESYS_API_KEY=yyyyy \\\nlego --dns glesys -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `GLESYS_API_KEY` | API key |\n| `GLESYS_API_USER` | API user |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `GLESYS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `GLESYS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 20) |\n| `GLESYS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 1200) |\n| `GLESYS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://github.com/GleSYS/API/wiki/API-Documentation)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/glesys/glesys.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_godaddy.md",
    "content": "---\ntitle: \"Go Daddy\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: godaddy\ndnsprovider:\n  since:    \"v0.5.0\"\n  code:     \"godaddy\"\n  url:      \"https://godaddy.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/godaddy/godaddy.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Go Daddy](https://godaddy.com).\n\n\n<!--more-->\n\n- Code: `godaddy`\n- Since: v0.5.0\n\n\nHere is an example bash command using the Go Daddy provider:\n\n```bash\nGODADDY_API_KEY=xxxxxxxx \\\nGODADDY_API_SECRET=yyyyyyyy \\\nlego --dns godaddy -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `GODADDY_API_KEY` | API key |\n| `GODADDY_API_SECRET` | API secret |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `GODADDY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `GODADDY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `GODADDY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `GODADDY_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\nGoDaddy has recently (2024-04) updated the account requirements to access parts of their production Domains API:\n\n- Availability API: Limited to accounts with 50 or more domains.\n- Management and DNS APIs: Limited to accounts with 10 or more domains and/or an active Discount Domain Club plan.\n\nhttps://community.letsencrypt.org/t/getting-unauthorized-url-error-while-trying-to-get-cert-for-subdomains/217329/12\n\n\n\n## More information\n\n- [API documentation](https://developer.godaddy.com/doc/endpoint/domains)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/godaddy/godaddy.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_googledomains.md",
    "content": "---\ntitle: \"Google Domains\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: googledomains\ndnsprovider:\n  since:    \"v4.11.0\"\n  code:     \"googledomains\"\n  url:      \"https://github.com/go-acme/lego/issues/2553\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/googledomains/googledomains.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\nThe Google Domains DNS provider has shut down.\n\n\n\n<!--more-->\n\n- Code: `googledomains`\n- Since: v4.11.0\n\n\nHere is an example bash command using the Google Domains provider:\n\n```bash\nGOOGLE_DOMAINS_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns googledomains -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `GOOGLE_DOMAINS_ACCESS_TOKEN` | Access token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `GOOGLE_DOMAINS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `GOOGLE_DOMAINS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `GOOGLE_DOMAINS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n\n- [Go client](https://github.com/googleapis/google-api-go-client)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/googledomains/googledomains.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_gravity.md",
    "content": "---\ntitle: \"Gravity\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: gravity\ndnsprovider:\n  since:    \"v4.30.0\"\n  code:     \"gravity\"\n  url:      \"https://gravity.beryju.io/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/gravity/gravity.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Gravity](https://gravity.beryju.io/).\n\n\n<!--more-->\n\n- Code: `gravity`\n- Since: v4.30.0\n\n\nHere is an example bash command using the Gravity provider:\n\n```bash\nGRAVITY_SERVER_URL=\"https://example.org:1234\" \\\nGRAVITY_USERNAME=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nGRAVITY_PASSWORD=\"yyyyyyyyyyyyyyyyyyyyy\" \\\nlego --dns gravity -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `GRAVITY_PASSWORD` | Password |\n| `GRAVITY_SERVER_URL` | URL of the server |\n| `GRAVITY_USERNAME` | Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `GRAVITY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `GRAVITY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `GRAVITY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `GRAVITY_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 1) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://gravity.beryju.io/docs/api/reference/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/gravity/gravity.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_hetzner.md",
    "content": "---\ntitle: \"Hetzner\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: hetzner\ndnsprovider:\n  since:    \"v3.7.0\"\n  code:     \"hetzner\"\n  url:      \"https://hetzner.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/hetzner/hetzner.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Hetzner](https://hetzner.com).\n\n\n<!--more-->\n\n- Code: `hetzner`\n- Since: v3.7.0\n\n\nHere is an example bash command using the Hetzner provider:\n\n```bash\nHETZNER_API_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns hetzner -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `HETZNER_API_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `HETZNER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `HETZNER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `HETZNER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `HETZNER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://docs.hetzner.cloud/reference/cloud#dns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/hetzner/hetzner.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_hostingde.md",
    "content": "---\ntitle: \"Hosting.de\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: hostingde\ndnsprovider:\n  since:    \"v1.1.0\"\n  code:     \"hostingde\"\n  url:      \"https://www.hosting.de/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/hostingde/hostingde.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Hosting.de](https://www.hosting.de/).\n\n\n<!--more-->\n\n- Code: `hostingde`\n- Since: v1.1.0\n\n\nHere is an example bash command using the Hosting.de provider:\n\n```bash\nHOSTINGDE_API_KEY=xxxxxxxx \\\nlego --dns hostingde -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `HOSTINGDE_API_KEY` | API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `HOSTINGDE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `HOSTINGDE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `HOSTINGDE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `HOSTINGDE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n| `HOSTINGDE_ZONE_NAME` | Zone name in ACE format |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.hosting.de/api/#dns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/hostingde/hostingde.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_hostinger.md",
    "content": "---\ntitle: \"Hostinger\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: hostinger\ndnsprovider:\n  since:    \"v4.27.0\"\n  code:     \"hostinger\"\n  url:      \"https://www.hostinger.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/hostinger/hostinger.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Hostinger](https://www.hostinger.com/).\n\n\n<!--more-->\n\n- Code: `hostinger`\n- Since: v4.27.0\n\n\nHere is an example bash command using the Hostinger provider:\n\n```bash\nHOSTINGER_API_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns hostinger -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `HOSTINGER_API_TOKEN` | API Token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `HOSTINGER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `HOSTINGER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `HOSTINGER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `HOSTINGER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developers.hostinger.com/#tag/dns-zone)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/hostinger/hostinger.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_hostingnl.md",
    "content": "---\ntitle: \"Hosting.nl\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: hostingnl\ndnsprovider:\n  since:    \"v4.30.0\"\n  code:     \"hostingnl\"\n  url:      \"https://hosting.nl\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/hostingnl/hostingnl.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Hosting.nl](https://hosting.nl).\n\n\n<!--more-->\n\n- Code: `hostingnl`\n- Since: v4.30.0\n\n\nHere is an example bash command using the Hosting.nl provider:\n\n```bash\nHOSTINGNL_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns hostingnl -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `HOSTINGNL_API_KEY` | The API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `HOSTINGNL_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `HOSTINGNL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `HOSTINGNL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `HOSTINGNL_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.hosting.nl/api/documentation)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/hostingnl/hostingnl.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_hosttech.md",
    "content": "---\ntitle: \"Hosttech\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: hosttech\ndnsprovider:\n  since:    \"v4.5.0\"\n  code:     \"hosttech\"\n  url:      \"https://www.hosttech.eu/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/hosttech/hosttech.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Hosttech](https://www.hosttech.eu/).\n\n\n<!--more-->\n\n- Code: `hosttech`\n- Since: v4.5.0\n\n\nHere is an example bash command using the Hosttech provider:\n\n```bash\nHOSTTECH_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns hosttech -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `HOSTTECH_API_KEY` | API login |\n| `HOSTTECH_PASSWORD` | API password |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `HOSTTECH_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `HOSTTECH_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `HOSTTECH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `HOSTTECH_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.ns1.hosttech.eu/api/documentation)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/hosttech/hosttech.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_httpnet.md",
    "content": "---\ntitle: \"http.net\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: httpnet\ndnsprovider:\n  since:    \"v4.15.0\"\n  code:     \"httpnet\"\n  url:      \"https://www.http.net/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/httpnet/httpnet.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [http.net](https://www.http.net/).\n\n\n<!--more-->\n\n- Code: `httpnet`\n- Since: v4.15.0\n\n\nHere is an example bash command using the http.net provider:\n\n```bash\nHTTPNET_API_KEY=xxxxxxxx \\\nlego --dns httpnet -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `HTTPNET_API_KEY` | API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `HTTPNET_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `HTTPNET_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `HTTPNET_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `HTTPNET_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n| `HTTPNET_ZONE_NAME` | Zone name in ACE format |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.http.net/docs/api/#dns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/httpnet/httpnet.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_httpreq.md",
    "content": "---\ntitle: \"HTTP request\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: httpreq\ndnsprovider:\n  since:    \"v2.0.0\"\n  code:     \"httpreq\"\n  url:      \"/lego/dns/httpreq/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/httpreq/httpreq.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [HTTP request](/lego/dns/httpreq/).\n\n\n<!--more-->\n\n- Code: `httpreq`\n- Since: v2.0.0\n\n\nHere is an example bash command using the HTTP request provider:\n\n```bash\nHTTPREQ_ENDPOINT=http://my.server.com:9090 \\\nlego --dns httpreq -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `HTTPREQ_ENDPOINT` | The URL of the server |\n| `HTTPREQ_MODE` | `RAW`, none |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `HTTPREQ_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `HTTPREQ_PASSWORD` | Basic authentication password |\n| `HTTPREQ_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `HTTPREQ_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `HTTPREQ_USERNAME` | Basic authentication username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Description\n\nThe server must provide:\n\n- `POST` `/present`\n- `POST` `/cleanup`\n\nThe URL of the server must be defined by `HTTPREQ_ENDPOINT`.\n\n### Mode\n\nThere are 2 modes (`HTTPREQ_MODE`):\n\n- default mode:\n```json\n{\n  \"fqdn\": \"_acme-challenge.domain.\",\n  \"value\": \"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM\"\n}\n```\n\n- `RAW`\n```json\n{\n  \"domain\": \"domain\",\n  \"token\": \"token\",\n  \"keyAuth\": \"key\"\n}\n```\n\n### Authentication\n\nBasic authentication (optional) can be set with some environment variables:\n\n- `HTTPREQ_USERNAME` and `HTTPREQ_PASSWORD`\n- both values must be set, otherwise basic authentication is not defined.\n\n\n\n\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/httpreq/httpreq.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_huaweicloud.md",
    "content": "---\ntitle: \"Huawei Cloud\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: huaweicloud\ndnsprovider:\n  since:    \"v4.19\"\n  code:     \"huaweicloud\"\n  url:      \"https://huaweicloud.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/huaweicloud/huaweicloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Huawei Cloud](https://huaweicloud.com).\n\n\n<!--more-->\n\n- Code: `huaweicloud`\n- Since: v4.19\n\n\nHere is an example bash command using the Huawei Cloud provider:\n\n```bash\nHUAWEICLOUD_ACCESS_KEY_ID=your-access-key-id \\\nHUAWEICLOUD_SECRET_ACCESS_KEY=your-secret-access-key \\\nHUAWEICLOUD_REGION=cn-south-1 \\\nlego --dns huaweicloud -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `HUAWEICLOUD_ACCESS_KEY_ID` | Access key ID |\n| `HUAWEICLOUD_REGION` | Region |\n| `HUAWEICLOUD_SECRET_ACCESS_KEY` | Access Key secret |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `HUAWEICLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `HUAWEICLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `HUAWEICLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `HUAWEICLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://console-intl.huaweicloud.com/apiexplorer/#/openapi/DNS/doc?locale=en-us)\n- [Go client](https://github.com/huaweicloud/huaweicloud-sdk-go-v3)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/huaweicloud/huaweicloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_hurricane.md",
    "content": "---\ntitle: \"Hurricane Electric DNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: hurricane\ndnsprovider:\n  since:    \"v4.3.0\"\n  code:     \"hurricane\"\n  url:      \"https://dns.he.net/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/hurricane/hurricane.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Hurricane Electric DNS](https://dns.he.net/).\n\n\n<!--more-->\n\n- Code: `hurricane`\n- Since: v4.3.0\n\n\nHere is an example bash command using the Hurricane Electric DNS provider:\n\n```bash\nHURRICANE_TOKENS=example.org:token \\\nlego --dns hurricane -d '*.example.com' -d example.com run\n\nHURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2 \\\nlego --dns hurricane -d my.example.org -d demo.example.org\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `HURRICANE_TOKENS` | TXT record names and tokens |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `HURRICANE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `HURRICANE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `HURRICANE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation (Default: 300) |\n| `HURRICANE_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\nBefore using lego to request a certificate for a given domain or wildcard (such as `my.example.org` or `*.my.example.org`),\ncreate a TXT record named `_acme-challenge.my.example.org`, and enable dynamic updates on it.\nGenerate a token for each URL with Hurricane Electric's UI, and copy it down.\nStick to alphanumeric tokens for greatest reliability.\n\nTo authenticate with the Hurricane Electric API,\nadd each record name/token pair you want to update to the `HURRICANE_TOKENS` environment variable, as shown in the examples.\nRecord names (without the `_acme-challenge.` component) and their tokens are separated with colons,\nwhile the credential pairs are concatenated into a comma-separated list, like so:\n\n```\nHURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2\n```\n\nIf you are issuing both a wildcard certificate and a standard certificate for a given subdomain,\nyou should not have repeat entries for that name, as both will use the same credential.\n\n```\nHURRICANE_TOKENS=example.org:token\n```\n\n\n\n## More information\n\n- [API documentation](https://dns.he.net/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/hurricane/hurricane.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_hyperone.md",
    "content": "---\ntitle: \"HyperOne\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: hyperone\ndnsprovider:\n  since:    \"v3.9.0\"\n  code:     \"hyperone\"\n  url:      \"https://www.hyperone.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/hyperone/hyperone.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [HyperOne](https://www.hyperone.com).\n\n\n<!--more-->\n\n- Code: `hyperone`\n- Since: v3.9.0\n\n\nHere is an example bash command using the HyperOne provider:\n\n```bash\nlego --dns hyperone -d '*.example.com' -d example.com run\n```\n\n\n\n\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `HYPERONE_API_URL` | Allows to pass custom API Endpoint to be used in the challenge (default https://api.hyperone.com/v2) |\n| `HYPERONE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `HYPERONE_LOCATION_ID` | Specifies location (region) to be used in API calls. (default pl-waw-1) |\n| `HYPERONE_PASSPORT_LOCATION` | Allows to pass custom passport file location (default ~/.h1/passport.json) |\n| `HYPERONE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 60) |\n| `HYPERONE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 2) |\n| `HYPERONE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Description\n\nDefault configuration does not require any additional environment variables,\njust a passport file in `~/.h1/passport.json` location.\n\n### Generating passport file using H1 CLI\n\nTo use this application you have to generate passport file for `sa`:\n\n```\nh1 iam project sa credential generate --name my-passport --project <project ID> --sa <sa ID> --passport-output-file ~/.h1/passport.json\n```\n\n### Required permissions\n\nThe application requires following permissions:\n-  `dns/zone/list`\n-  `dns/zone.recordset/list`\n-  `dns/zone.recordset/create`\n-  `dns/zone.recordset/delete`\n-  `dns/zone.record/create`\n-  `dns/zone.record/list`\n-  `dns/zone.record/delete`\n\nAll required permissions are available via platform role `tool.lego`.\n\n\n\n## More information\n\n- [API documentation](https://api.hyperone.com/v2/docs)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/hyperone/hyperone.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_ibmcloud.md",
    "content": "---\ntitle: \"IBM Cloud (SoftLayer)\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: ibmcloud\ndnsprovider:\n  since:    \"v4.5.0\"\n  code:     \"ibmcloud\"\n  url:      \"https://www.ibm.com/cloud/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ibmcloud/ibmcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [IBM Cloud (SoftLayer)](https://www.ibm.com/cloud/).\n\n\n<!--more-->\n\n- Code: `ibmcloud`\n- Since: v4.5.0\n\n\nHere is an example bash command using the IBM Cloud (SoftLayer) provider:\n\n```bash\nSOFTLAYER_USERNAME=xxxxx \\\nSOFTLAYER_API_KEY=yyyyy \\\nlego --dns ibmcloud -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `SOFTLAYER_API_KEY` | Classic Infrastructure API key |\n| `SOFTLAYER_USERNAME` | Username (IBM Cloud is {accountID}_{emailAddress}) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `SOFTLAYER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `SOFTLAYER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `SOFTLAYER_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `SOFTLAYER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://cloud.ibm.com/docs/dns?topic=dns-getting-started-with-the-dns-api)\n- [Go client](https://github.com/softlayer/softlayer-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ibmcloud/ibmcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_iij.md",
    "content": "---\ntitle: \"Internet Initiative Japan\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: iij\ndnsprovider:\n  since:    \"v1.1.0\"\n  code:     \"iij\"\n  url:      \"https://www.iij.ad.jp/en/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/iij/iij.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Internet Initiative Japan](https://www.iij.ad.jp/en/).\n\n\n<!--more-->\n\n- Code: `iij`\n- Since: v1.1.0\n\n\nHere is an example bash command using the Internet Initiative Japan provider:\n\n```bash\nIIJ_API_ACCESS_KEY=xxxxxxxx \\\nIIJ_API_SECRET_KEY=yyyyyy \\\nIIJ_DO_SERVICE_CODE=zzzzzz \\\nlego --dns iij -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `IIJ_API_ACCESS_KEY` | API access key |\n| `IIJ_API_SECRET_KEY` | API secret key |\n| `IIJ_DO_SERVICE_CODE` | DO service code |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `IIJ_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 4) |\n| `IIJ_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 240) |\n| `IIJ_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://manual.iij.jp/p2/pubapi/)\n- [Go client](https://github.com/iij/doapi)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/iij/iij.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_iijdpf.md",
    "content": "---\ntitle: \"IIJ DNS Platform Service\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: iijdpf\ndnsprovider:\n  since:    \"v4.7.0\"\n  code:     \"iijdpf\"\n  url:      \"https://www.iij.ad.jp/en/biz/dns-pfm/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/iijdpf/iijdpf.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [IIJ DNS Platform Service](https://www.iij.ad.jp/en/biz/dns-pfm/).\n\n\n<!--more-->\n\n- Code: `iijdpf`\n- Since: v4.7.0\n\n\nHere is an example bash command using the IIJ DNS Platform Service provider:\n\n```bash\nIIJ_DPF_API_TOKEN=xxxxxxxx \\\nIIJ_DPF_DPM_SERVICE_CODE=yyyyyy \\\nlego --dns iijdpf -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `IIJ_DPF_API_TOKEN` | API token |\n| `IIJ_DPF_DPM_SERVICE_CODE` | IIJ Managed DNS Service's service code |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `IIJ_DPF_API_ENDPOINT` | API endpoint URL, defaults to https://api.dns-platform.jp/dpf/v1 |\n| `IIJ_DPF_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) |\n| `IIJ_DPF_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 660) |\n| `IIJ_DPF_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://manual.iij.jp/dpf/dpfapi/)\n- [Go client](https://github.com/mimuret/golang-iij-dpf)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/iijdpf/iijdpf.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_infoblox.md",
    "content": "---\ntitle: \"Infoblox\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: infoblox\ndnsprovider:\n  since:    \"v4.4.0\"\n  code:     \"infoblox\"\n  url:      \"https://www.infoblox.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/infoblox/infoblox.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Infoblox](https://www.infoblox.com/).\n\n\n<!--more-->\n\n- Code: `infoblox`\n- Since: v4.4.0\n\n\nHere is an example bash command using the Infoblox provider:\n\n```bash\nINFOBLOX_USERNAME=api-user-529 \\\nINFOBLOX_PASSWORD=b9841238feb177a84330febba8a83208921177bffe733 \\\nINFOBLOX_HOST=infoblox.example.org\nlego --dns infoblox -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `INFOBLOX_HOST` | Host URI |\n| `INFOBLOX_PASSWORD` | Account Password |\n| `INFOBLOX_USERNAME` | Account Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `INFOBLOX_CA_CERTIFICATE` | The path to the CA certificate (PEM encoded) |\n| `INFOBLOX_DNS_VIEW` | The view for the TXT records (Default: External) |\n| `INFOBLOX_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `INFOBLOX_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `INFOBLOX_PORT` | The port for the infoblox grid manager  (Default: 443) |\n| `INFOBLOX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `INFOBLOX_SSL_VERIFY` | Whether or not to verify the TLS certificate  (Default: true) |\n| `INFOBLOX_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n| `INFOBLOX_WAPI_VERSION` | The version of WAPI being used  (Default: 2.11) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\nWhen creating an API's user ensure it has the proper permissions for the view you are working with.\n\n\n\n## More information\n\n- [API documentation](https://your.infoblox.server/wapidoc/)\n- [Go client](https://github.com/infobloxopen/infoblox-go-client)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/infoblox/infoblox.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_infomaniak.md",
    "content": "---\ntitle: \"Infomaniak\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: infomaniak\ndnsprovider:\n  since:    \"v4.1.0\"\n  code:     \"infomaniak\"\n  url:      \"https://www.infomaniak.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/infomaniak/infomaniak.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Infomaniak](https://www.infomaniak.com/).\n\n\n<!--more-->\n\n- Code: `infomaniak`\n- Since: v4.1.0\n\n\nHere is an example bash command using the Infomaniak provider:\n\n```bash\nINFOMANIAK_ACCESS_TOKEN=1234567898765432 \\\nlego --dns infomaniak -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `INFOMANIAK_ACCESS_TOKEN` | Access token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `INFOMANIAK_ENDPOINT` | https://api.infomaniak.com |\n| `INFOMANIAK_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `INFOMANIAK_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `INFOMANIAK_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `INFOMANIAK_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Access token\n\nAccess token can be created at the url https://manager.infomaniak.com/v3/infomaniak-api.\nYou will need domain scope.\n\n\n\n## More information\n\n- [API documentation](https://api.infomaniak.com/doc)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/infomaniak/infomaniak.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_internetbs.md",
    "content": "---\ntitle: \"Internet.bs\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: internetbs\ndnsprovider:\n  since:    \"v4.5.0\"\n  code:     \"internetbs\"\n  url:      \"https://internetbs.net\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/internetbs/internetbs.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Internet.bs](https://internetbs.net).\n\n\n<!--more-->\n\n- Code: `internetbs`\n- Since: v4.5.0\n\n\nHere is an example bash command using the Internet.bs provider:\n\n```bash\nINTERNET_BS_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \\\nINTERNET_BS_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \\\nlego --dns internetbs -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `INTERNET_BS_API_KEY` | API key |\n| `INTERNET_BS_PASSWORD` | API password |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `INTERNET_BS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `INTERNET_BS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `INTERNET_BS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `INTERNET_BS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://internetbs.net/internet-bs-api.pdf)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/internetbs/internetbs.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_inwx.md",
    "content": "---\ntitle: \"INWX\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: inwx\ndnsprovider:\n  since:    \"v2.0.0\"\n  code:     \"inwx\"\n  url:      \"https://www.inwx.de/en\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/inwx/inwx.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [INWX](https://www.inwx.de/en).\n\n\n<!--more-->\n\n- Code: `inwx`\n- Since: v2.0.0\n\n\nHere is an example bash command using the INWX provider:\n\n```bash\nINWX_USERNAME=xxxxxxxxxx \\\nINWX_PASSWORD=yyyyyyyyyy \\\nlego --dns inwx -d '*.example.com' -d example.com run\n\n# 2FA\nINWX_USERNAME=xxxxxxxxxx \\\nINWX_PASSWORD=yyyyyyyyyy \\\nINWX_SHARED_SECRET=zzzzzzzzzz \\\nlego --dns inwx -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `INWX_PASSWORD` | Password |\n| `INWX_USERNAME` | Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `INWX_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `INWX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 360) |\n| `INWX_SANDBOX` | Activate the sandbox (boolean) |\n| `INWX_SHARED_SECRET` | shared secret related to 2FA |\n| `INWX_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.inwx.de/en/help/apidoc)\n- [Go client](https://github.com/nrdcg/goinwx)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/inwx/inwx.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_ionos.md",
    "content": "---\ntitle: \"Ionos\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: ionos\ndnsprovider:\n  since:    \"v4.2.0\"\n  code:     \"ionos\"\n  url:      \"https://ionos.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ionos/ionos.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Ionos](https://ionos.com).\n\n\n<!--more-->\n\n- Code: `ionos`\n- Since: v4.2.0\n\n\nHere is an example bash command using the Ionos provider:\n\n```bash\nIONOS_API_KEY=xxxxxxxx \\\nlego --dns ionos -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `IONOS_API_KEY` | API key `<prefix>.<secret>` https://developer.hosting.ionos.com/docs/getstarted |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `IONOS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `IONOS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `IONOS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 900) |\n| `IONOS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developer.hosting.ionos.com/docs/dns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ionos/ionos.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_ionoscloud.md",
    "content": "---\ntitle: \"Ionos Cloud\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: ionoscloud\ndnsprovider:\n  since:    \"v4.30.0\"\n  code:     \"ionoscloud\"\n  url:      \"https://cloud.ionos.de/network/cloud-dns\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ionoscloud/ionoscloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Ionos Cloud](https://cloud.ionos.de/network/cloud-dns).\n\n\n<!--more-->\n\n- Code: `ionoscloud`\n- Since: v4.30.0\n\n\nHere is an example bash command using the Ionos Cloud provider:\n\n```bash\nIONOSCLOUD_API_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns ionoscloud -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `IONOSCLOUD_API_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `IONOSCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `IONOSCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `IONOSCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `IONOSCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.ionos.com/docs/dns/v1/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ionoscloud/ionoscloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_ipv64.md",
    "content": "---\ntitle: \"IPv64\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: ipv64\ndnsprovider:\n  since:    \"v4.13.0\"\n  code:     \"ipv64\"\n  url:      \"https://ipv64.net/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ipv64/ipv64.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [IPv64](https://ipv64.net/).\n\n\n<!--more-->\n\n- Code: `ipv64`\n- Since: v4.13.0\n\n\nHere is an example bash command using the IPv64 provider:\n\n```bash\nIPV64_API_KEY=xxxxxx \\\nlego --dns ipv64 -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `IPV64_API_KEY` | Account API Key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `IPV64_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `IPV64_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `IPV64_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://ipv64.net/dyndns_updater_api)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ipv64/ipv64.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_ispconfig.md",
    "content": "---\ntitle: \"ISPConfig 3\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: ispconfig\ndnsprovider:\n  since:    \"v4.31.0\"\n  code:     \"ispconfig\"\n  url:      \"https://www.ispconfig.org/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ispconfig/ispconfig.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [ISPConfig 3](https://www.ispconfig.org/).\n\n\n<!--more-->\n\n- Code: `ispconfig`\n- Since: v4.31.0\n\n\nHere is an example bash command using the ISPConfig 3 provider:\n\n```bash\nISPCONFIG_SERVER_URL=\"https://example.com:8080/remote/json.php\" \\\nISPCONFIG_USERNAME=\"xxx\" \\\nISPCONFIG_PASSWORD=\"yyy\" \\\nlego --dns ispconfig -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ISPCONFIG_PASSWORD` | Password |\n| `ISPCONFIG_SERVER_URL` | Server URL |\n| `ISPCONFIG_USERNAME` | Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ISPCONFIG_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `ISPCONFIG_INSECURE_SKIP_VERIFY` | Whether to verify the API certificate |\n| `ISPCONFIG_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `ISPCONFIG_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `ISPCONFIG_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/index.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ispconfig/ispconfig.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_ispconfigddns.md",
    "content": "---\ntitle: \"ISPConfig 3 - Dynamic DNS (DDNS) Module\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: ispconfigddns\ndnsprovider:\n  since:    \"v4.31.0\"\n  code:     \"ispconfigddns\"\n  url:      \"https://www.ispconfig.org/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ispconfigddns/ispconfigddns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [ISPConfig 3 - Dynamic DNS (DDNS) Module](https://www.ispconfig.org/).\n\n\n<!--more-->\n\n- Code: `ispconfigddns`\n- Since: v4.31.0\n\n\nHere is an example bash command using the ISPConfig 3 - Dynamic DNS (DDNS) Module provider:\n\n```bash\nISPCONFIG_DDNS_SERVER_URL=\"https://panel.example.com:8080\" \\\nISPCONFIG_DDNS_TOKEN=xxxxxx \\\nlego --dns ispconfigddns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ISPCONFIG_DDNS_SERVER_URL` | API server URL (ex: https://panel.example.com:8080) |\n| `ISPCONFIG_DDNS_TOKEN` | DDNS API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ISPCONFIG_DDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `ISPCONFIG_DDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `ISPCONFIG_DDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `ISPCONFIG_DDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\nISPConfig DNS provider supports leveraging the [ISPConfig 3 Dynamic DNS (DDNS) Module](https://github.com/mhofer117/ispconfig-ddns-module).\n\nRequires the DDNS module described at https://www.ispconfig.org/ispconfig/download/\n\nSee https://www.howtoforge.com/community/threads/ispconfig-3-danymic-dns-ddns-module.87967/ for additional details.\n\n\n\n## More information\n\n- [API documentation](https://github.com/mhofer117/ispconfig-ddns-module/tree/master/lib/updater)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ispconfigddns/ispconfigddns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_iwantmyname.md",
    "content": "---\ntitle: \"iwantmyname (Deprecated)\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: iwantmyname\ndnsprovider:\n  since:    \"v4.7.0\"\n  code:     \"iwantmyname\"\n  url:      \"https://iwantmyname.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/iwantmyname/iwantmyname.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\nThe iwantmyname API has shut down.\n\nhttps://github.com/go-acme/lego/issues/2563\n\n\n\n<!--more-->\n\n- Code: `iwantmyname`\n- Since: v4.7.0\n\n\nHere is an example bash command using the iwantmyname (Deprecated) provider:\n\n```bash\nIWANTMYNAME_USERNAME=xxxxxxxx \\\nIWANTMYNAME_PASSWORD=xxxxxxxx \\\nlego --dns iwantmyname -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `IWANTMYNAME_PASSWORD` | API password |\n| `IWANTMYNAME_USERNAME` | API username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `IWANTMYNAME_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `IWANTMYNAME_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `IWANTMYNAME_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `IWANTMYNAME_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://iwantmyname.com/developer/domain-dns-api)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/iwantmyname/iwantmyname.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_jdcloud.md",
    "content": "---\ntitle: \"JD Cloud\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: jdcloud\ndnsprovider:\n  since:    \"v4.31.0\"\n  code:     \"jdcloud\"\n  url:      \"https://www.jdcloud.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/jdcloud/jdcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [JD Cloud](https://www.jdcloud.com/).\n\n\n<!--more-->\n\n- Code: `jdcloud`\n- Since: v4.31.0\n\n\nHere is an example bash command using the JD Cloud provider:\n\n```bash\nJDCLOUD_ACCESS_KEY_ID=\"xxx\" \\\nJDCLOUD_ACCESS_KEY_SECRET=\"yyy\" \\\nlego --dns jdcloud -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `JDCLOUD_ACCESS_KEY_ID` | Access key ID |\n| `JDCLOUD_ACCESS_KEY_SECRET` | Access key secret |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `JDCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `JDCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `JDCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `JDCLOUD_REGION_ID` | Region ID (Default: cn-north-1) |\n| `JDCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://docs.jdcloud.com/cn/jd-cloud-dns/api/overview)\n- [Go client](https://github.com/jdcloud-api/jdcloud-sdk-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/jdcloud/jdcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_joker.md",
    "content": "---\ntitle: \"Joker\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: joker\ndnsprovider:\n  since:    \"v2.6.0\"\n  code:     \"joker\"\n  url:      \"https://joker.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/joker/joker.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Joker](https://joker.com).\n\n\n<!--more-->\n\n- Code: `joker`\n- Since: v2.6.0\n\n\nHere is an example bash command using the Joker provider:\n\n```bash\n# SVC\nJOKER_API_MODE=SVC \\\nJOKER_USERNAME=<your email> \\\nJOKER_PASSWORD=<your password> \\\nlego --dns joker -d '*.example.com' -d example.com run\n\n# DMAPI\nJOKER_API_MODE=DMAPI \\\nJOKER_USERNAME=<your email> \\\nJOKER_PASSWORD=<your password> \\\nlego --dns joker -d '*.example.com' -d example.com run\n## or\nJOKER_API_MODE=DMAPI \\\nJOKER_API_KEY=<your API key> \\\nlego --dns joker -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `JOKER_API_KEY` | API key (only with DMAPI mode) |\n| `JOKER_API_MODE` | 'DMAPI' or 'SVC'. DMAPI is for resellers accounts. (Default: DMAPI) |\n| `JOKER_PASSWORD` | Joker.com password |\n| `JOKER_USERNAME` | Joker.com username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `JOKER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) |\n| `JOKER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `JOKER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `JOKER_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60), only with 'SVC' mode |\n| `JOKER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## SVC mode\n\nIn the SVC mode, username and passsword are not your email and account passwords, but those displayed in Joker.com domain dashboard when enabling Dynamic DNS.\n\nAs per [Joker.com documentation](https://joker.com/faq/content/6/496/en/let_s-encrypt-support.html):\n\n> 1. please log in at Joker.com, visit 'My Domains',\n>    find the domain you want to add  Let's Encrypt certificate for, and chose \"DNS\" in the menu\n>\n> 2. on the top right, you will find the setting for 'Dynamic DNS'.\n>    If not already active, please activate it.\n>    It will not affect any other already existing DNS records of this domain.\n>\n> 3. please take a note of the credentials which are now shown as 'Dynamic DNS Authentication', consisting of a 'username' and a 'password'.\n>\n> 4. this is all you have to do here - and only once per domain.\n\n\n\n## More information\n\n- [API documentation](https://joker.com/faq/category/39/22-dmapi.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/joker/joker.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_keyhelp.md",
    "content": "---\ntitle: \"KeyHelp\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: keyhelp\ndnsprovider:\n  since:    \"v4.26.0\"\n  code:     \"keyhelp\"\n  url:      \"https://www.keyweb.de/en/keyhelp/keyhelp/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/keyhelp/keyhelp.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [KeyHelp](https://www.keyweb.de/en/keyhelp/keyhelp/).\n\n\n<!--more-->\n\n- Code: `keyhelp`\n- Since: v4.26.0\n\n\nHere is an example bash command using the KeyHelp provider:\n\n```bash\nKEYHELP_BASE_URL=\"https://keyhelp.example.com\" \\\nKEYHELP_API_KEY=\"xxx\" \\\nlego --dns keyhelp -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `KEYHELP_API_KEY` | API key |\n| `KEYHELP_BASE_URL` | Server URL |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `KEYHELP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `KEYHELP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `KEYHELP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `KEYHELP_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://app.swaggerhub.com/apis-docs/keyhelp/api/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/keyhelp/keyhelp.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_leaseweb.md",
    "content": "---\ntitle: \"Leaseweb\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: leaseweb\ndnsprovider:\n  since:    \"v4.32.0\"\n  code:     \"leaseweb\"\n  url:      \"https://www.leaseweb.com/en/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/leaseweb/leaseweb.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Leaseweb](https://www.leaseweb.com/en/).\n\n\n<!--more-->\n\n- Code: `leaseweb`\n- Since: v4.32.0\n\n\nHere is an example bash command using the Leaseweb provider:\n\n```bash\nLEASEWEB_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns leaseweb -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `LEASEWEB_API_KEY` | API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `LEASEWEB_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `LEASEWEB_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `LEASEWEB_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `LEASEWEB_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developer.leaseweb.com/docs/#tag/DNS)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/leaseweb/leaseweb.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_liara.md",
    "content": "---\ntitle: \"Liara\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: liara\ndnsprovider:\n  since:    \"v4.10.0\"\n  code:     \"liara\"\n  url:      \"https://liara.ir\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/liara/liara.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Liara](https://liara.ir).\n\n\n<!--more-->\n\n- Code: `liara`\n- Since: v4.10.0\n\n\nHere is an example bash command using the Liara provider:\n\n```bash\nLIARA_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns liara -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `LIARA_API_KEY` | The API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `LIARA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `LIARA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `LIARA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `LIARA_TEAM_ID` | The team ID to access services in a team |\n| `LIARA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://openapi.liara.ir/?urls.primaryName=DNS)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/liara/liara.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_lightsail.md",
    "content": "---\ntitle: \"Amazon Lightsail\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: lightsail\ndnsprovider:\n  since:    \"v0.5.0\"\n  code:     \"lightsail\"\n  url:      \"https://aws.amazon.com/lightsail/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/lightsail/lightsail.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Amazon Lightsail](https://aws.amazon.com/lightsail/).\n\n\n<!--more-->\n\n- Code: `lightsail`\n- Since: v0.5.0\n\n\n{{% notice note %}}\n_Please contribute by adding a CLI example._\n{{% /notice %}}\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `AWS_ACCESS_KEY_ID` | Managed by the AWS client. Access key ID (`AWS_ACCESS_KEY_ID_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead) |\n| `AWS_SECRET_ACCESS_KEY` | Managed by the AWS client. Secret access key (`AWS_SECRET_ACCESS_KEY_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead) |\n| `DNS_ZONE` | Domain name of the DNS zone |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `AWS_SHARED_CREDENTIALS_FILE` | Managed by the AWS client. Shared credentials file. |\n| `LIGHTSAIL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `LIGHTSAIL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Description\n\nAWS Credentials are automatically detected in the following locations and prioritized in the following order:\n\n1. Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, [`AWS_SESSION_TOKEN`]\n2. Shared credentials file (defaults to `~/.aws/credentials`, profiles can be specified using `AWS_PROFILE`)\n3. Amazon EC2 IAM role\n\nAWS region is not required to set as the Lightsail DNS zone is in global (us-east-1) region.\n\n## Policy\n\nThe following AWS IAM policy document describes the minimum permissions required for lego to complete the DNS challenge.\n\n```json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"lightsail:DeleteDomainEntry\",\n        \"lightsail:CreateDomainEntry\"\n      ],\n      \"Resource\": \"<Lightsail DNS zone ARN>\"\n    }\n  ]\n}\n```\n\nReplace the `Resource` value with your Lightsail DNS zone ARN.\nYou can retrieve the ARN using aws cli by running `aws lightsail get-domains --region us-east-1` (Lightsail web console does not show the ARN, unfortunately).\nIt should be in the format of `arn:aws:lightsail:global:<ACCOUNT ID>:Domain/<DOMAIN ID>`.\nYou also need to replace the region in the ARN to `us-east-1` (instead of `global`).\n\nAlternatively, you can also set the `Resource` to `*` (wildcard), which allow to access all domain, but this is not recommended.\n\n\n\n## More information\n\n\n- [Go client](https://github.com/aws/aws-sdk-go-v2)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/lightsail/lightsail.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_limacity.md",
    "content": "---\ntitle: \"Lima-City\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: limacity\ndnsprovider:\n  since:    \"v4.18.0\"\n  code:     \"limacity\"\n  url:      \"https://www.lima-city.de\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/limacity/limacity.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Lima-City](https://www.lima-city.de).\n\n\n<!--more-->\n\n- Code: `limacity`\n- Since: v4.18.0\n\n\nHere is an example bash command using the Lima-City provider:\n\n```bash\nLIMACITY_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns limacity -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `LIMACITY_API_KEY` | The API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `LIMACITY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `LIMACITY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 80) |\n| `LIMACITY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 480) |\n| `LIMACITY_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 90) |\n| `LIMACITY_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.lima-city.de/hilfe/lima-city-api)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/limacity/limacity.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_linode.md",
    "content": "---\ntitle: \"Linode (v4)\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: linode\ndnsprovider:\n  since:    \"v1.1.0\"\n  code:     \"linode\"\n  url:      \"https://www.linode.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/linode/linode.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Linode (v4)](https://www.linode.com/).\n\n\n<!--more-->\n\n- Code: `linode`\n- Since: v1.1.0\n\n\nHere is an example bash command using the Linode (v4) provider:\n\n```bash\nLINODE_TOKEN=xxxxx \\\nlego --dns linode -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `LINODE_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `LINODE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `LINODE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 15) |\n| `LINODE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `LINODE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developers.linode.com/api/v4)\n- [Go client](https://github.com/linode/linodego)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/linode/linode.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_liquidweb.md",
    "content": "---\ntitle: \"Liquid Web\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: liquidweb\ndnsprovider:\n  since:    \"v3.1.0\"\n  code:     \"liquidweb\"\n  url:      \"https://liquidweb.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/liquidweb/liquidweb.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Liquid Web](https://liquidweb.com).\n\n\n<!--more-->\n\n- Code: `liquidweb`\n- Since: v3.1.0\n\n\nHere is an example bash command using the Liquid Web provider:\n\n```bash\nLWAPI_USERNAME=someuser \\\nLWAPI_PASSWORD=somepass \\\nlego --dns liquidweb -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `LWAPI_PASSWORD` | Liquid Web API Password |\n| `LWAPI_USERNAME` | Liquid Web API Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `LWAPI_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) |\n| `LWAPI_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `LWAPI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `LWAPI_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n| `LWAPI_URL` | Liquid Web API endpoint |\n| `LWAPI_ZONE` | DNS Zone |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.liquidweb.com/docs/)\n- [Go client](https://github.com/liquidweb/liquidweb-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/liquidweb/liquidweb.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_loopia.md",
    "content": "---\ntitle: \"Loopia\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: loopia\ndnsprovider:\n  since:    \"v4.2.0\"\n  code:     \"loopia\"\n  url:      \"https://loopia.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/loopia/loopia.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Loopia](https://loopia.com).\n\n\n<!--more-->\n\n- Code: `loopia`\n- Since: v4.2.0\n\n\nHere is an example bash command using the Loopia provider:\n\n```bash\nLOOPIA_API_USER=xxxxxxxx \\\nLOOPIA_API_PASSWORD=yyyyyyyy \\\nlego --dns loopia -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `LOOPIA_API_PASSWORD` | API password |\n| `LOOPIA_API_USER` | API username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `LOOPIA_API_URL` | API endpoint. Ex: https://api.loopia.se/RPCSERV or https://api.loopia.rs/RPCSERV |\n| `LOOPIA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) |\n| `LOOPIA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2400) |\n| `LOOPIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `LOOPIA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n### API user\n\nYou can [generate a new API user](https://customerzone.loopia.com/api/) from your account page.\n\nIt needs to have the following permissions:\n\n* addZoneRecord\n* getZoneRecords\n* removeZoneRecord\n* removeSubdomain\n\n\n\n## More information\n\n- [API documentation](https://www.loopia.com/api)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/loopia/loopia.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_luadns.md",
    "content": "---\ntitle: \"LuaDNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: luadns\ndnsprovider:\n  since:    \"v3.7.0\"\n  code:     \"luadns\"\n  url:      \"https://luadns.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/luadns/luadns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [LuaDNS](https://luadns.com).\n\n\n<!--more-->\n\n- Code: `luadns`\n- Since: v3.7.0\n\n\nHere is an example bash command using the LuaDNS provider:\n\n```bash\nLUADNS_API_USERNAME=youremail \\\nLUADNS_API_TOKEN=xxxxxxxx \\\nlego --dns luadns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `LUADNS_API_TOKEN` | API token |\n| `LUADNS_API_USERNAME` | Username (your email) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `LUADNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `LUADNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `LUADNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `LUADNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://luadns.com/api.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/luadns/luadns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_mailinabox.md",
    "content": "---\ntitle: \"Mail-in-a-Box\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: mailinabox\ndnsprovider:\n  since:    \"v4.16.0\"\n  code:     \"mailinabox\"\n  url:      \"https://mailinabox.email\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/mailinabox/mailinabox.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Mail-in-a-Box](https://mailinabox.email).\n\n\n<!--more-->\n\n- Code: `mailinabox`\n- Since: v4.16.0\n\n\nHere is an example bash command using the Mail-in-a-Box provider:\n\n```bash\nMAILINABOX_EMAIL=user@example.com \\\nMAILINABOX_PASSWORD=yyyy \\\nMAILINABOX_BASE_URL=https://box.example.com \\\nlego --dns mailinabox -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `MAILINABOX_BASE_URL` | Base API URL (ex: https://box.example.com) |\n| `MAILINABOX_EMAIL` | User email |\n| `MAILINABOX_PASSWORD` | User password |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `MAILINABOX_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `MAILINABOX_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 4) |\n| `MAILINABOX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://mailinabox.email/api-docs.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/mailinabox/mailinabox.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_manageengine.md",
    "content": "---\ntitle: \"ManageEngine CloudDNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: manageengine\ndnsprovider:\n  since:    \"v4.21.0\"\n  code:     \"manageengine\"\n  url:      \"https://clouddns.manageengine.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/manageengine/manageengine.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [ManageEngine CloudDNS](https://clouddns.manageengine.com).\n\n\n<!--more-->\n\n- Code: `manageengine`\n- Since: v4.21.0\n\n\nHere is an example bash command using the ManageEngine CloudDNS provider:\n\n```bash\nMANAGEENGINE_CLIENT_ID=\"xxx\" \\\nMANAGEENGINE_CLIENT_SECRET=\"yyy\" \\\nlego --dns manageengine -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `MANAGEENGINE_CLIENT_ID` | Client ID |\n| `MANAGEENGINE_CLIENT_SECRET` | Client Secret |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `MANAGEENGINE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `MANAGEENGINE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `MANAGEENGINE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/manageengine/manageengine.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_manual.md",
    "content": "---\ntitle: \"Manual\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: manual\ndnsprovider:\n  since:    \"v0.3.0\"\n  code:     \"manual\"\n  url:      \"\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/manual/manual.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\nSolving the DNS-01 challenge using CLI prompt.\n\n\n<!--more-->\n\n- Code: `manual`\n- Since: v0.3.0\n\n\nHere is an example bash command using the Manual provider:\n\n```bash\nlego --dns manual -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Example\n\nTo start using the CLI prompt \"provider\", start lego with `--dns manual`:\n\n```console\n$ lego --dns manual -d example.com run\n```\n\nWhat follows are a few log print-outs, interspersed with some prompts, asking for you to do perform some actions:\n\n```txt\nNo key found for account you@example.com. Generating a P256 key.\nSaved key to ./.lego/accounts/acme-v02.api.letsencrypt.org/you@example.com/keys/you@example.com.key\nPlease review the TOS at https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf\nDo you accept the TOS? Y/n\n```\n\nIf you accept the linked Terms of Service, hit `Enter`.\n\n```txt\n[INFO] acme: Registering account for you@example.com\n!!!! HEADS UP !!!!\n\nYour account credentials have been saved in your\nconfiguration directory at \"./.lego/accounts\".\n\nYou should make a secure backup of this folder now. This\nconfiguration directory will also contain private keys\ngenerated by lego and certificates obtained from the ACME\nserver. Making regular backups of this folder is ideal.\n[INFO] [example.com] acme: Obtaining bundled SAN certificate\n[INFO] [example.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/2345678901\n[INFO] [example.com] acme: Could not find solver for: tls-alpn-01\n[INFO] [example.com] acme: Could not find solver for: http-01\n[INFO] [example.com] acme: use dns-01 solver\n[INFO] [example.com] acme: Preparing to solve DNS-01\nlego: Please create the following TXT record in your example.com. zone:\n_acme-challenge.example.com. 120 IN TXT \"hX0dPkG6Gfs9hUvBAchQclkyyoEKbShbpvJ9mY5q2JQ\"\nlego: Press 'Enter' when you are done\n```\n\nDo as instructed, and create the TXT records, and hit `Enter`.\n\n```txt\n[INFO] [example.com] acme: Trying to solve DNS-01\n[INFO] [example.com] acme: Checking DNS record propagation using [192.168.8.1:53]\n[INFO] Wait for propagation [timeout: 1m0s, interval: 2s]\n[INFO] [example.com] acme: Waiting for DNS record propagation.\n[INFO] [example.com] The server validated our request\n[INFO] [example.com] acme: Cleaning DNS-01 challenge\nlego: You can now remove this TXT record from your example.com. zone:\n_acme-challenge.example.com. 120 IN TXT \"hX0dPkG6Gfs9hUvBAchQclkyyoEKbShbpvJ9mY5q2JQ\"\n[INFO] [example.com] acme: Validations succeeded; requesting certificates\n[INFO] [example.com] Server responded with a certificate.\n```\n\nAs mentioned, you can now remove the TXT record again.\n\n\n\n\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/manual/manual.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_metaname.md",
    "content": "---\ntitle: \"Metaname\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: metaname\ndnsprovider:\n  since:    \"v4.13.0\"\n  code:     \"metaname\"\n  url:      \"https://metaname.net\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/metaname/metaname.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Metaname](https://metaname.net).\n\n\n<!--more-->\n\n- Code: `metaname`\n- Since: v4.13.0\n\n\nHere is an example bash command using the Metaname provider:\n\n```bash\nMETANAME_ACCOUNT_REFERENCE=xxxx \\\nMETANAME_API_KEY=yyyyyyy \\\nlego --dns metaname -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `METANAME_ACCOUNT_REFERENCE` | The four-digit reference of a Metaname account |\n| `METANAME_API_KEY` | API Key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `METANAME_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `METANAME_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `METANAME_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://metaname.net/api/1.1/doc)\n- [Go client](https://github.com/nzdjb/go-metaname)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/metaname/metaname.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_metaregistrar.md",
    "content": "---\ntitle: \"Metaregistrar\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: metaregistrar\ndnsprovider:\n  since:    \"v4.23.0\"\n  code:     \"metaregistrar\"\n  url:      \"https://metaregistrar.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/metaregistrar/metaregistrar.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Metaregistrar](https://metaregistrar.com/).\n\n\n<!--more-->\n\n- Code: `metaregistrar`\n- Since: v4.23.0\n\n\nHere is an example bash command using the Metaregistrar provider:\n\n```bash\nMETAREGISTRAR_API_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns metaregistrar -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `METAREGISTRAR_API_TOKEN` | The API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `METAREGISTRAR_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `METAREGISTRAR_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `METAREGISTRAR_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `METAREGISTRAR_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://metaregistrar.dev/docu/metaapi/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/metaregistrar/metaregistrar.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_mijnhost.md",
    "content": "---\ntitle: \"mijn.host\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: mijnhost\ndnsprovider:\n  since:    \"v4.18.0\"\n  code:     \"mijnhost\"\n  url:      \"https://mijn.host/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/mijnhost/mijnhost.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [mijn.host](https://mijn.host/).\n\n\n<!--more-->\n\n- Code: `mijnhost`\n- Since: v4.18.0\n\n\nHere is an example bash command using the mijn.host provider:\n\n```bash\nMIJNHOST_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns mijnhost -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `MIJNHOST_API_KEY` | The API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `MIJNHOST_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `MIJNHOST_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `MIJNHOST_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `MIJNHOST_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n| `MIJNHOST_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://mijn.host/api/doc/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/mijnhost/mijnhost.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_mittwald.md",
    "content": "---\ntitle: \"Mittwald\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: mittwald\ndnsprovider:\n  since:    \"v1.48.0\"\n  code:     \"mittwald\"\n  url:      \"https://www.mittwald.de/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/mittwald/mittwald.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Mittwald](https://www.mittwald.de/).\n\n\n<!--more-->\n\n- Code: `mittwald`\n- Since: v1.48.0\n\n\nHere is an example bash command using the Mittwald provider:\n\n```bash\nMITTWALD_TOKEN=my-token \\\nlego --dns mittwald -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `MITTWALD_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `MITTWALD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `MITTWALD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `MITTWALD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `MITTWALD_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 120) |\n| `MITTWALD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.mittwald.de/v2/docs/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/mittwald/mittwald.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_myaddr.md",
    "content": "---\ntitle: \"myaddr.{tools,dev,io}\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: myaddr\ndnsprovider:\n  since:    \"v4.22.0\"\n  code:     \"myaddr\"\n  url:      \"https://myaddr.tools/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/myaddr/myaddr.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [myaddr.{tools,dev,io}](https://myaddr.tools/).\n\n\n<!--more-->\n\n- Code: `myaddr`\n- Since: v4.22.0\n\n\nHere is an example bash command using the myaddr.{tools,dev,io} provider:\n\n```bash\nMYADDR_PRIVATE_KEYS_MAPPING=\"example:123,test:456\" \\\nlego --dns myaddr -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `MYADDR_PRIVATE_KEYS_MAPPING` | Mapping between subdomains and private keys. The format is: `<subdomain1>:<private_key1>,<subdomain2>:<private_key2>,<subdomain3>:<private_key3>` |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `MYADDR_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `MYADDR_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `MYADDR_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `MYADDR_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 2) |\n| `MYADDR_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://myaddr.tools/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/myaddr/myaddr.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_mydnsjp.md",
    "content": "---\ntitle: \"MyDNS.jp\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: mydnsjp\ndnsprovider:\n  since:    \"v1.2.0\"\n  code:     \"mydnsjp\"\n  url:      \"https://www.mydns.jp\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/mydnsjp/mydnsjp.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [MyDNS.jp](https://www.mydns.jp).\n\n\n<!--more-->\n\n- Code: `mydnsjp`\n- Since: v1.2.0\n\n\nHere is an example bash command using the MyDNS.jp provider:\n\n```bash\nMYDNSJP_MASTER_ID=xxxxx \\\nMYDNSJP_PASSWORD=xxxxx \\\nlego --dns mydnsjp -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `MYDNSJP_MASTER_ID` | Master ID |\n| `MYDNSJP_PASSWORD` | Password |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `MYDNSJP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `MYDNSJP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `MYDNSJP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.mydns.jp/?MENU=030)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/mydnsjp/mydnsjp.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_mythicbeasts.md",
    "content": "---\ntitle: \"MythicBeasts\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: mythicbeasts\ndnsprovider:\n  since:    \"v0.3.7\"\n  code:     \"mythicbeasts\"\n  url:      \"https://www.mythic-beasts.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/mythicbeasts/mythicbeasts.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [MythicBeasts](https://www.mythic-beasts.com/).\n\n\n<!--more-->\n\n- Code: `mythicbeasts`\n- Since: v0.3.7\n\n\nHere is an example bash command using the MythicBeasts provider:\n\n```bash\nMYTHICBEASTS_USERNAME=myuser \\\nMYTHICBEASTS_PASSWORD=mypass \\\nlego --dns mythicbeasts -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `MYTHICBEASTS_PASSWORD` | Password |\n| `MYTHICBEASTS_USERNAME` | User name |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `MYTHICBEASTS_API_ENDPOINT` | The endpoint for the API (must implement v2) |\n| `MYTHICBEASTS_AUTH_API_ENDPOINT` | The endpoint for Mythic Beasts' Authentication |\n| `MYTHICBEASTS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `MYTHICBEASTS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `MYTHICBEASTS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `MYTHICBEASTS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\nIf you are using specific API keys, then the username is the API ID for your API key, and the password is the API secret.\n\nYour API key name is not needed to operate lego.\n\n\n\n## More information\n\n- [API documentation](https://www.mythic-beasts.com/support/api/dnsv2)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/mythicbeasts/mythicbeasts.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_namecheap.md",
    "content": "---\ntitle: \"Namecheap\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: namecheap\ndnsprovider:\n  since:    \"v0.3.0\"\n  code:     \"namecheap\"\n  url:      \"https://www.namecheap.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/namecheap/namecheap.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Namecheap](https://www.namecheap.com).\n\n**To enable API access on the Namecheap production environment, some opaque requirements must be met.**\nMore information in the section [Enabling API Access](https://www.namecheap.com/support/api/intro/) of the Namecheap documentation.\n(2020-08: Account balance of $50+, 20+ domains in your account, or purchases totaling $50+ within the last 2 years.)\n\n\n\n<!--more-->\n\n- Code: `namecheap`\n- Since: v0.3.0\n\n\nHere is an example bash command using the Namecheap provider:\n\n```bash\nNAMECHEAP_API_USER=user \\\nNAMECHEAP_API_KEY=key \\\nlego --dns namecheap -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `NAMECHEAP_API_KEY` | API key |\n| `NAMECHEAP_API_USER` | API user |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `NAMECHEAP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) |\n| `NAMECHEAP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 15) |\n| `NAMECHEAP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 3600) |\n| `NAMECHEAP_SANDBOX` | Activate the sandbox (boolean) |\n| `NAMECHEAP_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.namecheap.com/support/api/methods.aspx)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/namecheap/namecheap.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_namedotcom.md",
    "content": "---\ntitle: \"Name.com\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: namedotcom\ndnsprovider:\n  since:    \"v0.5.0\"\n  code:     \"namedotcom\"\n  url:      \"https://www.name.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/namedotcom/namedotcom.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Name.com](https://www.name.com).\n\n\n<!--more-->\n\n- Code: `namedotcom`\n- Since: v0.5.0\n\n\nHere is an example bash command using the Name.com provider:\n\n```bash\nNAMECOM_USERNAME=foo.bar \\\nNAMECOM_API_TOKEN=a379a6f6eeafb9a55e378c118034e2751e682fab \\\nlego --dns namedotcom -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `NAMECOM_API_TOKEN` | API token |\n| `NAMECOM_USERNAME` | Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `NAMECOM_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `NAMECOM_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 20) |\n| `NAMECOM_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 900) |\n| `NAMECOM_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.name.com/api-docs/DNS)\n- [Go client](https://github.com/namedotcom/go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/namedotcom/namedotcom.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_namesilo.md",
    "content": "---\ntitle: \"Namesilo\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: namesilo\ndnsprovider:\n  since:    \"v2.7.0\"\n  code:     \"namesilo\"\n  url:      \"https://www.namesilo.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/namesilo/namesilo.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Namesilo](https://www.namesilo.com/).\n\n\n<!--more-->\n\n- Code: `namesilo`\n- Since: v2.7.0\n\n\nHere is an example bash command using the Namesilo provider:\n\n```bash\nNAMESILO_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \\\nlego --dns namesilo -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `NAMESILO_API_KEY` | Client ID |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `NAMESILO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `NAMESILO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60), it is better to set larger than 15 minutes |\n| `NAMESILO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600), should be in [3600, 2592000] |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.namesilo.com/api_reference.php)\n- [Go client](https://github.com/nrdcg/namesilo)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/namesilo/namesilo.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_namesurfer.md",
    "content": "---\ntitle: \"FusionLayer NameSurfer\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: namesurfer\ndnsprovider:\n  since:    \"v4.32.0\"\n  code:     \"namesurfer\"\n  url:      \"https://www.fusionlayer.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/namesurfer/namesurfer.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [FusionLayer NameSurfer](https://www.fusionlayer.com/).\n\n\n<!--more-->\n\n- Code: `namesurfer`\n- Since: v4.32.0\n\n\nHere is an example bash command using the FusionLayer NameSurfer provider:\n\n```bash\nNAMESURFER_BASE_URL=https://foo.example.com:8443/API/NSService_10 \\\nNAMESURFER_API_KEY=xxx \\\nNAMESURFER_API_SECRET=yyy \\\nlego --dns namesurfer -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `NAMESURFER_API_KEY` | API key name |\n| `NAMESURFER_API_SECRET` | API secret |\n| `NAMESURFER_BASE_URL` | The base URL of NameSurfer API (jsonrpc10) endpoint URL (e.g., https://foo.example.com:8443/API/NSService_10) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `NAMESURFER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `NAMESURFER_INSECURE_SKIP_VERIFY` | Whether to verify the API certificate |\n| `NAMESURFER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `NAMESURFER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `NAMESURFER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n| `NAMESURFER_VIEW` | DNS view name (optional, default: empty string) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://web.archive.org/web/20260213170737/http://95.128.3.201:8053/API/NSService_10)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/namesurfer/namesurfer.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_nearlyfreespeech.md",
    "content": "---\ntitle: \"NearlyFreeSpeech.NET\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: nearlyfreespeech\ndnsprovider:\n  since:    \"v4.8.0\"\n  code:     \"nearlyfreespeech\"\n  url:      \"https://nearlyfreespeech.net/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/nearlyfreespeech/nearlyfreespeech.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [NearlyFreeSpeech.NET](https://nearlyfreespeech.net/).\n\n\n<!--more-->\n\n- Code: `nearlyfreespeech`\n- Since: v4.8.0\n\n\nHere is an example bash command using the NearlyFreeSpeech.NET provider:\n\n```bash\nNEARLYFREESPEECH_API_KEY=xxxxxx \\\nNEARLYFREESPEECH_LOGIN=xxxx \\\nlego --dns nearlyfreespeech -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `NEARLYFREESPEECH_API_KEY` | API Key for API requests |\n| `NEARLYFREESPEECH_LOGIN` | Username for API requests |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `NEARLYFREESPEECH_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `NEARLYFREESPEECH_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `NEARLYFREESPEECH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `NEARLYFREESPEECH_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n| `NEARLYFREESPEECH_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://members.nearlyfreespeech.net/wiki/API/Reference)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/nearlyfreespeech/nearlyfreespeech.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_neodigit.md",
    "content": "---\ntitle: \"Neodigit\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: neodigit\ndnsprovider:\n  since:    \"v4.30.0\"\n  code:     \"neodigit\"\n  url:      \"https://www.neodigit.net\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/neodigit/neodigit.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Neodigit](https://www.neodigit.net).\n\n\n<!--more-->\n\n- Code: `neodigit`\n- Since: v4.30.0\n\n\nHere is an example bash command using the Neodigit provider:\n\n```bash\nNEODIGIT_TOKEN=xxxxxx \\\nlego --dns neodigit -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `NEODIGIT_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `NEODIGIT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `NEODIGIT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `NEODIGIT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |\n| `NEODIGIT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developers.neodigit.net/#dns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/neodigit/neodigit.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_netcup.md",
    "content": "---\ntitle: \"Netcup\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: netcup\ndnsprovider:\n  since:    \"v1.1.0\"\n  code:     \"netcup\"\n  url:      \"https://www.netcup.eu/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/netcup/netcup.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Netcup](https://www.netcup.eu/).\n\n\n<!--more-->\n\n- Code: `netcup`\n- Since: v1.1.0\n\n\nHere is an example bash command using the Netcup provider:\n\n```bash\nNETCUP_CUSTOMER_NUMBER=xxxx \\\nNETCUP_API_KEY=yyyy \\\nNETCUP_API_PASSWORD=zzzz \\\nlego --dns netcup -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `NETCUP_API_KEY` | API key |\n| `NETCUP_API_PASSWORD` | API password |\n| `NETCUP_CUSTOMER_NUMBER` | Customer number |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `NETCUP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `NETCUP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 30) |\n| `NETCUP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 900) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.netcup-wiki.de/wiki/DNS_API)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/netcup/netcup.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_netlify.md",
    "content": "---\ntitle: \"Netlify\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: netlify\ndnsprovider:\n  since:    \"v3.7.0\"\n  code:     \"netlify\"\n  url:      \"https://www.netlify.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/netlify/netlify.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Netlify](https://www.netlify.com).\n\n\n<!--more-->\n\n- Code: `netlify`\n- Since: v3.7.0\n\n\nHere is an example bash command using the Netlify provider:\n\n```bash\nNETLIFY_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns netlify -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `NETLIFY_TOKEN` | Token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `NETLIFY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `NETLIFY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `NETLIFY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `NETLIFY_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://open-api.netlify.com/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/netlify/netlify.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_nicmanager.md",
    "content": "---\ntitle: \"Nicmanager\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: nicmanager\ndnsprovider:\n  since:    \"v4.5.0\"\n  code:     \"nicmanager\"\n  url:      \"https://www.nicmanager.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/nicmanager/nicmanager.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Nicmanager](https://www.nicmanager.com/).\n\n\n<!--more-->\n\n- Code: `nicmanager`\n- Since: v4.5.0\n\n\nHere is an example bash command using the Nicmanager provider:\n\n```bash\n## Login using email\n\nNICMANAGER_API_EMAIL = \"you@example.com\" \\\nNICMANAGER_API_PASSWORD = \"password\" \\\n\n# Optionally, if your account has TOTP enabled, set the secret here\nNICMANAGER_API_OTP = \"long-secret\" \\\n\nlego --dns nicmanager -d '*.example.com' -d example.com run\n\n## Login using account name + username\n\nNICMANAGER_API_LOGIN = \"myaccount\" \\\nNICMANAGER_API_USERNAME = \"myuser\" \\\nNICMANAGER_API_PASSWORD = \"password\" \\\n\n# Optionally, if your account has TOTP enabled, set the secret here\nNICMANAGER_API_OTP = \"long-secret\" \\\n\nlego --dns nicmanager -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `NICMANAGER_API_EMAIL` | Email-based login |\n| `NICMANAGER_API_LOGIN` | Login, used for Username-based login |\n| `NICMANAGER_API_PASSWORD` | Password, always required |\n| `NICMANAGER_API_USERNAME` | Username, used for Username-based login |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `NICMANAGER_API_MODE` | mode: 'anycast' or 'zones' (for FreeDNS) (default: 'anycast') |\n| `NICMANAGER_API_OTP` | TOTP Secret (optional) |\n| `NICMANAGER_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `NICMANAGER_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `NICMANAGER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |\n| `NICMANAGER_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 900) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Description\n\nYou can log in using your account name + username or using your email address.\nOptionally, if TOTP is configured for your account, set `NICMANAGER_API_OTP`.\n\n\n\n## More information\n\n- [API documentation](https://api.nicmanager.com/docs/v1/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/nicmanager/nicmanager.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_nicru.md",
    "content": "---\ntitle: \"RU CENTER\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: nicru\ndnsprovider:\n  since:    \"v4.24.0\"\n  code:     \"nicru\"\n  url:      \"https://nic.ru/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/nicru/nicru.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [RU CENTER](https://nic.ru/).\n\n\n<!--more-->\n\n- Code: `nicru`\n- Since: v4.24.0\n\n\nHere is an example bash command using the RU CENTER provider:\n\n```bash\nNICRU_USER=\"<your_user>\" \\\nNICRU_PASSWORD=\"<your_password>\" \\\nNICRU_SERVICE_ID=\"<service_id>\" \\\nNICRU_SECRET=\"<service_secret>\" \\\nlego --dns nicru -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `NICRU_PASSWORD` | Password for an account in RU CENTER |\n| `NICRU_SECRET` | Secret for application in DNS-hosting RU CENTER |\n| `NICRU_SERVICE_ID` | Service ID for application in DNS-hosting RU CENTER |\n| `NICRU_SERVICE_NAME` | Service Name for DNS-hosting RU CENTER |\n| `NICRU_USER` | Agreement for an account in RU CENTER |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `NICRU_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 60) |\n| `NICRU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 600) |\n| `NICRU_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 30) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Credential information\n\nYou can find information about service ID and secret https://www.nic.ru/manager/oauth.cgi?step=oauth.app_list\n\n| ENV Variable        | Parameter from page            | Example           |\n|---------------------|--------------------------------|-------------------|\n| NICRU_USER          | Username (Number of agreement) | NNNNNNN/NIC-D     |\n| NICRU_PASSWORD      | Password account               |                   |\n| NICRU_SERVICE_ID    | Application ID                 | hex-based, len 32 |\n| NICRU_SECRET        | Identity endpoint              | string len 91     |\n\n\n\n## More information\n\n- [API documentation](https://www.nic.ru/help/api-dns-hostinga_3643.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/nicru/nicru.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_nifcloud.md",
    "content": "---\ntitle: \"NIFCloud\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: nifcloud\ndnsprovider:\n  since:    \"v1.1.0\"\n  code:     \"nifcloud\"\n  url:      \"https://www.nifcloud.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/nifcloud/nifcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [NIFCloud](https://www.nifcloud.com/).\n\n\n<!--more-->\n\n- Code: `nifcloud`\n- Since: v1.1.0\n\n\nHere is an example bash command using the NIFCloud provider:\n\n```bash\nNIFCLOUD_ACCESS_KEY_ID=xxxx \\\nNIFCLOUD_SECRET_ACCESS_KEY=yyyy \\\nlego --dns nifcloud -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `NIFCLOUD_ACCESS_KEY_ID` | Access key |\n| `NIFCLOUD_SECRET_ACCESS_KEY` | Secret access key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `NIFCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `NIFCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `NIFCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `NIFCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://mbaas.nifcloud.com/doc/current/rest/common/format.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/nifcloud/nifcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_njalla.md",
    "content": "---\ntitle: \"Njalla\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: njalla\ndnsprovider:\n  since:    \"v4.3.0\"\n  code:     \"njalla\"\n  url:      \"https://njal.la\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/njalla/njalla.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Njalla](https://njal.la).\n\n\n<!--more-->\n\n- Code: `njalla`\n- Since: v4.3.0\n\n\nHere is an example bash command using the Njalla provider:\n\n```bash\nNJALLA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns njalla -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `NJALLA_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `NJALLA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `NJALLA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `NJALLA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `NJALLA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://njal.la/api/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/njalla/njalla.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_nodion.md",
    "content": "---\ntitle: \"Nodion\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: nodion\ndnsprovider:\n  since:    \"v4.11.0\"\n  code:     \"nodion\"\n  url:      \"https://www.nodion.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/nodion/nodion.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Nodion](https://www.nodion.com).\n\n\n<!--more-->\n\n- Code: `nodion`\n- Since: v4.11.0\n\n\nHere is an example bash command using the Nodion provider:\n\n```bash\nNODION_API_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns nodion -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `NODION_API_TOKEN` | The API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `NODION_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `NODION_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `NODION_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `NODION_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.nodion.com/en/docs/dns/api/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/nodion/nodion.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_ns1.md",
    "content": "---\ntitle: \"NS1\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: ns1\ndnsprovider:\n  since:    \"v0.4.0\"\n  code:     \"ns1\"\n  url:      \"https://ns1.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ns1/ns1.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [NS1](https://ns1.com).\n\n\n<!--more-->\n\n- Code: `ns1`\n- Since: v0.4.0\n\n\nHere is an example bash command using the NS1 provider:\n\n```bash\nNS1_API_KEY=xxxx \\\nlego --dns ns1 -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `NS1_API_KEY` | API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `NS1_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `NS1_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `NS1_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `NS1_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://ns1.com/api)\n- [Go client](https://github.com/ns1/ns1-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ns1/ns1.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_octenium.md",
    "content": "---\ntitle: \"Octenium\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: octenium\ndnsprovider:\n  since:    \"v4.27.0\"\n  code:     \"octenium\"\n  url:      \"https://octenium.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/octenium/octenium.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Octenium](https://octenium.com/).\n\n\n<!--more-->\n\n- Code: `octenium`\n- Since: v4.27.0\n\n\nHere is an example bash command using the Octenium provider:\n\n```bash\nOCTENIUM_API_KEY=\"xxx\" \\\nlego --dns octenium -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `OCTENIUM_API_KEY` | API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `OCTENIUM_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `OCTENIUM_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `OCTENIUM_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `OCTENIUM_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://octenium.com/api#tag/Domains-DNS)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/octenium/octenium.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_oraclecloud.md",
    "content": "---\ntitle: \"Oracle Cloud\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: oraclecloud\ndnsprovider:\n  since:    \"v2.3.0\"\n  code:     \"oraclecloud\"\n  url:      \"https://cloud.oracle.com/home\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/oraclecloud/oraclecloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Oracle Cloud](https://cloud.oracle.com/home).\n\n\n<!--more-->\n\n- Code: `oraclecloud`\n- Since: v2.3.0\n\n\nHere is an example bash command using the Oracle Cloud provider:\n\n```bash\n# Using API Key authentication:\nOCI_PRIVATE_KEY_PATH=\"~/.oci/oci_api_key.pem\" \\\nOCI_PRIVATE_KEY_PASSWORD=\"secret\" \\\nOCI_TENANCY_OCID=\"ocid1.tenancy.oc1..secret\" \\\nOCI_USER_OCID=\"ocid1.user.oc1..secret\" \\\nOCI_FINGERPRINT=\"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00\" \\\nOCI_REGION=\"us-phoenix-1\" \\\nOCI_COMPARTMENT_OCID=\"ocid1.tenancy.oc1..secret\" \\\nlego --dns oraclecloud -d '*.example.com' -d example.com run\n\n# Using Instance Principal authentication (when running on OCI compute instances):\n# https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm\nOCI_AUTH_TYPE=\"instance_principal\" \\\nOCI_COMPARTMENT_OCID=\"ocid1.tenancy.oc1..secret\" \\\nlego --dns oraclecloud -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `OCI_COMPARTMENT_OCID` | Compartment OCID |\n| `OCI_FINGERPRINT` | Public key fingerprint (ignored if `OCI_AUTH_TYPE=instance_principal`) |\n| `OCI_PRIVATE_KEY_PASSWORD` | Private key password (ignored if `OCI_AUTH_TYPE=instance_principal`) |\n| `OCI_PRIVATE_KEY_PATH` | Private key file (ignored if `OCI_AUTH_TYPE=instance_principal`) |\n| `OCI_REGION` | Region (it can be empty if `OCI_AUTH_TYPE=instance_principal`). |\n| `OCI_TENANCY_OCID` | Tenancy OCID (ignored if `OCI_AUTH_TYPE=instance_principal`) |\n| `OCI_USER_OCID` | User OCID (ignored if `OCI_AUTH_TYPE=instance_principal`) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `OCI_AUTH_TYPE` | Authorization type. Possible values: 'instance_principal', ''  (Default: '') |\n| `OCI_HTTP_TIMEOUT` | API request timeout in seconds (Default: 60) |\n| `OCI_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `OCI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `OCI_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n| `TF_VAR_fingerprint` | Alias on `OCI_FINGERPRINT` |\n| `TF_VAR_private_key_path` | Alias on `OCI_PRIVATE_KEY_PATH` |\n| `TF_VAR_region` | Alias on `OCI_REGION` |\n| `TF_VAR_tenancy_ocid` | Alias on `OCI_TENANCY_OCID` |\n| `TF_VAR_user_ocid` | Alias on `OCI_USER_OCID` |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm)\n- [Go client](https://github.com/oracle/oci-go-sdk)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/oraclecloud/oraclecloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_otc.md",
    "content": "---\ntitle: \"Open Telekom Cloud\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: otc\ndnsprovider:\n  since:    \"v0.4.1\"\n  code:     \"otc\"\n  url:      \"https://cloud.telekom.de/en\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/otc/otc.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Open Telekom Cloud](https://cloud.telekom.de/en).\n\n\n<!--more-->\n\n- Code: `otc`\n- Since: v0.4.1\n\n\nHere is an example bash command using the Open Telekom Cloud provider:\n\n```bash\nOTC_DOMAIN_NAME=domain_name \\\nOTC_USER_NAME=user_name \\\nOTC_PASSWORD=password \\\nOTC_PROJECT_NAME=project_name \\\nlego --dns otc -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `OTC_DOMAIN_NAME` | Domain name |\n| `OTC_PASSWORD` | Password |\n| `OTC_PROJECT_NAME` | Project name |\n| `OTC_USER_NAME` | User name |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `OTC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `OTC_IDENTITY_ENDPOINT` | Identity endpoint URL (default: https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens) |\n| `OTC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `OTC_PRIVATE_ZONE` | Set to true to use private zones only (default: use public zones only) |\n| `OTC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `OTC_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n| `OTC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://docs.otc.t-systems.com/domain-name-service/api-ref/index.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/otc/otc.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_ovh.md",
    "content": "---\ntitle: \"OVH\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: ovh\ndnsprovider:\n  since:    \"v0.4.0\"\n  code:     \"ovh\"\n  url:      \"https://www.ovh.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ovh/ovh.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [OVH](https://www.ovh.com/).\n\n\n<!--more-->\n\n- Code: `ovh`\n- Since: v0.4.0\n\n\nHere is an example bash command using the OVH provider:\n\n```bash\n# Application Key authentication:\n\nOVH_APPLICATION_KEY=1234567898765432 \\\nOVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \\\nOVH_CONSUMER_KEY=256vfsd347245sdfg \\\nOVH_ENDPOINT=ovh-eu \\\nlego --dns ovh -d '*.example.com' -d example.com run\n\n# Or Access Token:\n\nOVH_ACCESS_TOKEN=xxx \\\nOVH_ENDPOINT=ovh-eu \\\nlego --dns ovh -d '*.example.com' -d example.com run\n\n# Or OAuth2:\n\nOVH_CLIENT_ID=yyy \\\nOVH_CLIENT_SECRET=xxx \\\nOVH_ENDPOINT=ovh-eu \\\nlego --dns ovh -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `OVH_ACCESS_TOKEN` | Access token |\n| `OVH_APPLICATION_KEY` | Application key (Application Key authentication) |\n| `OVH_APPLICATION_SECRET` | Application secret (Application Key authentication) |\n| `OVH_CLIENT_ID` | Client ID (OAuth2) |\n| `OVH_CLIENT_SECRET` | Client secret (OAuth2) |\n| `OVH_CONSUMER_KEY` | Consumer key (Application Key authentication) |\n| `OVH_ENDPOINT` | Endpoint URL (ovh-eu or ovh-ca) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `OVH_HTTP_TIMEOUT` | API request timeout in seconds (Default: 180) |\n| `OVH_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `OVH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `OVH_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Application Key and Secret\n\nApplication key and secret can be created by following the [OVH guide](https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/).\n\nWhen requesting the consumer key, the following configuration can be used to define access rights:\n\n```json\n{\n  \"accessRules\": [\n    {\n      \"method\": \"POST\",\n      \"path\": \"/domain/zone/*\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/domain/zone/*\"\n    }\n  ]\n}\n```\n\n## OAuth2 Client Credentials\n\nAnother method for authentication is by using OAuth2 client credentials.\n\nAn IAM policy and service account can be created by following the [OVH guide](https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343).\n\nFollowing IAM policies need to be authorized for the affected domain:\n\n* dnsZone:apiovh:record/create\n* dnsZone:apiovh:record/delete\n* dnsZone:apiovh:refresh\n\n## Important Note\n\nBoth authentication methods cannot be used at the same time.\n\n\n\n## More information\n\n- [API documentation](https://eu.api.ovh.com/)\n- [Go client](https://github.com/ovh/go-ovh)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ovh/ovh.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_pdns.md",
    "content": "---\ntitle: \"PowerDNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: pdns\ndnsprovider:\n  since:    \"v0.4.0\"\n  code:     \"pdns\"\n  url:      \"https://www.powerdns.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/pdns/pdns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [PowerDNS](https://www.powerdns.com/).\n\n\n<!--more-->\n\n- Code: `pdns`\n- Since: v0.4.0\n\n\nHere is an example bash command using the PowerDNS provider:\n\n```bash\nPDNS_API_URL=http://pdns-server:80/ \\\nPDNS_API_KEY=xxxx \\\nlego --dns pdns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `PDNS_API_KEY` | API key |\n| `PDNS_API_URL` | API URL |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `PDNS_API_VERSION` | Skip API version autodetection and use the provided version number. |\n| `PDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `PDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `PDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `PDNS_SERVER_NAME` | Name of the server in the URL, 'localhost' by default |\n| `PDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Information\n\nTested and confirmed to work with PowerDNS authoritative server 3.4.8 and 4.0.1. Refer to [PowerDNS documentation](https://doc.powerdns.com/md/httpapi/README/) instructions on how to enable the built-in API interface.\n\nPowerDNS Notes:\n- PowerDNS API does not currently support SSL, therefore you should take care to ensure that traffic between lego and the PowerDNS API is over a trusted network, VPN etc.\n- In order to have the SOA serial automatically increment each time the `_acme-challenge` record is added/modified via the API, set `SOA-EDIT-API` to `INCEPTION-INCREMENT` for the zone in the `domainmetadata` table\n- Some PowerDNS servers doesn't have root API endpoints enabled and API version autodetection will not work. In that case version number can be defined using `PDNS_API_VERSION`.\n\n\n\n## More information\n\n- [API documentation](https://doc.powerdns.com/md/httpapi/README/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/pdns/pdns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_plesk.md",
    "content": "---\ntitle: \"plesk.com\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: plesk\ndnsprovider:\n  since:    \"v4.11.0\"\n  code:     \"plesk\"\n  url:      \"https://www.plesk.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/plesk/plesk.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [plesk.com](https://www.plesk.com/).\n\n\n<!--more-->\n\n- Code: `plesk`\n- Since: v4.11.0\n\n\nHere is an example bash command using the plesk.com provider:\n\n```bash\nPLESK_SERVER_BASE_URL=\"https://plesk.myserver.com:8443\" \\\nPLESK_USERNAME=xxxxxx \\\nPLESK_PASSWORD=yyyyyy \\\nlego --dns plesk -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `PLESK_PASSWORD` | API password |\n| `PLESK_SERVER_BASE_URL` | Base URL of the server (ex: https://plesk.myserver.com:8443) |\n| `PLESK_USERNAME` | API username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `PLESK_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `PLESK_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `PLESK_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `PLESK_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference.28784/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/plesk/plesk.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_porkbun.md",
    "content": "---\ntitle: \"Porkbun\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: porkbun\ndnsprovider:\n  since:    \"v4.4.0\"\n  code:     \"porkbun\"\n  url:      \"https://porkbun.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/porkbun/porkbun.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Porkbun](https://porkbun.com/).\n\n\n<!--more-->\n\n- Code: `porkbun`\n- Since: v4.4.0\n\n\nHere is an example bash command using the Porkbun provider:\n\n```bash\nPORKBUN_SECRET_API_KEY=xxxxxx \\\nPORKBUN_API_KEY=yyyyyy \\\nlego --dns porkbun -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `PORKBUN_API_KEY` | API key |\n| `PORKBUN_SECRET_API_KEY` | secret API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `PORKBUN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `PORKBUN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `PORKBUN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 600) |\n| `PORKBUN_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://porkbun.com/api/json/v3/documentation)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/porkbun/porkbun.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_rackspace.md",
    "content": "---\ntitle: \"Rackspace\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: rackspace\ndnsprovider:\n  since:    \"v0.4.0\"\n  code:     \"rackspace\"\n  url:      \"https://www.rackspace.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/rackspace/rackspace.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Rackspace](https://www.rackspace.com/).\n\n\n<!--more-->\n\n- Code: `rackspace`\n- Since: v0.4.0\n\n\nHere is an example bash command using the Rackspace provider:\n\n```bash\nRACKSPACE_USER=xxxx \\\nRACKSPACE_API_KEY=yyyy \\\nlego --dns rackspace -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `RACKSPACE_API_KEY` | API key |\n| `RACKSPACE_USER` | API user |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `RACKSPACE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `RACKSPACE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 3) |\n| `RACKSPACE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `RACKSPACE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developer.rackspace.com/docs/cloud-dns/v1/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/rackspace/rackspace.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_rainyun.md",
    "content": "---\ntitle: \"Rain Yun/雨云\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: rainyun\ndnsprovider:\n  since:    \"v4.21.0\"\n  code:     \"rainyun\"\n  url:      \"https://www.rainyun.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/rainyun/rainyun.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Rain Yun/雨云](https://www.rainyun.com).\n\n\n<!--more-->\n\n- Code: `rainyun`\n- Since: v4.21.0\n\n\nHere is an example bash command using the Rain Yun/雨云 provider:\n\n```bash\nRAINYUN_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns rainyun -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `RAINYUN_API_KEY` | API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `RAINYUN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `RAINYUN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `RAINYUN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `RAINYUN_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.apifox.cn/apidoc/shared-a4595cc8-44c5-4678-a2a3-eed7738dab03/api-151416609)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/rainyun/rainyun.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_rcodezero.md",
    "content": "---\ntitle: \"RcodeZero\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: rcodezero\ndnsprovider:\n  since:    \"v4.13\"\n  code:     \"rcodezero\"\n  url:      \"https://www.rcodezero.at/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/rcodezero/rcodezero.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [RcodeZero](https://www.rcodezero.at/).\n\n\n<!--more-->\n\n- Code: `rcodezero`\n- Since: v4.13\n\n\nHere is an example bash command using the RcodeZero provider:\n\n```bash\nRCODEZERO_API_TOKEN=<mytoken> \\\nlego --dns rcodezero -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `RCODEZERO_API_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `RCODEZERO_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `RCODEZERO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `RCODEZERO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 240) |\n| `RCODEZERO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Description\n\nGenerate your API Token via https://my.rcodezero.at with the `ACME` permissions.\nThese are special tokens with limited access for ACME requests only.\n\nRcodeZero is an Anycast Network so the distribution of the DNS01-Challenge can take up to 2 minutes.\n\n\n\n\n## More information\n\n- [API documentation](https://my.rcodezero.at/openapi)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/rcodezero/rcodezero.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_regfish.md",
    "content": "---\ntitle: \"Regfish\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: regfish\ndnsprovider:\n  since:    \"v4.20.0\"\n  code:     \"regfish\"\n  url:      \"https://regfish.de/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/regfish/regfish.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Regfish](https://regfish.de/).\n\n\n<!--more-->\n\n- Code: `regfish`\n- Since: v4.20.0\n\n\nHere is an example bash command using the Regfish provider:\n\n```bash\nREGFISH_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns regfish -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `REGFISH_API_KEY` | API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `REGFISH_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `REGFISH_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `REGFISH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `REGFISH_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://regfish.readme.io/)\n- [Go client](https://github.com/regfish/regfish-dnsapi-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/regfish/regfish.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_regru.md",
    "content": "---\ntitle: \"reg.ru\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: regru\ndnsprovider:\n  since:    \"v3.5.0\"\n  code:     \"regru\"\n  url:      \"https://www.reg.ru/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/regru/regru.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [reg.ru](https://www.reg.ru/).\n\n\n<!--more-->\n\n- Code: `regru`\n- Since: v3.5.0\n\n\nHere is an example bash command using the reg.ru provider:\n\n```bash\nREGRU_USERNAME=xxxxxx \\\nREGRU_PASSWORD=yyyyyy \\\nlego --dns regru -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `REGRU_PASSWORD` | API password |\n| `REGRU_USERNAME` | API username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `REGRU_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `REGRU_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `REGRU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `REGRU_TLS_CERT` | authentication certificate |\n| `REGRU_TLS_KEY` | authentication private key |\n| `REGRU_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.reg.ru/support/help/api2)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/regru/regru.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_rfc2136.md",
    "content": "---\ntitle: \"RFC2136\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: rfc2136\ndnsprovider:\n  since:    \"v0.3.0\"\n  code:     \"rfc2136\"\n  url:      \"https://www.rfc-editor.org/rfc/rfc2136.html\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/rfc2136/rfc2136.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [RFC2136](https://www.rfc-editor.org/rfc/rfc2136.html).\n\n\n<!--more-->\n\n- Code: `rfc2136`\n- Since: v0.3.0\n\n\nHere is an example bash command using the RFC2136 provider:\n\n```bash\nRFC2136_NAMESERVER=127.0.0.1 \\\nRFC2136_TSIG_KEY=example.com \\\nRFC2136_TSIG_ALGORITHM=hmac-sha256. \\\nRFC2136_TSIG_SECRET=YWJjZGVmZGdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNDU= \\\nlego --dns rfc2136 -d '*.example.com' -d example.com run\n\n## ---\n\nkeyname=example.com; keyfile=example.com.key; tsig-keygen $keyname > $keyfile\n\nRFC2136_NAMESERVER=127.0.0.1 \\\nRFC2136_TSIG_FILE=\"$keyfile\" \\\nlego --dns rfc2136 -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `RFC2136_NAMESERVER` | Network address in the form \"host\" or \"host:port\" |\n| `RFC2136_TSIG_ALGORITHM` | TSIG algorithm. See [miekg/dns#tsig.go](https://github.com/miekg/dns/blob/master/tsig.go) for supported values. To disable TSIG authentication, leave the `RFC2136_TSIG_KEY` or `RFC2136_TSIG_SECRET` variables unset. |\n| `RFC2136_TSIG_KEY` | Name of the secret key as defined in DNS server configuration. To disable TSIG authentication, leave the `RFC2136_TSIG_KEY` variable unset. |\n| `RFC2136_TSIG_SECRET` | Secret key payload. To disable TSIG authentication, leave the `RFC2136_TSIG_SECRET` variable unset. |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `RFC2136_DNS_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `RFC2136_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `RFC2136_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `RFC2136_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n| `RFC2136_TSIG_FILE` | Path to a key file generated by tsig-keygen |\n| `RFC2136_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.rfc-editor.org/rfc/rfc2136.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/rfc2136/rfc2136.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_rimuhosting.md",
    "content": "---\ntitle: \"RimuHosting\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: rimuhosting\ndnsprovider:\n  since:    \"v0.3.5\"\n  code:     \"rimuhosting\"\n  url:      \"https://rimuhosting.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/rimuhosting/rimuhosting.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [RimuHosting](https://rimuhosting.com).\n\n\n<!--more-->\n\n- Code: `rimuhosting`\n- Since: v0.3.5\n\n\nHere is an example bash command using the RimuHosting provider:\n\n```bash\nRIMUHOSTING_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns rimuhosting -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `RIMUHOSTING_API_KEY` | User API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `RIMUHOSTING_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `RIMUHOSTING_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `RIMUHOSTING_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `RIMUHOSTING_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://rimuhosting.com/dns/dyndns.jsp)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/rimuhosting/rimuhosting.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_route53.md",
    "content": "---\ntitle: \"Amazon Route 53\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: route53\ndnsprovider:\n  since:    \"v0.3.0\"\n  code:     \"route53\"\n  url:      \"https://aws.amazon.com/route53/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/route53/route53.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Amazon Route 53](https://aws.amazon.com/route53/).\n\n\n<!--more-->\n\n- Code: `route53`\n- Since: v0.3.0\n\n\nHere is an example bash command using the Amazon Route 53 provider:\n\n```bash\nAWS_ACCESS_KEY_ID=your_key_id \\\nAWS_SECRET_ACCESS_KEY=your_secret_access_key \\\nAWS_REGION=aws-region \\\nAWS_HOSTED_ZONE_ID=your_hosted_zone_id \\\nlego --dns route53 -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `AWS_ACCESS_KEY_ID` | Managed by the AWS client. Access key ID (`AWS_ACCESS_KEY_ID_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead) |\n| `AWS_ASSUME_ROLE_ARN` | Managed by the AWS Role ARN (`AWS_ASSUME_ROLE_ARN_FILE` is not supported) |\n| `AWS_EXTERNAL_ID` | Managed by STS AssumeRole API operation (`AWS_EXTERNAL_ID_FILE` is not supported) |\n| `AWS_HOSTED_ZONE_ID` | Override the hosted zone ID. |\n| `AWS_PROFILE` | Managed by the AWS client (`AWS_PROFILE_FILE` is not supported) |\n| `AWS_REGION` | Managed by the AWS client (`AWS_REGION_FILE` is not supported) |\n| `AWS_SDK_LOAD_CONFIG` | Managed by the AWS client. Retrieve the region from the CLI config file (`AWS_SDK_LOAD_CONFIG_FILE` is not supported) |\n| `AWS_SECRET_ACCESS_KEY` | Managed by the AWS client. Secret access key (`AWS_SECRET_ACCESS_KEY_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead) |\n| `AWS_WAIT_FOR_RECORD_SETS_CHANGED` | Wait for changes to be INSYNC (it can be unstable) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `AWS_MAX_RETRIES` | The number of maximum returns the service will use to make an individual API request |\n| `AWS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 4) |\n| `AWS_PRIVATE_ZONE` | Set to true to use private zones only (default: use public zones only) |\n| `AWS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `AWS_SHARED_CREDENTIALS_FILE` | Managed by the AWS client. Shared credentials file. |\n| `AWS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 10) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Description\n\nAWS Credentials are automatically detected in the following locations and prioritized in the following order:\n\n1. Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, [`AWS_SESSION_TOKEN`]\n2. Shared credentials file (defaults to `~/.aws/credentials`, profiles can be specified using `AWS_PROFILE`)\n3. Amazon EC2 IAM role\n\nThe AWS Region is automatically detected in the following locations and prioritized in the following order:\n\n1. Environment variables: `AWS_REGION`\n2. Shared configuration file if `AWS_SDK_LOAD_CONFIG` is set (defaults to `~/.aws/config`, profiles can be specified using `AWS_PROFILE`)\n\nIf `AWS_HOSTED_ZONE_ID` is not set, Lego tries to determine the correct public hosted zone via the FQDN.\n\nSee also:\n\n- [sessions](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/sessions.html)\n- [Setting AWS Credentials](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials)\n- [Setting AWS Region](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-the-region)\n\n## IAM Policy Examples\n\n### Broad privileges for testing purposes\n\nThe following [IAM policy](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html) document grants access to the required APIs needed by lego to complete the DNS challenge.\nA word of caution:\nThese permissions grant write access to any DNS record in any hosted zone,\nso it is recommended to narrow them down as much as possible if you are using this policy in production.\n\n```json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"route53:GetChange\",\n        \"route53:ChangeResourceRecordSets\",\n        \"route53:ListResourceRecordSets\"\n      ],\n      \"Resource\": [\n        \"arn:aws:route53:::hostedzone/*\",\n        \"arn:aws:route53:::change/*\"\n      ]\n    },\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": \"route53:ListHostedZonesByName\",\n      \"Resource\": \"*\"\n    }\n  ]\n}\n```\n\n### Least privilege policy for production purposes\n\nThe following AWS IAM policy document describes the least privilege permissions required for lego to complete the DNS challenge.\nWrite access is limited to a specified hosted zone's DNS TXT records with a key of `_acme-challenge.example.com`.\nReplace `Z11111112222222333333` with your hosted zone ID and `example.com` with your domain name to use this policy.\n\n```json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": \"route53:GetChange\",\n      \"Resource\": \"arn:aws:route53:::change/*\"\n    },\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": \"route53:ListHostedZonesByName\",\n      \"Resource\": \"*\"\n    },\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"route53:ListResourceRecordSets\"\n      ],\n      \"Resource\": [\n        \"arn:aws:route53:::hostedzone/Z11111112222222333333\"\n      ]\n    },\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"route53:ChangeResourceRecordSets\"\n      ],\n      \"Resource\": [\n        \"arn:aws:route53:::hostedzone/Z11111112222222333333\"\n      ],\n      \"Condition\": {\n        \"ForAllValues:StringEquals\": {\n          \"route53:ChangeResourceRecordSetsNormalizedRecordNames\": [\n            \"_acme-challenge.example.com\"\n          ],\n          \"route53:ChangeResourceRecordSetsRecordTypes\": [\n            \"TXT\"\n          ]\n        }\n      }\n    }\n  ]\n}\n```\n\n\n\n## More information\n\n- [API documentation](https://docs.aws.amazon.com/Route53/latest/APIReference/API_Operations_Amazon_Route_53.html)\n- [Go client](https://github.com/aws/aws-sdk-go-v2)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/route53/route53.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_safedns.md",
    "content": "---\ntitle: \"ANS SafeDNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: safedns\ndnsprovider:\n  since:    \"v4.6.0\"\n  code:     \"safedns\"\n  url:      \"https://www.ans.co.uk/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/safedns/safedns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [ANS SafeDNS](https://www.ans.co.uk/).\n\n\n<!--more-->\n\n- Code: `safedns`\n- Since: v4.6.0\n\n\nHere is an example bash command using the ANS SafeDNS provider:\n\n```bash\nSAFEDNS_AUTH_TOKEN=xxxxxx \\\nlego --dns safedns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `SAFEDNS_AUTH_TOKEN` | Authentication token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `SAFEDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `SAFEDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `SAFEDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `SAFEDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developers.ukfast.io/documentation/safedns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/safedns/safedns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_sakuracloud.md",
    "content": "---\ntitle: \"Sakura Cloud\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: sakuracloud\ndnsprovider:\n  since:    \"v1.1.0\"\n  code:     \"sakuracloud\"\n  url:      \"https://cloud.sakura.ad.jp/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/sakuracloud/sakuracloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Sakura Cloud](https://cloud.sakura.ad.jp/).\n\n\n<!--more-->\n\n- Code: `sakuracloud`\n- Since: v1.1.0\n\n\nHere is an example bash command using the Sakura Cloud provider:\n\n```bash\nSAKURACLOUD_ACCESS_TOKEN=xxxxx \\\nSAKURACLOUD_ACCESS_TOKEN_SECRET=yyyyy \\\nlego --dns sakuracloud -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `SAKURACLOUD_ACCESS_TOKEN` | Access token |\n| `SAKURACLOUD_ACCESS_TOKEN_SECRET` | Access token secret |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `SAKURACLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `SAKURACLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `SAKURACLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `SAKURACLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developer.sakura.ad.jp/cloud/api/1.1/)\n- [Go client](https://github.com/sacloud/iaas-api-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/sakuracloud/sakuracloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_scaleway.md",
    "content": "---\ntitle: \"Scaleway\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: scaleway\ndnsprovider:\n  since:    \"v3.4.0\"\n  code:     \"scaleway\"\n  url:      \"https://developers.scaleway.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/scaleway/scaleway.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Scaleway](https://developers.scaleway.com/).\n\n\n<!--more-->\n\n- Code: `scaleway`\n- Since: v3.4.0\n\n\nHere is an example bash command using the Scaleway provider:\n\n```bash\nSCW_SECRET_KEY=xxxxxxx-xxxxx-xxxx-xxx-xxxxxx \\\nlego --dns scaleway -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `SCW_PROJECT_ID` | Project to use (optional) |\n| `SCW_SECRET_KEY` | Secret key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `SCW_ACCESS_KEY` | Access key |\n| `SCW_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `SCW_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `SCW_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `SCW_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developers.scaleway.com/en/products/domain/dns/api/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/scaleway/scaleway.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_selectel.md",
    "content": "---\ntitle: \"Selectel\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: selectel\ndnsprovider:\n  since:    \"v1.2.0\"\n  code:     \"selectel\"\n  url:      \"https://kb.selectel.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/selectel/selectel.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Selectel](https://kb.selectel.com/).\n\n\n<!--more-->\n\n- Code: `selectel`\n- Since: v1.2.0\n\n\nHere is an example bash command using the Selectel provider:\n\n```bash\nSELECTEL_API_TOKEN=xxxxx \\\nlego --dns selectel -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `SELECTEL_API_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `SELECTEL_BASE_URL` | API endpoint URL |\n| `SELECTEL_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `SELECTEL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `SELECTEL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `SELECTEL_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://kb.selectel.com/23136054.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/selectel/selectel.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_selectelv2.md",
    "content": "---\ntitle: \"Selectel v2\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: selectelv2\ndnsprovider:\n  since:    \"v4.17.0\"\n  code:     \"selectelv2\"\n  url:      \"https://selectel.ru\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/selectelv2/selectelv2.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Selectel v2](https://selectel.ru).\n\n\n<!--more-->\n\n- Code: `selectelv2`\n- Since: v4.17.0\n\n\nHere is an example bash command using the Selectel v2 provider:\n\n```bash\nSELECTELV2_USERNAME=trex \\\nSELECTELV2_PASSWORD=xxxxx \\\nSELECTELV2_ACCOUNT_ID=1234567 \\\nSELECTELV2_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \\\nlego --dns selectelv2 -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `SELECTELV2_ACCOUNT_ID` | Selectel account ID (INT) |\n| `SELECTELV2_PASSWORD` | Openstack username's password |\n| `SELECTELV2_PROJECT_ID` | Cloud project ID (UUID) |\n| `SELECTELV2_USERNAME` | Openstack username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `SELECTELV2_AUTH_REGION` | Location for auth endpoint like ResellAPI or Keystone (default: 'ru-1') |\n| `SELECTELV2_AUTH_URL` | Identity endpoint (defaul: 'https://cloud.api.selcloud.ru/identity/v3/') |\n| `SELECTELV2_BASE_URL` | API endpoint URL |\n| `SELECTELV2_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `SELECTELV2_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) |\n| `SELECTELV2_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `SELECTELV2_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n| `SELECTELV2_USER_DOMAIN_NAME` | To specify the domain name (account ID) where the user is located. (default: SELECTELV2_ACCOUNT_ID) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developers.selectel.ru/docs/cloud-services/dns_api/dns_api_actual/)\n- [Go client](https://github.com/selectel/domains-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/selectelv2/selectelv2.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_selfhostde.md",
    "content": "---\ntitle: \"SelfHost.(de|eu)\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: selfhostde\ndnsprovider:\n  since:    \"v4.19.0\"\n  code:     \"selfhostde\"\n  url:      \"https://www.selfhost.de\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/selfhostde/selfhostde.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [SelfHost.(de|eu)](https://www.selfhost.de).\n\n\n<!--more-->\n\n- Code: `selfhostde`\n- Since: v4.19.0\n\n\nHere is an example bash command using the SelfHost.(de|eu) provider:\n\n```bash\nSELFHOSTDE_USERNAME=xxx \\\nSELFHOSTDE_PASSWORD=yyy \\\nSELFHOSTDE_RECORDS_MAPPING=my.example.com:123 \\\nlego --dns selfhostde -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `SELFHOSTDE_PASSWORD` | Password |\n| `SELFHOSTDE_RECORDS_MAPPING` | Record IDs mapping with domains (ex: example.com:123:456,example.org:789,foo.example.com:147) |\n| `SELFHOSTDE_USERNAME` | Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `SELFHOSTDE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `SELFHOSTDE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 30) |\n| `SELFHOSTDE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 240) |\n| `SELFHOSTDE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\nSelfHost.de doesn't have an API to create or delete TXT records,\nthere is only an \"unofficial\" and undocumented endpoint to update an existing TXT record.\n\nSo, before using lego to request a certificate for a given domain or wildcard (such as `my.example.org` or `*.my.example.org`),\nyou must create:\n\n- one TXT record named `_acme-challenge.my.example.org` if you are **not** using wildcard for this domain.\n- two TXT records named `_acme-challenge.my.example.org` if you are using wildcard for this domain.\n\nAfter that you must edit the TXT record(s) to get the ID(s).\n\nYou then must prepare the `SELFHOSTDE_RECORDS_MAPPING` environment variable with the following format:\n\n```\n<domain_A>:<record_id_A1>:<record_id_A2>,<domain_B>:<record_id_B1>:<record_id_B2>,<domain_C>:<record_id_C1>:<record_id_C2>\n```\n\nwhere each group of domain + record ID(s) is separated with a comma (`,`),\nand the domain and record ID(s) are separated with a colon (`:`).\n\nFor example, if you want to create or renew a certificate for `my.example.org`, `*.my.example.org`, and `other.example.org`,\nyou would need:\n\n- two separate records for `_acme-challenge.my.example.org`\n- and another separate record for `_acme-challenge.other.example.org`\n\nThe resulting environment variable would then be: `SELFHOSTDE_RECORDS_MAPPING=my.example.com:123:456,other.example.com:789`\n\n\n\n\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/selfhostde/selfhostde.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_servercow.md",
    "content": "---\ntitle: \"Servercow\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: servercow\ndnsprovider:\n  since:    \"v3.4.0\"\n  code:     \"servercow\"\n  url:      \"https://servercow.de/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/servercow/servercow.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Servercow](https://servercow.de/).\n\n\n<!--more-->\n\n- Code: `servercow`\n- Since: v3.4.0\n\n\nHere is an example bash command using the Servercow provider:\n\n```bash\nSERVERCOW_USERNAME=xxxxxxxx \\\nSERVERCOW_PASSWORD=xxxxxxxx \\\nlego --dns servercow -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `SERVERCOW_PASSWORD` | API password |\n| `SERVERCOW_USERNAME` | API username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `SERVERCOW_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `SERVERCOW_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `SERVERCOW_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `SERVERCOW_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://wiki.servercow.de/en/domains/dns_api/api-syntax/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/servercow/servercow.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_shellrent.md",
    "content": "---\ntitle: \"Shellrent\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: shellrent\ndnsprovider:\n  since:    \"v4.16.0\"\n  code:     \"shellrent\"\n  url:      \"https://www.shellrent.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/shellrent/shellrent.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Shellrent](https://www.shellrent.com/).\n\n\n<!--more-->\n\n- Code: `shellrent`\n- Since: v4.16.0\n\n\nHere is an example bash command using the Shellrent provider:\n\n```bash\nSHELLRENT_USERNAME=xxxx \\\nSHELLRENT_TOKEN=yyyy \\\nlego --dns shellrent -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `SHELLRENT_TOKEN` | Token |\n| `SHELLRENT_USERNAME` | Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `SHELLRENT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `SHELLRENT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `SHELLRENT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |\n| `SHELLRENT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.shellrent.com/section/api2)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/shellrent/shellrent.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_simply.md",
    "content": "---\ntitle: \"Simply.com\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: simply\ndnsprovider:\n  since:    \"v4.4.0\"\n  code:     \"simply\"\n  url:      \"https://www.simply.com/en/domains/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/simply/simply.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Simply.com](https://www.simply.com/en/domains/).\n\n\n<!--more-->\n\n- Code: `simply`\n- Since: v4.4.0\n\n\nHere is an example bash command using the Simply.com provider:\n\n```bash\nSIMPLY_ACCOUNT_NAME=xxxxxx \\\nSIMPLY_API_KEY=yyyyyy \\\nlego --dns simply -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `SIMPLY_ACCOUNT_NAME` | Account name |\n| `SIMPLY_API_KEY` | API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `SIMPLY_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `SIMPLY_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `SIMPLY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |\n| `SIMPLY_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.simply.com/en/docs/api/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/simply/simply.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_sonic.md",
    "content": "---\ntitle: \"Sonic\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: sonic\ndnsprovider:\n  since:    \"v4.4.0\"\n  code:     \"sonic\"\n  url:      \"https://www.sonic.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/sonic/sonic.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Sonic](https://www.sonic.com/).\n\n\n<!--more-->\n\n- Code: `sonic`\n- Since: v4.4.0\n\n\nHere is an example bash command using the Sonic provider:\n\n```bash\nSONIC_USER_ID=12345 \\\nSONIC_API_KEY=4d6fbf2f9ab0fa11697470918d37625851fc0c51 \\\nlego --dns sonic -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `SONIC_API_KEY` | API Key |\n| `SONIC_USER_ID` | User ID |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `SONIC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `SONIC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `SONIC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `SONIC_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n| `SONIC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## API keys\n\nThe API keys must be generated by calling the `dyndns/api_key` endpoint.\n\nExample:\n\n```bash\n$ curl -X POST -H \"Content-Type: application/json\" --data '{\"username\":\"notarealuser\",\"password\":\"notarealpassword\",\"hostname\":\"example.com\"}' https://public-api.sonic.net/dyndns/api_key\n{\"userid\":\"12345\",\"apikey\":\"4d6fbf2f9ab0fa11697470918d37625851fc0c51\",\"result\":200,\"message\":\"OK\"}\n```\n\nSee https://public-api.sonic.net/dyndns/#requesting_an_api_key for additional details.\n\nThis `userid` and `apikey` combo allow modifications to any DNS entries connected to the managed domain (hostname).\n\nHostname should be the toplevel domain managed e.g. `example.com` not `www.example.com`.\n\n\n\n## More information\n\n- [API documentation](https://public-api.sonic.net/dyndns/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/sonic/sonic.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_spaceship.md",
    "content": "---\ntitle: \"Spaceship\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: spaceship\ndnsprovider:\n  since:    \"v4.22.0\"\n  code:     \"spaceship\"\n  url:      \"https://www.spaceship.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/spaceship/spaceship.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Spaceship](https://www.spaceship.com/).\n\n\n<!--more-->\n\n- Code: `spaceship`\n- Since: v4.22.0\n\n\nHere is an example bash command using the Spaceship provider:\n\n```bash\nSPACESHIP_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nSPACESHIP_API_SECRET=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns spaceship -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `SPACESHIP_API_KEY` | API key |\n| `SPACESHIP_API_SECRET` | API secret |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `SPACESHIP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `SPACESHIP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `SPACESHIP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `SPACESHIP_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://docs.spaceship.dev/#tag/DNS-records)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/spaceship/spaceship.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_stackpath.md",
    "content": "---\ntitle: \"Stackpath\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: stackpath\ndnsprovider:\n  since:    \"v1.1.0\"\n  code:     \"stackpath\"\n  url:      \"https://www.stackpath.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/stackpath/stackpath.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Stackpath](https://www.stackpath.com/).\n\n\n<!--more-->\n\n- Code: `stackpath`\n- Since: v1.1.0\n\n\nHere is an example bash command using the Stackpath provider:\n\n```bash\nSTACKPATH_CLIENT_ID=xxxxx \\\nSTACKPATH_CLIENT_SECRET=yyyyy \\\nSTACKPATH_STACK_ID=zzzzz \\\nlego --dns stackpath -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `STACKPATH_CLIENT_ID` | Client ID |\n| `STACKPATH_CLIENT_SECRET` | Client secret |\n| `STACKPATH_STACK_ID` | Stack ID |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `STACKPATH_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `STACKPATH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `STACKPATH_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developer.stackpath.com/en/api/dns/#tag/Zone)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/stackpath/stackpath.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_syse.md",
    "content": "---\ntitle: \"Syse\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: syse\ndnsprovider:\n  since:    \"v4.30.0\"\n  code:     \"syse\"\n  url:      \"https://www.syse.no/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/syse/syse.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Syse](https://www.syse.no/).\n\n\n<!--more-->\n\n- Code: `syse`\n- Since: v4.30.0\n\n\nHere is an example bash command using the Syse provider:\n\n```bash\nSYSE_CREDENTIALS=example.com:password \\\nlego --dns syse -d '*.example.com' -d example.com run\n\nSYSE_CREDENTIALS=example.org:password1,example.com:password2 \\\nlego --dns syse -d '*.example.org' -d example.org -d '*.example.com' -d example.com\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `SYSE_CREDENTIALS` | Comma-separated list of `zone:password` credential pairs |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `SYSE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `SYSE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `SYSE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 1200) |\n| `SYSE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.syse.no/api/dns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/syse/syse.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_technitium.md",
    "content": "---\ntitle: \"Technitium\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: technitium\ndnsprovider:\n  since:    \"v4.20.0\"\n  code:     \"technitium\"\n  url:      \"https://technitium.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/technitium/technitium.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Technitium](https://technitium.com/).\n\n\n<!--more-->\n\n- Code: `technitium`\n- Since: v4.20.0\n\n\nHere is an example bash command using the Technitium provider:\n\n```bash\nTECHNITIUM_SERVER_BASE_URL=\"https://localhost:5380\" \\\nTECHNITIUM_API_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns technitium -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `TECHNITIUM_API_TOKEN` | API token |\n| `TECHNITIUM_SERVER_BASE_URL` | Server base URL |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `TECHNITIUM_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `TECHNITIUM_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `TECHNITIUM_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `TECHNITIUM_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\nTechnitium DNS Server supports Dynamic Updates (RFC2136) for primary zones,\nso you can also use the [RFC2136 provider](https://go-acme.github.io/lego/dns/rfc2136/index.html).\n\n[RFC2136 provider](https://go-acme.github.io/lego/dns/rfc2136/index.html) is much better compared to the HTTP API option from security perspective.\nTechnitium recommends to use it in production over the HTTP API.\n\n\n\n## More information\n\n- [API documentation](https://github.com/TechnitiumSoftware/DnsServer/blob/0f83d23e605956b66ac76921199e241d9cc061bd/APIDOCS.md)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/technitium/technitium.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_tencentcloud.md",
    "content": "---\ntitle: \"Tencent Cloud DNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: tencentcloud\ndnsprovider:\n  since:    \"v4.6.0\"\n  code:     \"tencentcloud\"\n  url:      \"https://cloud.tencent.com/product/dns\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/tencentcloud/tencentcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Tencent Cloud DNS](https://cloud.tencent.com/product/dns).\n\n\n<!--more-->\n\n- Code: `tencentcloud`\n- Since: v4.6.0\n\n\nHere is an example bash command using the Tencent Cloud DNS provider:\n\n```bash\nTENCENTCLOUD_SECRET_ID=abcdefghijklmnopqrstuvwx \\\nTENCENTCLOUD_SECRET_KEY=your-secret-key \\\nlego --dns tencentcloud -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `TENCENTCLOUD_SECRET_ID` | Access key ID |\n| `TENCENTCLOUD_SECRET_KEY` | Access Key secret |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `TENCENTCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `TENCENTCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `TENCENTCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `TENCENTCLOUD_REGION` | Region |\n| `TENCENTCLOUD_SESSION_TOKEN` | Access Key token |\n| `TENCENTCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://cloud.tencent.com/document/product/1427/56153)\n- [Go client](https://github.com/tencentcloud/tencentcloud-sdk-go)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/tencentcloud/tencentcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_timewebcloud.md",
    "content": "---\ntitle: \"Timeweb Cloud\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: timewebcloud\ndnsprovider:\n  since:    \"v4.20.0\"\n  code:     \"timewebcloud\"\n  url:      \"https://timeweb.cloud/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/timewebcloud/timewebcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Timeweb Cloud](https://timeweb.cloud/).\n\n\n<!--more-->\n\n- Code: `timewebcloud`\n- Since: v4.20.0\n\n\nHere is an example bash command using the Timeweb Cloud provider:\n\n```bash\nTIMEWEBCLOUD_AUTH_TOKEN=xxxxxx \\\nlego --dns timewebcloud -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `TIMEWEBCLOUD_AUTH_TOKEN` | Authentication token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `TIMEWEBCLOUD_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) |\n| `TIMEWEBCLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `TIMEWEBCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://timeweb.cloud/api-docs)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/timewebcloud/timewebcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_todaynic.md",
    "content": "---\ntitle: \"TodayNIC/时代互联\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: todaynic\ndnsprovider:\n  since:    \"v4.32.0\"\n  code:     \"todaynic\"\n  url:      \"https://www.todaynic.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/todaynic/todaynic.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [TodayNIC/时代互联](https://www.todaynic.com/).\n\n\n<!--more-->\n\n- Code: `todaynic`\n- Since: v4.32.0\n\n\nHere is an example bash command using the TodayNIC/时代互联 provider:\n\n```bash\nTODAYNIC_AUTH_USER_ID=\"xxx\" \\\nTODAYNIC_API_KEY=\"yyy\" \\\nlego --dns todaynic -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `TODAYNIC_API_KEY` | API key |\n| `TODAYNIC_AUTH_USER_ID` | account ID |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `TODAYNIC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `TODAYNIC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `TODAYNIC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `TODAYNIC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.todaynic.com/partner/mode_Http_Api_detail.php)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/todaynic/todaynic.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_transip.md",
    "content": "---\ntitle: \"TransIP\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: transip\ndnsprovider:\n  since:    \"v2.0.0\"\n  code:     \"transip\"\n  url:      \"https://www.transip.nl/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/transip/transip.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [TransIP](https://www.transip.nl/).\n\n\n<!--more-->\n\n- Code: `transip`\n- Since: v2.0.0\n\n\nHere is an example bash command using the TransIP provider:\n\n```bash\nTRANSIP_ACCOUNT_NAME = \"Account name\" \\\nTRANSIP_PRIVATE_KEY_PATH = \"transip.key\" \\\nlego --dns transip -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `TRANSIP_ACCOUNT_NAME` | Account name |\n| `TRANSIP_PRIVATE_KEY_PATH` | Private key path |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `TRANSIP_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `TRANSIP_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `TRANSIP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 600) |\n| `TRANSIP_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 10) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.transip.eu/rest/docs.html)\n- [Go client](https://github.com/transip/gotransip)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/transip/transip.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_ultradns.md",
    "content": "---\ntitle: \"Ultradns\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: ultradns\ndnsprovider:\n  since:    \"v4.10.0\"\n  code:     \"ultradns\"\n  url:      \"https://vercara.com/authoritative-dns\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ultradns/ultradns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Ultradns](https://vercara.com/authoritative-dns).\n\n\n<!--more-->\n\n- Code: `ultradns`\n- Since: v4.10.0\n\n\nHere is an example bash command using the Ultradns provider:\n\n```bash\nULTRADNS_USERNAME=username \\\nULTRADNS_PASSWORD=password \\\nlego --dns ultradns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ULTRADNS_PASSWORD` | API Password |\n| `ULTRADNS_USERNAME` | API Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ULTRADNS_ENDPOINT` | API endpoint URL, defaults to https://api.ultradns.com/ |\n| `ULTRADNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 4) |\n| `ULTRADNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `ULTRADNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://ultra-portalstatic.ultradns.com/static/docs/REST-API_User_Guide.pdf)\n- [Go client](https://github.com/ultradns/ultradns-go-sdk)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/ultradns/ultradns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_uniteddomains.md",
    "content": "---\ntitle: \"United-Domains\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: uniteddomains\ndnsprovider:\n  since:    \"v4.29.0\"\n  code:     \"uniteddomains\"\n  url:      \"https://www.united-domains.de/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/uniteddomains/uniteddomains.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [United-Domains](https://www.united-domains.de/).\n\n\n<!--more-->\n\n- Code: `uniteddomains`\n- Since: v4.29.0\n\n\nHere is an example bash command using the United-Domains provider:\n\n```bash\nUNITEDDOMAINS_API_KEY=xxxxxxxx \\\nlego --dns uniteddomains -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `UNITEDDOMAINS_API_KEY` | API key `<prefix>.<secret>` https://www.united-domains.de/help/faq-article/getting-started-with-the-united-domains-dns-api/ |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `UNITEDDOMAINS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `UNITEDDOMAINS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `UNITEDDOMAINS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 900) |\n| `UNITEDDOMAINS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.united-domains.de/dns-apidoc/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/uniteddomains/uniteddomains.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_variomedia.md",
    "content": "---\ntitle: \"Variomedia\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: variomedia\ndnsprovider:\n  since:    \"v4.8.0\"\n  code:     \"variomedia\"\n  url:      \"https://www.variomedia.de/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/variomedia/variomedia.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Variomedia](https://www.variomedia.de/).\n\n\n<!--more-->\n\n- Code: `variomedia`\n- Since: v4.8.0\n\n\nHere is an example bash command using the Variomedia provider:\n\n```bash\nVARIOMEDIA_API_TOKEN=xxxx \\\nlego --dns variomedia -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `VARIOMEDIA_API_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `VARIOMEDIA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `VARIOMEDIA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `VARIOMEDIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `VARIOMEDIA_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n| `VARIOMEDIA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.variomedia.de/docs/dns-records.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/variomedia/variomedia.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_vegadns.md",
    "content": "---\ntitle: \"VegaDNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: vegadns\ndnsprovider:\n  since:    \"v1.1.0\"\n  code:     \"vegadns\"\n  url:      \"https://github.com/shupp/VegaDNS-API\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/vegadns/vegadns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [VegaDNS](https://github.com/shupp/VegaDNS-API).\n\n\n<!--more-->\n\n- Code: `vegadns`\n- Since: v1.1.0\n\n\n{{% notice note %}}\n_Please contribute by adding a CLI example._\n{{% /notice %}}\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `SECRET_VEGADNS_KEY` | API key |\n| `SECRET_VEGADNS_SECRET` | API secret |\n| `VEGADNS_URL` | API endpoint URL |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `VEGADNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 60) |\n| `VEGADNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 720) |\n| `VEGADNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 10) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://github.com/shupp/VegaDNS-API)\n- [Go client](https://github.com/OpenDNS/vegadns2client)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/vegadns/vegadns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_vercel.md",
    "content": "---\ntitle: \"Vercel\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: vercel\ndnsprovider:\n  since:    \"v4.7.0\"\n  code:     \"vercel\"\n  url:      \"https://vercel.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/vercel/vercel.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Vercel](https://vercel.com).\n\n\n<!--more-->\n\n- Code: `vercel`\n- Since: v4.7.0\n\n\nHere is an example bash command using the Vercel provider:\n\n```bash\nVERCEL_API_TOKEN=xxxxxx \\\nlego --dns vercel -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `VERCEL_API_TOKEN` | Authentication token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `VERCEL_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `VERCEL_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) |\n| `VERCEL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `VERCEL_TEAM_ID` | Team ID (ex: team_xxxxxxxxxxxxxxxxxxxxxxxx) |\n| `VERCEL_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://vercel.com/docs/rest-api#endpoints/dns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/vercel/vercel.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_versio.md",
    "content": "---\ntitle: \"Versio.[nl|eu|uk]\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: versio\ndnsprovider:\n  since:    \"v2.7.0\"\n  code:     \"versio\"\n  url:      \"https://www.versio.nl/domeinnamen\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/versio/versio.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Versio.[nl|eu|uk]](https://www.versio.nl/domeinnamen).\n\n\n<!--more-->\n\n- Code: `versio`\n- Since: v2.7.0\n\n\nHere is an example bash command using the Versio.[nl|eu|uk] provider:\n\n```bash\nVERSIO_USERNAME=<your login> \\\nVERSIO_PASSWORD=<your password> \\\nlego --dns versio -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `VERSIO_PASSWORD` | Basic authentication password |\n| `VERSIO_USERNAME` | Basic authentication username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `VERSIO_ENDPOINT` | The endpoint URL of the API Server |\n| `VERSIO_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `VERSIO_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) |\n| `VERSIO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `VERSIO_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n| `VERSIO_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\nTo test with the sandbox environment set ```VERSIO_ENDPOINT=https://www.versio.nl/testapi/v1/```\n\n\n\n## More information\n\n- [API documentation](https://www.versio.nl/RESTapidoc/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/versio/versio.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_vinyldns.md",
    "content": "---\ntitle: \"VinylDNS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: vinyldns\ndnsprovider:\n  since:    \"v4.4.0\"\n  code:     \"vinyldns\"\n  url:      \"https://www.vinyldns.io\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/vinyldns/vinyldns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [VinylDNS](https://www.vinyldns.io).\n\n\n<!--more-->\n\n- Code: `vinyldns`\n- Since: v4.4.0\n\n\nHere is an example bash command using the VinylDNS provider:\n\n```bash\nVINYLDNS_ACCESS_KEY=xxxxxx \\\nVINYLDNS_SECRET_KEY=yyyyy \\\nVINYLDNS_HOST=https://api.vinyldns.example.org:9443 \\\nlego --dns vinyldns -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `VINYLDNS_ACCESS_KEY` | The VinylDNS API key |\n| `VINYLDNS_HOST` | The VinylDNS API URL |\n| `VINYLDNS_SECRET_KEY` | The VinylDNS API Secret key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `VINYLDNS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `VINYLDNS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 4) |\n| `VINYLDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `VINYLDNS_QUOTE_VALUE` | Adds quotes around the TXT record value (Default: false) |\n| `VINYLDNS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 30) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\nThe vinyldns integration makes use of dotted hostnames to ease permission management.\nUsers are required to have DELETE ACL level or zone admin permissions on the VinylDNS zone containing the target host.\n\n\n\n## More information\n\n- [API documentation](https://www.vinyldns.io/api/)\n- [Go client](https://github.com/vinyldns/go-vinyldns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/vinyldns/vinyldns.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_virtualname.md",
    "content": "---\ntitle: \"Virtualname\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: virtualname\ndnsprovider:\n  since:    \"v4.30.0\"\n  code:     \"virtualname\"\n  url:      \"https://www.virtualname.es/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/virtualname/virtualname.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Virtualname](https://www.virtualname.es/).\n\n\n<!--more-->\n\n- Code: `virtualname`\n- Since: v4.30.0\n\n\nHere is an example bash command using the Virtualname provider:\n\n```bash\nVIRTUALNAME_TOKEN=xxxxxx \\\nlego --dns virtualname -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `VIRTUALNAME_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `VIRTUALNAME_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `VIRTUALNAME_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `VIRTUALNAME_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |\n| `VIRTUALNAME_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developers.virtualname.net/#dns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/virtualname/virtualname.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_vkcloud.md",
    "content": "---\ntitle: \"VK Cloud\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: vkcloud\ndnsprovider:\n  since:    \"v4.9.0\"\n  code:     \"vkcloud\"\n  url:      \"https://mcs.mail.ru/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/vkcloud/vkcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [VK Cloud](https://mcs.mail.ru/).\n\n\n<!--more-->\n\n- Code: `vkcloud`\n- Since: v4.9.0\n\n\nHere is an example bash command using the VK Cloud provider:\n\n```bash\nVK_CLOUD_PROJECT_ID=\"<your_project_id>\" \\\nVK_CLOUD_USERNAME=\"<your_email>\" \\\nVK_CLOUD_PASSWORD=\"<your_password>\" \\\nlego --dns vkcloud -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `VK_CLOUD_PASSWORD` | Password for VK Cloud account |\n| `VK_CLOUD_PROJECT_ID` | String ID of project in VK Cloud |\n| `VK_CLOUD_USERNAME` | Email of VK Cloud account |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `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 |\n| `VK_CLOUD_DOMAIN_NAME` | Openstack users domain name. Defaults to `users` but can be changed for usage with private clouds |\n| `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 |\n| `VK_CLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `VK_CLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `VK_CLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## Credential information\n\nYou can find all required and additional information on [\"Project/Keys\" page](https://mcs.mail.ru/app/en/project/keys) of your cloud.\n\n| ENV Variable               | Parameter from page |\n|----------------------------|---------------------|\n| VK_CLOUD_PROJECT_ID        | Project ID          |\n| VK_CLOUD_USERNAME          | Username            |\n| VK_CLOUD_DOMAIN_NAME       | User Domain Name    |\n| VK_CLOUD_IDENTITY_ENDPOINT | Identity endpoint   |\n\n\n\n## More information\n\n- [API documentation](https://mcs.mail.ru/docs/networks/vnet/networks/publicdns/api)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/vkcloud/vkcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_volcengine.md",
    "content": "---\ntitle: \"Volcano Engine/火山引擎\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: volcengine\ndnsprovider:\n  since:    \"v4.19.0\"\n  code:     \"volcengine\"\n  url:      \"https://www.volcengine.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/volcengine/volcengine.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Volcano Engine/火山引擎](https://www.volcengine.com/).\n\n\n<!--more-->\n\n- Code: `volcengine`\n- Since: v4.19.0\n\n\nHere is an example bash command using the Volcano Engine/火山引擎 provider:\n\n```bash\nVOLC_ACCESSKEY=xxx \\\nVOLC_SECRETKEY=yyy \\\nlego --dns volcengine -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `VOLC_ACCESSKEY` | Access Key ID (AK) |\n| `VOLC_SECRETKEY` | Secret Access Key (SK) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `VOLC_HOST` | API host |\n| `VOLC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 15) |\n| `VOLC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `VOLC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 240) |\n| `VOLC_REGION` | Region |\n| `VOLC_SCHEME` | API scheme |\n| `VOLC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.volcengine.com/docs/6758/155086)\n- [Go client](https://github.com/volcengine/volc-sdk-golang)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/volcengine/volcengine.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_vscale.md",
    "content": "---\ntitle: \"Vscale\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: vscale\ndnsprovider:\n  since:    \"v2.0.0\"\n  code:     \"vscale\"\n  url:      \"https://vscale.io/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/vscale/vscale.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Vscale](https://vscale.io/).\n\n\n<!--more-->\n\n- Code: `vscale`\n- Since: v2.0.0\n\n\nHere is an example bash command using the Vscale provider:\n\n```bash\nVSCALE_API_TOKEN=xxxxx \\\nlego --dns vscale -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `VSCALE_API_TOKEN` | API token |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `VSCALE_BASE_URL` | API endpoint URL |\n| `VSCALE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `VSCALE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `VSCALE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `VSCALE_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://developers.vscale.io/documentation/api/v1/#api-Domains_Records)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/vscale/vscale.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_vultr.md",
    "content": "---\ntitle: \"Vultr\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: vultr\ndnsprovider:\n  since:    \"v0.3.1\"\n  code:     \"vultr\"\n  url:      \"https://www.vultr.com/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/vultr/vultr.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Vultr](https://www.vultr.com/).\n\n\n<!--more-->\n\n- Code: `vultr`\n- Since: v0.3.1\n\n\nHere is an example bash command using the Vultr provider:\n\n```bash\nVULTR_API_KEY=xxxxx \\\nlego --dns vultr -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `VULTR_API_KEY` | API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `VULTR_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `VULTR_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `VULTR_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `VULTR_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.vultr.com/api/#dns)\n- [Go client](https://github.com/vultr/govultr)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/vultr/vultr.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_webnames.md",
    "content": "---\ntitle: \"webnames.ru\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: webnames\ndnsprovider:\n  since:    \"v4.15.0\"\n  code:     \"webnames\"\n  url:      \"https://www.webnames.ru/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/webnames/webnames.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [webnames.ru](https://www.webnames.ru/).\n\n\n<!--more-->\n\n- Code: `webnames`\n- Since: v4.15.0\n\n\nHere is an example bash command using the webnames.ru provider:\n\n```bash\nWEBNAMESRU_API_KEY=xxxxxx \\\nlego --dns webnamesru -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `WEBNAMESRU_API_KEY` | Domain API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `WEBNAMESRU_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `WEBNAMESRU_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `WEBNAMESRU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## API Key\n\nTo obtain the key, you need to change the DNS server to `*.nameself.com`: Personal account / My domains and services / Select the required domain / DNS servers\n\nThe API key can be found: Personal account / My domains and services / Select the required domain / Zone management / acme.sh or certbot settings\n\n\n\n## More information\n\n- [API documentation](https://github.com/regtime-ltd/certbot-dns-webnames)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/webnames/webnames.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_webnamesca.md",
    "content": "---\ntitle: \"webnames.ca\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: webnamesca\ndnsprovider:\n  since:    \"v4.28.0\"\n  code:     \"webnamesca\"\n  url:      \"https://www.webnames.ca/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/webnamesca/webnamesca.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [webnames.ca](https://www.webnames.ca/).\n\n\n<!--more-->\n\n- Code: `webnamesca`\n- Since: v4.28.0\n\n\nHere is an example bash command using the webnames.ca provider:\n\n```bash\nWEBNAMESCA_API_USER=\"xxx\" \\\nWEBNAMESCA_API_KEY=\"yyy\" \\\nlego --dns webnamesca -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `WEBNAMESCA_API_KEY` | API key |\n| `WEBNAMESCA_API_USER` | API username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `WEBNAMESCA_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `WEBNAMESCA_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `WEBNAMESCA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `WEBNAMESCA_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 120) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.webnames.ca/_/swagger/index.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/webnamesca/webnamesca.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_websupport.md",
    "content": "---\ntitle: \"Websupport\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: websupport\ndnsprovider:\n  since:    \"v4.10.0\"\n  code:     \"websupport\"\n  url:      \"https://websupport.sk\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/websupport/websupport.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Websupport](https://websupport.sk).\n\n\n<!--more-->\n\n- Code: `websupport`\n- Since: v4.10.0\n\n\nHere is an example bash command using the Websupport provider:\n\n```bash\nWEBSUPPORT_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nWEBSUPPORT_SECRET=\"yyyyyyyyyyyyyyyyyyyyy\" \\\nlego --dns websupport -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `WEBSUPPORT_API_KEY` | API key |\n| `WEBSUPPORT_SECRET` | API secret |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `WEBSUPPORT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `WEBSUPPORT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `WEBSUPPORT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `WEBSUPPORT_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) |\n| `WEBSUPPORT_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://rest.websupport.sk/v2/docs)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/websupport/websupport.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_wedos.md",
    "content": "---\ntitle: \"WEDOS\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: wedos\ndnsprovider:\n  since:    \"v4.4.0\"\n  code:     \"wedos\"\n  url:      \"https://www.wedos.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/wedos/wedos.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [WEDOS](https://www.wedos.com).\n\n\n<!--more-->\n\n- Code: `wedos`\n- Since: v4.4.0\n\n\nHere is an example bash command using the WEDOS provider:\n\n```bash\nWEDOS_USERNAME=xxxxxxxx \\\nWEDOS_WAPI_PASSWORD=xxxxxxxx \\\nlego --dns wedos -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `WEDOS_USERNAME` | Username is the same as for the admin account |\n| `WEDOS_WAPI_PASSWORD` | Password needs to be generated and IP allowed in the admin interface |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `WEDOS_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `WEDOS_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `WEDOS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 600) |\n| `WEDOS_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://kb.wedos.com/en/kategorie/wapi-api-interface/wdns-en/)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/wedos/wedos.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_westcn.md",
    "content": "---\ntitle: \"West.cn/西部数码\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: westcn\ndnsprovider:\n  since:    \"v4.21.0\"\n  code:     \"westcn\"\n  url:      \"https://www.west.cn\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/westcn/westcn.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [West.cn/西部数码](https://www.west.cn).\n\n\n<!--more-->\n\n- Code: `westcn`\n- Since: v4.21.0\n\n\nHere is an example bash command using the West.cn/西部数码 provider:\n\n```bash\nWESTCN_USERNAME=\"xxx\" \\\nWESTCN_PASSWORD=\"yyy\" \\\nlego --dns westcn -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `WESTCN_PASSWORD` | API password |\n| `WESTCN_USERNAME` | Username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `WESTCN_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `WESTCN_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 10) |\n| `WESTCN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 120) |\n| `WESTCN_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://www.west.cn/CustomerCenter/doc/domain_v2.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/westcn/westcn.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_yandex.md",
    "content": "---\ntitle: \"Yandex PDD\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: yandex\ndnsprovider:\n  since:    \"v3.7.0\"\n  code:     \"yandex\"\n  url:      \"https://pdd.yandex.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/yandex/yandex.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Yandex PDD](https://pdd.yandex.com).\n\n\n<!--more-->\n\n- Code: `yandex`\n- Since: v3.7.0\n\n\nHere is an example bash command using the Yandex PDD provider:\n\n```bash\nYANDEX_PDD_TOKEN=<your PDD Token> \\\nlego --dns yandex -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `YANDEX_PDD_TOKEN` | Basic authentication username |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `YANDEX_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `YANDEX_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `YANDEX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `YANDEX_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 21600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://yandex.com/dev/domain/doc/concepts/api-dns.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/yandex/yandex.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_yandex360.md",
    "content": "---\ntitle: \"Yandex 360\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: yandex360\ndnsprovider:\n  since:    \"v4.14.0\"\n  code:     \"yandex360\"\n  url:      \"https://360.yandex.ru\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/yandex360/yandex360.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Yandex 360](https://360.yandex.ru).\n\n\n<!--more-->\n\n- Code: `yandex360`\n- Since: v4.14.0\n\n\nHere is an example bash command using the Yandex 360 provider:\n\n```bash\nYANDEX360_OAUTH_TOKEN=<your OAuth Token> \\\nYANDEX360_ORG_ID=<your organization ID> \\\nlego --dns yandex360 -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `YANDEX360_OAUTH_TOKEN` | The OAuth Token |\n| `YANDEX360_ORG_ID` | The organization ID |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `YANDEX360_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `YANDEX360_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `YANDEX360_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `YANDEX360_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 21600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://yandex.ru/dev/api360/doc/ref/DomainDNSService.html)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/yandex360/yandex360.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_yandexcloud.md",
    "content": "---\ntitle: \"Yandex Cloud\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: yandexcloud\ndnsprovider:\n  since:    \"v4.9.0\"\n  code:     \"yandexcloud\"\n  url:      \"https://cloud.yandex.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/yandexcloud/yandexcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Yandex Cloud](https://cloud.yandex.com).\n\n\n<!--more-->\n\n- Code: `yandexcloud`\n- Since: v4.9.0\n\n\nHere is an example bash command using the Yandex Cloud provider:\n\n```bash\nYANDEX_CLOUD_IAM_TOKEN=<base64_IAM_token> \\\nYANDEX_CLOUD_FOLDER_ID=<folder/project_id> \\\nlego --dns yandexcloud -d '*.example.com' -d example.com run\n\n# ---\n\nYANDEX_CLOUD_IAM_TOKEN=$(echo '{ \\\n  \"id\": \"<string id>\", \\\n  \"service_account_id\": \"<string id>\", \\\n  \"created_at\": \"<datetime>\", \\\n  \"key_algorithm\": \"RSA_2048\", \\\n  \"public_key\": \"-----BEGIN PUBLIC KEY-----<rsa public key>-----END PUBLIC KEY-----\", \\\n  \"private_key\": \"-----BEGIN PRIVATE KEY-----<rsa private key>-----END PRIVATE KEY-----\" \\\n}' | base64) \\\nYANDEX_CLOUD_FOLDER_ID=<yandex cloud folder(project) id> \\\nlego --dns yandexcloud -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `YANDEX_CLOUD_FOLDER_ID` | The string id of folder (aka project) in Yandex Cloud |\n| `YANDEX_CLOUD_IAM_TOKEN` | The base64 encoded json which contains information about iam token of service account with `dns.admin` permissions |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `YANDEX_CLOUD_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `YANDEX_CLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `YANDEX_CLOUD_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n## IAM Token\n\nThe simplest way to retrieve IAM access token is usage of yc-cli,\nfollow [docs](https://cloud.yandex.ru/docs/iam/operations/iam-token/create-for-sa) to get it\n\n```bash\nyc iam key create --service-account-name my-robot --output key.json\ncat key.json | base64\n```\n\n\n\n## More information\n\n- [API documentation](https://cloud.yandex.com/en/docs/dns/quickstart)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/yandexcloud/yandexcloud.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_zoneedit.md",
    "content": "---\ntitle: \"ZoneEdit\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: zoneedit\ndnsprovider:\n  since:    \"v4.25.0\"\n  code:     \"zoneedit\"\n  url:      \"https://www.zoneedit.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/zoneedit/zoneedit.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [ZoneEdit](https://www.zoneedit.com).\n\n\n<!--more-->\n\n- Code: `zoneedit`\n- Since: v4.25.0\n\n\nHere is an example bash command using the ZoneEdit provider:\n\n```bash\nZONEEDIT_USER=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nZONEEDIT_AUTH_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns zoneedit -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ZONEEDIT_AUTH_TOKEN` | Authentication token |\n| `ZONEEDIT_USER` | User ID |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ZONEEDIT_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `ZONEEDIT_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `ZONEEDIT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://support.zoneedit.com/en/knowledgebase/article/changes-to-dynamic-dns)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/zoneedit/zoneedit.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_zoneee.md",
    "content": "---\ntitle: \"Zone.ee\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: zoneee\ndnsprovider:\n  since:    \"v2.1.0\"\n  code:     \"zoneee\"\n  url:      \"https://www.zone.ee/\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/zoneee/zoneee.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Zone.ee](https://www.zone.ee/).\n\n\n<!--more-->\n\n- Code: `zoneee`\n- Since: v2.1.0\n\n\nHere is an example bash command using the Zone.ee provider:\n\n```bash\nZONEEE_API_USER=xxxxx \\\nZONEEE_API_KEY=yyyyy \\\nlego --dns zoneee -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ZONEEE_API_KEY` | API key |\n| `ZONEEE_API_USER` | API user |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ZONEEE_ENDPOINT` | API endpoint URL |\n| `ZONEEE_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `ZONEEE_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 5) |\n| `ZONEEE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 300) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://api.zone.eu/v2)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/zoneee/zoneee.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/dns/zz_gen_zonomi.md",
    "content": "---\ntitle: \"Zonomi\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: zonomi\ndnsprovider:\n  since:    \"v3.5.0\"\n  code:     \"zonomi\"\n  url:      \"https://zonomi.com\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/zonomi/zonomi.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n\nConfiguration for [Zonomi](https://zonomi.com).\n\n\n<!--more-->\n\n- Code: `zonomi`\n- Since: v3.5.0\n\n\nHere is an example bash command using the Zonomi provider:\n\n```bash\nZONOMI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns zonomi -d '*.example.com' -d example.com run\n```\n\n\n\n\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n| `ZONOMI_API_KEY` | User API key |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n| `ZONOMI_HTTP_TIMEOUT` | API request timeout in seconds (Default: 30) |\n| `ZONOMI_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) |\n| `ZONOMI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) |\n| `ZONOMI_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600) |\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{% ref \"dns#configuration-and-credentials\" %}}).\n\n\n\n\n## More information\n\n- [API documentation](https://zonomi.com/app/dns/dyndns.jsp)\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- providers/dns/zonomi/zonomi.toml -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "docs/content/installation/_index.md",
    "content": "---\ntitle: \"Installation\"\ndate: 2019-03-03T16:39:46+01:00\nweight: 1\ndraft: false\n---\n\n## Binaries\n\nTo get the binary just download the latest release for your OS/Arch from [the release page](https://github.com/go-acme/lego/releases) and put the binary somewhere convenient.\nlego does not assume anything about the location you run it from.\n\n## From Docker\n\n```bash\ndocker run goacme/lego -h\n```\n\n## From package managers\n\n- [ArchLinux](https://archlinux.org/packages/extra/x86_64/lego/) (official):\n\n  ```bash\n  pacman -S lego\n  ```\n\n- [ArchLinux (AUR)](https://aur.archlinux.org/packages/lego-bin) (official):\n\n  ```bash\n  yay -S lego-bin\n  ```\n\n- [Snap](https://snapcraft.io/lego) (official):\n\n  ```bash\n  sudo snap install lego\n  ```\n  Note: The snap can only write to the `/var/snap/lego/common/.lego` directory.\n\n- [FreeBSD (Ports)](https://www.freshports.org/security/lego) (unofficial):\n\n  ```bash\n  pkg install lego\n  ```\n\n- [Gentoo](https://gitweb.gentoo.org/repo/proj/guru.git/tree/app-crypt/lego) (unofficial):\n\n  You can [enable GURU](https://wiki.gentoo.org/wiki/Project:GURU/Information_for_End_Users) repository and then:\n\n  ```bash\n  emerge app-crypt/lego\n  ```\n\n- [Homebrew](https://formulae.brew.sh/formula/lego) (unofficial):\n\n  ```bash\n  brew install lego\n  ```\n\n  or\n\n  ```bash\n  pkg install lego\n  ```\n\n- [OpenBSD (Ports)](https://openports.pl/path/security/lego) (unofficial):\n\n  ```bash\n  pkg_add lego\n  ```\n\n\n## From sources\n\nRequirements:\n\n- go1.22+.\n- environment variable: `GO111MODULE=on`\n\nTo install the latest version from sources, just run:\n\n```bash\ngo install github.com/go-acme/lego/v4/cmd/lego@latest\n```\n\nor\n\n```bash\ngit clone git@github.com:go-acme/lego.git\ncd lego\nmake        # tests + doc + build\nmake build  # only build\n```\n"
  },
  {
    "path": "docs/content/usage/_index.md",
    "content": "---\ntitle: \"Usage\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nweight: 2\n---\n\n{{% children style=\"h2\" description=\"true\" %}}\n"
  },
  {
    "path": "docs/content/usage/cli/General-Instructions.md",
    "content": "---\ntitle: General Instructions\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nsummary: Read this first to clarify some assumptions made by the following guides.\nweight: 1\n---\n\nThese examples assume you have [lego installed]({{% ref \"installation\" %}}).\nYou can get a pre-built binary from the [releases](https://github.com/go-acme/lego/releases) page.\n\nThe web server examples require that the `lego` binary has permission to bind to ports 80 and 443.\nIf your environment does not allow you to bind to these ports, please read [Running without root privileges]({{% ref \"usage/cli/Options#running-without-root-privileges\" %}}) and [Port Usage]({{% ref \"usage/cli/Options#port-usage\" %}}).\n\nUnless otherwise instructed with the `--path` command line flag, lego will look for a directory named `.lego` in the *current working directory*.\nIf you run `cd /dir/a && lego ... run`, lego will create a directory `/dir/a/.lego` where it will save account registration and certificate files into.\nIf you later try to renew a certificate with `cd /dir/b && lego ... renew`, lego will likely produce an error.\n"
  },
  {
    "path": "docs/content/usage/cli/Obtain-a-Certificate.md",
    "content": "---\ntitle: Obtain a Certificate\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nweight: 2\n---\n\nThis guide explains various ways to obtain a new certificate.\n\n<!--more-->\n\n## Using the built-in web server\n\nOpen a terminal, and execute the following command (insert your own email address and domain):\n\n```bash\nlego --email=\"you@example.com\" --domains=\"example.com\" --http run\n```\n\nYou will find your certificate in the `.lego` folder of the current working directory:\n\n```console\n$ ls -1 ./.lego/certificates\nexample.com.crt\nexample.com.issuer.crt\nexample.com.json\nexample.com.key\n[maybe more files for different domains...]\n```\n\nwhere\n\n- `example.com.crt` is the server certificate (including the CA certificate),\n- `example.com.key` is the private key needed for the server certificate,\n- `example.com.issuer.crt` is the CA certificate, and\n- `example.com.json` contains some JSON encoded meta information.\n\nFor each domain, you will have a set of these four files.\nFor wildcard certificates (`*.example.com`), the filenames will look like `_.example.com.crt`.\n\nThe `.crt` and `.key` files are PEM-encoded x509 certificates and private keys.\nIf you're looking for a `cert.pem` and `privkey.pem`, you can just use `example.com.crt` and `example.com.key`.\n\n\n## Using a DNS provider\n\nIf you can't or don't want to start a web server, you need to use a DNS provider.\nlego comes with [support for many]({{% ref \"dns#dns-providers\" %}}) providers,\nand you need to pick the one where your domain's DNS settings are set up.\nTypically, this is the registrar where you bought the domain, but in some cases this can be another third-party provider.\n\nFor this example, let's assume you have set up Gandi for your domain.\n\nExecute this command:\n\n```bash\nGANDI_API_KEY=xxx \\\nlego --email \"you@example.com\" --dns gandi --domains \"example.org\" --domains \"*.example.org\" run\n```\n\n{{% notice title=\"For a zone that has multiple SOAs\" icon=\"info-circle\" %}}\n\nThis can often be found where your DNS provider has a zone entry for an internal network (i.e. a corporate network, or home LAN) as well as the public internet.\nIn this case, point lego at an external authoritative server for the zone using the additional parameter `--dns.resolvers`.\n\n```bash\nGANDI_API_KEY=xxx \\\nlego --email \"you@example.com\" --dns gandi --dns.resolvers 9.9.9.9:53 --domains \"example.org\" --domains \"*.example.org\" run\n\n```\n\n[More information about resolvers.]({{% ref \"options#dns-resolvers-and-challenge-verification\" %}})\n\n{{% /notice %}}\n\n\n## Using a custom certificate signing request (CSR)\n\nThe first step in the process of obtaining certificates involves creating a signing request.\nThis CSR bundles various information, including the domain name(s) and a public key.\nBy default, lego will hide this step from you, but if you already have a CSR, you can easily reuse it:\n\n```bash\nlego --email=\"you@example.com\" --http --csr=\"/path/to/csr.pem\" run\n```\n\nlego will infer the domains to be validated based on the contents of the CSR, so make sure the CSR's Common Name and optional SubjectAltNames are set correctly.\n\n\n## Using an existing, running web server\n\nIf you have an existing server running on port 80, the `--http` option also requires the `--http.webroot` option.\nThis just writes the http-01 challenge token to the given directory in the folder `.well-known/acme-challenge` and does not start a server.\n\nThe given directory **should** be publicly served as `/` on the domain(s) for the validation to complete.\n\nIf the given directory is not publicly served you will have to support rewriting the request to the directory;\n\nYou could also implement a rewrite to rewrite `.well-known/acme-challenge` to the given directory `.well-known/acme-challenge`.\n\nYou should be able to run an existing webserver on port 80 and have lego write the token file with the HTTP-01 challenge key authorization to `<webroot dir>/.well-known/acme-challenge/` by running something like:\n\n```bash\nlego --accept-tos --email you@example.com --http --http.webroot /path/to/webroot --domains example.com run\n```\n\n## Running a script afterward\n\nYou can easily hook into the certificate-obtaining process by providing the path to a script:\n\n```bash\nlego --email=\"you@example.com\" --domains=\"example.com\" --http run --run-hook=\"./myscript.sh\"\n```\n\nSome information is provided through environment variables:\n\n- `LEGO_ACCOUNT_EMAIL`: the email of the account.\n- `LEGO_CERT_DOMAIN`: the main domain of the certificate.\n- `LEGO_CERT_PATH`: the path of the certificate.\n- `LEGO_CERT_KEY_PATH`: the path of the certificate key.\n- `LEGO_CERT_PEM_PATH`: (only with `--pem`) the path to the PEM certificate.\n- `LEGO_CERT_PFX_PATH`: (only with `--pfx`) the path to the PFX certificate.\n\n### Use case\n\nA typical use case is distribute the certificate for other services and reload them if necessary.\nSince PEM-formatted TLS certificates are understood by many programs, it is relatively simple to use certificates for more than a web server.\n\nThis example script installs the new certificate for a mail server, and reloads it.\nBeware: this is just a starting point, error checking is omitted for brevity.\n\n```bash\n#!/bin/bash\n\n# copy certificates to a directory controlled by Postfix\npostfix_cert_dir=\"/etc/postfix/certificates\"\n\n# our Postfix server only handles mail for @example.com domain\nif [ \"$LEGO_CERT_DOMAIN\" = \"example.com\" ]; then\n  install -u postfix -g postfix -m 0644 \"$LEGO_CERT_PATH\" \"$postfix_cert_dir\"\n  install -u postfix -g postfix -m 0640 \"$LEGO_CERT_KEY_PATH\"  \"$postfix_cert_dir\"\n\n  systemctl reload postfix@-service\nfi\n```\n"
  },
  {
    "path": "docs/content/usage/cli/Options.md",
    "content": "---\ntitle: \"Options\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nsummary: This page describes various command line options.\nweight: 4\n---\n\n## Usage\n\n{{< clihelp >}}\n\nWhen using the standard `--path` option, all certificates and account configurations are saved to a folder `.lego` in the current working directory.\n\n\n## Let's Encrypt ACME server\n\nlego defaults to communicating with the production Let's Encrypt ACME server.\nIf you'd like to test something without issuing real certificates, consider using the staging endpoint instead:\n\n```bash\nlego --server=https://acme-staging-v02.api.letsencrypt.org/directory …\n```\n\n## Running without root privileges\n\nThe CLI does not require root permissions but needs to bind to port 80 and 443 for certain challenges.\nTo run the CLI without `sudo`, you have four options:\n\n- Use `setcap 'cap_net_bind_service=+ep' /path/to/lego` (Linux only)\n- Pass the `--http.port` or/and the `--tls.port` option and specify a custom port to bind to. In this case you have to forward port 80/443 to these custom ports (see [Port Usage](#port-usage)).\n- Pass the `--http.webroot` option and specify the path to your webroot folder. In this case the challenge will be written in a file in `.well-known/acme-challenge/` inside your webroot.\n- Pass the `--dns` option and specify a DNS provider.\n\n## Port Usage\n\nBy default, lego assumes it is able to bind to ports 80 and 443 to solve challenges.\nIf this is not possible in your environment, you can use the `--http.port` and `--tls.port` options to instruct\nlego to listen on that interface:port for any incoming challenges.\n\nIf you are using either of these options, make sure you setup a proxy to redirect traffic to the chosen ports.\n\n**HTTP Port:** All plaintext HTTP requests to port **80** which begin with a request path of `/.well-known/acme-challenge/` for the HTTP challenge[^header].\n\n**TLS Port:** All TLS handshakes on port **443** for the TLS-ALPN challenge.\n\nThis traffic redirection is only needed as long as lego solves challenges. As soon as you have received your certificates you can deactivate the forwarding.\n\n[^header]: You must ensure that incoming validation requests contains the correct value for the HTTP `Host` header. If you operate lego behind a non-transparent reverse proxy (such as Apache or NGINX), you might need to alter the header field using `--http.proxy-header X-Forwarded-Host`.\n\n## DNS Resolvers and Challenge Verification\n\nWhen using a DNS challenge provider (via `--dns <name>`), Lego tries to ensure the ACME challenge token is properly setup before instructing the ACME provider to perform the validation.\n\nThis involves a few DNS queries to different servers:\n\n1. Determining the DNS zone and resolving CNAMEs.\n\n   The DNS zone for a given domain is determined by the SOA record, which contains the authoritative name server for the domain and all its subdomains.\n   For simple domains like `example.com`, this is usually `example.com` itself.\n   For other domains (like `fra.eu.cdn.example.com`), this can get complicated, as `cdn.example.com` may be delegated to the CDN provider, which means for `cdn.example.com` must exist a different SOA record.\n\n   To find the correct zone, Lego requests the SOA record for each DNS label (starting on the leaf domain, i.e. the left-most DNS label).\n   If there is no SOA record, Lego requests the SOA record of the parent label, then for its parent, etc., until it reaches the apex domain[^apex].\n   Should any DNS label on the way be a CNAME, it is resolved as per usual.\n\n   In the default configuration, Lego uses the system name servers for this, and falls back to Google's DNS servers, should they be absent.\n\n2. Verifying the challenge token.\n\n   The `_acme-challenge.<yourdomain>` TXT record must be correctly installed.\n   Lego verifies this by directly querying the authoritative name server for this record (as detected in the previous step).\n\nStrictly speaking, this verification step is not necessary, but helps to protect your ACME account.\nRemember that some ACME providers impose a rate limit on certain actions (at the time of writing, Let's Encrypt allows 300 new certificate orders per account per 3 hours).\n\nThere are also situations, where this verification step doesn't work as expected:\n\n- A \"split DNS\" setup gives different answers to clients on the internal network (Lego) vs. on the public internet (Let's Encrypt).\n- With \"hidden master\" setups, Lego may be able to directly talk to the primary DNS server, while the `_acme-challenge` record might not have fully propagated to the (public) secondary servers, yet.\n\nThe effect is the same: Lego determined the challenge token to be installed correctly, while Let's Encrypt has a different view, and rejects the certificate order.\n\nIn these cases, you can instruct Lego to use a different DNS resolver, using the `--dns.resolvers` flag.\nYou should prefer one on the public internet, otherwise you might be susceptible to the same problem.\n\n[^apex]: The apex domain is the domain you have registered with your domain registrar. For gTLDs (`.com`, `.fyi`) this is the 2nd level domain, but for ccTLDs, this can either be the 2nd level (`.de`) or 3rd level domain (`.co.uk`).\n\n## Other options\n\n### LEGO_CA_CERTIFICATES\n\nThe environment variable `LEGO_CA_CERTIFICATES` allows to specify the path to PEM-encoded CA certificates\nthat can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list.\n\nMultiple file paths can be added by using `:` (unix) or `;` (Windows) as a separator.\n\nExample:\n\n```bash\n# On Unix system\nLEGO_CA_CERTIFICATES=/foo/cert1.pem:/foo/cert2.pem\n```\n\n### LEGO_CA_SYSTEM_CERT_POOL\n\nThe environment variable `LEGO_CA_SYSTEM_CERT_POOL` can be used to define if the certificates pool must use a copy of the system cert pool.\n\nExample:\n\n```bash\nLEGO_CA_SYSTEM_CERT_POOL=true\n```\n\n### LEGO_CA_SERVER_NAME\n\nThe environment variable `LEGO_CA_SERVER_NAME` allows to specify the CA server name used to authenticate an ACME server\nwith an HTTPS certificate not issued by a CA in the system-wide trusted root list.\n\nExample:\n\n```bash\nLEGO_CA_SERVER_NAME=foo\n```\n\n### LEGO_DISABLE_CNAME_SUPPORT\n\nBy default, lego follows CNAME, the environment variable `LEGO_DISABLE_CNAME_SUPPORT` allows to disable this support.\n\nExample:\n\n```bash\nLEGO_DISABLE_CNAME_SUPPORT=false\n```\n\n### LEGO_DEBUG_CLIENT_VERBOSE_ERROR\n\nThe environment variable `LEGO_DEBUG_CLIENT_VERBOSE_ERROR` allows to enrich error messages from some of the DNS clients.\n\nExample:\n\n```bash\nLEGO_DEBUG_CLIENT_VERBOSE_ERROR=true\n```\n\n### LEGO_DEBUG_DNS_API_HTTP_CLIENT\n\n> **⚠️ WARNING: This will expose credentials in the log output! ⚠️**\n> \n> 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).\n> \n> You have been warned. Here be dragons.\n\nThe environment variable `LEGO_DEBUG_DNS_API_HTTP_CLIENT` allows debugging the DNS API interaction.\nIt will dump the full request and response to the log output.\n\nSome DNS providers don't support this option.\n\nExample:\n\n```bash\nLEGO_DEBUG_DNS_API_HTTP_CLIENT=true\n```\n\n### LEGO_DEBUG_ACME_HTTP_CLIENT\n\nThe environment variable `LEGO_DEBUG_ACME_HTTP_CLIENT` allows debug the calls to the ACME server.\n\nExample:\n\n```bash\nLEGO_DEBUG_ACME_HTTP_CLIENT=true\n```\n"
  },
  {
    "path": "docs/content/usage/cli/Renew-a-Certificate.md",
    "content": "---\ntitle: Renew a Certificate\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nweight: 3\n---\n\nThis guide describes how to renew existing certificates.\n\n<!--more-->\n\nCertificates issues by Let's Encrypt are valid for a period of 90 days.\nTo avoid certificate errors, you need to ensure that you renew your certificate *before* it expires.\n\nIn order to renew a certificate, follow the general instructions laid out under [Obtain a Certificate]({{% ref \"usage/cli/Obtain-a-Certificate\" %}}), and replace `lego ... run` with `lego ... renew`.\nNote that the `renew` sub-command supports a slightly different set of some command line flags.\n\n## Using the built-in web server\n\nBy default, and following best practices, a certificate is only renewed if its expiry date is less than 30 days in the future.\n\n```bash\nlego --email=\"you@example.com\" --domains=\"example.com\" --http renew\n```\n\nIf the certificate needs to renewed earlier, you can specify the number of remaining days:\n\n```bash\nlego --email=\"you@example.com\" --domains=\"example.com\" --http renew --days 45\n```\n\n## Using a DNS provider\n\nIf you can't or don't want to start a web server, you need to use a DNS provider.\nlego comes with [support for many]({{% ref \"dns#dns-providers\" %}}) providers,\nand you need to pick the one where your domain's DNS settings are set up.\nTypically, this is the registrar where you bought the domain, but in some cases this can be another third-party provider.\n\nFor this example, let's assume you have set up CloudFlare for your domain.\n\nExecute this command:\n\n```bash\nCLOUDFLARE_EMAIL=\"you@example.com\" \\\nCLOUDFLARE_API_KEY=\"yourprivatecloudflareapikey\" \\\nlego --email \"you@example.com\" --dns cloudflare --domains \"example.org\" renew\n```\n\n## Running a script afterward\n\nYou can easily hook into the certificate-obtaining process by providing the path to a script.\nThe hook is executed only when the certificates are effectively renewed.\n\n```bash\nlego --email=\"you@example.com\" --domains=\"example.com\" --http renew --renew-hook=\"./myscript.sh\"\n```\n\nSome information is provided through environment variables:\n\n- `LEGO_ACCOUNT_EMAIL`: the email of the account.\n- `LEGO_CERT_DOMAIN`: the main domain of the certificate.\n- `LEGO_CERT_PATH`: the path of the certificate.\n- `LEGO_CERT_KEY_PATH`: the path of the certificate key.\n- `LEGO_CERT_PEM_PATH`: (only with `--pem`) the path to the PEM certificate.\n- `LEGO_CERT_PFX_PATH`: (only with `--pfx`) the path to the PFX certificate.\n\nSee [Obtain a Certificate → Use case]({{% ref \"usage/cli/Obtain-a-Certificate#use-case\" %}}) for an example script.\n\n## Automatic renewal\n\nIt is tempting to create a cron job (or systemd timer) to automatically renew all you certificates.\n\nWhen doing so, please note that some cron defaults will cause measurable load on the ACME provider's infrastructure.\nNotably `@daily` jobs run at midnight.\n\nTo both counteract load spikes (caused by all lego users) and reduce subsequent renewal failures, we were asked to implement a small random delay for non-interactive renewals.[^loadspikes]\nSince v4.8.0, lego will pause for up to 8 minutes to help spread the load.\n\nYou can help further, by adjusting your crontab entry, like so:\n\n```ruby\n# avoid:\n#@daily      /usr/bin/lego ... renew\n#@midnight   /usr/bin/lego ... renew\n#0 0 * * *   /usr/bin/lego ... renew\n\n# instead, use a randomly chosen time:\n35 3 * * *  /usr/bin/lego ... renew\n```\n\nIf you use systemd timers, consider doing something similar, and/or introduce a `RandomizedDelaySec`:\n\n```ini\n[Unit]\nDescription=Renew certificates\n\n[Timer]\nPersistent=true\n# avoid:\n#OnCalendar=*-*-* 00:00:00\n#OnCalendar=daily\n\n# instead, use a randomly chosen time:\nOnCalendar=*-*-* 3:35\n# add extra delay, here up to 1 hour:\nRandomizedDelaySec=1h\n\n[Install]\nWantedBy=timers.target\n```\n\n[^loadspikes]: See [GitHub issue #1656](https://github.com/go-acme/lego/issues/1656) for an excellent problem description.\n"
  },
  {
    "path": "docs/content/usage/cli/_index.md",
    "content": "---\ntitle: \"CLI\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\n---\n\nLego can be use as a CLI.\n\n<!--more-->\n\n{{% children style=\"h2\" description=\"true\" %}}\n"
  },
  {
    "path": "docs/content/usage/cli/examples.md",
    "content": "---\ntitle: Examples\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nhidden: true\n---\n\n{{% notice note %}}\n**Heads up!** We've restructured the content a bit.\n{{% /notice %}}\n\nYou'll find the content now at one of these pages:\n\n- Guide: [**How to obtain a certificate**]({{% ref \"usage/cli/Obtain-a-Certificate\" %}})\n  - Using the built-in web server\n  - Using a DNS provider\n  - Using a custom certificate signing request (CSR)\n  - Using an existing, running web server\n  - Running a script afterward\n  - Use case\n- Guide: [**How to renew a certificate**]({{% ref \"usage/cli/Renew-a-Certificate\" %}})\n  - Using the built-in web server\n  - Using a DNS provider\n  - Running a script afterward\n  - Automatic renewal\n- Reference: [**Command line options**]({{% ref \"usage/cli/Options\" %}})\n  - Usage\n  - Let's Encrypt ACME server\n  - Running without root privileges\n  - Port Usage\n"
  },
  {
    "path": "docs/content/usage/library/Writing-a-Challenge-Solver.md",
    "content": "---\ntitle: \"Writing a Challenge Solver\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\n---\n\nLego can solve multiple ACME challenge types out of the box, but sometimes you have custom requirements.\n\n<!--more-->\n\nFor example, you may want to write a solver for the DNS-01 challenge that works with a different DNS provider (lego already supports CloudFlare, AWS, DigitalOcean, and others).\n\nThe DNS-01 challenge is advantageous when other challenge types are impossible.\nFor example, the HTTP-01 challenge doesn't work well behind a load balancer or CDN and the TLS-ALPN-01 challenge breaks behind TLS termination.\n\nBut even if using HTTP-01 or TLS-ALPN-01 challenges, you may have specific needs that lego does not consider by default.\n\nYou can write something called a `challenge.Provider` that implements [this interface](https://pkg.go.dev/github.com/go-acme/lego/v4/challenge#Provider):\n\n```go\ntype Provider interface {\n\tPresent(domain, token, keyAuth string) error\n\tCleanUp(domain, token, keyAuth string) error\n}\n```\n\nThis provides the means to solve a challenge.\nFirst you present a token to the ACME server in a way defined by the challenge type you're solving for, then you \"clean up\" after the challenge finishes.\n\n## Writing a challenge.Provider\n\nPretend we want to write our own DNS-01 challenge provider (other challenge types have different requirements but the same principles apply).\n\nThis will let us prove ownership of domain names parked at a new, imaginary DNS service called BestDNS without having to start our own HTTP server.\nBestDNS has an API that, given an authentication token, allows us to manipulate DNS records.\n\nThis simplistic example has only one field to store the auth token, but in reality you may need to keep more state.\n\n```go\ntype DNSProviderBestDNS struct {\n\tapiAuthToken string\n}\n```\n\nWe should provide a constructor that returns a *pointer* to the `struct`.\nThis is important in case we need to maintain state in the `struct`.\n\n```go\nfunc NewDNSProviderBestDNS(apiAuthToken string) (*DNSProviderBestDNS, error) {\n\treturn &DNSProviderBestDNS{apiAuthToken: apiAuthToken}, nil\n}\n```\n\nNow we need to implement the interface.\nWe'll start with the `Present` method.\nYou'll be passed the `domain` name for which you're proving ownership, a `token`, and a `keyAuth` string.\nHow your provider uses `token` and `keyAuth`, or if you even use them at all, depends on the challenge type.\nFor DNS-01, we'll just use `domain` and `keyAuth`.\n\n```go\nfunc (d *DNSProviderBestDNS) Present(domain, token, keyAuth string) error {\n    info := dns01.GetChallengeInfo(domain, keyAuth)\n    // make API request to set a TXT record on fqdn with value and TTL\n    return nil\n}\n```\n\nAfter calling `dns01.GetChallengeInfo(domain, keyAuth)`, we now have the information we need to make our API request and set the TXT record:\n- `FQDN` is the fully qualified domain name on which to set the TXT record.\n- `EffectiveFQDN` is the fully qualified domain name after the CNAMEs resolutions on which to set the TXT record.\n- `Value` is the record's value to set on the record.\n\nSo then you make an API request to the DNS service according to their docs.\nOnce the TXT record is set on the domain, you may return and the challenge will proceed.\n\nThe ACME server will then verify that you did what it required you to do, and once it is finished, lego will call your `CleanUp` method.\nIn our case, we want to remove the TXT record we just created.\n\n```go\nfunc (d *DNSProviderBestDNS) CleanUp(domain, token, keyAuth string) error {\n    // clean up any state you created in Present, like removing the TXT record\n}\n```\n\nIn our case, we'd just make another API request to have the DNS record deleted; no need to keep it and clutter the zone file.\n\n## Using your new challenge.Provider\n\nTo use your new challenge provider, call [`client.Challenge.SetDNS01Provider`](https://pkg.go.dev/github.com/go-acme/lego/v4/challenge/resolver#SolverManager.SetDNS01Provider) to tell lego, \"For this challenge, use this provider\".\nIn our case:\n\n```go\nbestDNS, err := NewDNSProviderBestDNS(\"my-auth-token\")\nif err != nil {\n    return err\n}\n\nclient.Challenge.SetDNS01Provider(bestDNS)\n```\n\nThen, when this client tries to solve the DNS-01 challenge, it will use our new provider, which sets TXT records on a domain name hosted by BestDNS.\n\nThat's really all there is to it.\nGo make awesome things!\n"
  },
  {
    "path": "docs/content/usage/library/_index.md",
    "content": "---\ntitle: \"Library\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\n---\n\nLego can be used as a Go Library.\n\n<!--more-->\n\n## GoDoc\n\nThe GoDoc can be found here: [Go Reference](https://pkg.go.dev/github.com/go-acme/lego/v4).\n\n## Usage\n\nA valid, but bare-bones example use of the acme package:\n\n```go\npackage main\n\nimport (\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/go-acme/lego/v4/certificate\"\n\t\"github.com/go-acme/lego/v4/challenge/http01\"\n\t\"github.com/go-acme/lego/v4/challenge/tlsalpn01\"\n\t\"github.com/go-acme/lego/v4/lego\"\n\t\"github.com/go-acme/lego/v4/registration\"\n)\n\n// You'll need a user or account type that implements acme.User\ntype MyUser struct {\n\tEmail        string\n\tRegistration *registration.Resource\n\tkey          crypto.PrivateKey\n}\n\nfunc (u *MyUser) GetEmail() string {\n\treturn u.Email\n}\nfunc (u MyUser) GetRegistration() *registration.Resource {\n\treturn u.Registration\n}\nfunc (u *MyUser) GetPrivateKey() crypto.PrivateKey {\n\treturn u.key\n}\n\nfunc main() {\n\n\t// Create a user. New accounts need an email and private key to start.\n\tprivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tmyUser := MyUser{\n\t\tEmail: \"you@yours.com\",\n\t\tkey:   privateKey,\n\t}\n\n\tconfig := lego.NewConfig(&myUser)\n\n\t// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM.\n\tconfig.CADirURL = \"http://192.168.99.100:4000/directory\"\n\tconfig.Certificate.KeyType = certcrypto.RSA2048\n\n\t// A client facilitates communication with the CA server.\n\tclient, err := lego.NewClient(config)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// We specify an HTTP port of 5002 and an TLS port of 5001 on all interfaces\n\t// because we aren't running as root and can't bind a listener to port 80 and 443\n\t// (used later when we attempt to pass challenges). Keep in mind that you still\n\t// need to proxy challenge traffic to port 5002 and 5001.\n\terr = client.Challenge.SetHTTP01Provider(http01.NewProviderServer(\"\", \"5002\"))\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\terr = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer(\"\", \"5001\"))\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// New users will need to register\n\treg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tmyUser.Registration = reg\n\n\trequest := certificate.ObtainRequest{\n\t\tDomains: []string{\"mydomain.com\"},\n\t\tBundle:  true,\n\t}\n\tcertificates, err := client.Certificate.Obtain(request)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// Each certificate comes back with the cert bytes, the bytes of the client's\n\t// private key, and a certificate URL. SAVE THESE TO DISK.\n\tfmt.Printf(\"%#v\\n\", certificates)\n\n\t// ... all done.\n}\n```\n"
  },
  {
    "path": "docs/data/zz_cli_help.toml",
    "content": "# THIS FILE IS AUTO-GENERATED. PLEASE DO NOT EDIT.\n\n\n[[command]]\ntitle   = \"lego help\"\ncontent = \"\"\"\nNAME:\n   lego - Let's Encrypt client written in Go\n\nUSAGE:\n   lego [global options] command [command options]\n\nCOMMANDS:\n   run      Register an account, then create and install a certificate\n   revoke   Revoke a certificate\n   renew    Renew a certificate\n   dnshelp  Shows additional help for the '--dns' global option\n   list     Display certificates and accounts information.\n   help, h  Shows a list of commands or help for one command\n\nGLOBAL OPTIONS:\n   --domains value, -d value [ --domains value, -d value ]      Add a domain to the process. Can be specified multiple times.\n   --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]\n   --accept-tos, -a                                             By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service. (default: false)\n   --email value, -m value                                      Email used for registration and recovery contact. [$LEGO_EMAIL]\n   --disable-cn                                                 Disable the use of the common name in the CSR. (default: false)\n   --csr value, -c value                                        Certificate signing request filename, if an external CSR is to be used.\n   --eab                                                        Use External Account Binding for account registration. Requires --kid and --hmac. (default: false) [$LEGO_EAB]\n   --kid value                                                  Key identifier from External CA. Used for External Account Binding. [$LEGO_EAB_KID]\n   --hmac value                                                 MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding. [$LEGO_EAB_HMAC]\n   --key-type value, -k value                                   Key type to use for private keys. Supported: rsa2048, rsa3072, rsa4096, rsa8192, ec256, ec384. (default: \"ec256\")\n   --filename value                                             (deprecated) Filename of the generated certificate.\n   --path value                                                 Directory to use for storing the data. (default: \"./.lego\") [$LEGO_PATH]\n   --http                                                       Use the HTTP-01 challenge to solve challenges. Can be mixed with other types of challenges. (default: false)\n   --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\")\n   --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)\n   --http.proxy-header value                                    Validate against this HTTP header when solving HTTP-01 based challenges behind a reverse proxy. (default: \"Host\")\n   --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\n   --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.\n   --http.s3-bucket value                                       Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.\n   --tls                                                        Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges. (default: false)\n   --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\")\n   --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)\n   --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.\n   --dns.disable-cp                                             (deprecated) use dns.propagation-disable-ans instead. (default: false)\n   --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)\n   --dns.propagation-rns                                        By setting this flag to true, use all the recursive nameservers to check the propagation of the TXT record. (default: false)\n   --dns.propagation-wait value                                 By setting this flag, disables all the propagation checks of the TXT record and uses a wait duration instead. (default: 0s)\n   --dns.resolvers value [ --dns.resolvers value ]              Set the resolvers to use for performing (recursive) CNAME resolving and apex domain determination. For DNS-01 challenge verification, the authoritative DNS server is queried directly. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.\n   --http-timeout value                                         Set the HTTP timeout value to a specific value in seconds. (default: 0)\n   --tls-skip-verify                                            Skip the TLS verification of the ACME server. (default: false)\n   --dns-timeout value                                          Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name server queries. (default: 10)\n   --pem                                                        Generate an additional .pem (base64) file by concatenating the .key and .crt files together. (default: false)\n   --pfx                                                        Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. (default: false) [$LEGO_PFX]\n   --pfx.pass value                                             The password used to encrypt the .pfx (PCKS#12) file. (default: \"changeit\") [$LEGO_PFX_PASSWORD]\n   --pfx.format value                                           The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256. (default: \"RC2\") [$LEGO_PFX_FORMAT]\n   --cert.timeout value                                         Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30)\n   --overall-request-limit value                                ACME overall requests limit. (default: 18)\n   --user-agent value                                           Add to the user-agent sent to the CA to identify an application embedding lego-cli\n   --help, -h                                                   show help\n\"\"\"\n\n[[command]]\ntitle   = \"lego help run\"\ncontent = \"\"\"\nNAME:\n   lego run - Register an account, then create and install a certificate\n\nUSAGE:\n   lego run [command options]\n\nOPTIONS:\n   --no-bundle                               Do not create a certificate bundle by adding the issuers certificate to the new certificate. (default: false)\n   --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)\n   --not-before value                        Set the notBefore field in the certificate (RFC3339 format)\n   --not-after value                         Set the notAfter field in the certificate (RFC3339 format)\n   --private-key value                       Path to private key (in PEM encoding) for the certificate. By default, the private key is generated.\n   --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.\n   --profile value                           If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.\n   --always-deactivate-authorizations value  Force the authorizations to be relinquished even if the certificate request was successful.\n   --run-hook value                          Define a hook. The hook is executed when the certificates are effectively created.\n   --run-hook-timeout value                  Define the timeout for the hook execution. (default: 2m0s)\n   --help, -h                                show help\n\"\"\"\n\n[[command]]\ntitle   = \"lego help renew\"\ncontent = \"\"\"\nNAME:\n   lego renew - Renew a certificate\n\nUSAGE:\n   lego renew [command options]\n\nOPTIONS:\n   --days value                              The number of days left on a certificate to renew it. (default: 30)\n   --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)\n   --ari-disable                             Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed. (default: false)\n   --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)\n   --reuse-key                               Used to indicate you want to reuse your current private key for the new certificate. (default: false)\n   --no-bundle                               Do not create a certificate bundle by adding the issuers certificate to the new certificate. (default: false)\n   --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)\n   --not-before value                        Set the notBefore field in the certificate (RFC3339 format)\n   --not-after value                         Set the notAfter field in the certificate (RFC3339 format)\n   --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.\n   --profile value                           If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.\n   --always-deactivate-authorizations value  Force the authorizations to be relinquished even if the certificate request was successful.\n   --renew-hook value                        Define a hook. The hook is executed only when the certificates are effectively renewed.\n   --renew-hook-timeout value                Define the timeout for the hook execution. (default: 2m0s)\n   --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)\n   --force-cert-domains                      Check and ensure that the cert's domain list matches those passed in the domains argument. (default: false)\n   --help, -h                                show help\n\"\"\"\n\n[[command]]\ntitle   = \"lego help revoke\"\ncontent = \"\"\"\nNAME:\n   lego revoke - Revoke a certificate\n\nUSAGE:\n   lego revoke [command options]\n\nOPTIONS:\n   --keep, -k      Keep the certificates after the revocation instead of archiving them. (default: false)\n   --reason value  Identifies the reason for the certificate revocation. See https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1. Valid values are: 0 (unspecified), 1 (keyCompromise), 2 (cACompromise), 3 (affiliationChanged), 4 (superseded), 5 (cessationOfOperation), 6 (certificateHold), 8 (removeFromCRL), 9 (privilegeWithdrawn), or 10 (aACompromise). (default: 0)\n   --help, -h      show help\n\"\"\"\n\n[[command]]\ntitle   = \"lego help list\"\ncontent = \"\"\"\nNAME:\n   lego list - Display certificates and accounts information.\n\nUSAGE:\n   lego list [command options]\n\nOPTIONS:\n   --accounts, -a  Display accounts. (default: false)\n   --names, -n     Display certificate common names only. (default: false)\n   --help, -h      show help\n\"\"\"\n\n[[command]]\ntitle   = \"lego dnshelp\"\ncontent = \"\"\"\nCredentials for DNS providers must be passed through environment variables.\n\nTo display the documentation for a specific DNS provider, run:\n\n  $ lego dnshelp -c code\n\nSupported DNS providers:\n  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\n\nMore information: https://go-acme.github.io/lego/dns\n\"\"\"\n"
  },
  {
    "path": "docs/go.mod",
    "content": "module github.com/go-acme/lego/docs\n\ngo 1.20\n\nrequire github.com/McShelby/hugo-theme-relearn v0.0.0-20250707094454-9803d5122ebb\n"
  },
  {
    "path": "docs/go.sum",
    "content": "github.com/McShelby/hugo-theme-relearn v0.0.0-20250707094454-9803d5122ebb h1:iTGWOs8uKUaYmd7+wHRyPGXxt+SS5Bhvx2RRboYRXlI=\ngithub.com/McShelby/hugo-theme-relearn v0.0.0-20250707094454-9803d5122ebb/go.mod h1:mKQQdxZNIlLvAj8X3tMq+RzntIJSr9z7XdzuMomt0IM=\n"
  },
  {
    "path": "docs/hugo.toml",
    "content": "baseURL = \"https://go-acme.github.io/lego/\"\nlanguageCode = \"en-us\"\ntitle = \"Lego\"\n\n[permalinks]\n  dns = \"/dns/:slug/\"\n\n[params]\n  # Description of the site, will be used in meta information\n#  description = \"\"\n  # Shows a checkmark for visited pages on the menu\n  showVisitedLinks = true\n  # Change default color scheme with a variant one. Can be \"red\", \"blue\", \"green\".\n  themeVariant = \"blue\"\n  custom_css = [\"css/theme-custom.css\"]\n  disableLandingPageButton = true\n  hideAuthorEmail = true\n  hideAuthorName = true\n\n  # Author of the site, will be used in meta information\n  [params.author]\n    name = \"Lego Team\"\n\n[Languages]\n[Languages.en]\n  title = \"Let’s Encrypt client and ACME library written in Go.\"\n  weight = 1\n  languageName = \"English\"\n\n[[Languages.en.menu.shortcuts]]\n  name = \"<i class='fab fa-fw fa-github'></i> GitHub repo\"\n  identifier = \"ds\"\n  url = \"https://github.com/go-acme/lego\"\n  weight = 10\n\n[[Languages.en.menu.shortcuts]]\n  name = \"<i class='fas fa-fw fa-bug'></i> Issues\"\n  url = \"https://github.com/go-acme/lego/issues\"\n  weight = 11\n\n[[Languages.en.menu.shortcuts]]\n  name = \"<i class='fas fa-fw fa-comments'></i> Discussions\"\n  url = \"https://github.com/go-acme/lego/discussions\"\n  weight = 12\n\n[outputs]\n  home = ['html', 'rss', 'print']\n\n[module]\n[[module.imports]]\n  path = \"github.com/McShelby/hugo-theme-relearn\"\n"
  },
  {
    "path": "docs/layouts/partials/logo.html",
    "content": "<a id=\"logo\" href=\"/lego\"><img src=\"/lego/images/lego-logo-white.min.svg\" alt=\"lego logo\"></a>\n"
  },
  {
    "path": "docs/layouts/shortcodes/clihelp.html",
    "content": "{{ $tabs := slice }}\n\n{{ $commands := index $.Site.Data.zz_cli_help \"command\" }}\n{{ range $idx, $tab := $commands }}\n  {{ $content := (print \"```\\n\" $tab.content \"\\n```\") }}\n  {{ $tabs = $tabs | append (dict \"title\" $tab.title \"content\" ($content | page.RenderString) \"icon\" \"terminal\") }}\n{{ end }}\n\n{{ partial \"shortcodes/tabs.html\" (dict\n  \"page\" page\n  \"content\" $tabs\n) }}\n"
  },
  {
    "path": "docs/layouts/shortcodes/tableofdnsproviders.html",
    "content": "{{ $_hugo_config := `{ \"version\": 1 }` }}\n\n<table>\n  <thead>\n    <tr>\n      <th colspan=\"2\">Provider name</th>\n      <th>CLI flag name</th>\n      <th>Required lego version</th>\n    </tr>\n  </thead>\n  <tbody>\n    {{- range .Site.AllPages.ByWeight -}}\n      {{- if .Params.dnsprovider -}}\n      {{- $params := .Params.dnsprovider -}}\n        <tr>\n          <td>\n            <a href=\"{{ .RelPermalink }}\">{{ .Title }}</a>\n          </td>\n          <td>\n            {{- if $params.url -}}\n              <a href=\"{{ $params.url }}\" target=\"_blank\">Website</a>\n            {{- end -}}\n          </td>\n          <td>\n            <code>{{ $params.code }}</code>\n          </td>\n          <td>{{ $params.since }}</td>\n        </tr>\n      {{ end }}\n    {{ end }}\n  </tbody>\n</table>\n"
  },
  {
    "path": "docs/static/.nojekyll",
    "content": ""
  },
  {
    "path": "docs/static/css/theme-custom.css",
    "content": "#top-bar-sticky-wrapper,\n#top-bar,\n#body-inner {\n  max-width: 72em;\n  margin: 0 auto;\n}\n"
  },
  {
    "path": "e2e/challenges_test.go",
    "content": "package e2e\n\nimport (\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/go-acme/lego/v4/certificate\"\n\t\"github.com/go-acme/lego/v4/challenge/http01\"\n\t\"github.com/go-acme/lego/v4/challenge/tlsalpn01\"\n\t\"github.com/go-acme/lego/v4/e2e/loader\"\n\t\"github.com/go-acme/lego/v4/lego\"\n\t\"github.com/go-acme/lego/v4/registration\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestDomain1 = \"acme.localhost\"\n\ttestDomain2 = \"lego.localhost\"\n\ttestDomain3 = \"acme.lego.localhost\"\n\ttestDomain4 = \"légô.localhost\"\n)\n\nconst (\n\ttestEmail1 = \"lego@example.com\"\n\ttestEmail2 = \"acme@example.com\"\n)\n\nvar load = loader.EnvLoader{\n\tPebbleOptions: &loader.CmdOption{\n\t\tHealthCheckURL: \"https://localhost:14000/dir\",\n\t\tArgs:           []string{\"-strict\", \"-config\", \"fixtures/pebble-config.json\"},\n\t\tEnv:            []string{\"PEBBLE_VA_NOSLEEP=1\", \"PEBBLE_WFE_NONCEREJECT=20\"},\n\t},\n\tLegoOptions: []string{\n\t\t\"LEGO_CA_CERTIFICATES=./fixtures/certs/pebble.minica.pem\",\n\t\t\"LEGO_DEBUG_ACME_HTTP_CLIENT=1\",\n\t},\n}\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(load.MainTest(m))\n}\n\nfunc TestHelp(t *testing.T) {\n\toutput, err := load.RunLegoCombinedOutput(\"-h\")\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"%s\\n\", output)\n\t\tt.Fatal(err)\n\t}\n\n\tfmt.Fprintf(os.Stdout, \"%s\\n\", output)\n}\n\nfunc TestChallengeHTTP_Run(t *testing.T) {\n\tloader.CleanLegoFiles()\n\n\terr := load.RunLego(\n\t\t\"-m\", testEmail1,\n\t\t\"--accept-tos\",\n\t\t\"-s\", \"https://localhost:14000/dir\",\n\t\t\"-d\", testDomain1,\n\t\t\"--http\",\n\t\t\"--http.port\", \":5002\",\n\t\t\"run\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestChallengeTLS_Run_Domains(t *testing.T) {\n\tloader.CleanLegoFiles()\n\n\terr := load.RunLego(\n\t\t\"-m\", testEmail1,\n\t\t\"--accept-tos\",\n\t\t\"-s\", \"https://localhost:14000/dir\",\n\t\t\"-d\", testDomain1,\n\t\t\"--tls\",\n\t\t\"--tls.port\", \":5001\",\n\t\t\"run\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestChallengeTLS_Run_IP(t *testing.T) {\n\tloader.CleanLegoFiles()\n\n\terr := load.RunLego(\n\t\t\"-m\", testEmail1,\n\t\t\"--accept-tos\",\n\t\t\"-s\", \"https://localhost:14000/dir\",\n\t\t\"-d\", \"127.0.0.1\",\n\t\t\"--tls\",\n\t\t\"--tls.port\", \":5001\",\n\t\t\"run\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestChallengeTLS_Run_CSR(t *testing.T) {\n\tloader.CleanLegoFiles()\n\n\tcsrPath := createTestCSRFile(t, true)\n\n\terr := load.RunLego(\n\t\t\"-m\", testEmail1,\n\t\t\"--accept-tos\",\n\t\t\"-s\", \"https://localhost:14000/dir\",\n\t\t\"-csr\", csrPath,\n\t\t\"--tls\",\n\t\t\"--tls.port\", \":5001\",\n\t\t\"run\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestChallengeTLS_Run_CSR_PEM(t *testing.T) {\n\tloader.CleanLegoFiles()\n\n\tcsrPath := createTestCSRFile(t, false)\n\n\terr := load.RunLego(\n\t\t\"-m\", testEmail1,\n\t\t\"--accept-tos\",\n\t\t\"-s\", \"https://localhost:14000/dir\",\n\t\t\"-csr\", csrPath,\n\t\t\"--tls\",\n\t\t\"--tls.port\", \":5001\",\n\t\t\"run\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestChallengeTLS_Run_Revoke(t *testing.T) {\n\tloader.CleanLegoFiles()\n\n\terr := load.RunLego(\n\t\t\"-m\", testEmail1,\n\t\t\"--accept-tos\",\n\t\t\"-s\", \"https://localhost:14000/dir\",\n\t\t\"-d\", testDomain2,\n\t\t\"-d\", testDomain3,\n\t\t\"--tls\",\n\t\t\"--tls.port\", \":5001\",\n\t\t\"run\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = load.RunLego(\n\t\t\"-m\", testEmail1,\n\t\t\"--accept-tos\",\n\t\t\"-s\", \"https://localhost:14000/dir\",\n\t\t\"-d\", testDomain2,\n\t\t\"--tls\",\n\t\t\"--tls.port\", \":5001\",\n\t\t\"revoke\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestChallengeTLS_Run_Revoke_Non_ASCII(t *testing.T) {\n\tloader.CleanLegoFiles()\n\n\terr := load.RunLego(\n\t\t\"-m\", testEmail1,\n\t\t\"--accept-tos\",\n\t\t\"-s\", \"https://localhost:14000/dir\",\n\t\t\"-d\", testDomain4,\n\t\t\"--tls\",\n\t\t\"--tls.port\", \":5001\",\n\t\t\"run\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = load.RunLego(\n\t\t\"-m\", testEmail1,\n\t\t\"--accept-tos\",\n\t\t\"-s\", \"https://localhost:14000/dir\",\n\t\t\"-d\", testDomain4,\n\t\t\"--tls\",\n\t\t\"--tls.port\", \":5001\",\n\t\t\"revoke\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestChallengeHTTP_Client_Obtain(t *testing.T) {\n\terr := os.Setenv(\"LEGO_CA_CERTIFICATES\", \"./fixtures/certs/pebble.minica.pem\")\n\trequire.NoError(t, err)\n\n\tdefer func() { _ = os.Unsetenv(\"LEGO_CA_CERTIFICATES\") }()\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tuser := &fakeUser{privateKey: privateKey}\n\tconfig := lego.NewConfig(user)\n\tconfig.CADirURL = load.PebbleOptions.HealthCheckURL\n\n\tclient, err := lego.NewClient(config)\n\trequire.NoError(t, err)\n\n\terr = client.Challenge.SetHTTP01Provider(http01.NewProviderServer(\"\", \"5002\"))\n\trequire.NoError(t, err)\n\n\treg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})\n\trequire.NoError(t, err)\n\n\tuser.registration = reg\n\n\trequest := certificate.ObtainRequest{\n\t\tDomains: []string{testDomain1},\n\t\tBundle:  true,\n\t}\n\tresource, err := client.Certificate.Obtain(request)\n\trequire.NoError(t, err)\n\n\trequire.NotNil(t, resource)\n\tassert.Equal(t, testDomain1, resource.Domain)\n\tassert.Regexp(t, `https://localhost:14000/certZ/[\\w\\d]{14,}`, resource.CertURL)\n\tassert.Regexp(t, `https://localhost:14000/certZ/[\\w\\d]{14,}`, resource.CertStableURL)\n\tassert.NotEmpty(t, resource.Certificate)\n\tassert.NotEmpty(t, resource.IssuerCertificate)\n\tassert.Empty(t, resource.CSR)\n}\n\nfunc TestChallengeHTTP_Client_Obtain_profile(t *testing.T) {\n\terr := os.Setenv(\"LEGO_CA_CERTIFICATES\", \"./fixtures/certs/pebble.minica.pem\")\n\trequire.NoError(t, err)\n\n\tdefer func() { _ = os.Unsetenv(\"LEGO_CA_CERTIFICATES\") }()\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tuser := &fakeUser{privateKey: privateKey}\n\tconfig := lego.NewConfig(user)\n\tconfig.CADirURL = load.PebbleOptions.HealthCheckURL\n\n\tclient, err := lego.NewClient(config)\n\trequire.NoError(t, err)\n\n\terr = client.Challenge.SetHTTP01Provider(http01.NewProviderServer(\"\", \"5002\"))\n\trequire.NoError(t, err)\n\n\treg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})\n\trequire.NoError(t, err)\n\n\tuser.registration = reg\n\n\trequest := certificate.ObtainRequest{\n\t\tDomains: []string{testDomain1},\n\t\tBundle:  true,\n\t\tProfile: \"shortlived\",\n\t}\n\tresource, err := client.Certificate.Obtain(request)\n\trequire.NoError(t, err)\n\n\trequire.NotNil(t, resource)\n\tassert.Equal(t, testDomain1, resource.Domain)\n\tassert.Regexp(t, `https://localhost:14000/certZ/[\\w\\d]{14,}`, resource.CertURL)\n\tassert.Regexp(t, `https://localhost:14000/certZ/[\\w\\d]{14,}`, resource.CertStableURL)\n\tassert.NotEmpty(t, resource.Certificate)\n\tassert.NotEmpty(t, resource.IssuerCertificate)\n\tassert.Empty(t, resource.CSR)\n}\n\nfunc TestChallengeHTTP_Client_Obtain_emails_csr(t *testing.T) {\n\terr := os.Setenv(\"LEGO_CA_CERTIFICATES\", \"./fixtures/certs/pebble.minica.pem\")\n\trequire.NoError(t, err)\n\n\tdefer func() { _ = os.Unsetenv(\"LEGO_CA_CERTIFICATES\") }()\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tuser := &fakeUser{privateKey: privateKey}\n\tconfig := lego.NewConfig(user)\n\tconfig.CADirURL = load.PebbleOptions.HealthCheckURL\n\n\tclient, err := lego.NewClient(config)\n\trequire.NoError(t, err)\n\n\terr = client.Challenge.SetHTTP01Provider(http01.NewProviderServer(\"\", \"5002\"))\n\trequire.NoError(t, err)\n\n\treg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})\n\trequire.NoError(t, err)\n\n\tuser.registration = reg\n\n\trequest := certificate.ObtainRequest{\n\t\tDomains:        []string{testDomain1},\n\t\tBundle:         true,\n\t\tEmailAddresses: []string{testEmail1},\n\t}\n\tresource, err := client.Certificate.Obtain(request)\n\trequire.NoError(t, err)\n\n\trequire.NotNil(t, resource)\n\tassert.Equal(t, testDomain1, resource.Domain)\n\tassert.Regexp(t, `https://localhost:14000/certZ/[\\w\\d]{14,}`, resource.CertURL)\n\tassert.Regexp(t, `https://localhost:14000/certZ/[\\w\\d]{14,}`, resource.CertStableURL)\n\tassert.NotEmpty(t, resource.Certificate)\n\tassert.NotEmpty(t, resource.IssuerCertificate)\n\tassert.Empty(t, resource.CSR)\n}\n\nfunc TestChallengeHTTP_Client_Obtain_notBefore_notAfter(t *testing.T) {\n\terr := os.Setenv(\"LEGO_CA_CERTIFICATES\", \"./fixtures/certs/pebble.minica.pem\")\n\trequire.NoError(t, err)\n\n\tdefer func() { _ = os.Unsetenv(\"LEGO_CA_CERTIFICATES\") }()\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tuser := &fakeUser{privateKey: privateKey}\n\tconfig := lego.NewConfig(user)\n\tconfig.CADirURL = load.PebbleOptions.HealthCheckURL\n\n\tclient, err := lego.NewClient(config)\n\trequire.NoError(t, err)\n\n\terr = client.Challenge.SetHTTP01Provider(http01.NewProviderServer(\"\", \"5002\"))\n\trequire.NoError(t, err)\n\n\treg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})\n\trequire.NoError(t, err)\n\n\tuser.registration = reg\n\n\tnow := time.Now().UTC()\n\n\trequest := certificate.ObtainRequest{\n\t\tDomains:   []string{testDomain1},\n\t\tNotBefore: now.Add(1 * time.Hour),\n\t\tNotAfter:  now.Add(2 * time.Hour),\n\t\tBundle:    true,\n\t}\n\tresource, err := client.Certificate.Obtain(request)\n\trequire.NoError(t, err)\n\n\trequire.NotNil(t, resource)\n\tassert.Equal(t, testDomain1, resource.Domain)\n\tassert.Regexp(t, `https://localhost:14000/certZ/[\\w\\d]{14,}`, resource.CertURL)\n\tassert.Regexp(t, `https://localhost:14000/certZ/[\\w\\d]{14,}`, resource.CertStableURL)\n\tassert.NotEmpty(t, resource.Certificate)\n\tassert.NotEmpty(t, resource.IssuerCertificate)\n\tassert.Empty(t, resource.CSR)\n\n\tcert, err := certcrypto.ParsePEMCertificate(resource.Certificate)\n\trequire.NoError(t, err)\n\tassert.WithinDuration(t, now.Add(1*time.Hour), cert.NotBefore, 1*time.Second)\n\tassert.WithinDuration(t, now.Add(2*time.Hour), cert.NotAfter, 1*time.Second)\n}\n\nfunc TestChallengeHTTP_Client_Registration_QueryRegistration(t *testing.T) {\n\terr := os.Setenv(\"LEGO_CA_CERTIFICATES\", \"./fixtures/certs/pebble.minica.pem\")\n\trequire.NoError(t, err)\n\n\tdefer func() { _ = os.Unsetenv(\"LEGO_CA_CERTIFICATES\") }()\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tuser := &fakeUser{privateKey: privateKey}\n\tconfig := lego.NewConfig(user)\n\tconfig.CADirURL = load.PebbleOptions.HealthCheckURL\n\n\tclient, err := lego.NewClient(config)\n\trequire.NoError(t, err)\n\n\terr = client.Challenge.SetHTTP01Provider(http01.NewProviderServer(\"\", \"5002\"))\n\trequire.NoError(t, err)\n\n\treg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})\n\trequire.NoError(t, err)\n\n\tuser.registration = reg\n\n\tresource, err := client.Registration.QueryRegistration()\n\trequire.NoError(t, err)\n\n\trequire.NotNil(t, resource)\n\n\tassert.Equal(t, \"valid\", resource.Body.Status)\n\tassert.Regexp(t, `https://localhost:14000/list-orderz/[\\w\\d]+`, resource.Body.Orders)\n\tassert.Regexp(t, `https://localhost:14000/my-account/[\\w\\d]+`, resource.URI)\n}\n\nfunc TestChallengeTLS_Client_Obtain(t *testing.T) {\n\terr := os.Setenv(\"LEGO_CA_CERTIFICATES\", \"./fixtures/certs/pebble.minica.pem\")\n\trequire.NoError(t, err)\n\n\tdefer func() { _ = os.Unsetenv(\"LEGO_CA_CERTIFICATES\") }()\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tuser := &fakeUser{privateKey: privateKey}\n\tconfig := lego.NewConfig(user)\n\tconfig.CADirURL = load.PebbleOptions.HealthCheckURL\n\n\tclient, err := lego.NewClient(config)\n\trequire.NoError(t, err)\n\n\terr = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer(\"\", \"5001\"))\n\trequire.NoError(t, err)\n\n\treg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})\n\trequire.NoError(t, err)\n\n\tuser.registration = reg\n\n\t// https://github.com/letsencrypt/pebble/issues/285\n\tprivateKeyCSR, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\trequest := certificate.ObtainRequest{\n\t\tDomains:    []string{testDomain1},\n\t\tBundle:     true,\n\t\tPrivateKey: privateKeyCSR,\n\t}\n\tresource, err := client.Certificate.Obtain(request)\n\trequire.NoError(t, err)\n\n\trequire.NotNil(t, resource)\n\tassert.Equal(t, testDomain1, resource.Domain)\n\tassert.Regexp(t, `https://localhost:14000/certZ/[\\w\\d]{14,}`, resource.CertURL)\n\tassert.Regexp(t, `https://localhost:14000/certZ/[\\w\\d]{14,}`, resource.CertStableURL)\n\tassert.NotEmpty(t, resource.Certificate)\n\tassert.NotEmpty(t, resource.IssuerCertificate)\n\tassert.Empty(t, resource.CSR)\n}\n\nfunc TestChallengeTLS_Client_ObtainForCSR(t *testing.T) {\n\terr := os.Setenv(\"LEGO_CA_CERTIFICATES\", \"./fixtures/certs/pebble.minica.pem\")\n\trequire.NoError(t, err)\n\n\tdefer func() { _ = os.Unsetenv(\"LEGO_CA_CERTIFICATES\") }()\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tuser := &fakeUser{privateKey: privateKey}\n\tconfig := lego.NewConfig(user)\n\tconfig.CADirURL = load.PebbleOptions.HealthCheckURL\n\n\tclient, err := lego.NewClient(config)\n\trequire.NoError(t, err)\n\n\terr = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer(\"\", \"5001\"))\n\trequire.NoError(t, err)\n\n\treg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})\n\trequire.NoError(t, err)\n\n\tuser.registration = reg\n\n\tcsr, err := x509.ParseCertificateRequest(createTestCSR(t))\n\trequire.NoError(t, err)\n\n\tresource, err := client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{\n\t\tCSR:    csr,\n\t\tBundle: true,\n\t})\n\trequire.NoError(t, err)\n\n\trequire.NotNil(t, resource)\n\tassert.Equal(t, testDomain1, resource.Domain)\n\tassert.Regexp(t, `https://localhost:14000/certZ/[\\w\\d]{14,}`, resource.CertURL)\n\tassert.Regexp(t, `https://localhost:14000/certZ/[\\w\\d]{14,}`, resource.CertStableURL)\n\tassert.NotEmpty(t, resource.Certificate)\n\tassert.NotEmpty(t, resource.IssuerCertificate)\n\tassert.NotEmpty(t, resource.CSR)\n}\n\nfunc TestChallengeTLS_Client_ObtainForCSR_profile(t *testing.T) {\n\terr := os.Setenv(\"LEGO_CA_CERTIFICATES\", \"./fixtures/certs/pebble.minica.pem\")\n\trequire.NoError(t, err)\n\n\tdefer func() { _ = os.Unsetenv(\"LEGO_CA_CERTIFICATES\") }()\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tuser := &fakeUser{privateKey: privateKey}\n\tconfig := lego.NewConfig(user)\n\tconfig.CADirURL = load.PebbleOptions.HealthCheckURL\n\n\tclient, err := lego.NewClient(config)\n\trequire.NoError(t, err)\n\n\terr = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer(\"\", \"5001\"))\n\trequire.NoError(t, err)\n\n\treg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})\n\trequire.NoError(t, err)\n\n\tuser.registration = reg\n\n\tcsr, err := x509.ParseCertificateRequest(createTestCSR(t))\n\trequire.NoError(t, err)\n\n\tresource, err := client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{\n\t\tCSR:     csr,\n\t\tBundle:  true,\n\t\tProfile: \"shortlived\",\n\t})\n\trequire.NoError(t, err)\n\n\trequire.NotNil(t, resource)\n\tassert.Equal(t, testDomain1, resource.Domain)\n\tassert.Regexp(t, `https://localhost:14000/certZ/[\\w\\d]{14,}`, resource.CertURL)\n\tassert.Regexp(t, `https://localhost:14000/certZ/[\\w\\d]{14,}`, resource.CertStableURL)\n\tassert.NotEmpty(t, resource.Certificate)\n\tassert.NotEmpty(t, resource.IssuerCertificate)\n\tassert.NotEmpty(t, resource.CSR)\n}\n\nfunc TestRegistrar_UpdateAccount(t *testing.T) {\n\terr := os.Setenv(\"LEGO_CA_CERTIFICATES\", \"./fixtures/certs/pebble.minica.pem\")\n\trequire.NoError(t, err)\n\n\tdefer func() { _ = os.Unsetenv(\"LEGO_CA_CERTIFICATES\") }()\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tuser := &fakeUser{\n\t\tprivateKey: privateKey,\n\t\temail:      testEmail1,\n\t}\n\tconfig := lego.NewConfig(user)\n\tconfig.CADirURL = load.PebbleOptions.HealthCheckURL\n\n\tclient, err := lego.NewClient(config)\n\trequire.NoError(t, err)\n\n\tregOptions := registration.RegisterOptions{TermsOfServiceAgreed: true}\n\treg, err := client.Registration.Register(regOptions)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\"mailto:\" + testEmail1}, reg.Body.Contact)\n\tuser.registration = reg\n\n\tuser.email = testEmail2\n\tresource, err := client.Registration.UpdateRegistration(regOptions)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\"mailto:\" + testEmail2}, resource.Body.Contact)\n\trequire.Equal(t, reg.URI, resource.URI)\n}\n\ntype fakeUser struct {\n\temail        string\n\tprivateKey   crypto.PrivateKey\n\tregistration *registration.Resource\n}\n\nfunc (f *fakeUser) GetEmail() string                        { return f.email }\nfunc (f *fakeUser) GetRegistration() *registration.Resource { return f.registration }\nfunc (f *fakeUser) GetPrivateKey() crypto.PrivateKey        { return f.privateKey }\n\nfunc createTestCSRFile(t *testing.T, raw bool) string {\n\tt.Helper()\n\n\tcsr := createTestCSR(t)\n\n\tif raw {\n\t\tfilename := filepath.Join(t.TempDir(), \"csr.raw\")\n\n\t\tfileRaw, err := os.Create(filename)\n\t\trequire.NoError(t, err)\n\n\t\tdefer fileRaw.Close()\n\n\t\t_, err = fileRaw.Write(csr)\n\t\trequire.NoError(t, err)\n\n\t\treturn filename\n\t}\n\n\tfilename := filepath.Join(t.TempDir(), \"csr.cert\")\n\n\tfile, err := os.Create(filename)\n\trequire.NoError(t, err)\n\n\tdefer file.Close()\n\n\t_, err = file.Write(pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE REQUEST\", Bytes: csr}))\n\trequire.NoError(t, err)\n\n\treturn filename\n}\n\nfunc createTestCSR(t *testing.T) []byte {\n\tt.Helper()\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err)\n\n\tcsr, err := certcrypto.CreateCSR(privateKey, certcrypto.CSROptions{\n\t\tDomain: testDomain1,\n\t\tSAN: []string{\n\t\t\ttestDomain1,\n\t\t\ttestDomain2,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\treturn csr\n}\n"
  },
  {
    "path": "e2e/dnschallenge/dns_challenges_test.go",
    "content": "package dnschallenge\n\nimport (\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/certificate\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/e2e/loader\"\n\t\"github.com/go-acme/lego/v4/lego\"\n\t\"github.com/go-acme/lego/v4/providers/dns\"\n\t\"github.com/go-acme/lego/v4/registration\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestDomain1 = \"légo.localhost\"\n\ttestDomain2 = \"*.légo.localhost\"\n)\n\nvar load = loader.EnvLoader{\n\tPebbleOptions: &loader.CmdOption{\n\t\tHealthCheckURL: \"https://localhost:15000/dir\",\n\t\tArgs:           []string{\"-strict\", \"-config\", \"fixtures/pebble-config-dns.json\", \"-dnsserver\", \"localhost:8053\"},\n\t\tEnv:            []string{\"PEBBLE_VA_NOSLEEP=1\", \"PEBBLE_WFE_NONCEREJECT=20\"},\n\t\tDir:            \"../\",\n\t},\n\tLegoOptions: []string{\n\t\t\"LEGO_CA_CERTIFICATES=../fixtures/certs/pebble.minica.pem\",\n\t\t\"EXEC_PATH=../fixtures/update-dns.sh\",\n\t\t\"LEGO_DEBUG_ACME_HTTP_CLIENT=1\",\n\t},\n\tChallSrv: &loader.CmdOption{\n\t\tArgs: []string{\"-http01\", \":5012\", \"-tlsalpn01\", \":5011\"},\n\t},\n}\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(load.MainTest(m))\n}\n\nfunc TestDNSHelp(t *testing.T) {\n\toutput, err := load.RunLegoCombinedOutput(\"dnshelp\")\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"%s\\n\", output)\n\t\tt.Fatal(err)\n\t}\n\n\tfmt.Fprintf(os.Stdout, \"%s\\n\", output)\n}\n\nfunc TestChallengeDNS_Run(t *testing.T) {\n\tloader.CleanLegoFiles()\n\n\terr := load.RunLego(\n\t\t\"--accept-tos\",\n\t\t\"--dns\", \"exec\",\n\t\t\"--dns.resolvers\", \":8053\",\n\t\t\"--dns.disable-cp\",\n\t\t\"-s\", \"https://localhost:15000/dir\",\n\t\t\"-d\", testDomain2,\n\t\t\"-d\", testDomain1,\n\t\t\"run\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestChallengeDNS_Client_Obtain(t *testing.T) {\n\terr := os.Setenv(\"LEGO_CA_CERTIFICATES\", \"../fixtures/certs/pebble.minica.pem\")\n\trequire.NoError(t, err)\n\n\tdefer func() { _ = os.Unsetenv(\"LEGO_CA_CERTIFICATES\") }()\n\n\terr = os.Setenv(\"EXEC_PATH\", \"../fixtures/update-dns.sh\")\n\trequire.NoError(t, err)\n\n\tdefer func() { _ = os.Unsetenv(\"EXEC_PATH\") }()\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tuser := &fakeUser{privateKey: privateKey}\n\tconfig := lego.NewConfig(user)\n\tconfig.CADirURL = \"https://localhost:15000/dir\"\n\n\tclient, err := lego.NewClient(config)\n\trequire.NoError(t, err)\n\n\tprovider, err := dns.NewDNSChallengeProviderByName(\"exec\")\n\trequire.NoError(t, err)\n\n\terr = client.Challenge.SetDNS01Provider(provider,\n\t\tdns01.AddRecursiveNameservers([]string{\":8053\"}),\n\t\tdns01.DisableAuthoritativeNssPropagationRequirement())\n\trequire.NoError(t, err)\n\n\treg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})\n\trequire.NoError(t, err)\n\n\tuser.registration = reg\n\n\tdomains := []string{testDomain2, testDomain1}\n\n\t// https://github.com/letsencrypt/pebble/issues/285\n\tprivateKeyCSR, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\trequest := certificate.ObtainRequest{\n\t\tDomains:    domains,\n\t\tBundle:     true,\n\t\tPrivateKey: privateKeyCSR,\n\t}\n\tresource, err := client.Certificate.Obtain(request)\n\trequire.NoError(t, err)\n\n\trequire.NotNil(t, resource)\n\tassert.Equal(t, \"*.xn--lgo-bma.localhost\", resource.Domain)\n\tassert.Regexp(t, `https://localhost:15000/certZ/[\\w\\d]{14,}`, resource.CertURL)\n\tassert.Regexp(t, `https://localhost:15000/certZ/[\\w\\d]{14,}`, resource.CertStableURL)\n\tassert.NotEmpty(t, resource.Certificate)\n\tassert.NotEmpty(t, resource.IssuerCertificate)\n\tassert.Empty(t, resource.CSR)\n}\n\nfunc TestChallengeDNS_Client_Obtain_profile(t *testing.T) {\n\terr := os.Setenv(\"LEGO_CA_CERTIFICATES\", \"../fixtures/certs/pebble.minica.pem\")\n\trequire.NoError(t, err)\n\n\tdefer func() { _ = os.Unsetenv(\"LEGO_CA_CERTIFICATES\") }()\n\n\terr = os.Setenv(\"EXEC_PATH\", \"../fixtures/update-dns.sh\")\n\trequire.NoError(t, err)\n\n\tdefer func() { _ = os.Unsetenv(\"EXEC_PATH\") }()\n\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tuser := &fakeUser{privateKey: privateKey}\n\tconfig := lego.NewConfig(user)\n\tconfig.CADirURL = \"https://localhost:15000/dir\"\n\n\tclient, err := lego.NewClient(config)\n\trequire.NoError(t, err)\n\n\tprovider, err := dns.NewDNSChallengeProviderByName(\"exec\")\n\trequire.NoError(t, err)\n\n\terr = client.Challenge.SetDNS01Provider(provider,\n\t\tdns01.AddRecursiveNameservers([]string{\":8053\"}),\n\t\tdns01.DisableAuthoritativeNssPropagationRequirement())\n\trequire.NoError(t, err)\n\n\treg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})\n\trequire.NoError(t, err)\n\n\tuser.registration = reg\n\n\tdomains := []string{testDomain2, testDomain1}\n\n\t// https://github.com/letsencrypt/pebble/issues/285\n\tprivateKeyCSR, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\trequest := certificate.ObtainRequest{\n\t\tDomains:    domains,\n\t\tBundle:     true,\n\t\tPrivateKey: privateKeyCSR,\n\t\tProfile:    \"shortlived\",\n\t}\n\tresource, err := client.Certificate.Obtain(request)\n\trequire.NoError(t, err)\n\n\trequire.NotNil(t, resource)\n\tassert.Equal(t, \"*.xn--lgo-bma.localhost\", resource.Domain)\n\tassert.Regexp(t, `https://localhost:15000/certZ/[\\w\\d]{14,}`, resource.CertURL)\n\tassert.Regexp(t, `https://localhost:15000/certZ/[\\w\\d]{14,}`, resource.CertStableURL)\n\tassert.NotEmpty(t, resource.Certificate)\n\tassert.NotEmpty(t, resource.IssuerCertificate)\n\tassert.Empty(t, resource.CSR)\n}\n\ntype fakeUser struct {\n\temail        string\n\tprivateKey   crypto.PrivateKey\n\tregistration *registration.Resource\n}\n\nfunc (f *fakeUser) GetEmail() string                        { return f.email }\nfunc (f *fakeUser) GetRegistration() *registration.Resource { return f.registration }\nfunc (f *fakeUser) GetPrivateKey() crypto.PrivateKey        { return f.privateKey }\n"
  },
  {
    "path": "e2e/fixtures/certs/README.md",
    "content": "# certs/\n\nThis directory contains a CA certificate (`pebble.minica.pem`) and a private key\n(`pebble.minica.key.pem`) that are used to issue an end-entity certificate (See\n`certs/localhost`)  for the Pebble HTTPS server.\n\nTo get your **testing code** to use Pebble without HTTPS errors you should\nconfigure your ACME client to trust the `pebble.minica.pem` CA certificate. Your\nACME client should offer a runtime option to specify a list of root CAs that you\ncan configure to include the `pebble.minica.pem` file.\n\n**Do not** add this CA certificate to the system trust store or in production\ncode!!! The CA's private key is **public** and anyone can use it to issue\ncertificates that will be trusted by a system with the Pebble CA in the trust\nstore.\n\nTo re-create all certificates used by Pebble, run:\n\n    minica -ca-cert pebble.minica.pem \\\n           -ca-key pebble.minica.key.pem \\\n           -domains localhost,pebble \\\n           -ip-addresses 127.0.0.1\n\nFrom the `test/certs/` directory after [installing MiniCA](https://github.com/jsha/minica#installation)\n"
  },
  {
    "path": "e2e/fixtures/certs/localhost/README.md",
    "content": "# certs/localhost\n\nThis directory contains an end-entity (leaf) certificate (`cert.pem`) and\na private key (`key.pem`) for the Pebble HTTPS server. It includes `127.0.0.1`\nas an IP address SAN, and `[localhost, pebble]` as DNS SANs.\n"
  },
  {
    "path": "e2e/fixtures/certs/localhost/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDMDCCAhigAwIBAgIILDt8c2fMw2IwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE\nAxMVbWluaWNhIHJvb3QgY2EgNTM0NWU2MB4XDTI1MDkwMzIzNDAwNVoXDTI3MTAw\nMzIzNDAwNVowFDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEAmxTFtw113RK70H9pQmdKs9AxhFmnQ6BdDtp3jOZlWlUO\n0BltMXOUML5905etgtCbcC6RdKRtgSAiDfgx3VWiFMJH++4gUtnaB9SN8GhNSPBp\nFfSa2JhWPo9HQNUsAZqlGTV4SzcGRqtWvdZxUiOfQ2TcvyXIqsaD19ivvqI1NhT6\nbl3tredTZlzLLM6Wvkw6hfyHrJAPQP8LOlCIeDM4YIce6Gstv6qo9iCD4wJiY4u9\n5HVL7RK8t8JpZAb7VR+dPhbHEvVpjwuYd5Q05OZ280gFyrhbrKLbqst104GOQT4k\nQMJGWxGONyTX6np0Dx6O5jU7dvYvjVVawbJwGuaL6wIDAQABo3oweDAOBgNVHQ8B\nAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAfBgNV\nHSMEGDAWgBSu8RGpErgYUoYnQuwCq+/ggTiEjDAiBgNVHREEGzAZgglsb2NhbGhv\nc3SCBnBlYmJsZYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAAB0gkekXCNOwqWmY\nvQ2lLJ8Zk2WzQ9B+VOC27IgxEEuskZyCpyXAbJB9sCGQWZhAARyaI4SPRGGagcug\nd1SwDWdPGeSJzF3aDnXDYoP9Zw2KqiqVZTngeoiw8Yn0F8PNriANwRLybouX7mMc\n4V7T5+2k4SUs7pFH4KO0a0XBCcjXDjdKuBljftRTXCHzJzfRtmieCCuZlpnp5sHx\nhKa/uxKGyyZB+4Y3MrzsiQSCBOr9G4TH9RofmNcawl+tsVe08zLV/XVhrbakKEs7\nY7MGHSj3BkPFF32NObc0znqWzTaUD9hU+rXWGANM4sXd4dagdnxfrb7i0WYhcUFj\n9Try8Q==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "e2e/fixtures/certs/localhost/key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAmxTFtw113RK70H9pQmdKs9AxhFmnQ6BdDtp3jOZlWlUO0Blt\nMXOUML5905etgtCbcC6RdKRtgSAiDfgx3VWiFMJH++4gUtnaB9SN8GhNSPBpFfSa\n2JhWPo9HQNUsAZqlGTV4SzcGRqtWvdZxUiOfQ2TcvyXIqsaD19ivvqI1NhT6bl3t\nredTZlzLLM6Wvkw6hfyHrJAPQP8LOlCIeDM4YIce6Gstv6qo9iCD4wJiY4u95HVL\n7RK8t8JpZAb7VR+dPhbHEvVpjwuYd5Q05OZ280gFyrhbrKLbqst104GOQT4kQMJG\nWxGONyTX6np0Dx6O5jU7dvYvjVVawbJwGuaL6wIDAQABAoIBAGW9W/S6lO+DIcoo\nPHL+9sg+tq2gb5ZzN3nOI45BfI6lrMEjXTqLG9ZasovFP2TJ3J/dPTnrwZdr8Et/\n357YViwORVFnKLeSCnMGpFPq6YEHj7mCrq+YSURjlRhYgbVPsi52oMOfhrOIJrEG\nZXPAwPRi0Ftqu1omQEqz8qA7JHOkjB2p0i2Xc/uOSJccCmUDMlksRYz8zFe8wHuD\nXvUL2k23n2pBZ6wiez6Xjr0wUQ4ESI02x7PmYgA3aqF2Q6ECDwHhjVeQmAuypMF6\nIaTjIJkWdZCW96pPaK1t+5nTNZ+Mg7tpJ/PRE4BkJvqcfHEOOl6wAE8gSk5uVApY\nZRKGmGkCgYEAzF9iRXYo7A/UphL11bR0gqxB6qnQl54iLhqS/E6CVNcmwJ2d9pF8\n5HTfSo1/lOXT3hGV8gizN2S5RmWBrc9HBZ+dNrVo7FYeeBiHu+opbX1X/C1HC0m1\nwJNsyoXeqD1OFc1WbDpHz5iv4IOXzYdOdKiYEcTv5JkqE7jomqBLQk8CgYEAwkG/\nrnwr4ThUo/DG5oH+l0LVnHkrJY+BUSI33g3eQ3eM0MSbfJXGT7snh5puJW0oXP7Z\nGw88nK3Vnz2nTPesiwtO2OkUVgrIgWryIvKHaqrYnapZHuM+io30jbZOVaVTMR9c\nX/7/d5/evwXuP7p2DIdZKQKKFgROm1XnhNqVgaUCgYBD/ogHbCR5RVsOVciMbRlG\nUGEt3YmUp/vfMuAsKUKbT2mJM+dWHVlb+LZBa4pC06QFgfxNJi/aAhzSGvtmBEww\nxsXbaceauZwxgJfIIUPfNZCMSdQVIVTi2Smcx6UofBz6i/Jw14MEwlvhamaa7qVf\nkqflYYwelga1wRNCPopLaQKBgQCWsZqZKQqBNMm0Q9yIhN+TR+2d7QFjqeePoRPl\n1qxNejhq25ojE607vNv1ff9kWUGuoqSZMUC76r6FQba/JoNbefI4otd7x/GzM9uS\n8MHMJazU4okwROkHYwgLxxkNp6rZuJJYheB4VDTfyyH/ng5lubmY7rdgTQcNyZ5I\nmajRYQKBgAMKJ3RlII0qvAfNFZr4Y2bNIq+60Z+Qu2W5xokIHCFNly3W1XDDKGFe\nCCPHSvQljinke3P9gPt2HVdXxcnku9VkTti+JygxuLkVg7E0/SWwrWfGsaMJs+84\nfK+mTZay2d3v24r9WKEKwLykngYPyZw5+BdWU0E+xx5lGUd3U4gG\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "e2e/fixtures/certs/pebble.minica.key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAuVoGTaFSWp3Y+N5JC8lOdL8wmWpaM73UaNzhYiqA7ZqijzVk\nTTtoQvQFDcUwyXKOdWHONrv1ld3z224Us504jjlbZwI5uoquCOZ2WJbRhmXrRgzk\nFq+/MtoFmPkhtO/DLjjtocgyIirVXN8Yl2APvB5brvRfCm6kktYeecsWfW/O3ikf\ngdM7tmocwQiBypiloHOjdd5e2g8cWNw+rqvILSUVNLaLpsi23cxnLqVb424wz9dZ\n5dO0REg1gSxtf4N5LSb6iGuAVoFNhzIeKzQ+svDg9x8tx/DGOghJS/jDgmxSY1qo\nbTsXhcmWVfat5GJ5PQgLkCSjBBrjeBlOrc4VtQIDAQABAoIBAQCAoRoou6C0ZEDU\nDScyN8TrvlcS0LzClaWYFFmRT5/jxOG1cr8l3elwNXpgYQ2Hb6mvim2ajHxVQg/e\noxlYwO4jvWhSJzg63c0DPjS5LAlCNO6+0Wlk2RheSPGDhLlAoPeZ10YKdS1dis5B\nQk4Fl1O0IHlOBCcEzV4GzPOfYDI+X6/f4xY7qz1s+CgoIxjIeiG+1/WpZQpYhobY\n7CfSDdYDKtksXi7iQkc5earUAHBqZ1gQTq6e5LVm9AjRzENhMctFgcPs5zOjp2ak\nPluixrA8LTAfu9wQzvxDkPl0UarZVxCerw6nlAziILpQ+U6PtoPZj49VpntTc+cq\n1qjzkbhBAoGBANElJmFWY2X6LgBpszeqt0ZOSbkFg2bC0wHCJrMlRzUMEn83w9e8\nZ2Fqml9eCC5qxJcyxWDVQeoAX6090m0qgP8xNmGdafcVic2cUlrqtkqhhst2OHCO\nMCQEB7cdsjiidNNrOgLbQ3i1bYID8BVLf/TDhEbRgvTewDaz6XPdoSIRAoGBAOLg\nRuOec5gn50SrVycx8BLFO8AXjXojpZb1Xg26V5miz1IavSfDcgae/699ppSz+UWi\njGMFr/PokY2JxDVs3PyQLu7ahMzyFHr16Agvp5g5kq056XV+uI/HhqLHOWSQ09DS\n1Vrj7FOYpKRzge3/AC7ty9Vr35uMiebpm4/CLFVlAoGALnsIJZfSbWaFdLgJCXUa\nWDir77/G7T6dMIXanfPJ+IMfVUCqeLa5bxAHEOzP+qjl2giBjzy18nB00warTnGk\ny5I/WMBoPW5++sAkGWqSatGtKGi0sGcZUdfHcy3ZXvbT6eyprtrWCuyfUsbXQ5RM\n8rPFIQwNA6jBpSak2ohF+FECgYEAn+6IKncNd6pRfnfmdSvf1+uPxkcUJZCxb2xC\nxByjGhvKWE+fHkPJwt8c0SIbZuJEC5Gds0RUF/XPfV4roZm/Yo9ldl02lp7kTxXA\nXtzxIP8c5d5YM8qD4l8+Csu0Kq9pkeC+JFddxkRpc8A1TIehInPhZ+6mb6mvoMb3\nMW0pAX0CgYATT74RYuIYWZvx0TK4ZXIKTw2i6HObLF63Y6UwyPXXdEVie/ToYRNH\nJIxE1weVpHvnHZvVD6D3yGk39ZsCIt31VvKpatWXlWBm875MbBc6kuIGsYT+mSSj\ny9TXaE89E5zfL27nZe15QLJ+Xw8Io6PMLZ/jtC5TYoEixSZ9J8v6HA==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "e2e/fixtures/certs/pebble.minica.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDPzCCAiegAwIBAgIIU0Xm9UFdQxUwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE\nAxMVbWluaWNhIHJvb3QgY2EgNTM0NWU2MCAXDTI1MDkwMzIzNDAwNVoYDzIxMjUw\nOTAzMjM0MDA1WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA1MzQ1ZTYwggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5WgZNoVJandj43kkLyU50vzCZ\nalozvdRo3OFiKoDtmqKPNWRNO2hC9AUNxTDJco51Yc42u/WV3fPbbhSznTiOOVtn\nAjm6iq4I5nZYltGGZetGDOQWr78y2gWY+SG078MuOO2hyDIiKtVc3xiXYA+8Hluu\n9F8KbqSS1h55yxZ9b87eKR+B0zu2ahzBCIHKmKWgc6N13l7aDxxY3D6uq8gtJRU0\ntoumyLbdzGcupVvjbjDP11nl07RESDWBLG1/g3ktJvqIa4BWgU2HMh4rND6y8OD3\nHy3H8MY6CElL+MOCbFJjWqhtOxeFyZZV9q3kYnk9CAuQJKMEGuN4GU6tzhW1AgMB\nAAGjezB5MA4GA1UdDwEB/wQEAwIChDATBgNVHSUEDDAKBggrBgEFBQcDATASBgNV\nHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSu8RGpErgYUoYnQuwCq+/ggTiEjDAf\nBgNVHSMEGDAWgBSu8RGpErgYUoYnQuwCq+/ggTiEjDANBgkqhkiG9w0BAQsFAAOC\nAQEAXDVYov1+f6EL7S41LhYQkEX/GyNNzsEvqxE9U0+3Iri5JfkcNOiA9O9L6Z+Y\nbqcsXV93s3vi4r4WSWuc//wHyJYrVe5+tK4nlFpbJOvfBUtnoBDyKNxXzZCxFJVh\nf9uc8UejRfQMFbDbhWY/x83y9BDufJHHq32OjCIN7gp2UR8rnfYvlz7Zg4qkJBsn\nDG4dwd+pRTCFWJOVIG0JoNhK3ZmE7oJ1N4H38XkZ31NPcMksKxpsLLIS9+mosZtg\n4olL7tMPJklx5ZaeMFaKRDq4Gdxkbw4+O4vRgNm3Z8AXWKknOdfgdpqLUPPhRcP4\nv1lhy71EhBuXXwRQJry0lTdF+w==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "e2e/fixtures/pebble-config-dns.json",
    "content": "{\n  \"pebble\": {\n    \"listenAddress\": \"0.0.0.0:15000\",\n    \"certificate\": \"fixtures/certs/localhost/cert.pem\",\n    \"privateKey\": \"fixtures/certs/localhost/key.pem\",\n    \"httpPort\": 5004,\n    \"tlsPort\": 5003,\n    \"profiles\": {\n      \"default\": {\n        \"description\": \"The profile you know and love\",\n        \"validityPeriod\": 7776000\n      },\n      \"shortlived\": {\n        \"description\": \"A short-lived cert profile, without actual enforcement\",\n        \"validityPeriod\": 518400\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "e2e/fixtures/pebble-config.json",
    "content": "{\n  \"pebble\": {\n    \"listenAddress\": \"0.0.0.0:14000\",\n    \"certificate\": \"fixtures/certs/localhost/cert.pem\",\n    \"privateKey\": \"fixtures/certs/localhost/key.pem\",\n    \"httpPort\": 5002,\n    \"tlsPort\": 5001,\n    \"profiles\": {\n      \"default\": {\n        \"description\": \"The profile you know and love\",\n        \"validityPeriod\": 7776000\n      },\n      \"shortlived\": {\n        \"description\": \"A short-lived cert profile, without actual enforcement\",\n        \"validityPeriod\": 518400\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "e2e/fixtures/update-dns.sh",
    "content": "#!/usr/bin/env bash\n# Simple DNS challenge exec solver.\n# Use challtestsrv https://github.com/letsencrypt/boulder/tree/master/test/challtestsrv\n\nset -e\n\ncase \"$1\" in\n  \"present\")\n    echo  \"Present\"\n    payload=\"{\\\"host\\\":\\\"$2\\\", \\\"value\\\":\\\"$3\\\"}\"\n    echo \"payload=${payload}\"\n    curl -s -X POST -d \"${payload}\" localhost:8055/set-txt\n    ;;\n  \"cleanup\")\n    echo  \"cleanup\"\n    payload=\"{\\\"host\\\":\\\"$2\\\"}\"\n    echo \"payload=${payload}\"\n    curl -s -X POST -d \"${payload}\" localhost:8055/clear-txt\n    ;;\n  *)\n    echo \"OOPS\"\n    ;;\nesac\n"
  },
  {
    "path": "e2e/loader/loader.go",
    "content": "package loader\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n\t\"github.com/ldez/grignotin/goenv\"\n)\n\nconst (\n\tcmdNamePebble   = \"pebble\"\n\tcmdNameChallSrv = \"pebble-challtestsrv\"\n)\n\ntype CmdOption struct {\n\tHealthCheckURL string\n\tArgs           []string\n\tEnv            []string\n\tDir            string\n}\n\ntype EnvLoader struct {\n\tPebbleOptions *CmdOption\n\tLegoOptions   []string\n\tChallSrv      *CmdOption\n\tlego          string\n}\n\nfunc (l *EnvLoader) MainTest(m *testing.M) int {\n\tif _, e2e := os.LookupEnv(\"LEGO_E2E_TESTS\"); !e2e {\n\t\tfmt.Fprintln(os.Stderr, \"skipping test: e2e tests are disabled. (no 'LEGO_E2E_TESTS' env var)\")\n\t\tfmt.Println(\"PASS\")\n\n\t\treturn 0\n\t}\n\n\tif _, err := exec.LookPath(\"git\"); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"skipping because git command not found\")\n\t\tfmt.Println(\"PASS\")\n\n\t\treturn 0\n\t}\n\n\tif l.PebbleOptions != nil {\n\t\tif _, err := exec.LookPath(cmdNamePebble); err != nil {\n\t\t\tfmt.Fprintln(os.Stderr, \"skipping because pebble binary not found\")\n\t\t\tfmt.Println(\"PASS\")\n\n\t\t\treturn 0\n\t\t}\n\t}\n\n\tif l.ChallSrv != nil {\n\t\tif _, err := exec.LookPath(cmdNameChallSrv); err != nil {\n\t\t\tfmt.Fprintln(os.Stderr, \"skipping because challtestsrv binary not found\")\n\t\t\tfmt.Println(\"PASS\")\n\n\t\t\treturn 0\n\t\t}\n\t}\n\n\tpebbleTearDown := l.launchPebble()\n\tdefer pebbleTearDown()\n\n\tchallSrvTearDown := l.launchChallSrv()\n\tdefer challSrvTearDown()\n\n\tlegoBinary, tearDown, err := buildLego()\n\tdefer tearDown()\n\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, err)\n\t\treturn 1\n\t}\n\n\tl.lego = legoBinary\n\n\tif l.PebbleOptions != nil && l.PebbleOptions.HealthCheckURL != \"\" {\n\t\tpebbleHealthCheck(l.PebbleOptions)\n\t}\n\n\treturn m.Run()\n}\n\nfunc (l *EnvLoader) RunLegoCombinedOutput(arg ...string) ([]byte, error) {\n\tcmd := exec.Command(l.lego, arg...)\n\tcmd.Env = l.LegoOptions\n\n\tfmt.Printf(\"$ %s\\n\", strings.Join(cmd.Args, \" \"))\n\n\treturn cmd.CombinedOutput()\n}\n\nfunc (l *EnvLoader) RunLego(arg ...string) error {\n\tcmd := exec.Command(l.lego, arg...)\n\tcmd.Env = l.LegoOptions\n\n\tfmt.Printf(\"$ %s\\n\", strings.Join(cmd.Args, \" \"))\n\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create pipe: %w\", err)\n\t}\n\n\tcmd.Stderr = cmd.Stdout\n\n\terr = cmd.Start()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"start command: %w\", err)\n\t}\n\n\tscanner := bufio.NewScanner(stdout)\n\tfor scanner.Scan() {\n\t\tprintln(scanner.Text())\n\t}\n\n\terr = cmd.Wait()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wait command: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (l *EnvLoader) launchPebble() func() {\n\tif l.PebbleOptions == nil {\n\t\treturn func() {}\n\t}\n\n\tpebble, outPebble := l.cmdPebble()\n\n\tgo func() {\n\t\terr := pebble.Run()\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t}\n\t}()\n\n\treturn func() {\n\t\terr := pebble.Process.Kill()\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t}\n\n\t\tfmt.Println(outPebble.String())\n\t}\n}\n\nfunc (l *EnvLoader) cmdPebble() (*exec.Cmd, *bytes.Buffer) {\n\tcmd := exec.Command(cmdNamePebble, l.PebbleOptions.Args...)\n\tcmd.Env = l.PebbleOptions.Env\n\n\tdir, err := filepath.Abs(l.PebbleOptions.Dir)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tcmd.Dir = dir\n\n\tfmt.Printf(\"$ %s\\n\", strings.Join(cmd.Args, \" \"))\n\n\tvar b bytes.Buffer\n\n\tcmd.Stdout = &b\n\tcmd.Stderr = &b\n\n\treturn cmd, &b\n}\n\nfunc pebbleHealthCheck(options *CmdOption) {\n\tclient := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}}\n\n\terr := wait.For(\"pebble\", 10*time.Second, 500*time.Millisecond, func() (bool, error) {\n\t\tresp, err := client.Get(options.HealthCheckURL)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\treturn false, nil\n\t\t}\n\n\t\treturn true, nil\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc (l *EnvLoader) launchChallSrv() func() {\n\tif l.ChallSrv == nil {\n\t\treturn func() {}\n\t}\n\n\tchalltestsrv, outChalSrv := l.cmdChallSrv()\n\n\tgo func() {\n\t\terr := challtestsrv.Run()\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t}\n\t}()\n\n\treturn func() {\n\t\terr := challtestsrv.Process.Kill()\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t}\n\n\t\tfmt.Println(outChalSrv.String())\n\t}\n}\n\nfunc (l *EnvLoader) cmdChallSrv() (*exec.Cmd, *bytes.Buffer) {\n\tcmd := exec.Command(cmdNameChallSrv, l.ChallSrv.Args...)\n\n\tfmt.Printf(\"$ %s\\n\", strings.Join(cmd.Args, \" \"))\n\n\tvar b bytes.Buffer\n\n\tcmd.Stdout = &b\n\tcmd.Stderr = &b\n\n\treturn cmd, &b\n}\n\nfunc buildLego() (string, func(), error) {\n\there, err := os.Getwd()\n\tif err != nil {\n\t\treturn \"\", func() {}, err\n\t}\n\n\tdefer func() { _ = os.Chdir(here) }()\n\n\tbuildPath, err := os.MkdirTemp(\"\", \"lego_test\")\n\tif err != nil {\n\t\treturn \"\", func() {}, err\n\t}\n\n\tprojectRoot, err := getProjectRoot()\n\tif err != nil {\n\t\treturn \"\", func() {}, err\n\t}\n\n\tmainFolder := filepath.Join(projectRoot, \"cmd\", \"lego\")\n\n\terr = os.Chdir(mainFolder)\n\tif err != nil {\n\t\treturn \"\", func() {}, err\n\t}\n\n\tbinary := filepath.Join(buildPath, \"lego\")\n\n\terr = build(binary)\n\tif err != nil {\n\t\treturn \"\", func() {}, err\n\t}\n\n\terr = os.Chdir(here)\n\tif err != nil {\n\t\treturn \"\", func() {}, err\n\t}\n\n\treturn binary, func() {\n\t\t_ = os.RemoveAll(buildPath)\n\n\t\tCleanLegoFiles()\n\t}, nil\n}\n\nfunc getProjectRoot() (string, error) {\n\tgit := exec.Command(\"git\", \"rev-parse\", \"--show-toplevel\")\n\n\toutput, err := git.CombinedOutput()\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"%s\\n\", output)\n\t\treturn \"\", err\n\t}\n\n\treturn strings.TrimSpace(string(output)), nil\n}\n\nfunc build(binary string) error {\n\ttoolPath, err := goToolPath()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd := exec.Command(toolPath, \"build\", \"-o\", binary)\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"%s\\n\", output)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc goToolPath() (string, error) {\n\t// inspired by go1.11.1/src/internal/testenv/testenv.go\n\tif os.Getenv(\"GO_GCFLAGS\") != \"\" {\n\t\treturn \"\", errors.New(\"'go build' not compatible with setting $GO_GCFLAGS\")\n\t}\n\n\tif runtime.GOOS == \"darwin\" && strings.HasPrefix(runtime.GOARCH, \"arm\") {\n\t\treturn \"\", fmt.Errorf(\"skipping test: 'go build' not available on %s/%s\", runtime.GOOS, runtime.GOARCH)\n\t}\n\n\treturn goTool()\n}\n\nfunc goTool() (string, error) {\n\tvar exeSuffix string\n\tif runtime.GOOS == \"windows\" {\n\t\texeSuffix = \".exe\"\n\t}\n\n\tgoRoot, err := goenv.GetOne(context.Background(), goenv.GOROOT)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"cannot find go root: %w\", err)\n\t}\n\n\tpath := filepath.Join(goRoot, \"bin\", \"go\"+exeSuffix)\n\tif _, err = os.Stat(path); err == nil {\n\t\treturn path, nil\n\t}\n\n\tgoBin, err := exec.LookPath(\"go\" + exeSuffix)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"cannot find go tool: %w\", err)\n\t}\n\n\treturn goBin, nil\n}\n\nfunc CleanLegoFiles() {\n\tcmd := exec.Command(\"rm\", \"-rf\", \".lego\")\n\tfmt.Printf(\"$ %s\\n\", strings.Join(cmd.Args, \" \"))\n\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tfmt.Println(string(output))\n\t}\n}\n"
  },
  {
    "path": "e2e/readme.md",
    "content": "# E2E tests\n\n- Install [Pebble](https://github.com/letsencrypt/pebble):\n```bash\ngo install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.9.0\ngo install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.9.0\n```\n\n- Launch tests:\n```bash\nmake e2e\n```\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/go-acme/lego/v4\n\ngo 1.24.0\n\nrequire (\n\tcloud.google.com/go/compute/metadata v0.9.0\n\tgithub.com/Azure/azure-sdk-for-go v68.0.0+incompatible\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0\n\tgithub.com/Azure/go-autorest/autorest v0.11.30\n\tgithub.com/Azure/go-autorest/autorest/azure/auth v0.5.13\n\tgithub.com/Azure/go-autorest/autorest/to v0.4.1\n\tgithub.com/BurntSushi/toml v1.6.0\n\tgithub.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0\n\tgithub.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15\n\tgithub.com/alibabacloud-go/tea v1.4.0\n\tgithub.com/aliyun/credentials-go v1.4.7\n\tgithub.com/aws/aws-sdk-go-v2 v1.41.1\n\tgithub.com/aws/aws-sdk-go-v2/config v1.32.8\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.19.8\n\tgithub.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11\n\tgithub.com/aws/aws-sdk-go-v2/service/route53 v1.62.1\n\tgithub.com/aws/aws-sdk-go-v2/service/s3 v1.96.0\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.41.6\n\tgithub.com/aziontech/azionapi-go-sdk v0.144.0\n\tgithub.com/baidubce/bce-sdk-go v0.9.260\n\tgithub.com/cenkalti/backoff/v5 v5.0.3\n\tgithub.com/dnsimple/dnsimple-go/v4 v4.0.0\n\tgithub.com/exoscale/egoscale/v3 v3.1.33\n\tgithub.com/go-acme/alidns-20150109/v4 v4.7.0\n\tgithub.com/go-acme/esa-20240910/v2 v2.48.0\n\tgithub.com/go-acme/jdcloud-sdk-go v1.64.0\n\tgithub.com/go-acme/tencentclouddnspod v1.3.24\n\tgithub.com/go-acme/tencentedgdeone v1.3.38\n\tgithub.com/go-jose/go-jose/v4 v4.1.3\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/google/go-querystring v1.2.0\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gophercloud/gophercloud v1.14.1\n\tgithub.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56\n\tgithub.com/hashicorp/go-retryablehttp v0.7.8\n\tgithub.com/hashicorp/go-version v1.8.0\n\tgithub.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187\n\tgithub.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df\n\tgithub.com/infobloxopen/infoblox-go-client/v2 v2.10.0\n\tgithub.com/labbsr0x/bindman-dns-webhook v1.0.2\n\tgithub.com/ldez/grignotin v0.10.1\n\tgithub.com/linode/linodego v1.65.0\n\tgithub.com/liquidweb/liquidweb-go v1.6.4\n\tgithub.com/mattn/go-isatty v0.0.20\n\tgithub.com/miekg/dns v1.1.72\n\tgithub.com/mimuret/golang-iij-dpf v0.9.1\n\tgithub.com/namedotcom/go/v4 v4.0.2\n\tgithub.com/nrdcg/auroradns v1.2.0\n\tgithub.com/nrdcg/bunny-go v0.1.0\n\tgithub.com/nrdcg/desec v0.11.1\n\tgithub.com/nrdcg/dnspod-go v0.4.0\n\tgithub.com/nrdcg/freemyip v0.3.0\n\tgithub.com/nrdcg/goacmedns v0.2.0\n\tgithub.com/nrdcg/goinwx v0.12.0\n\tgithub.com/nrdcg/mailinabox v0.3.0\n\tgithub.com/nrdcg/namesilo v0.5.0\n\tgithub.com/nrdcg/nodion v0.1.0\n\tgithub.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2\n\tgithub.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2\n\tgithub.com/nrdcg/porkbun v0.4.0\n\tgithub.com/nrdcg/vegadns v0.3.0\n\tgithub.com/nzdjb/go-metaname v1.0.0\n\tgithub.com/ovh/go-ovh v1.9.0\n\tgithub.com/pquerna/otp v1.5.0\n\tgithub.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2\n\tgithub.com/regfish/regfish-dnsapi-go v0.1.1\n\tgithub.com/sacloud/api-client-go v0.3.3\n\tgithub.com/sacloud/iaas-api-go v1.23.1\n\tgithub.com/scaleway/scaleway-sdk-go v1.0.0-beta.36\n\tgithub.com/selectel/domains-go v1.1.0\n\tgithub.com/selectel/go-selvpcclient/v4 v4.1.0\n\tgithub.com/softlayer/softlayer-go v1.2.1\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48\n\tgithub.com/transip/gotransip/v6 v6.26.1\n\tgithub.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419\n\tgithub.com/urfave/cli/v2 v2.27.7\n\tgithub.com/vinyldns/go-vinyldns v0.9.17\n\tgithub.com/volcengine/volc-sdk-golang v1.0.237\n\tgithub.com/vultr/govultr/v3 v3.27.0\n\tgithub.com/yandex-cloud/go-genproto v0.54.0\n\tgithub.com/yandex-cloud/go-sdk/services/dns v0.0.36\n\tgithub.com/yandex-cloud/go-sdk/v2 v2.56.0\n\tgolang.org/x/crypto v0.48.0\n\tgolang.org/x/net v0.50.0\n\tgolang.org/x/oauth2 v0.35.0\n\tgolang.org/x/text v0.34.0\n\tgolang.org/x/time v0.14.0\n\tgoogle.golang.org/api v0.267.0\n\tgopkg.in/ns1/ns1-go.v2 v2.17.2\n\tgopkg.in/yaml.v2 v2.4.0\n\tsoftware.sslmate.com/src/go-pkcs12 v0.7.0\n)\n\nrequire (\n\tcloud.google.com/go/auth v0.18.1 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tgithub.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect\n\tgithub.com/Azure/go-autorest v14.2.0+incompatible // indirect\n\tgithub.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect\n\tgithub.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect\n\tgithub.com/Azure/go-autorest/autorest/date v0.3.0 // indirect\n\tgithub.com/Azure/go-autorest/logger v0.2.1 // indirect\n\tgithub.com/Azure/go-autorest/tracing v0.6.0 // indirect\n\tgithub.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect\n\tgithub.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect\n\tgithub.com/alibabacloud-go/debug v1.0.1 // indirect\n\tgithub.com/alibabacloud-go/openapi-util v0.1.1 // indirect\n\tgithub.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect\n\tgithub.com/aws/smithy-go v1.24.0 // indirect\n\tgithub.com/benbjohnson/clock v1.3.5 // indirect\n\tgithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/clbanning/mxj/v2 v2.7.0 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/dimchansky/utfbom v1.1.1 // indirect\n\tgithub.com/fatih/color v1.16.0 // indirect\n\tgithub.com/fatih/structs v1.1.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.3 // indirect\n\tgithub.com/ghodss/yaml v1.0.0 // indirect\n\tgithub.com/go-errors/errors v1.0.1 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.23.0 // indirect\n\tgithub.com/go-resty/resty/v2 v2.17.1 // indirect\n\tgithub.com/goccy/go-yaml v1.9.8 // indirect\n\tgithub.com/gofrs/flock v0.13.0 // indirect\n\tgithub.com/gofrs/uuid v4.4.0+incompatible // indirect\n\tgithub.com/golang-jwt/jwt/v4 v4.5.2 // indirect\n\tgithub.com/golang-jwt/jwt/v5 v5.3.0 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.17.0 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect\n\tgithub.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect\n\tgithub.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/labbsr0x/goh v1.0.1 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/liquidweb/liquidweb-cli v0.6.9 // indirect\n\tgithub.com/magiconair/properties v1.8.7 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.1.0 // indirect\n\tgithub.com/peterhellberg/link v1.2.0 // indirect\n\tgithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/sacloud/go-http v0.1.9 // indirect\n\tgithub.com/sacloud/packages-go v0.0.12 // indirect\n\tgithub.com/sagikazarmark/locafero v0.4.0 // indirect\n\tgithub.com/sagikazarmark/slog-shim v0.1.0 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.3 // indirect\n\tgithub.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect\n\tgithub.com/sony/gobreaker v1.0.0 // indirect\n\tgithub.com/sourcegraph/conc v0.3.0 // indirect\n\tgithub.com/spf13/afero v1.11.0 // indirect\n\tgithub.com/spf13/cast v1.7.0 // indirect\n\tgithub.com/spf13/pflag v1.0.7 // indirect\n\tgithub.com/spf13/viper v1.18.2 // indirect\n\tgithub.com/stretchr/objx v0.5.2 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/tjfoc/gmsm v1.4.1 // indirect\n\tgithub.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect\n\tgithub.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect\n\tgo.mongodb.org/mongo-driver v1.13.1 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect\n\tgo.opentelemetry.io/otel v1.39.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.39.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.39.0 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgo.uber.org/ratelimit v0.3.1 // indirect\n\tgo.uber.org/zap v1.27.0 // indirect\n\tgolang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect\n\tgolang.org/x/mod v0.32.0 // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/sys v0.41.0 // indirect\n\tgolang.org/x/tools v0.41.0 // indirect\n\tgolang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect\n\tgoogle.golang.org/grpc v1.78.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgopkg.in/ini.v1 v1.67.1 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n\nretract v4.30.0 // Problem related to misuse of sycalls by aliyun/credentials-go\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=\ncloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYsMyFh9qoE=\ngithub.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=\ngithub.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=\ngithub.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=\ngithub.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=\ngithub.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=\ngithub.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA=\ngithub.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE=\ngithub.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.22 h1:/GblQdIudfEM3AWWZ0mrYJQSd7JS4S/Mbzh6F0ov0Xc=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk=\ngithub.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM=\ngithub.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo=\ngithub.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc=\ngithub.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0=\ngithub.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=\ngithub.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU=\ngithub.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc=\ngithub.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M=\ngithub.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=\ngithub.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=\ngithub.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=\ngithub.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=\ngithub.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=\ngithub.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=\ngithub.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=\ngithub.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=\ngithub.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=\ngithub.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw=\ngithub.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=\ngithub.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0=\ngithub.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=\ngithub.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=\ngithub.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=\ngithub.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 h1:h/33OxYLqBk0BYmEbSUy7MlvgQR/m1w1/7OJFKoPL1I=\ngithub.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=\ngithub.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=\ngithub.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=\ngithub.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=\ngithub.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=\ngithub.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=\ngithub.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=\ngithub.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=\ngithub.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=\ngithub.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=\ngithub.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=\ngithub.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=\ngithub.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=\ngithub.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15 h1:Mubp9hXZMTPWZK+WxrR+kKOVFp4Q/PDZrIIM7ByXI9Y=\ngithub.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=\ngithub.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=\ngithub.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=\ngithub.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=\ngithub.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=\ngithub.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=\ngithub.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=\ngithub.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=\ngithub.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=\ngithub.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=\ngithub.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=\ngithub.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=\ngithub.com/alibabacloud-go/openapi-util v0.1.1 h1:ujGErJjG8ncRW6XtBBMphzHTvCxn4DjrVw4m04HsS28=\ngithub.com/alibabacloud-go/openapi-util v0.1.1/go.mod h1:/UehBSE2cf1gYT43GV4E+RxTdLRzURImCYY0aRmlXpw=\ngithub.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=\ngithub.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=\ngithub.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=\ngithub.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=\ngithub.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=\ngithub.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=\ngithub.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=\ngithub.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=\ngithub.com/alibabacloud-go/tea v1.4.0 h1:MSKhu/kWLPX7mplWMngki8nNt+CyUZ+kfkzaR5VpMhA=\ngithub.com/alibabacloud-go/tea v1.4.0/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=\ngithub.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=\ngithub.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=\ngithub.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=\ngithub.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=\ngithub.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=\ngithub.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=\ngithub.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=\ngithub.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=\ngithub.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=\ngithub.com/aliyun/credentials-go v1.4.7 h1:T17dLqEtPUFvjDRRb5giVvLh6dFT8IcNFJJb7MeyCxw=\ngithub.com/aliyun/credentials-go v1.4.7/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=\ngithub.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=\ngithub.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=\ngithub.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=\ngithub.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=\ngithub.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.8 h1:iu+64gwDKEoKnyTQskSku72dAwggKI5sV6rNvgSMpMs=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.8/go.mod h1:MI2XvA+qDi3i9AJxX1E2fu730syEBzp/jnXrjxuHwgI=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.8 h1:Jp2JYH1lRT3KhX4mshHPvVYsR5qqRec3hGvEarNYoR0=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.8/go.mod h1:fZG9tuvyVfxknv1rKibIz3DobRaFw1Poe8IKtXB3XYY=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0=\ngithub.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g=\ngithub.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11 h1:VM5e5M39zRSs+aT0O9SoxHjUXqXxhbw3Yi0FdMQWPIc=\ngithub.com/aws/aws-sdk-go-v2/service/lightsail v1.50.11/go.mod h1:0jvzYPIQGCpnY/dmdaotTk2JH4QuBlnW0oeyrcGLWJ4=\ngithub.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 h1:1jIdwWOulae7bBLIgB36OZ0DINACb1wxM6wdGlx4eHE=\ngithub.com/aws/aws-sdk-go-v2/service/route53 v1.62.1/go.mod h1:tE2zGlMIlxWv+7Otap7ctRp3qeKqtnja7DZguj3Vu/Y=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=\ngithub.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=\ngithub.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=\ngithub.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=\ngithub.com/aziontech/azionapi-go-sdk v0.144.0 h1:T+/w18o+FCiZsk3Z0ACBVVe7c/5EGLG15S3P8JfuPfo=\ngithub.com/aziontech/azionapi-go-sdk v0.144.0/go.mod h1:OKxP/R0iVXnJJakYwMhh2BGAXnud8Ruy55Ak9ANuWoU=\ngithub.com/baidubce/bce-sdk-go v0.9.260 h1:1v1+2GTP+NGK3L24rJ+bnoiTaDaIy2YoaUM+ot2GTcw=\ngithub.com/baidubce/bce-sdk-go v0.9.260/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=\ngithub.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=\ngithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=\ngithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=\ngithub.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=\ngithub.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=\ngithub.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=\ngithub.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=\ngithub.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=\ngithub.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=\ngithub.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=\ngithub.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=\ngithub.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=\ngithub.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=\ngithub.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=\ngithub.com/dnsimple/dnsimple-go/v4 v4.0.0 h1:nUCICZSyZDiiqimAAL+E8XL+0sKGks5VRki5S8XotRo=\ngithub.com/dnsimple/dnsimple-go/v4 v4.0.0/go.mod h1:AXT2yfAFOntJx6iMeo1J/zKBw0ggXFYBt4e97dqqPnc=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=\ngithub.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/exoscale/egoscale/v3 v3.1.33 h1:5Lk/pwZ+K0sjNu9obS0VYPfhZQffRkvvO0BpdPoir4o=\ngithub.com/exoscale/egoscale/v3 v3.1.33/go.mod h1:0iY8OxgHJCS5TKqDNhwOW95JBKCnBZl3YGU4Yt+NqkU=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=\ngithub.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=\ngithub.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=\ngithub.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=\ngithub.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=\ngithub.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=\ngithub.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo=\ngithub.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=\ngithub.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=\ngithub.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=\ngithub.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-acme/alidns-20150109/v4 v4.7.0 h1:PqJ/wR0JTpL4v0Owu1uM7bPQ1Yww0eQLAuuSdLjjQaQ=\ngithub.com/go-acme/alidns-20150109/v4 v4.7.0/go.mod h1:btQvB6xZoN6ykKB74cPhiR+uvhrEE2AFVXm6RDmCHm0=\ngithub.com/go-acme/esa-20240910/v2 v2.48.0 h1:muSDyhjDTejxUGe3FTthCPCqRaEdYY9cG3N/AmU52Lc=\ngithub.com/go-acme/esa-20240910/v2 v2.48.0/go.mod h1:shPb6hzc1rJL15IJBY8HQ4GZk4E8RC52+52twutEwIg=\ngithub.com/go-acme/jdcloud-sdk-go v1.64.0 h1:AW9j5khk8tRYbpBJPxKmqdwIqgLs2Fz3HUK3hn2YXjs=\ngithub.com/go-acme/jdcloud-sdk-go v1.64.0/go.mod h1:qc/m8HNX1Zgd7GAv2DSEinup8fwy3Ted3/VVx7LB5bU=\ngithub.com/go-acme/tencentclouddnspod v1.3.24 h1:uCSiOW1EJttcnOON+MVVyVDJguFL/Q4NIGkq1CrT9p8=\ngithub.com/go-acme/tencentclouddnspod v1.3.24/go.mod h1:RKcB2wSoZncjBA0OEFj59s1ko1XDy+ZsAtk+9uMxUF0=\ngithub.com/go-acme/tencentedgdeone v1.3.38 h1:5YsVl0H4A+cwtiUqR1eZbKFdr4OWfYp2KYJopifzKyQ=\ngithub.com/go-acme/tencentedgdeone v1.3.38/go.mod h1:yyjTKVmGpMtFv5HqGODqehHnZJ4KWAbG6dAiwWDgCDY=\ngithub.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=\ngithub.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=\ngithub.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=\ngithub.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=\ngithub.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=\ngithub.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=\ngithub.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=\ngithub.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=\ngithub.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=\ngithub.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=\ngithub.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA=\ngithub.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=\ngithub.com/goccy/go-yaml v1.9.8 h1:5gMyLUeU1/6zl+WFfR1hN7D2kf+1/eRGa7DFtToiBvQ=\ngithub.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=\ngithub.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=\ngithub.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=\ngithub.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=\ngithub.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=\ngithub.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=\ngithub.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=\ngithub.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=\ngithub.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=\ngithub.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=\ngithub.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=\ngithub.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=\ngithub.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=\ngithub.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw=\ngithub.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=\ngithub.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 h1:sH7xkTfYzxIEgzq1tDHIMKRh1vThOEOGNsettdEeLbE=\ngithub.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56/go.mod h1:VSalo4adEk+3sNkmVJLnhHoOyOYYS8sTWLG4mv5BKto=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=\ngithub.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=\ngithub.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=\ngithub.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=\ngithub.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=\ngithub.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=\ngithub.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=\ngithub.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=\ngithub.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=\ngithub.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=\ngithub.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=\ngithub.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=\ngithub.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 h1:J+U6+eUjIsBhefolFdZW5hQNJbkMj+7msxZrv56Cg2g=\ngithub.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=\ngithub.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5uPK3wHh96wCsqe3hCMKh8E=\ngithub.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=\ngithub.com/infobloxopen/infoblox-go-client/v2 v2.10.0 h1:AKsihjFT/t6Y0keEv3p59DACcOuh0inWXdUB0ZOzYH0=\ngithub.com/infobloxopen/infoblox-go-client/v2 v2.10.0/go.mod h1:NeNJpz09efw/edzqkVivGv1bWqBXTomqYBRFbP+XBqg=\ngithub.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=\ngithub.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=\ngithub.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=\ngithub.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=\ngithub.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=\ngithub.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=\ngithub.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=\ngithub.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=\ngithub.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=\ngithub.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=\ngithub.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=\ngithub.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/labbsr0x/bindman-dns-webhook v1.0.2 h1:I7ITbmQPAVwrDdhd6dHKi+MYJTJqPCK0jE6YNBAevnk=\ngithub.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA=\ngithub.com/labbsr0x/goh v1.0.1 h1:97aBJkDjpyBZGPbQuOK5/gHcSFbcr5aRsq3RSRJFpPk=\ngithub.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=\ngithub.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o=\ngithub.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas=\ngithub.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=\ngithub.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=\ngithub.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=\ngithub.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM=\ngithub.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=\ngithub.com/liquidweb/liquidweb-go v1.6.4 h1:6S0m3hHSpiLqGD7AFSb7lH/W/qr1wx+tKil9fgIbjMc=\ngithub.com/liquidweb/liquidweb-go v1.6.4/go.mod h1:B934JPIIcdA+uTq2Nz5PgOtG6CuCaEvQKe/Ge/5GgZ4=\ngithub.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=\ngithub.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=\ngithub.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=\ngithub.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=\ngithub.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=\ngithub.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=\ngithub.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=\ngithub.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=\ngithub.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=\ngithub.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34=\ngithub.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M=\ngithub.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=\ngithub.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=\ngithub.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=\ngithub.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=\ngithub.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=\ngithub.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=\ngithub.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=\ngithub.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/namedotcom/go/v4 v4.0.2 h1:4gNkPaPRG/2tqFNUUof7jAVsA6vDutFutEOd7ivnDwA=\ngithub.com/namedotcom/go/v4 v4.0.2/go.mod h1:J6sVueHMb0qbarPgdhrzEVhEaYp+R1SCaTGl2s6/J1Q=\ngithub.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q=\ngithub.com/nats-io/jwt/v2 v2.0.3/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY=\ngithub.com/nats-io/nats-server/v2 v2.5.0/go.mod h1:Kj86UtrXAL6LwYRA6H4RqzkHhK0Vcv2ZnKD5WbQ1t3g=\ngithub.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=\ngithub.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=\ngithub.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=\ngithub.com/nrdcg/auroradns v1.2.0 h1:Jg407vTdXZvZKsART9CNWMp8rQOyhBk04q0MsOf0YR4=\ngithub.com/nrdcg/auroradns v1.2.0/go.mod h1:hnByA4Z7MOmV4EPRw5eOmEaNRFavcCIz6kONpNxp9LI=\ngithub.com/nrdcg/bunny-go v0.1.0 h1:GAHTRpHaG/TxfLZlqoJ8OJFzw8rI74+jOTkzxWh0uHA=\ngithub.com/nrdcg/bunny-go v0.1.0/go.mod h1:u+C9dgsspgtWVaAz6QkyV17s9fxD8viwwKoxb9XMz1A=\ngithub.com/nrdcg/desec v0.11.1 h1:ilpKmCr4gGsLcyq3RHfHNmlRzm9fzT2XbWxoVaUCS0s=\ngithub.com/nrdcg/desec v0.11.1/go.mod h1:2LuxHlOcwML/7cntu0eimONmA1U+ZxFDAonoSXr4igQ=\ngithub.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U=\ngithub.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ=\ngithub.com/nrdcg/freemyip v0.3.0 h1:0D2rXgvLwe2RRaVIjyUcQ4S26+cIS2iFwnhzDsEuuwc=\ngithub.com/nrdcg/freemyip v0.3.0/go.mod h1:c1PscDvA0ukBF0dwelU/IwOakNKnVxetpAQ863RMJoM=\ngithub.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=\ngithub.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=\ngithub.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=\ngithub.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=\ngithub.com/nrdcg/mailinabox v0.3.0 h1:PHkC1elKXKAjEvdx2HHFMgcEGZFqudAl7aU3L2JDhM4=\ngithub.com/nrdcg/mailinabox v0.3.0/go.mod h1:1eFIGcM4lI+AfFOUpbs548SFGz1ZWoMOGbECBmkghw4=\ngithub.com/nrdcg/namesilo v0.5.0 h1:6QNxT/XxE+f5B+7QlfWorthNzOzcGlBLRQxqi6YeBrE=\ngithub.com/nrdcg/namesilo v0.5.0/go.mod h1:4UkwlwQfDt74kSGmhLaDylnBrD94IfflnpoEaj6T2qw=\ngithub.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw=\ngithub.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms=\ngithub.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 h1:OWijzl3nHUApvTivl+3+78dbBwmyEHOnb+W9m6ixGbk=\ngithub.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=\ngithub.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 h1:9LsjN/zaIN7H8JE61NHpbWhxF0UGY96+kMlk3g8OvGU=\ngithub.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2/go.mod h1:32vZH06TuwZSn+IDMO1qcDvC2vHVlzUALCwXGWPA+dc=\ngithub.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=\ngithub.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=\ngithub.com/nrdcg/vegadns v0.3.0 h1:11FQMw7xVIRUWO9o5+Z/5YZhmPWlm4oxUUH3F6EVqQU=\ngithub.com/nrdcg/vegadns v0.3.0/go.mod h1:NqSyRKZuJlAsv8VI/7rSubfPXN68NwaJ0aG9KxQVFVo=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/nzdjb/go-metaname v1.0.0 h1:sNASlZC1RM3nSudtBTE1a3ZVTDyTpjqI5WXRPrdZ9Hg=\ngithub.com/nzdjb/go-metaname v1.0.0/go.mod h1:0GR0LshZax1Lz4VrOrfNSE4dGvTp7HGjiemdczXT2H4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=\ngithub.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=\ngithub.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=\ngithub.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=\ngithub.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=\ngithub.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0=\ngithub.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=\ngithub.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=\ngithub.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=\ngithub.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=\ngithub.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=\ngithub.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=\ngithub.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=\ngithub.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=\ngithub.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=\ngithub.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=\ngithub.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=\ngithub.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=\ngithub.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=\ngithub.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM=\ngithub.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c=\ngithub.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc=\ngithub.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=\ngithub.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=\ngithub.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=\ngithub.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=\ngithub.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=\ngithub.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=\ngithub.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=\ngithub.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=\ngithub.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 h1:dq90+d51/hQRaHEqRAsQ1rE/pC1GUS4sc2rCbbFsAIY=\ngithub.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=\ngithub.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/regfish/regfish-dnsapi-go v0.1.1 h1:TJFtbePHkd47q5GZwYl1h3DIYXmoxdLjW/SBsPtB5IE=\ngithub.com/regfish/regfish-dnsapi-go v0.1.1/go.mod h1:ubIgXSfqarSnl3XHSn8hIFwFF3h0yrq0ZiWD93Y2VjY=\ngithub.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/sacloud/api-client-go v0.3.3 h1:ZpSAyGpITA8UFO3Hq4qMHZLGuNI1FgxAxo4sqBnCKDs=\ngithub.com/sacloud/api-client-go v0.3.3/go.mod h1:0p3ukcWYXRCc2AUWTl1aA+3sXLvurvvDqhRaLZRLBwo=\ngithub.com/sacloud/go-http v0.1.9 h1:Xa5PY8/pb7XWhwG9nAeXSrYXPbtfBWqawgzxD5co3VE=\ngithub.com/sacloud/go-http v0.1.9/go.mod h1:DpDG+MSyxYaBwPJ7l3aKLMzwYdTVtC5Bo63HActcgoE=\ngithub.com/sacloud/iaas-api-go v1.23.1 h1:rjYG0vVoxWyETiwc7R8YdD7CIzs9vVNEOzu7w6dgGzc=\ngithub.com/sacloud/iaas-api-go v1.23.1/go.mod h1:EGIHOWRB9azOv7HPCVM8WpOEl28WIV9TNRbnEVg+Q3U=\ngithub.com/sacloud/packages-go v0.0.12 h1:MKeZNN3FQn1heqUSRBrbZw89YusZA1n4kammjMFZYvQ=\ngithub.com/sacloud/packages-go v0.0.12/go.mod h1:XNF5MCTWcHo9NiqWnYctVbASSSZR3ZOmmQORIzcurJ8=\ngithub.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=\ngithub.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=\ngithub.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=\ngithub.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=\ngithub.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=\ngithub.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/selectel/domains-go v1.1.0 h1:futG50J43ALLKQAnZk9H9yOtLGnSUh7c5hSvuC5gSHo=\ngithub.com/selectel/domains-go v1.1.0/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA=\ngithub.com/selectel/go-selvpcclient/v4 v4.1.0 h1:22lBp+rzg9g2MP4iiGhpVAcCt0kMv7I7uV1W3taLSvQ=\ngithub.com/selectel/go-selvpcclient/v4 v4.1.0/go.mod h1:eFhL1KUW159KOJVeGO7k/Uxl0TYd/sBkWXjuF5WxmYk=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=\ngithub.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/softlayer/softlayer-go v1.2.1 h1:8ucHxn5laVsVPb0/aMGnr6tOMt1I9BgEtU5mn70OGKw=\ngithub.com/softlayer/softlayer-go v1.2.1/go.mod h1:Gz9/ktcmB7Z8EJlu+QEJJpkv8lAmnhYdB9Tc6gedjmo=\ngithub.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ=\ngithub.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums=\ngithub.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=\ngithub.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=\ngithub.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=\ngithub.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=\ngithub.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=\ngithub.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=\ngithub.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=\ngithub.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=\ngithub.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=\ngithub.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=\ngithub.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=\ngithub.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=\ngithub.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=\ngithub.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=\ngithub.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=\ngithub.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=\ngithub.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.24/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.38/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48 h1:bCs+z6dxRaHWm/C1D/XkSOcCZ0+W2+/6HmIXjpAj+fY=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=\ngithub.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=\ngithub.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=\ngithub.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/transip/gotransip/v6 v6.26.1 h1:MeqIjkTBBsZwWAK6giZyMkqLmKMclVHEuTNmoBdx4MA=\ngithub.com/transip/gotransip/v6 v6.26.1/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s=\ngithub.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=\ngithub.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 h1:/VaznPrb/b68e3iMvkr27fU7JqPKU4j7tIITZnjQX1k=\ngithub.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419/go.mod h1:QN0/PdenvYWB0GRMz6JJbPeZz2Lph2iys1p8AFVHm2c=\ngithub.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=\ngithub.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=\ngithub.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=\ngithub.com/vinyldns/go-vinyldns v0.9.17 h1:hfPZfCaxcRBX6Gsgl42rLCeoal58/BH8kkvJShzjjdI=\ngithub.com/vinyldns/go-vinyldns v0.9.17/go.mod h1:pwWhE9K/leGDOIduVhRGvQ3ecVMHWRfEnKYUTEU3gB4=\ngithub.com/volcengine/volc-sdk-golang v1.0.237 h1:hpLKiS2BwDcSBtZWSz034foCbd0h3FrHTKlUMqHIdc4=\ngithub.com/volcengine/volc-sdk-golang v1.0.237/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM=\ngithub.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=\ngithub.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=\ngithub.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=\ngithub.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=\ngithub.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=\ngithub.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=\ngithub.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=\ngithub.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=\ngithub.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=\ngithub.com/yandex-cloud/go-genproto v0.54.0 h1:LjEwDPBAtF39HvcPQe8I+ImCnFasCPCOVh2b2Sr2eAg=\ngithub.com/yandex-cloud/go-genproto v0.54.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=\ngithub.com/yandex-cloud/go-sdk/services/dns v0.0.36 h1:sD622+baDvJ2ujhCfoFsCH0XeNsaZNW6loRqvRavjtE=\ngithub.com/yandex-cloud/go-sdk/services/dns v0.0.36/go.mod h1:Hh7IKJxULaRzmyM19lQZw+yUDyMM8M3Qrk1LbWqhCkc=\ngithub.com/yandex-cloud/go-sdk/v2 v2.56.0 h1:rihPAZbPbHU/BKTLuT64nU1uhbBrO20HhdlLR3Hyoz0=\ngithub.com/yandex-cloud/go-sdk/v2 v2.56.0/go.mod h1:jzVBQgamNHoiDsmjog2dPZHMXuGZqmxf/epH+Qb7Emc=\ngithub.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=\ngithub.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=\ngithub.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=\ngo.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=\ngo.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=\ngo.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=\ngo.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=\ngo.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=\ngo.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=\ngo.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=\ngo.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=\ngo.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=\ngo.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=\ngo.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=\ngo.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=\ngo.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=\ngo.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=\ngo.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngo.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=\ngo.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=\ngo.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=\ngolang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=\ngolang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=\ngolang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=\ngolang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=\ngolang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=\ngolang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=\ngolang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=\ngolang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU=\ngolang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=\ngolang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=\ngolang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=\ngolang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=\ngolang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=\ngolang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=\ngolang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=\ngolang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=\ngolang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=\ngolang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=\ngolang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=\ngolang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=\ngolang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=\ngolang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=\ngolang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=\ngolang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=\ngolang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=\ngolang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=\ngolang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=\ngolang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=\ngolang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=\ngolang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=\ngolang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=\ngolang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=\ngolang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=\ngonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=\ngonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=\ngoogle.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=\ngoogle.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=\ngoogle.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=\ngopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=\ngopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=\ngopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=\ngopkg.in/ns1/ns1-go.v2 v2.17.2 h1:x8YKHqCJWkC/hddfUhw7FRqTG0x3fr/0ZnWYN+i4THs=\ngopkg.in/ns1/ns1-go.v2 v2.17.2/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=\ngopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=\ngopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\nsoftware.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0=\nsoftware.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=\n"
  },
  {
    "path": "internal/clihelp/generator.go",
    "content": "package main\n\n//go:generate go run .\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/go-acme/lego/v4/cmd\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nconst outputFile = \"../../docs/data/zz_cli_help.toml\"\n\nconst baseTemplate = `# THIS FILE IS AUTO-GENERATED. PLEASE DO NOT EDIT.\n\n{{ range .}}\n[[command]]\ntitle   = \"{{.Title}}\"\ncontent = \"\"\"\n{{.Help}}\n\"\"\"\n{{end -}}\n`\n\ntype commandHelp struct {\n\tTitle string\n\tHelp  string\n}\n\nfunc main() {\n\tlog.SetFlags(0)\n\n\terr := generate()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tlog.Println(\"cli_help.toml updated\")\n}\n\nfunc generate() error {\n\tapp := createStubApp()\n\n\toutputTpl := template.Must(template.New(\"output\").Parse(baseTemplate))\n\n\t// collect output of various help pages\n\tvar help []commandHelp\n\n\tfor _, args := range [][]string{\n\t\t{\"lego\", \"help\"},\n\t\t{\"lego\", \"help\", \"run\"},\n\t\t{\"lego\", \"help\", \"renew\"},\n\t\t{\"lego\", \"help\", \"revoke\"},\n\t\t{\"lego\", \"help\", \"list\"},\n\t\t{\"lego\", \"dnshelp\"},\n\t} {\n\t\tcontent, err := run(app, args)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"running %s failed: %w\", args, err)\n\t\t}\n\n\t\thelp = append(help, content)\n\t}\n\n\tf, err := os.Create(outputFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cannot open cli_help.toml: %w\", err)\n\t}\n\n\terr = outputTpl.Execute(f, help)\n\n\tdefer func() { _ = f.Close() }()\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write cli_help.toml: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// createStubApp Construct cli app, very similar to cmd/lego/main.go.\n// Notable differences:\n// - substitute \".\" for CWD in default config path, as the user will very likely see a different path\n// - do not include version information, because we're likely running against a snapshot\n// - skip DNS help and provider list, as initialization takes time, and we don't generate `lego dns --help` here.\nfunc createStubApp() *cli.App {\n\tapp := cli.NewApp()\n\tapp.Name = \"lego\"\n\tapp.HelpName = \"lego\"\n\tapp.Usage = \"Let's Encrypt client written in Go\"\n\tapp.Flags = cmd.CreateFlags(\"./.lego\")\n\tapp.Commands = cmd.CreateCommands()\n\n\treturn app\n}\n\nfunc run(app *cli.App, args []string) (h commandHelp, err error) {\n\tw := app.Writer\n\n\tdefer func() { app.Writer = w }()\n\n\tvar buf bytes.Buffer\n\n\tapp.Writer = &buf\n\n\tif err := app.Run(args); err != nil {\n\t\treturn h, err\n\t}\n\n\treturn commandHelp{\n\t\tTitle: strings.Join(args, \" \"),\n\t\tHelp:  strings.TrimSpace(buf.String()),\n\t}, nil\n}\n"
  },
  {
    "path": "internal/dns/descriptors/descriptors.go",
    "content": "package descriptors\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/BurntSushi/toml\"\n)\n\ntype Providers struct {\n\tProviders []Provider\n}\n\ntype Provider struct {\n\tName          string         // Real name of the DNS provider\n\tCode          string         // DNS code\n\tAliases       []string       // DNS code aliases (for compatibility/deprecation)\n\tSince         string         // First lego version\n\tURL           string         // DNS provider URL\n\tDescription   string         // Provider summary\n\tExample       string         // CLI example\n\tConfiguration *Configuration // Environment variables\n\tLinks         *Links         // Links\n\tAdditional    string         // Extra documentation\n\tGeneratedFrom string         // Source file\n}\n\ntype Configuration struct {\n\tCredentials map[string]string\n\tAdditional  map[string]string\n}\n\ntype Links struct {\n\tAPI      string\n\tGoClient string\n}\n\n// GetProviderInformation extract provider information from TOML description files.\nfunc GetProviderInformation(root string) (*Providers, error) {\n\tmodels := &Providers{}\n\n\terr := filepath.Walk(filepath.Join(root, \"providers\", \"dns\"), walker(root, models))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn models, nil\n}\n\nfunc walker(root string, prs *Providers) func(string, os.FileInfo, error) error {\n\treturn func(path string, _ os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif filepath.Ext(path) != \".toml\" {\n\t\t\treturn nil\n\t\t}\n\n\t\tm := Provider{}\n\n\t\tm.GeneratedFrom, err = filepath.Rel(root, path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t_, err = toml.DecodeFile(path, &m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tprs.Providers = append(prs.Providers, m)\n\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "internal/dns/docs/generator.go",
    "content": "package main\n\n//go:generate go run .\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"go/format\"\n\thtml \"html/template\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/go-acme/lego/v4/internal/dns/descriptors\"\n)\n\n//go:embed templates\nvar templateFS embed.FS\n\nconst (\n\troot = \"../../../\"\n\n\tcliOutput  = root + \"cmd/zz_gen_cmd_dnshelp.go\"\n\tdocOutput  = root + \"docs/content/dns\"\n\treadmePath = root + \"README.md\"\n)\n\nconst (\n\tmdTemplate     = \"templates/dns.md.tmpl\"\n\tcliTemplate    = \"templates/dns.go.tmpl\"\n\treadmeTemplate = \"templates/readme.md.tmpl\"\n)\n\nconst (\n\tstartLine = \"<!-- START DNS PROVIDERS LIST -->\"\n\tendLine   = \"<!-- END DNS PROVIDERS LIST -->\"\n)\n\nfunc main() {\n\tmodels, err := descriptors.GetProviderInformation(root)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\terr = cleanDocumentation()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tfor _, m := range models.Providers {\n\t\t// generate documentation\n\t\terr = generateDocumentation(m)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t}\n\n\t// generate CLI help\n\terr = generateCLIHelp(models)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// generate README.md\n\terr = generateReadMe(models)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tfmt.Printf(\"Documentation for %d DNS providers has been generated.\\n\", len(models.Providers)+1)\n}\n\nfunc cleanDocumentation() error {\n\tpaths, err := filepath.Glob(filepath.Join(docOutput, \"zz_gen_*.md\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, p := range paths {\n\t\terr = os.RemoveAll(p)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc generateDocumentation(m descriptors.Provider) error {\n\tfilename := filepath.Join(docOutput, \"zz_gen_\"+m.Code+\".md\")\n\n\tfile, err := os.Create(filename)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() { _ = file.Close() }()\n\n\treturn template.Must(template.ParseFS(templateFS, mdTemplate)).Execute(file, m)\n}\n\nfunc generateCLIHelp(models *descriptors.Providers) error {\n\tfilename := filepath.Clean(cliOutput)\n\n\tfile, err := os.Create(filename)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() { _ = file.Close() }()\n\n\tb := &bytes.Buffer{}\n\n\terr = template.Must(\n\t\ttemplate.New(filepath.Base(cliTemplate)).Funcs(map[string]any{\n\t\t\t\"safe\": func(src string) string {\n\t\t\t\treturn strings.ReplaceAll(src, \"`\", \"'\")\n\t\t\t},\n\t\t}).ParseFS(templateFS, cliTemplate),\n\t).Execute(b, models)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// gofmt\n\tsource, err := format.Source(b.Bytes())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = file.Write(source)\n\n\treturn err\n}\n\nfunc generateReadMe(models *descriptors.Providers) error {\n\ttpl := html.Must(html.New(filepath.Base(readmeTemplate)).ParseFS(templateFS, readmeTemplate))\n\tproviders := orderProviders(models)\n\n\tfile, err := os.Open(readmePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() { _ = file.Close() }()\n\n\tvar skip bool\n\n\tbuffer := bytes.NewBufferString(\"\")\n\n\tfileScanner := bufio.NewScanner(file)\n\tfor fileScanner.Scan() {\n\t\ttext := fileScanner.Text()\n\n\t\tif text == startLine {\n\t\t\t_, _ = fmt.Fprintln(buffer, text)\n\t\t\tif err = tpl.Execute(buffer, providers); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tskip = true\n\t\t}\n\n\t\tif text == endLine {\n\t\t\tskip = false\n\t\t}\n\n\t\tif skip {\n\t\t\tcontinue\n\t\t}\n\n\t\t_, _ = fmt.Fprintln(buffer, text)\n\t}\n\n\tif fileScanner.Err() != nil {\n\t\treturn fileScanner.Err()\n\t}\n\n\tif skip {\n\t\treturn errors.New(\"missing end tag\")\n\t}\n\n\treturn os.WriteFile(readmePath, buffer.Bytes(), 0o666)\n}\n\nfunc orderProviders(models *descriptors.Providers) [][]descriptors.Provider {\n\tconst nbCol = 4\n\n\tslices.SortFunc(models.Providers, func(a, b descriptors.Provider) int {\n\t\treturn strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))\n\t})\n\n\tvar (\n\t\tmatrix [][]descriptors.Provider\n\t\trow    []descriptors.Provider\n\t)\n\n\tfor i, p := range models.Providers {\n\t\tswitch {\n\t\tcase len(row) == nbCol:\n\t\t\tmatrix = append(matrix, row)\n\t\t\trow = []descriptors.Provider{p}\n\n\t\tcase i == len(models.Providers)-1:\n\t\t\trow = append(row, p)\n\t\t\tfor j := len(row); j < nbCol; j++ {\n\t\t\t\trow = append(row, descriptors.Provider{})\n\t\t\t}\n\n\t\t\tmatrix = append(matrix, row)\n\n\t\tdefault:\n\t\t\trow = append(row, p)\n\t\t}\n\t}\n\n\tif len(row) < nbCol {\n\t\tfor j := len(row); j < nbCol; j++ {\n\t\t\trow = append(row, descriptors.Provider{})\n\t\t}\n\n\t\tmatrix = append(matrix, row)\n\t}\n\n\treturn matrix\n}\n"
  },
  {
    "path": "internal/dns/docs/templates/dns.go.tmpl",
    "content": "// Code generated by 'make generate-dns'; DO NOT EDIT.\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/tabwriter\"\n)\n\nfunc allDNSCodes() string {\n\tproviders := []string{\n{{- range $provider := .Providers }}\n\t\t\"{{ $provider.Code }}\",\n{{- end}}\n\t}\n\tsort.Strings(providers)\n\treturn strings.Join(providers, \", \")\n}\n\nfunc displayDNSHelp(w io.Writer, name string) error {\n\tw = tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)\n\tew := &errWriter{w: w}\n\n\tswitch name {\n{{- range $provider := .Providers }}\n\tcase \"{{ $provider.Code }}\":\n\t\t// generated from: {{ .GeneratedFrom }}\n\t\tew.writeln(`Configuration for {{ $provider.Name }}.`)\n\t\tew.writeln(`Code:\t'{{ $provider.Code }}'`)\n\t\tew.writeln(`Since:\t'{{ $provider.Since }}'`)\n\t\tew.writeln()\n{{if $provider.Configuration }}{{if $provider.Configuration.Credentials }}\n\t\tew.writeln(`Credentials:`)\n{{- range $k, $v := $provider.Configuration.Credentials }}\n\t\tew.writeln(`\t- \"{{ $k }}\":\t{{ safe $v }}`)\n{{- end}}\n\t\tew.writeln()\n{{end}}{{if $provider.Configuration.Additional }}\n\t\tew.writeln(`Additional Configuration:`)\n{{- range $k, $v := $provider.Configuration.Additional }}\n\t\tew.writeln(`\t- \"{{ $k }}\":\t{{ safe $v }}`)\n{{- end}}\n{{end}}{{end}}\n\t\tew.writeln()\n\t\tew.writeln(`More information: https://go-acme.github.io/lego/dns/{{ $provider.Code }}`)\n{{end}}\n\tdefault:\n\t\treturn fmt.Errorf(\"%q is not yet supported\", name)\n\t}\n\n\tif flusher, ok := w.(interface{ Flush() error }); ok {\n\t\treturn flusher.Flush()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dns/docs/templates/dns.md.tmpl",
    "content": "---\ntitle: \"{{ .Name }}\"\ndate: 2019-03-03T16:39:46+01:00\ndraft: false\nslug: {{ .Code }}\ndnsprovider:\n  since:    \"{{ .Since }}\"\n  code:     \"{{ .Code }}\"\n  url:      \"{{ .URL }}\"\n---\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- {{ .GeneratedFrom }} -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n\n{{if .Description -}}\n{{ .Description }}\n{{else}}\nConfiguration for [{{ .Name }}]({{ .URL }}).\n{{end}}\n\n<!--more-->\n\n- Code: `{{ .Code }}`\n- Since: {{ .Since }}\n\n{{if .Example }}\nHere is an example bash command using the {{ .Name }} provider:\n\n```bash\n{{ .Example -}}\n```\n{{else}}\n{{ \"{{\" }}% notice note %}}\n_Please contribute by adding a CLI example._\n{{ \"{{\" }}% /notice %}}\n{{end}}\n\n{{if .Configuration }}\n{{if .Configuration.Credentials }}\n## Credentials\n\n| Environment Variable Name | Description |\n|-----------------------|-------------|\n{{- range $k, $v := .Configuration.Credentials }}\n| `{{$k}}` | {{$v}} |\n{{- end}}\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{ `{{% ref \"dns#configuration-and-credentials\" %}}` }}).\n{{- end}}\n\n{{if .Configuration.Additional }}\n## Additional Configuration\n\n| Environment Variable Name | Description |\n|--------------------------------|-------------|\n{{- range $k, $v := .Configuration.Additional }}\n| `{{$k}}` | {{$v}} |\n{{- end}}\n\nThe environment variable names can be suffixed by `_FILE` to reference a file instead of a value.\nMore information [here]({{ `{{% ref \"dns#configuration-and-credentials\" %}}` }}).\n{{- end}}\n{{- end}}\n\n{{ .Additional }}\n\n{{if .Links }}\n## More information\n\n{{if .Links.API -}}\n- [API documentation]({{ .Links.API }})\n{{- end}}\n{{- if .Links.GoClient }}\n- [Go client]({{ .Links.GoClient }})\n{{- end}}\n\n{{- end}}\n\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n<!-- {{ .GeneratedFrom }} -->\n<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->\n"
  },
  {
    "path": "internal/dns/docs/templates/readme.md.tmpl",
    "content": "\n<table>\n{{- range . -}}\n<tr>\n  {{- range . }}\n  <td>{{if .Code }}<a href=\"https://go-acme.github.io/lego/dns/{{ .Code }}/\">{{ .Name }}</a>{{end}}</td>\n  {{- end }}\n</tr>\n{{- end -}}\n</table>\n\n"
  },
  {
    "path": "internal/dns/providers/dns_providers.go.tmpl",
    "content": "// Code generated by 'make generate-dns'; DO NOT EDIT.\n\npackage dns\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n{{- range $provider := .Providers }}\n     \"github.com/go-acme/lego/v4/providers/dns/{{ cleanName $provider.Code }}\"\n{{- end}}\n)\n\n// NewDNSChallengeProviderByName Factory for DNS providers.\nfunc NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {\n\tswitch name {\n{{- range $provider := .Providers }}\n\tcase \"{{ $provider.Code }}\"{{range $alias := $provider.Aliases }},\"{{ $alias }}\"{{end}}:\n\t\treturn {{ cleanName $provider.Code }}.NewDNSProvider()\n{{- end}}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unrecognized DNS provider: %s\", name)\n\t}\n}\n"
  },
  {
    "path": "internal/dns/providers/generator.go",
    "content": "package main\n\n//go:generate go run .\n\nimport (\n\t\"bytes\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"go/format\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/go-acme/lego/v4/internal/dns/descriptors\"\n)\n\nconst (\n\troot = \"../../../\"\n\n\toutputPath = \"providers/dns/zz_gen_dns_providers.go\"\n)\n\n//go:embed dns_providers.go.tmpl\nvar srcTemplate string\n\nfunc main() {\n\terr := generate()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\nfunc generate() error {\n\tinfo, err := descriptors.GetProviderInformation(root)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfile, err := os.Create(filepath.Join(root, outputPath))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() { _ = file.Close() }()\n\n\tb := &bytes.Buffer{}\n\n\terr = template.Must(\n\t\ttemplate.New(\"\").Funcs(map[string]any{\n\t\t\t\"cleanName\": func(src string) string {\n\t\t\t\treturn strings.ReplaceAll(src, \"-\", \"\")\n\t\t\t},\n\t\t}).Parse(srcTemplate),\n\t).Execute(b, info)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// gofmt\n\tsource, err := format.Source(b.Bytes())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = file.Write(source)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Printf(\"Switch mapping for %d DNS providers has been generated.\\n\", len(info.Providers)+1)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/releaser/generator.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"embed\"\n\t\"fmt\"\n\t\"go/format\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"text/template\"\n)\n\nconst (\n\tdnsTemplate   = \"templates/dns.go.tmpl\"\n\tdnsTargetFile = \"./providers/dns/internal/useragent/useragent.go\"\n)\n\nconst (\n\tsenderTemplate   = \"templates/sender.go.tmpl\"\n\tsenderTargetFile = \"./acme/api/internal/sender/useragent.go\"\n)\n\nconst (\n\tversionTemplate   = \"templates/version.go.tmpl\"\n\tversionTargetFile = \"./cmd/lego/zz_gen_version.go\"\n)\n\n//go:embed templates\nvar templateFS embed.FS\n\ntype Generator struct {\n\ttemplatePath string\n\ttargetFile   string\n}\n\nfunc NewGenerator(templatePath, targetFile string) *Generator {\n\treturn &Generator{templatePath: templatePath, targetFile: targetFile}\n}\n\nfunc (g *Generator) Generate(version, comment string) error {\n\ttmpl, err := template.New(filepath.Base(g.templatePath)).ParseFS(templateFS, g.templatePath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parsing template (%s): %w\", g.templatePath, err)\n\t}\n\n\tb := &bytes.Buffer{}\n\n\terr = tmpl.Execute(b, map[string]string{\n\t\t\"version\": version,\n\t\t\"comment\": comment,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"execute template (%s): %w\", g.templatePath, err)\n\t}\n\n\tsource, err := format.Source(b.Bytes())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"format generated content (%s): %w\", g.targetFile, err)\n\t}\n\n\terr = os.WriteFile(g.targetFile, source, 0o644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"write file (%s): %w\", g.targetFile, err)\n\t}\n\n\treturn nil\n}\n\nfunc generate(targetVersion, comment string) error {\n\tgenerators := []*Generator{\n\t\tNewGenerator(dnsTemplate, dnsTargetFile),\n\t\tNewGenerator(senderTemplate, senderTargetFile),\n\t\tNewGenerator(versionTemplate, versionTargetFile),\n\t}\n\n\tfor _, generator := range generators {\n\t\terr := generator.Generate(targetVersion, comment)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"generate file(s): %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/releaser/releaser.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"log\"\n\t\"os\"\n\t\"strconv\"\n\n\thcversion \"github.com/hashicorp/go-version\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nconst flgMode = \"mode\"\n\nconst (\n\tmodePatch = \"patch\"\n\tmodeMinor = \"minor\"\n\tmodeMajor = \"major\"\n)\n\nconst versionSourceFile = \"./cmd/lego/zz_gen_version.go\"\n\nconst (\n\tcommentRelease = \"release\"\n\tcommentDetach  = \"detach\"\n)\n\nfunc main() {\n\tapp := cli.NewApp()\n\tapp.Name = \"lego-releaser\"\n\tapp.Usage = \"Lego releaser\"\n\tapp.HelpName = \"releaser\"\n\tapp.Commands = []*cli.Command{\n\t\t{\n\t\t\tName:   \"release\",\n\t\t\tUsage:  \"Update file for a release\",\n\t\t\tAction: release,\n\t\t\tBefore: func(ctx *cli.Context) error {\n\t\t\t\tmode := ctx.String(\"mode\")\n\t\t\t\tswitch mode {\n\t\t\t\tcase modePatch, modeMinor, modeMajor:\n\t\t\t\t\treturn nil\n\t\t\t\tdefault:\n\t\t\t\t\treturn fmt.Errorf(\"invalid mode: %s\", mode)\n\t\t\t\t}\n\t\t\t},\n\t\t\tFlags: []cli.Flag{\n\t\t\t\t&cli.StringFlag{\n\t\t\t\t\tName:    flgMode,\n\t\t\t\t\tAliases: []string{\"m\"},\n\t\t\t\t\tValue:   modePatch,\n\t\t\t\t\tUsage:   fmt.Sprintf(\"The release mode: %s|%s|%s\", modePatch, modeMinor, modeMajor),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:   \"detach\",\n\t\t\tUsage:  \"Update file post release\",\n\t\t\tAction: detach,\n\t\t},\n\t}\n\n\terr := app.Run(os.Args)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\nfunc release(ctx *cli.Context) error {\n\tmode := ctx.String(flgMode)\n\n\tcurrentVersion, err := readCurrentVersion(versionSourceFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"read current version: %w\", err)\n\t}\n\n\tnextVersion, err := bumpVersion(mode, currentVersion)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bump version: %w\", err)\n\t}\n\n\terr = generate(nextVersion, commentRelease)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc detach(_ *cli.Context) error {\n\tcurrentVersion, err := readCurrentVersion(versionSourceFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"read current version: %w\", err)\n\t}\n\n\tv := currentVersion.Core().String()\n\n\terr = generate(v, commentDetach)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc readCurrentVersion(filename string) (*hcversion.Version, error) {\n\tfset := token.NewFileSet()\n\n\tfile, err := parser.ParseFile(fset, filename, nil, parser.AllErrors)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tv := visitor{data: make(map[string]string)}\n\tast.Walk(v, file)\n\n\tcurrent, err := hcversion.NewSemver(v.data[\"defaultVersion\"])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn current, nil\n}\n\ntype visitor struct {\n\tdata map[string]string\n}\n\nfunc (v visitor) Visit(n ast.Node) ast.Visitor {\n\tif n == nil {\n\t\treturn nil\n\t}\n\n\tswitch d := n.(type) {\n\tcase *ast.GenDecl:\n\t\tif d.Tok == token.CONST {\n\t\t\tfor _, spec := range d.Specs {\n\t\t\t\tvalueSpec, ok := spec.(*ast.ValueSpec)\n\t\t\t\tif !ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif len(valueSpec.Names) != 1 || len(valueSpec.Values) != 1 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tva, ok := valueSpec.Values[0].(*ast.BasicLit)\n\t\t\t\tif !ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif va.Kind != token.STRING {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\ts, err := strconv.Unquote(va.Value)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tv.data[valueSpec.Names[0].String()] = s\n\t\t\t}\n\t\t}\n\tdefault:\n\t\t// noop\n\t}\n\n\treturn v\n}\n\nfunc bumpVersion(mode string, v *hcversion.Version) (string, error) {\n\tsegments := v.Segments()\n\n\tswitch mode {\n\tcase modePatch:\n\t\treturn fmt.Sprintf(\"%d.%d.%d\", segments[0], segments[1], segments[2]+1), nil\n\tcase modeMinor:\n\t\treturn fmt.Sprintf(\"%d.%d.0\", segments[0], segments[1]+1), nil\n\tcase modeMajor:\n\t\treturn fmt.Sprintf(\"%d.0.0\", segments[0]+1), nil\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"invalid mode: %s\", mode)\n\t}\n}\n"
  },
  {
    "path": "internal/releaser/templates/dns.go.tmpl",
    "content": "// Code generated by 'internal/releaser'; DO NOT EDIT.\n\npackage useragent\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"runtime\"\n)\n\nconst (\n\t// ourUserAgent is the User-Agent of this underlying library package.\n\tourUserAgent = \"goacme-lego/{{ .version }}\"\n\n\t// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.\n\t// values: detach|release\n\t// NOTE: Update this with each tagged release.\n\tourUserAgentComment = \"{{ .comment }}\"\n)\n\n// Get builds and returns the User-Agent string.\nfunc Get() string {\n\treturn fmt.Sprintf(\"%s (%s; %s; %s)\", ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH)\n}\n\n// SetHeader sets the User-Agent header.\nfunc SetHeader(h http.Header) {\n\th.Set(\"User-Agent\", Get())\n}\n"
  },
  {
    "path": "internal/releaser/templates/sender.go.tmpl",
    "content": "// Code generated by 'internal/releaser'; DO NOT EDIT.\n\npackage sender\n\nconst (\n\t// ourUserAgent is the User-Agent of this underlying library package.\n\tourUserAgent = \"xenolf-acme/{{ .version }}\"\n\n\t// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.\n\t// values: detach|release\n\t// NOTE: Update this with each tagged release.\n\tourUserAgentComment = \"{{ .comment }}\"\n)\n"
  },
  {
    "path": "internal/releaser/templates/version.go.tmpl",
    "content": "// Code generated by 'internal/releaser'; DO NOT EDIT.\n\npackage main\n\nconst defaultVersion = \"v{{ .version }}+dev{{ if .comment }}-{{ .comment }}{{end}}\"\n\nvar version = \"\"\n\nfunc getVersion() string {\n\tif version == \"\" {\n\t\treturn defaultVersion\n\t}\n\n\treturn version\n}\n"
  },
  {
    "path": "lego/client.go",
    "content": "package lego\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/certificate\"\n\t\"github.com/go-acme/lego/v4/challenge/resolver\"\n\t\"github.com/go-acme/lego/v4/registration\"\n)\n\n// Client is the user-friendly way to ACME.\ntype Client struct {\n\tCertificate  *certificate.Certifier\n\tChallenge    *resolver.SolverManager\n\tRegistration *registration.Registrar\n\tcore         *api.Core\n}\n\n// NewClient creates a new ACME client on behalf of the user.\n// The client will depend on the ACME directory located at CADirURL for the rest of its actions.\n// A private key of type keyType (see KeyType constants) will be generated when requesting a new certificate if one isn't provided.\nfunc NewClient(config *Config) (*Client, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"a configuration must be provided\")\n\t}\n\n\t_, err := url.Parse(config.CADirURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif config.HTTPClient == nil {\n\t\treturn nil, errors.New(\"the HTTP client cannot be nil\")\n\t}\n\n\tprivateKey := config.User.GetPrivateKey()\n\tif privateKey == nil {\n\t\treturn nil, errors.New(\"private key was nil\")\n\t}\n\n\tvar kid string\n\tif reg := config.User.GetRegistration(); reg != nil {\n\t\tkid = reg.URI\n\t}\n\n\tcore, err := api.New(config.HTTPClient, config.UserAgent, config.CADirURL, kid, privateKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsolversManager := resolver.NewSolversManager(core)\n\n\tprober := resolver.NewProber(solversManager)\n\n\toptions := certificate.CertifierOptions{\n\t\tKeyType:             config.Certificate.KeyType,\n\t\tTimeout:             config.Certificate.Timeout,\n\t\tOverallRequestLimit: config.Certificate.OverallRequestLimit,\n\t\tDisableCommonName:   config.Certificate.DisableCommonName,\n\t}\n\n\tcertifier := certificate.NewCertifier(core, prober, options)\n\n\treturn &Client{\n\t\tCertificate:  certifier,\n\t\tChallenge:    solversManager,\n\t\tRegistration: registration.NewRegistrar(core, config.User),\n\t\tcore:         core,\n\t}, nil\n}\n\n// GetToSURL returns the current ToS URL from the Directory.\nfunc (c *Client) GetToSURL() string {\n\treturn c.core.GetDirectory().Meta.TermsOfService\n}\n\n// GetExternalAccountRequired returns the External Account Binding requirement of the Directory.\nfunc (c *Client) GetExternalAccountRequired() bool {\n\treturn c.core.GetDirectory().Meta.ExternalAccountRequired\n}\n"
  },
  {
    "path": "lego/client_config.go",
    "content": "package lego\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/go-acme/lego/v4/registration\"\n)\n\nconst (\n\t// caCertificatesEnvVar is the environment variable name that can be used to\n\t// specify the path to PEM encoded CA Certificates that can be used to\n\t// authenticate an ACME server with an HTTPS certificate not issued by a CA in\n\t// the system-wide trusted root list.\n\t// Multiple file paths can be added by using os.PathListSeparator as a separator.\n\tcaCertificatesEnvVar = \"LEGO_CA_CERTIFICATES\"\n\n\t// caSystemCertPool is the environment variable name that can be used to define\n\t// if the certificates pool must use a copy of the system cert pool.\n\tcaSystemCertPool = \"LEGO_CA_SYSTEM_CERT_POOL\"\n\n\t// caServerNameEnvVar is the environment variable name that can be used to\n\t// specify the CA server name that can be used to\n\t// authenticate an ACME server with an HTTPS certificate not issued by a CA in\n\t// the system-wide trusted root list.\n\tcaServerNameEnvVar = \"LEGO_CA_SERVER_NAME\"\n\n\t// LEDirectoryProduction URL to the Let's Encrypt production.\n\tLEDirectoryProduction = \"https://acme-v02.api.letsencrypt.org/directory\"\n\n\t// LEDirectoryStaging URL to the Let's Encrypt staging.\n\tLEDirectoryStaging = \"https://acme-staging-v02.api.letsencrypt.org/directory\"\n)\n\ntype Config struct {\n\tCADirURL    string\n\tUser        registration.User\n\tUserAgent   string\n\tHTTPClient  *http.Client\n\tCertificate CertificateConfig\n}\n\nfunc NewConfig(user registration.User) *Config {\n\treturn &Config{\n\t\tCADirURL:   LEDirectoryProduction,\n\t\tUser:       user,\n\t\tHTTPClient: createDefaultHTTPClient(),\n\t\tCertificate: CertificateConfig{\n\t\t\tKeyType: certcrypto.RSA2048,\n\t\t\tTimeout: 30 * time.Second,\n\t\t},\n\t}\n}\n\ntype CertificateConfig struct {\n\tKeyType             certcrypto.KeyType\n\tTimeout             time.Duration\n\tOverallRequestLimit int\n\tDisableCommonName   bool\n}\n\n// createDefaultHTTPClient Creates an HTTP client with a reasonable timeout value\n// and potentially a custom *x509.CertPool\n// based on the caCertificatesEnvVar environment variable (see the `initCertPool` function).\nfunc createDefaultHTTPClient() *http.Client {\n\treturn &http.Client{\n\t\tTimeout: 2 * time.Minute,\n\t\tTransport: &http.Transport{\n\t\t\tProxy: http.ProxyFromEnvironment,\n\t\t\tDialContext: (&net.Dialer{\n\t\t\t\tTimeout:   30 * time.Second,\n\t\t\t\tKeepAlive: 30 * time.Second,\n\t\t\t}).DialContext,\n\t\t\tTLSHandshakeTimeout:   30 * time.Second,\n\t\t\tResponseHeaderTimeout: 30 * time.Second,\n\t\t\tTLSClientConfig: &tls.Config{\n\t\t\t\tServerName: os.Getenv(caServerNameEnvVar),\n\t\t\t\tRootCAs:    initCertPool(),\n\t\t\t},\n\t\t},\n\t}\n}\n\n// initCertPool creates a *x509.CertPool populated with the PEM certificates\n// found in the filepath specified in the caCertificatesEnvVar OS environment variable.\n// If the caCertificatesEnvVar is not set then initCertPool will return nil.\n// If there is an error creating a *x509.CertPool from the provided caCertificatesEnvVar value then initCertPool will panic.\n// If the caSystemCertPool is set to a \"truthy value\" (`1`, `t`, `T`, `TRUE`, `true`, `True`) then a copy of system cert pool will be used.\n// caSystemCertPool requires caCertificatesEnvVar to be set.\nfunc initCertPool() *x509.CertPool {\n\tcustomCACertsPath := os.Getenv(caCertificatesEnvVar)\n\tif customCACertsPath == \"\" {\n\t\treturn nil\n\t}\n\n\tuseSystemCertPool, _ := strconv.ParseBool(os.Getenv(caSystemCertPool))\n\n\tcaCerts := strings.Split(customCACertsPath, string(os.PathListSeparator))\n\n\tcertPool, err := CreateCertPool(caCerts, useSystemCertPool)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"create certificates pool: %v\", err))\n\t}\n\n\treturn certPool\n}\n\n// CreateCertPool creates a *x509.CertPool populated with the PEM certificates.\nfunc CreateCertPool(caCerts []string, useSystemCertPool bool) (*x509.CertPool, error) {\n\tif len(caCerts) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tcertPool := newCertPool(useSystemCertPool)\n\n\tfor _, customPath := range caCerts {\n\t\tcustomCAs, err := os.ReadFile(customPath)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error reading %q: %w\", customPath, err)\n\t\t}\n\n\t\tif ok := certPool.AppendCertsFromPEM(customCAs); !ok {\n\t\t\treturn nil, fmt.Errorf(\"error creating x509 cert pool from %q: %w\", customPath, err)\n\t\t}\n\t}\n\n\treturn certPool, nil\n}\n\nfunc newCertPool(useSystemCertPool bool) *x509.CertPool {\n\tif !useSystemCertPool {\n\t\treturn x509.NewCertPool()\n\t}\n\n\tpool, err := x509.SystemCertPool()\n\tif err == nil {\n\t\treturn pool\n\t}\n\n\treturn x509.NewCertPool()\n}\n"
  },
  {
    "path": "lego/client_test.go",
    "content": "package lego\n\nimport (\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/registration\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewClient(t *testing.T) {\n\tserver := tester.MockACMEServer().BuildHTTPS(t)\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tuser := mockUser{\n\t\temail:      \"test@test.com\",\n\t\tregres:     new(registration.Resource),\n\t\tprivatekey: key,\n\t}\n\n\tconfig := NewConfig(user)\n\tconfig.CADirURL = server.URL + \"/dir\"\n\tconfig.HTTPClient = server.Client()\n\n\tclient, err := NewClient(config)\n\trequire.NoError(t, err, \"Could not create client\")\n\n\tassert.NotNil(t, client)\n}\n\ntype mockUser struct {\n\temail      string\n\tregres     *registration.Resource\n\tprivatekey *rsa.PrivateKey\n}\n\nfunc (u mockUser) GetEmail() string                        { return u.email }\nfunc (u mockUser) GetRegistration() *registration.Resource { return u.regres }\nfunc (u mockUser) GetPrivateKey() crypto.PrivateKey        { return u.privatekey }\n"
  },
  {
    "path": "log/logger.go",
    "content": "package log\n\nimport (\n\t\"log\"\n\t\"os\"\n)\n\n// Logger is an optional custom logger.\nvar Logger StdLogger = log.New(os.Stderr, \"\", log.LstdFlags)\n\n// StdLogger interface for Standard Logger.\ntype StdLogger interface {\n\tFatal(args ...any)\n\tFatalln(args ...any)\n\tFatalf(format string, args ...any)\n\tPrint(args ...any)\n\tPrintln(args ...any)\n\tPrintf(format string, args ...any)\n}\n\n// Fatal writes a log entry.\n// It uses Logger if not nil, otherwise it uses the default log.Logger.\nfunc Fatal(args ...any) {\n\tLogger.Fatal(args...)\n}\n\n// Fatalf writes a log entry.\n// It uses Logger if not nil, otherwise it uses the default log.Logger.\nfunc Fatalf(format string, args ...any) {\n\tLogger.Fatalf(format, args...)\n}\n\n// Print writes a log entry.\n// It uses Logger if not nil, otherwise it uses the default log.Logger.\nfunc Print(args ...any) {\n\tLogger.Print(args...)\n}\n\n// Println writes a log entry.\n// It uses Logger if not nil, otherwise it uses the default log.Logger.\nfunc Println(args ...any) {\n\tLogger.Println(args...)\n}\n\n// Printf writes a log entry.\n// It uses Logger if not nil, otherwise it uses the default log.Logger.\nfunc Printf(format string, args ...any) {\n\tLogger.Printf(format, args...)\n}\n\n// Warnf writes a log entry.\nfunc Warnf(format string, args ...any) {\n\tPrintf(\"[WARN] \"+format, args...)\n}\n\n// Infof writes a log entry.\nfunc Infof(format string, args ...any) {\n\tPrintf(\"[INFO] \"+format, args...)\n}\n"
  },
  {
    "path": "platform/config/env/env.go",
    "content": "package env\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/log\"\n)\n\n// Get environment variables.\nfunc Get(names ...string) (map[string]string, error) {\n\tvalues := map[string]string{}\n\n\tvar missingEnvVars []string\n\n\tfor _, envVar := range names {\n\t\tvalue := GetOrFile(envVar)\n\t\tif value == \"\" {\n\t\t\tmissingEnvVars = append(missingEnvVars, envVar)\n\t\t}\n\n\t\tvalues[envVar] = value\n\t}\n\n\tif len(missingEnvVars) > 0 {\n\t\treturn nil, fmt.Errorf(\"some credentials information are missing: %s\", strings.Join(missingEnvVars, \",\"))\n\t}\n\n\treturn values, nil\n}\n\n// GetWithFallback Get environment variable values.\n// The first name in each group is use as key in the result map.\n//\n// case 1:\n//\n//\t// LEGO_ONE=\"ONE\"\n//\t// LEGO_TWO=\"TWO\"\n//\tenv.GetWithFallback([]string{\"LEGO_ONE\", \"LEGO_TWO\"})\n//\t// => \"LEGO_ONE\" = \"ONE\"\n//\n// case 2:\n//\n//\t// LEGO_ONE=\"\"\n//\t// LEGO_TWO=\"TWO\"\n//\tenv.GetWithFallback([]string{\"LEGO_ONE\", \"LEGO_TWO\"})\n//\t// => \"LEGO_ONE\" = \"TWO\"\n//\n// case 3:\n//\n//\t// LEGO_ONE=\"\"\n//\t// LEGO_TWO=\"\"\n//\tenv.GetWithFallback([]string{\"LEGO_ONE\", \"LEGO_TWO\"})\n//\t// => error\nfunc GetWithFallback(groups ...[]string) (map[string]string, error) {\n\tvalues := map[string]string{}\n\n\tvar missingEnvVars []string\n\n\tfor _, names := range groups {\n\t\tif len(names) == 0 {\n\t\t\treturn nil, errors.New(\"undefined environment variable names\")\n\t\t}\n\n\t\tvalue, envVar := getOneWithFallback(names[0], names[1:]...)\n\t\tif value == \"\" {\n\t\t\tmissingEnvVars = append(missingEnvVars, envVar)\n\t\t\tcontinue\n\t\t}\n\n\t\tvalues[envVar] = value\n\t}\n\n\tif len(missingEnvVars) > 0 {\n\t\treturn nil, fmt.Errorf(\"some credentials information are missing: %s\", strings.Join(missingEnvVars, \",\"))\n\t}\n\n\treturn values, nil\n}\n\nfunc GetOneWithFallback[T any](main string, defaultValue T, fn func(string) (T, error), names ...string) T {\n\tv, _ := getOneWithFallback(main, names...)\n\n\tvalue, err := fn(v)\n\tif err != nil {\n\t\treturn defaultValue\n\t}\n\n\treturn value\n}\n\nfunc getOneWithFallback(main string, names ...string) (string, string) {\n\tvalue := GetOrFile(main)\n\tif value != \"\" {\n\t\treturn value, main\n\t}\n\n\tfor _, name := range names {\n\t\tvalue := GetOrFile(name)\n\t\tif value != \"\" {\n\t\t\treturn value, main\n\t\t}\n\t}\n\n\treturn \"\", main\n}\n\n// GetOrDefaultString returns the given environment variable value as a string.\n// Returns the default if the env var cannot be found.\nfunc GetOrDefaultString(envVar, defaultValue string) string {\n\treturn getOrDefault(envVar, defaultValue, ParseString)\n}\n\n// GetOrDefaultBool returns the given environment variable value as a boolean.\n// Returns the default if the env var cannot be coopered to a boolean, or is not found.\nfunc GetOrDefaultBool(envVar string, defaultValue bool) bool {\n\treturn getOrDefault(envVar, defaultValue, strconv.ParseBool)\n}\n\n// GetOrDefaultInt returns the given environment variable value as an integer.\n// Returns the default if the env var cannot be coopered to an int, or is not found.\nfunc GetOrDefaultInt(envVar string, defaultValue int) int {\n\treturn getOrDefault(envVar, defaultValue, strconv.Atoi)\n}\n\n// GetOrDefaultSecond returns the given environment variable value as a time.Duration (second).\n// Returns the default if the env var cannot be coopered to an int, or is not found.\nfunc GetOrDefaultSecond(envVar string, defaultValue time.Duration) time.Duration {\n\treturn getOrDefault(envVar, defaultValue, ParseSecond)\n}\n\nfunc getOrDefault[T any](envVar string, defaultValue T, fn func(string) (T, error)) T {\n\tv, err := fn(GetOrFile(envVar))\n\tif err != nil {\n\t\treturn defaultValue\n\t}\n\n\treturn v\n}\n\n// GetOrFile Attempts to resolve 'key' as an environment variable.\n// Failing that, it will check to see if '<key>_FILE' exists.\n// If so, it will attempt to read from the referenced file to populate a value.\nfunc GetOrFile(envVar string) string {\n\tenvVarValue := os.Getenv(envVar)\n\tif envVarValue != \"\" {\n\t\treturn envVarValue\n\t}\n\n\tfileVar := envVar + \"_FILE\"\n\n\tfileVarValue := os.Getenv(fileVar)\n\tif fileVarValue == \"\" {\n\t\treturn envVarValue\n\t}\n\n\tfileContents, err := os.ReadFile(fileVarValue)\n\tif err != nil {\n\t\tlog.Printf(\"Failed to read the file %s (defined by env var %s): %s\", fileVarValue, fileVar, err)\n\t\treturn \"\"\n\t}\n\n\treturn strings.TrimSuffix(string(fileContents), \"\\n\")\n}\n\n// ParseSecond parses env var value (string) to a second (time.Duration).\nfunc ParseSecond(s string) (time.Duration, error) {\n\tv, err := strconv.Atoi(s)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif v < 0 {\n\t\treturn 0, fmt.Errorf(\"unsupported value: %d\", v)\n\t}\n\n\treturn time.Duration(v) * time.Second, nil\n}\n\n// ParseString parses env var value (string) to a string but throws an error when the string is empty.\nfunc ParseString(s string) (string, error) {\n\tif s == \"\" {\n\t\treturn \"\", errors.New(\"empty string\")\n\t}\n\n\treturn s, nil\n}\n\n// ParsePairs parses a raw string of comma-separated key-value pairs into a map.\n// Keys and values are separated by a colon and are trimmed of whitespace.\nfunc ParsePairs(raw string) (map[string]string, error) {\n\tresult := make(map[string]string)\n\n\tfor pair := range strings.SplitSeq(strings.TrimSuffix(raw, \",\"), \",\") {\n\t\tdata := strings.Split(pair, \":\")\n\t\tif len(data) != 2 {\n\t\t\treturn nil, fmt.Errorf(\"incorrect pair: %s\", pair)\n\t\t}\n\n\t\tresult[strings.TrimSpace(data[0])] = strings.TrimSpace(data[1])\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "platform/config/env/env_test.go",
    "content": "package env\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetWithFallback(t *testing.T) {\n\tvar1Exist := os.Getenv(\"TEST_LEGO_VAR_EXIST_1\")\n\tvar2Exist := os.Getenv(\"TEST_LEGO_VAR_EXIST_2\")\n\tvar1Missing := os.Getenv(\"TEST_LEGO_VAR_MISSING_1\")\n\tvar2Missing := os.Getenv(\"TEST_LEGO_VAR_MISSING_2\")\n\n\tt.Cleanup(func() {\n\t\t_ = os.Setenv(\"TEST_LEGO_VAR_EXIST_1\", var1Exist)\n\t\t_ = os.Setenv(\"TEST_LEGO_VAR_EXIST_2\", var2Exist)\n\t\t_ = os.Setenv(\"TEST_LEGO_VAR_MISSING_1\", var1Missing)\n\t\t_ = os.Setenv(\"TEST_LEGO_VAR_MISSING_2\", var2Missing)\n\t})\n\n\terr := os.Setenv(\"TEST_LEGO_VAR_EXIST_1\", \"VAR1\")\n\trequire.NoError(t, err)\n\terr = os.Setenv(\"TEST_LEGO_VAR_EXIST_2\", \"VAR2\")\n\trequire.NoError(t, err)\n\terr = os.Unsetenv(\"TEST_LEGO_VAR_MISSING_1\")\n\trequire.NoError(t, err)\n\terr = os.Unsetenv(\"TEST_LEGO_VAR_MISSING_2\")\n\trequire.NoError(t, err)\n\n\ttype expected struct {\n\t\tvalue map[string]string\n\t\terror string\n\t}\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tgroups   [][]string\n\t\texpected expected\n\t}{\n\t\t{\n\t\t\tdesc:   \"no groups\",\n\t\t\tgroups: nil,\n\t\t\texpected: expected{\n\t\t\t\tvalue: map[string]string{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"empty groups\",\n\t\t\tgroups: [][]string{{}, {}},\n\t\t\texpected: expected{\n\t\t\t\terror: \"undefined environment variable names\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"missing env var\",\n\t\t\tgroups: [][]string{{\"TEST_LEGO_VAR_MISSING_1\"}},\n\t\t\texpected: expected{\n\t\t\t\terror: \"some credentials information are missing: TEST_LEGO_VAR_MISSING_1\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"all env var in a groups are missing\",\n\t\t\tgroups: [][]string{{\"TEST_LEGO_VAR_MISSING_1\", \"TEST_LEGO_VAR_MISSING_2\"}},\n\t\t\texpected: expected{\n\t\t\t\terror: \"some credentials information are missing: TEST_LEGO_VAR_MISSING_1\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"only the first env var have a value\",\n\t\t\tgroups: [][]string{{\"TEST_LEGO_VAR_EXIST_1\", \"TEST_LEGO_VAR_MISSING_1\"}},\n\t\t\texpected: expected{\n\t\t\t\tvalue: map[string]string{\"TEST_LEGO_VAR_EXIST_1\": \"VAR1\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"only the second env var have a value\",\n\t\t\tgroups: [][]string{{\"TEST_LEGO_VAR_MISSING_1\", \"TEST_LEGO_VAR_EXIST_1\"}},\n\t\t\texpected: expected{\n\t\t\t\tvalue: map[string]string{\"TEST_LEGO_VAR_MISSING_1\": \"VAR1\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"all env vars in a groups have a value\",\n\t\t\tgroups: [][]string{{\"TEST_LEGO_VAR_EXIST_1\", \"TEST_LEGO_VAR_EXIST_2\"}},\n\t\t\texpected: expected{\n\t\t\t\tvalue: map[string]string{\"TEST_LEGO_VAR_EXIST_1\": \"VAR1\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvalue, err := GetWithFallback(test.groups...)\n\t\t\tif test.expected.error != \"\" {\n\t\t\t\tassert.EqualError(t, err, test.expected.error)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.expected.value, value)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetOneWithFallback(t *testing.T) {\n\tvar1Exist := os.Getenv(\"TEST_LEGO_VAR_EXIST_1\")\n\tvar2Exist := os.Getenv(\"TEST_LEGO_VAR_EXIST_2\")\n\tvar1Missing := os.Getenv(\"TEST_LEGO_VAR_MISSING_1\")\n\tvar2Missing := os.Getenv(\"TEST_LEGO_VAR_MISSING_2\")\n\n\tt.Cleanup(func() {\n\t\t_ = os.Setenv(\"TEST_LEGO_VAR_EXIST_1\", var1Exist)\n\t\t_ = os.Setenv(\"TEST_LEGO_VAR_EXIST_2\", var2Exist)\n\t\t_ = os.Setenv(\"TEST_LEGO_VAR_MISSING_1\", var1Missing)\n\t\t_ = os.Setenv(\"TEST_LEGO_VAR_MISSING_2\", var2Missing)\n\t})\n\n\terr := os.Setenv(\"TEST_LEGO_VAR_EXIST_1\", \"VAR1\")\n\trequire.NoError(t, err)\n\terr = os.Setenv(\"TEST_LEGO_VAR_EXIST_2\", \"VAR2\")\n\trequire.NoError(t, err)\n\terr = os.Unsetenv(\"TEST_LEGO_VAR_MISSING_1\")\n\trequire.NoError(t, err)\n\terr = os.Unsetenv(\"TEST_LEGO_VAR_MISSING_2\")\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tdesc         string\n\t\tmain         string\n\t\tdefaultValue string\n\t\talts         []string\n\t\texpected     string\n\t}{\n\t\t{\n\t\t\tdesc:         \"with value and no alternative\",\n\t\t\tmain:         \"TEST_LEGO_VAR_EXIST_1\",\n\t\t\tdefaultValue: \"oops\",\n\t\t\texpected:     \"VAR1\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"with value and alternatives\",\n\t\t\tmain:         \"TEST_LEGO_VAR_EXIST_1\",\n\t\t\tdefaultValue: \"oops\",\n\t\t\talts:         []string{\"TEST_LEGO_VAR_MISSING_1\"},\n\t\t\texpected:     \"VAR1\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"without value and no alternatives\",\n\t\t\tmain:         \"TEST_LEGO_VAR_MISSING_1\",\n\t\t\tdefaultValue: \"oops\",\n\t\t\texpected:     \"oops\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"without value and alternatives\",\n\t\t\tmain:         \"TEST_LEGO_VAR_MISSING_1\",\n\t\t\tdefaultValue: \"oops\",\n\t\t\talts:         []string{\"TEST_LEGO_VAR_EXIST_1\"},\n\t\t\texpected:     \"VAR1\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvalue := GetOneWithFallback(test.main, test.defaultValue, ParseString, test.alts...)\n\t\t\tassert.Equal(t, test.expected, value)\n\t\t})\n\t}\n}\n\nfunc TestGetOrDefaultInt(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc         string\n\t\tenvValue     string\n\t\tdefaultValue int\n\t\texpected     int\n\t}{\n\t\t{\n\t\t\tdesc:         \"valid value\",\n\t\t\tenvValue:     \"100\",\n\t\t\tdefaultValue: 2,\n\t\t\texpected:     100,\n\t\t},\n\t\t{\n\t\t\tdesc:         \"invalid content, use default value\",\n\t\t\tenvValue:     \"abc123\",\n\t\t\tdefaultValue: 2,\n\t\t\texpected:     2,\n\t\t},\n\t\t{\n\t\t\tdesc:         \"valid negative value\",\n\t\t\tenvValue:     \"-111\",\n\t\t\tdefaultValue: 2,\n\t\t\texpected:     -111,\n\t\t},\n\t\t{\n\t\t\tdesc:         \"float: invalid type, use default value\",\n\t\t\tenvValue:     \"1.11\",\n\t\t\tdefaultValue: 2,\n\t\t\texpected:     2,\n\t\t},\n\t}\n\n\tconst key = \"LEGO_ENV_TC\"\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Setenv(key, test.envValue)\n\n\t\t\tresult := GetOrDefaultInt(key, test.defaultValue)\n\t\t\tassert.Equal(t, test.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestGetOrDefaultSecond(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc         string\n\t\tenvValue     string\n\t\tdefaultValue time.Duration\n\t\texpected     time.Duration\n\t}{\n\t\t{\n\t\t\tdesc:         \"valid value\",\n\t\t\tenvValue:     \"100\",\n\t\t\tdefaultValue: 2 * time.Second,\n\t\t\texpected:     100 * time.Second,\n\t\t},\n\t\t{\n\t\t\tdesc:         \"invalid content, use default value\",\n\t\t\tenvValue:     \"abc123\",\n\t\t\tdefaultValue: 2 * time.Second,\n\t\t\texpected:     2 * time.Second,\n\t\t},\n\t\t{\n\t\t\tdesc:         \"invalid content, negative value\",\n\t\t\tenvValue:     \"-111\",\n\t\t\tdefaultValue: 2 * time.Second,\n\t\t\texpected:     2 * time.Second,\n\t\t},\n\t\t{\n\t\t\tdesc:         \"float: invalid type, use default value\",\n\t\t\tenvValue:     \"1.11\",\n\t\t\tdefaultValue: 2 * time.Second,\n\t\t\texpected:     2 * time.Second,\n\t\t},\n\t}\n\n\tkey := \"LEGO_ENV_TC\"\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Setenv(key, test.envValue)\n\n\t\t\tresult := GetOrDefaultSecond(key, test.defaultValue)\n\t\t\tassert.Equal(t, test.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestGetOrDefaultString(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc         string\n\t\tenvValue     string\n\t\tdefaultValue string\n\t\texpected     string\n\t}{\n\t\t{\n\t\t\tdesc:         \"missing env var\",\n\t\t\tdefaultValue: \"foo\",\n\t\t\texpected:     \"foo\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"with env var\",\n\t\t\tenvValue:     \"bar\",\n\t\t\tdefaultValue: \"foo\",\n\t\t\texpected:     \"bar\",\n\t\t},\n\t}\n\n\tkey := \"LEGO_ENV_TC\"\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Setenv(key, test.envValue)\n\n\t\t\tactual := GetOrDefaultString(key, test.defaultValue)\n\t\t\tassert.Equal(t, test.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestGetOrDefaultBool(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc         string\n\t\tenvValue     string\n\t\tdefaultValue bool\n\t\texpected     bool\n\t}{\n\t\t{\n\t\t\tdesc:         \"missing env var\",\n\t\t\tdefaultValue: true,\n\t\t\texpected:     true,\n\t\t},\n\t\t{\n\t\t\tdesc:         \"with env var\",\n\t\t\tenvValue:     \"true\",\n\t\t\tdefaultValue: false,\n\t\t\texpected:     true,\n\t\t},\n\t\t{\n\t\t\tdesc:         \"invalid value\",\n\t\t\tenvValue:     \"foo\",\n\t\t\tdefaultValue: false,\n\t\t\texpected:     false,\n\t\t},\n\t}\n\n\tkey := \"LEGO_ENV_TC\"\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Setenv(key, test.envValue)\n\n\t\t\tactual := GetOrDefaultBool(key, test.defaultValue)\n\t\t\tassert.Equal(t, test.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestGetOrFile_ReadsEnvVars(t *testing.T) {\n\tt.Setenv(\"TEST_LEGO_ENV_VAR\", \"lego_env\")\n\n\tvalue := GetOrFile(\"TEST_LEGO_ENV_VAR\")\n\n\tassert.Equal(t, \"lego_env\", value)\n}\n\nfunc TestGetOrFile_ReadsFiles(t *testing.T) {\n\tvarEnvFileName := \"TEST_LEGO_ENV_VAR_FILE\"\n\tvarEnvName := \"TEST_LEGO_ENV_VAR\"\n\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tfileContent []byte\n\t}{\n\t\t{\n\t\t\tdesc:        \"simple\",\n\t\t\tfileContent: []byte(\"lego_file\"),\n\t\t},\n\t\t{\n\t\t\tdesc:        \"with an empty last line\",\n\t\t\tfileContent: []byte(\"lego_file\\n\"),\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\terr := os.Unsetenv(varEnvFileName)\n\t\t\trequire.NoError(t, err)\n\t\t\terr = os.Unsetenv(varEnvName)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tfile, err := os.CreateTemp(t.TempDir(), \"lego\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tt.Cleanup(func() { _ = file.Close() })\n\n\t\t\terr = os.WriteFile(file.Name(), []byte(\"lego_file\\n\"), 0o644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tt.Setenv(varEnvFileName, file.Name())\n\n\t\t\tvalue := GetOrFile(varEnvName)\n\n\t\t\tassert.Equal(t, \"lego_file\", value)\n\t\t})\n\t}\n}\n\nfunc TestGetOrFile_PrefersEnvVars(t *testing.T) {\n\tvarEnvFileName := \"TEST_LEGO_ENV_VAR_FILE\"\n\tvarEnvName := \"TEST_LEGO_ENV_VAR\"\n\n\terr := os.Unsetenv(varEnvFileName)\n\trequire.NoError(t, err)\n\terr = os.Unsetenv(varEnvName)\n\trequire.NoError(t, err)\n\n\tfile, err := os.CreateTemp(t.TempDir(), \"lego\")\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() { _ = file.Close() })\n\n\terr = os.WriteFile(file.Name(), []byte(\"lego_file\"), 0o644)\n\trequire.NoError(t, err)\n\n\tt.Setenv(varEnvFileName, file.Name())\n\tt.Setenv(varEnvName, \"lego_env\")\n\n\tvalue := GetOrFile(varEnvName)\n\n\tassert.Equal(t, \"lego_env\", value)\n}\n\nfunc TestParsePairs(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tvalue    string\n\t\texpected map[string]string\n\t}{\n\t\t{\n\t\t\tdesc:     \"one pair\",\n\t\t\tvalue:    \"foo:bar\",\n\t\t\texpected: map[string]string{\"foo\": \"bar\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"multiple pairs\",\n\t\t\tvalue:    \"foo:bar,a:b,c:d\",\n\t\t\texpected: map[string]string{\"a\": \"b\", \"c\": \"d\", \"foo\": \"bar\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"multiple pairs with spaces\",\n\t\t\tvalue:    \"foo:bar, a:b , c: d\",\n\t\t\texpected: map[string]string{\"a\": \"b\", \"c\": \"d\", \"foo\": \"bar\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"empty value pair\",\n\t\t\tvalue:    \"foo:\",\n\t\t\texpected: map[string]string{\"foo\": \"\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"empty key pair\",\n\t\t\tvalue:    \":bar\",\n\t\t\texpected: map[string]string{\"\": \"bar\"},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tpairs, err := ParsePairs(test.value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.expected, pairs)\n\t\t})\n\t}\n}\n\nfunc TestParsePairs_error(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc  string\n\t\tvalue string\n\t}{\n\t\t{\n\t\t\tdesc:  \"empty value\",\n\t\t\tvalue: \"\",\n\t\t},\n\t\t{\n\t\t\tdesc:  \"multiple colons\",\n\t\t\tvalue: \"foo:bar:bir\",\n\t\t},\n\t\t{\n\t\t\tdesc:  \"valid pair and multiple colons\",\n\t\t\tvalue: \"a:b,foo:bar:bir\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t_, err := ParsePairs(test.value)\n\t\t\trequire.Error(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "platform/tester/api.go",
    "content": "package tester\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n)\n\n// MockACMEServer Minimal stub ACME server for validation.\nfunc MockACMEServer() *servermock.Builder[*httptest.Server] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*httptest.Server, error) {\n\t\t\treturn server, nil\n\t\t}).\n\t\tRoute(\"GET /dir\", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\tserverURL := fmt.Sprintf(\"https://%s\", req.Context().Value(http.LocalAddrContextKey))\n\n\t\t\tservermock.JSONEncode(acme.Directory{\n\t\t\t\tNewNonceURL:   serverURL + \"/nonce\",\n\t\t\t\tNewAccountURL: serverURL + \"/account\",\n\t\t\t\tNewOrderURL:   serverURL + \"/newOrder\",\n\t\t\t\tRevokeCertURL: serverURL + \"/revokeCert\",\n\t\t\t\tKeyChangeURL:  serverURL + \"/keyChange\",\n\t\t\t\tRenewalInfo:   serverURL + \"/renewalInfo\",\n\t\t\t}).ServeHTTP(rw, req)\n\t\t})).\n\t\tRoute(\"HEAD /nonce\", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\trw.Header().Set(\"Replay-Nonce\", \"12345\")\n\t\t\trw.Header().Set(\"Retry-After\", \"0\")\n\t\t}))\n}\n\n// WriteJSONResponse marshals the body as JSON and writes it to the response.\nfunc WriteJSONResponse(w http.ResponseWriter, body any) error {\n\tbs, err := json.Marshal(body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\tif _, err := w.Write(bs); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "platform/tester/dnsmock/dnsmock.go",
    "content": "package dnsmock\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst noType uint16 = math.MaxUint16\n\ntype Option func(*dns.Server) error\n\ntype Builder struct {\n\t// domain -> op -> type\n\troutes map[string]map[int]map[uint16]dns.Handler\n\n\tstringToType map[string]uint16\n}\n\nfunc NewServer() *Builder {\n\tstringToType := make(map[string]uint16)\n\tfor typ, str := range dns.TypeToString {\n\t\tstringToType[str] = typ\n\t}\n\n\treturn &Builder{\n\t\troutes:       make(map[string]map[int]map[uint16]dns.Handler),\n\t\tstringToType: stringToType,\n\t}\n}\n\nfunc (b *Builder) Query(pattern string, handler dns.HandlerFunc) *Builder {\n\troute, err := b.route(pattern, dns.OpcodeQuery, handler)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\treturn route\n}\n\nfunc (b *Builder) Update(pattern string, handler dns.HandlerFunc) *Builder {\n\troute, err := b.route(pattern, dns.OpcodeUpdate, handler)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\treturn route\n}\n\nfunc (b *Builder) route(pattern string, op int, handler dns.HandlerFunc) (*Builder, error) {\n\tparts := strings.Fields(pattern)\n\n\tdomain := parts[0]\n\n\t_, ok := dns.IsDomainName(domain)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"%s: invalid domain: %s\", dns.OpcodeToString[op], domain)\n\t}\n\n\tif _, ok := b.routes[domain]; !ok {\n\t\tb.routes[domain] = make(map[int]map[uint16]dns.Handler)\n\t}\n\n\tif _, ok := b.routes[domain][op]; !ok {\n\t\tb.routes[domain][op] = make(map[uint16]dns.Handler)\n\t}\n\n\tif _, ok := b.routes[domain][op][noType]; ok {\n\t\treturn nil, fmt.Errorf(\"%s: a global route already exists for the domain: %s\", dns.OpcodeToString[op], domain)\n\t}\n\n\tswitch len(parts) {\n\tcase 1:\n\t\tif len(b.routes[domain][op]) > 0 {\n\t\t\treturn nil, fmt.Errorf(\"%s: global route and specific routes cannot be mixed for the same domain: %s\", dns.OpcodeToString[op], domain)\n\t\t}\n\n\t\tb.routes[domain][op][noType] = handler\n\n\t\treturn b, nil\n\n\tcase 2:\n\t\traw := parts[1]\n\n\t\tqType, ok := b.stringToType[raw]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"%s: unknown type: %s\", dns.OpcodeToString[op], raw)\n\t\t}\n\n\t\tif _, ok := b.routes[domain][op][qType]; ok {\n\t\t\treturn nil, fmt.Errorf(\"%s: duplicate route: %s\", dns.OpcodeToString[op], pattern)\n\t\t}\n\n\t\tb.routes[domain][op][qType] = handler\n\n\t\treturn b, nil\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"%s: invalid pattern: %s\", dns.OpcodeToString[op], pattern)\n\t}\n}\n\nfunc (b *Builder) Build(t *testing.T, options ...Option) net.Addr {\n\tt.Helper()\n\n\tmux := dns.NewServeMux()\n\n\tserver := &dns.Server{\n\t\tAddr:         \"127.0.0.1:0\",\n\t\tNet:          \"udp\",\n\t\tReadTimeout:  time.Hour,\n\t\tWriteTimeout: time.Hour,\n\t\tHandler:      mux,\n\t\tMsgAcceptFunc: func(dh dns.Header) dns.MsgAcceptAction {\n\t\t\t// bypass defaultMsgAcceptFunc to allow dynamic update (https://github.com/miekg/dns/pull/830)\n\t\t\treturn dns.MsgAccept\n\t\t},\n\t}\n\n\tfor _, option := range options {\n\t\trequire.NoError(t, option(server))\n\t}\n\n\tfor pattern, ops := range b.routes {\n\t\tmux.HandleFunc(pattern, func(w dns.ResponseWriter, req *dns.Msg) {\n\t\t\tmTypes, ok := ops[req.Opcode]\n\t\t\tif !ok {\n\t\t\t\t_ = w.WriteMsg(new(dns.Msg).SetRcode(req, dns.RcodeNotImplemented))\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif h, found := mTypes[noType]; found {\n\t\t\t\th.ServeDNS(w, req)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// For safety but it doesn't happen.\n\t\t\tif len(req.Question) == 0 {\n\t\t\t\t_ = w.WriteMsg(new(dns.Msg).SetRcode(req, dns.RcodeRefused))\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// For safety but it doesn't happen.\n\t\t\tif req.Question[0].Qclass != dns.ClassINET {\n\t\t\t\t_ = w.WriteMsg(new(dns.Msg).SetRcode(req, dns.RcodeRefused))\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Works only for [Query].\n\t\t\th, ok := mTypes[req.Question[0].Qtype]\n\t\t\tif !ok {\n\t\t\t\t_ = w.WriteMsg(new(dns.Msg).SetRcode(req, dns.RcodeNotImplemented))\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\th.ServeDNS(w, req)\n\t\t})\n\t}\n\n\tt.Cleanup(func() {\n\t\t_ = server.Shutdown()\n\t})\n\n\twaitLock := sync.Mutex{}\n\twaitLock.Lock()\n\n\tserver.NotifyStartedFunc = waitLock.Unlock\n\n\tgo func() {\n\t\terr := server.ListenAndServe()\n\t\tif err != nil {\n\t\t\tt.Log(err)\n\t\t}\n\t}()\n\n\twaitLock.Lock()\n\n\treturn server.PacketConn.LocalAddr()\n}\n"
  },
  {
    "path": "platform/tester/dnsmock/dnsmock_test.go",
    "content": "package dnsmock\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestServer_Query_matchType(t *testing.T) {\n\taddr := NewServer().\n\t\tQuery(\"example.com. SOA\", Noop).\n\t\tBuild(t)\n\n\tclient := &dns.Client{Timeout: 1 * time.Second}\n\n\tm := new(dns.Msg).SetQuestion(\"example.com.\", dns.TypeSOA)\n\n\tr, _, err := client.Exchange(m, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Equalf(t, dns.RcodeSuccess, r.Rcode,\n\t\t\"expected %s, got %s\", dns.RcodeToString[dns.RcodeSuccess], dns.RcodeToString[r.Rcode])\n\tassert.Equal(t, m.Question, r.Question)\n}\n\nfunc TestServer_Query_noType(t *testing.T) {\n\taddr := NewServer().\n\t\tQuery(\"example.com.\", Noop).\n\t\tBuild(t)\n\n\tclient := &dns.Client{Timeout: 1 * time.Second}\n\n\tm := new(dns.Msg).SetQuestion(\"example.com.\", dns.TypeSOA)\n\n\tr, _, err := client.Exchange(m, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Equalf(t, dns.RcodeSuccess, r.Rcode,\n\t\t\"expected %s, got %s\", dns.RcodeToString[dns.RcodeSuccess], dns.RcodeToString[r.Rcode])\n\tassert.Equal(t, m.Question, r.Question)\n}\n\nfunc TestServer_Query_noMatch_domain(t *testing.T) {\n\taddr := NewServer().\n\t\tQuery(\"example.com. SOA\", Noop).\n\t\tBuild(t)\n\n\tclient := &dns.Client{Timeout: 1 * time.Second}\n\n\tm := new(dns.Msg).SetQuestion(\"example.org.\", dns.TypeSOA)\n\n\tr, _, err := client.Exchange(m, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Equalf(t, dns.RcodeRefused, r.Rcode,\n\t\t\"expected %s, got %s\", dns.RcodeToString[dns.RcodeRefused], dns.RcodeToString[r.Rcode])\n\tassert.Equal(t, m.Question, r.Question)\n}\n\nfunc TestServer_Query_noMatch_type(t *testing.T) {\n\taddr := NewServer().\n\t\tQuery(\"example.com. SOA\", Noop).\n\t\tBuild(t)\n\n\tclient := &dns.Client{Timeout: 1 * time.Second}\n\n\tm := new(dns.Msg).SetQuestion(\"example.com.\", dns.TypeTXT)\n\n\tr, _, err := client.Exchange(m, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Equalf(t, dns.RcodeNotImplemented, r.Rcode,\n\t\t\"expected %s, got %s\", dns.RcodeToString[dns.RcodeNotImplemented], dns.RcodeToString[r.Rcode])\n\tassert.Equal(t, m.Question, r.Question)\n}\n\nfunc TestServer_Query_noMatch_opType(t *testing.T) {\n\taddr := NewServer().\n\t\tQuery(\"example.com.\", Noop).\n\t\tBuild(t)\n\n\tclient := &dns.Client{Timeout: 1 * time.Second}\n\n\tm := new(dns.Msg).SetUpdate(\"example.com.\")\n\tm.Insert([]dns.RR{\n\t\t&dns.TXT{\n\t\t\tHdr: dns.RR_Header{Name: \"example.com.\", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1},\n\t\t\tTxt: []string{\"foo\"},\n\t\t},\n\t})\n\n\tr, _, err := client.Exchange(m, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Equalf(t, dns.RcodeNotImplemented, r.Rcode,\n\t\t\"expected %s, got %s\", dns.RcodeToString[dns.RcodeNotImplemented], dns.RcodeToString[r.Rcode])\n\tassert.Equal(t, m.Question, r.Question)\n}\n\nfunc TestServer_Query_unknownType(t *testing.T) {\n\tassert.PanicsWithValue(t, \"QUERY: unknown type: ABC\", func() {\n\t\tNewServer().\n\t\t\tQuery(\"example.com. ABC\", Noop).\n\t\t\tBuild(t)\n\t})\n}\n\nfunc TestServer_Query_duplicate(t *testing.T) {\n\tassert.PanicsWithValue(t, \"QUERY: duplicate route: example.com. SOA\", func() {\n\t\tNewServer().\n\t\t\tQuery(\"example.com. SOA\", Noop).\n\t\t\tQuery(\"example.com. SOA\", Noop).\n\t\t\tBuild(t)\n\t})\n}\n\nfunc TestServer_Query_duplicateGlobal(t *testing.T) {\n\tassert.PanicsWithValue(t, \"QUERY: a global route already exists for the domain: example.com.\", func() {\n\t\tNewServer().\n\t\t\tQuery(\"example.com.\", Noop).\n\t\t\tQuery(\"example.com.\", Noop).\n\t\t\tBuild(t)\n\t})\n}\n\nfunc TestServer_Query_mixed(t *testing.T) {\n\tassert.PanicsWithValue(t, \"QUERY: global route and specific routes cannot be mixed for the same domain: example.com.\", func() {\n\t\tNewServer().\n\t\t\tQuery(\"example.com. SOA\", Noop).\n\t\t\tQuery(\"example.com.\", Noop).\n\t\t\tBuild(t)\n\t})\n}\n\nfunc TestServer_Query_invalidDomain(t *testing.T) {\n\tassert.PanicsWithValue(t, \"QUERY: invalid domain: .example.com.\", func() {\n\t\tNewServer().\n\t\t\tQuery(\".example.com. SOA\", Noop).\n\t\t\tBuild(t)\n\t})\n}\n\nfunc TestServer_Query_invalidPattern(t *testing.T) {\n\tassert.PanicsWithValue(t, \"QUERY: invalid pattern: example.com. SOA 13\", func() {\n\t\tNewServer().\n\t\t\tQuery(\"example.com. SOA 13\", Noop).\n\t\t\tBuild(t)\n\t})\n}\n\nfunc TestServer_Update(t *testing.T) {\n\taddr := NewServer().\n\t\tUpdate(\"example.com.\", Noop).\n\t\tBuild(t)\n\n\tclient := &dns.Client{Timeout: 1 * time.Second}\n\n\tm := new(dns.Msg).SetUpdate(\"example.com.\")\n\tm.Insert([]dns.RR{\n\t\t&dns.TXT{\n\t\t\tHdr: dns.RR_Header{Name: \"example.com.\", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1},\n\t\t\tTxt: []string{\"foo\"},\n\t\t},\n\t})\n\n\tr, _, err := client.Exchange(m, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Equalf(t, dns.RcodeSuccess, r.Rcode,\n\t\t\"expected %s, got %s\", dns.RcodeToString[dns.RcodeSuccess], dns.RcodeToString[r.Rcode])\n\tassert.Equal(t, m.Question, r.Question)\n}\n\nfunc TestServer_Update_noMatch_domain(t *testing.T) {\n\taddr := NewServer().\n\t\tUpdate(\"example.com.\", Noop).\n\t\tBuild(t)\n\n\tclient := &dns.Client{Timeout: 1 * time.Second}\n\n\tm := new(dns.Msg).SetUpdate(\"example.org.\")\n\tm.Insert([]dns.RR{\n\t\t&dns.TXT{\n\t\t\tHdr: dns.RR_Header{Name: \"example.org.\", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1},\n\t\t\tTxt: []string{\"foo\"},\n\t\t},\n\t})\n\n\tr, _, err := client.Exchange(m, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Equalf(t, dns.RcodeRefused, r.Rcode,\n\t\t\"expected %s, got %s\", dns.RcodeToString[dns.RcodeRefused], dns.RcodeToString[r.Rcode])\n\tassert.Equal(t, m.Question, r.Question)\n}\n\nfunc TestServer_Update_noMatch_opType(t *testing.T) {\n\taddr := NewServer().\n\t\tUpdate(\"example.com.\", Noop).\n\t\tBuild(t)\n\n\tclient := &dns.Client{Timeout: 1 * time.Second}\n\n\tm := new(dns.Msg).SetQuestion(\"example.com.\", dns.TypeTXT)\n\n\tr, _, err := client.Exchange(m, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Equalf(t, dns.RcodeNotImplemented, r.Rcode,\n\t\t\"expected %s, got %s\", dns.RcodeToString[dns.RcodeNotImplemented], dns.RcodeToString[r.Rcode])\n\tassert.Equal(t, m.Question, r.Question)\n}\n\nfunc TestServer_Update_duplicate(t *testing.T) {\n\tassert.PanicsWithValue(t, \"UPDATE: a global route already exists for the domain: example.com.\", func() {\n\t\tNewServer().\n\t\t\tUpdate(\"example.com.\", Noop).\n\t\t\tUpdate(\"example.com.\", Noop).\n\t\t\tBuild(t)\n\t})\n}\n\nfunc TestServer_Update_invalidDomain(t *testing.T) {\n\tassert.PanicsWithValue(t, \"UPDATE: invalid domain: .example.com.\", func() {\n\t\tNewServer().\n\t\t\tUpdate(\".example.com.\", Noop).\n\t\t\tBuild(t)\n\t})\n}\n\nfunc TestServer_Update_invalidPattern(t *testing.T) {\n\tassert.PanicsWithValue(t, \"UPDATE: invalid pattern: example.com. SOA 13\", func() {\n\t\tNewServer().\n\t\t\tUpdate(\"example.com. SOA 13\", Noop).\n\t\t\tBuild(t)\n\t})\n}\n"
  },
  {
    "path": "platform/tester/dnsmock/handlers.go",
    "content": "package dnsmock\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/miekg/dns\"\n)\n\nfunc DumpRequest() dns.HandlerFunc {\n\treturn func(w dns.ResponseWriter, req *dns.Msg) {\n\t\tfmt.Println(req)\n\n\t\tNoop(w, req)\n\t}\n}\n\nfunc SOA(name string) dns.HandlerFunc {\n\treturn func(w dns.ResponseWriter, req *dns.Msg) {\n\t\tif name == \"\" {\n\t\t\tname = req.Question[0].Name\n\t\t}\n\n\t\t// Handle TLD\n\t\tbase := name\n\t\tif dns.CountLabel(req.Question[0].Name) == 1 {\n\t\t\tbase = \"nic.\" + req.Question[0].Name\n\t\t}\n\n\t\tanswer := &dns.SOA{\n\t\t\tHdr:     dns.RR_Header{Name: name, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 120},\n\t\t\tNs:      \"ns1.\" + base,\n\t\t\tMbox:    \"admin.\" + base,\n\t\t\tSerial:  2016022801,\n\t\t\tRefresh: 28800,\n\t\t\tRetry:   7200,\n\t\t\tExpire:  2419200,\n\t\t\tMinttl:  1200,\n\t\t}\n\n\t\tAnswer(answer)(w, req)\n\t}\n}\n\nfunc CNAME(target string) dns.HandlerFunc {\n\treturn func(w dns.ResponseWriter, req *dns.Msg) {\n\t\tanswer := &dns.CNAME{\n\t\t\tHdr:    dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 1},\n\t\t\tTarget: dns.Fqdn(target),\n\t\t}\n\n\t\tAnswer(answer)(w, req)\n\t}\n}\n\nfunc Noop(w dns.ResponseWriter, req *dns.Msg) {\n\t_ = w.WriteMsg(new(dns.Msg).SetReply(req))\n}\n\nfunc Error(rcode int) dns.HandlerFunc {\n\treturn func(w dns.ResponseWriter, req *dns.Msg) {\n\t\t_ = w.WriteMsg(new(dns.Msg).SetRcode(req, rcode))\n\t}\n}\n\nfunc Answer(answer ...dns.RR) func(w dns.ResponseWriter, req *dns.Msg) {\n\treturn func(w dns.ResponseWriter, req *dns.Msg) {\n\t\tm := new(dns.Msg).SetReply(req)\n\n\t\tm.Answer = answer\n\n\t\terr := w.WriteMsg(m)\n\t\tif err != nil {\n\t\t\tpanic(err.Error())\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "platform/tester/dnsmock/handlers_test.go",
    "content": "package dnsmock\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSOA_self(t *testing.T) {\n\taddr := NewServer().\n\t\tQuery(\"example.com. SOA\", SOA(\"\")).\n\t\tBuild(t)\n\n\tclient := &dns.Client{Timeout: 1 * time.Second}\n\n\tm := new(dns.Msg).SetQuestion(\"example.com.\", dns.TypeSOA)\n\n\tr, _, err := client.Exchange(m, addr.String())\n\trequire.NoError(t, err)\n\n\texpectedSOA := []dns.RR{&dns.SOA{\n\t\tHdr:     dns.RR_Header{Name: \"example.com.\", Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 120, Rdlength: 56},\n\t\tNs:      \"ns1.example.com.\",\n\t\tMbox:    \"admin.example.com.\",\n\t\tSerial:  2016022801,\n\t\tRefresh: 28800,\n\t\tRetry:   7200,\n\t\tExpire:  2419200,\n\t\tMinttl:  1200,\n\t}}\n\n\trequire.Equal(t, dns.RcodeSuccess, r.Rcode)\n\tassert.Equal(t, expectedSOA, r.Answer)\n\tassert.Equal(t, m.Question, r.Question)\n}\n\nfunc TestSOA_differentDomain(t *testing.T) {\n\taddr := NewServer().\n\t\tQuery(\"example.com. SOA\", SOA(\"example.org.\")).\n\t\tBuild(t)\n\n\tclient := &dns.Client{Timeout: 1 * time.Second}\n\n\tm := new(dns.Msg).SetQuestion(\"example.com.\", dns.TypeSOA)\n\n\tr, _, err := client.Exchange(m, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Equalf(t, dns.RcodeSuccess, r.Rcode,\n\t\t\"expected %s, got %s\", dns.RcodeToString[dns.RcodeSuccess], dns.RcodeToString[r.Rcode])\n\n\texpectedSOA := []dns.RR{&dns.SOA{\n\t\tHdr:     dns.RR_Header{Name: \"example.org.\", Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 120, Rdlength: 56},\n\t\tNs:      \"ns1.example.org.\",\n\t\tMbox:    \"admin.example.org.\",\n\t\tSerial:  2016022801,\n\t\tRefresh: 28800,\n\t\tRetry:   7200,\n\t\tExpire:  2419200,\n\t\tMinttl:  1200,\n\t}}\n\n\tassert.Equal(t, expectedSOA, r.Answer)\n\tassert.Equal(t, m.Question, r.Question)\n}\n\nfunc TestSOA_tld(t *testing.T) {\n\taddr := NewServer().\n\t\tQuery(\"com. SOA\", SOA(\"\")).\n\t\tBuild(t)\n\n\tclient := &dns.Client{Timeout: 1 * time.Second}\n\n\tm := new(dns.Msg).SetQuestion(\"com.\", dns.TypeSOA)\n\n\tr, _, err := client.Exchange(m, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Equalf(t, dns.RcodeSuccess, r.Rcode,\n\t\t\"expected %s, got %s\", dns.RcodeToString[dns.RcodeSuccess], dns.RcodeToString[r.Rcode])\n\n\texpectedSOA := []dns.RR{&dns.SOA{\n\t\tHdr:     dns.RR_Header{Name: \"com.\", Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 120, Rdlength: 48},\n\t\tNs:      \"ns1.nic.com.\",\n\t\tMbox:    \"admin.nic.com.\",\n\t\tSerial:  2016022801,\n\t\tRefresh: 28800,\n\t\tRetry:   7200,\n\t\tExpire:  2419200,\n\t\tMinttl:  1200,\n\t}}\n\n\tassert.Equal(t, expectedSOA, r.Answer)\n\tassert.Equal(t, m.Question, r.Question)\n}\n\nfunc TestCNAME(t *testing.T) {\n\taddr := NewServer().\n\t\tQuery(\"example.com. CNAME\", CNAME(\"example.org.\")).\n\t\tBuild(t)\n\n\tclient := &dns.Client{Timeout: 1 * time.Second}\n\n\tm := new(dns.Msg).SetQuestion(\"example.com.\", dns.TypeCNAME)\n\n\tr, _, err := client.Exchange(m, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Equalf(t, dns.RcodeSuccess, r.Rcode,\n\t\t\"expected %s, got %s\", dns.RcodeToString[dns.RcodeSuccess], dns.RcodeToString[r.Rcode])\n\n\texpectedCNAME := []dns.RR{&dns.CNAME{\n\t\tHdr:    dns.RR_Header{Name: \"example.com.\", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 1, Rdlength: 13},\n\t\tTarget: \"example.org.\",\n\t}}\n\n\tassert.Equal(t, expectedCNAME, r.Answer)\n\tassert.Equal(t, m.Question, r.Question)\n}\n\nfunc TestNoop(t *testing.T) {\n\taddr := NewServer().\n\t\tQuery(\"example.com. CNAME\", Noop).\n\t\tBuild(t)\n\n\tclient := &dns.Client{Timeout: 1 * time.Second}\n\n\tm := new(dns.Msg).SetQuestion(\"example.com.\", dns.TypeCNAME)\n\n\tr, _, err := client.Exchange(m, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Equalf(t, dns.RcodeSuccess, r.Rcode,\n\t\t\"expected %s, got %s\", dns.RcodeToString[dns.RcodeSuccess], dns.RcodeToString[r.Rcode])\n\tassert.Equal(t, m.Question, r.Question)\n}\n\nfunc TestError(t *testing.T) {\n\taddr := NewServer().\n\t\tQuery(\"example.com. CNAME\", Error(dns.RcodeNameError)).\n\t\tBuild(t)\n\n\tclient := &dns.Client{Timeout: 1 * time.Second}\n\n\tm := new(dns.Msg).SetQuestion(\"example.com.\", dns.TypeCNAME)\n\n\tr, _, err := client.Exchange(m, addr.String())\n\trequire.NoError(t, err)\n\n\trequire.Equalf(t, dns.RcodeNameError, r.Rcode,\n\t\t\"expected %s, got %s\", dns.RcodeToString[dns.RcodeNameError], dns.RcodeToString[r.Rcode])\n\tassert.Equal(t, m.Question, r.Question)\n}\n"
  },
  {
    "path": "platform/tester/env.go",
    "content": "package tester\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"slices\"\n)\n\n// EnvTest Environment variables manager for tests.\ntype EnvTest struct {\n\tkeys   []string\n\tvalues map[string]string\n\n\tliveTestHook      func() bool\n\tliveTestExtraHook func() bool\n\n\tdomain    string\n\tdomainKey string\n}\n\n// NewEnvTest Creates an EnvTest.\nfunc NewEnvTest(keys ...string) *EnvTest {\n\tvalues := make(map[string]string)\n\n\tfor _, key := range keys {\n\t\tvalue := os.Getenv(key)\n\t\tif value != \"\" {\n\t\t\tvalues[key] = value\n\t\t}\n\t}\n\n\treturn &EnvTest{\n\t\tkeys:   keys,\n\t\tvalues: values,\n\t}\n}\n\n// WithDomain Defines the name of the environment variable used to define the domain related to the DNS request.\n// If the domain is defined, it was considered mandatory to define a test as a \"live\" test.\nfunc (e *EnvTest) WithDomain(key string) *EnvTest {\n\te.domainKey = key\n\te.domain = os.Getenv(key)\n\n\treturn e\n}\n\n// WithLiveTestRequirements Defines the environment variables required to define a test as a \"live\" test.\n// Replaces the default behavior (all keys are required).\nfunc (e *EnvTest) WithLiveTestRequirements(keys ...string) *EnvTest {\n\tvar countValuedVars int\n\n\tfor _, key := range keys {\n\t\tif e.domainKey != key && !e.isManagedKey(key) {\n\t\t\tpanic(fmt.Sprintf(\"Unauthorized action, the env var %s is not managed, or it's not the key of the domain.\", key))\n\t\t}\n\n\t\tif e.domainKey == key {\n\t\t\tcountValuedVars++\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, ok := e.values[key]; ok {\n\t\t\tcountValuedVars++\n\t\t}\n\t}\n\n\tlive := countValuedVars != 0 && len(keys) == countValuedVars\n\n\te.liveTestHook = func() bool {\n\t\treturn live\n\t}\n\n\treturn e\n}\n\n// WithLiveTestExtra Allows to define an additional condition to flag a test as \"live\" test.\n// This does not replace the default behavior.\nfunc (e *EnvTest) WithLiveTestExtra(extra func() bool) *EnvTest {\n\te.liveTestExtraHook = extra\n\treturn e\n}\n\n// GetDomain Gets the domain value associated with the DNS challenge (linked to WithDomain method).\nfunc (e *EnvTest) GetDomain() string {\n\treturn e.domain\n}\n\n// IsLiveTest Checks whether environment variables allow running a \"live\" test.\nfunc (e *EnvTest) IsLiveTest() bool {\n\tliveTest := e.liveTestExtra()\n\n\tif e.liveTestHook != nil {\n\t\treturn liveTest && e.liveTestHook()\n\t}\n\n\tliveTest = liveTest && len(e.values) == len(e.keys)\n\n\tif liveTest && e.domainKey != \"\" && e.domain == \"\" {\n\t\treturn false\n\t}\n\n\treturn liveTest\n}\n\n// RestoreEnv Restores the environment variables to the initial state.\nfunc (e *EnvTest) RestoreEnv() {\n\tfor key, value := range e.values {\n\t\tos.Setenv(key, value)\n\t}\n}\n\n// ClearEnv Deletes all environment variables related to the test.\nfunc (e *EnvTest) ClearEnv() {\n\tfor _, key := range e.keys {\n\t\tos.Unsetenv(key)\n\t}\n}\n\n// GetValue Gets the stored value of an environment variable.\nfunc (e *EnvTest) GetValue(key string) string {\n\treturn e.values[key]\n}\n\nfunc (e *EnvTest) liveTestExtra() bool {\n\tif e.liveTestExtraHook == nil {\n\t\treturn true\n\t}\n\n\treturn e.liveTestExtraHook()\n}\n\n// Apply Sets/Unsets environment variables.\n// Not related to the main environment variables.\nfunc (e *EnvTest) Apply(envVars map[string]string) {\n\tfor key, value := range envVars {\n\t\tif !e.isManagedKey(key) {\n\t\t\tpanic(fmt.Sprintf(\"Unauthorized action, the env var %s is not managed.\", key))\n\t\t}\n\n\t\tif value == \"\" {\n\t\t\tos.Unsetenv(key)\n\t\t} else {\n\t\t\tos.Setenv(key, value)\n\t\t}\n\t}\n}\n\nfunc (e *EnvTest) isManagedKey(varName string) bool {\n\treturn slices.Contains(e.keys, varName)\n}\n"
  },
  {
    "path": "platform/tester/env_test.go",
    "content": "package tester_test\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst (\n\tenvNamespace = \"LEGO_TEST_\"\n\tenvVar01     = envNamespace + \"01\"\n\tenvVar02     = envNamespace + \"02\"\n\tenvVarDomain = envNamespace + \"DOMAIN\"\n)\n\nfunc TestMain(m *testing.M) {\n\texitCode := m.Run()\n\n\tclearEnv()\n\tos.Exit(exitCode)\n}\n\nfunc applyEnv(envVars map[string]string) {\n\tfor key, value := range envVars {\n\t\tif value == \"\" {\n\t\t\tos.Unsetenv(key)\n\t\t} else {\n\t\t\tos.Setenv(key, value)\n\t\t}\n\t}\n}\n\nfunc clearEnv() {\n\tenviron := os.Environ()\n\tfor _, key := range environ {\n\t\tif strings.HasPrefix(key, envNamespace) {\n\t\t\tos.Unsetenv(strings.Split(key, \"=\")[0])\n\t\t}\n\t}\n\n\tos.Unsetenv(\"EXTRA_LEGO_TEST\")\n}\n\nfunc TestEnvTest(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc         string\n\t\tenvVars      map[string]string\n\t\tenvTestSetup func() *tester.EnvTest\n\t\texpected     func(t *testing.T, envTest *tester.EnvTest)\n\t}{\n\t\t{\n\t\t\tdesc: \"simple\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar01: \"A\",\n\t\t\t\tenvVar02: \"B\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02)\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.True(t, envTest.IsLiveTest())\n\t\t\t\tassert.Equal(t, \"A\", envTest.GetValue(envVar01))\n\t\t\t\tassert.Equal(t, \"B\", envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing env var\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar02: \"B\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02)\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.False(t, envTest.IsLiveTest())\n\t\t\t\tassert.Empty(t, envTest.GetValue(envVar01))\n\t\t\t\tassert.Equal(t, \"B\", envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"WithDomain\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar01:     \"A\",\n\t\t\t\tenvVar02:     \"B\",\n\t\t\t\tenvVarDomain: \"D\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02).WithDomain(envVarDomain)\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.True(t, envTest.IsLiveTest())\n\t\t\t\tassert.Equal(t, \"A\", envTest.GetValue(envVar01))\n\t\t\t\tassert.Equal(t, \"B\", envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetValue(envVarDomain))\n\t\t\t\tassert.Equal(t, \"D\", envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"WithDomain missing env var\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar01:     \"A\",\n\t\t\t\tenvVarDomain: \"D\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02).WithDomain(envVarDomain)\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.False(t, envTest.IsLiveTest())\n\t\t\t\tassert.Equal(t, \"A\", envTest.GetValue(envVar01))\n\t\t\t\tassert.Empty(t, envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetValue(envVarDomain))\n\t\t\t\tassert.Equal(t, \"D\", envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"WithDomain missing domain\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar01: \"A\",\n\t\t\t\tenvVar02: \"B\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02).WithDomain(envVarDomain)\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.False(t, envTest.IsLiveTest())\n\t\t\t\tassert.Equal(t, \"A\", envTest.GetValue(envVar01))\n\t\t\t\tassert.Equal(t, \"B\", envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetValue(envVarDomain))\n\t\t\t\tassert.Empty(t, envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"WithLiveTestRequirements\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar01: \"A\",\n\t\t\t\tenvVar02: \"B\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02).WithLiveTestRequirements(envVar02)\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.True(t, envTest.IsLiveTest())\n\t\t\t\tassert.Equal(t, \"A\", envTest.GetValue(envVar01))\n\t\t\t\tassert.Equal(t, \"B\", envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"WithLiveTestRequirements with domain as requirement\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar01: \"A\",\n\t\t\t\tenvVar02: \"B\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02).WithDomain(envVarDomain).WithLiveTestRequirements(envVar02, envVarDomain)\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.True(t, envTest.IsLiveTest())\n\t\t\t\tassert.Equal(t, \"A\", envTest.GetValue(envVar01))\n\t\t\t\tassert.Equal(t, \"B\", envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"WithLiveTestRequirements non required var missing\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar02: \"B\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02).WithLiveTestRequirements(envVar02)\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.True(t, envTest.IsLiveTest())\n\t\t\t\tassert.Empty(t, envTest.GetValue(envVar01))\n\t\t\t\tassert.Equal(t, \"B\", envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"WithLiveTestRequirements required var missing\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar01: \"A\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02).WithLiveTestRequirements(envVar02)\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.False(t, envTest.IsLiveTest())\n\t\t\t\tassert.Equal(t, \"A\", envTest.GetValue(envVar01))\n\t\t\t\tassert.Empty(t, envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"WithLiveTestRequirements WithDomain\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar01:     \"A\",\n\t\t\t\tenvVar02:     \"B\",\n\t\t\t\tenvVarDomain: \"D\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02).\n\t\t\t\t\tWithDomain(envVarDomain).\n\t\t\t\t\tWithLiveTestRequirements(envVar02)\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.True(t, envTest.IsLiveTest())\n\t\t\t\tassert.Equal(t, \"A\", envTest.GetValue(envVar01))\n\t\t\t\tassert.Equal(t, \"B\", envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetValue(envVarDomain))\n\t\t\t\tassert.Equal(t, \"D\", envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"WithLiveTestRequirements WithDomain without domain\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar01: \"A\",\n\t\t\t\tenvVar02: \"B\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02).\n\t\t\t\t\tWithDomain(envVarDomain).\n\t\t\t\t\tWithLiveTestRequirements(envVar02)\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.True(t, envTest.IsLiveTest())\n\t\t\t\tassert.Equal(t, \"A\", envTest.GetValue(envVar01))\n\t\t\t\tassert.Equal(t, \"B\", envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetValue(envVarDomain))\n\t\t\t\tassert.Empty(t, envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"WithLiveTestExtra true\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar01: \"A\",\n\t\t\t\tenvVar02: \"B\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02).\n\t\t\t\t\tWithLiveTestExtra(func() bool { return true })\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.True(t, envTest.IsLiveTest())\n\t\t\t\tassert.Equal(t, \"A\", envTest.GetValue(envVar01))\n\t\t\t\tassert.Equal(t, \"B\", envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"WithLiveTestExtra false\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar01: \"A\",\n\t\t\t\tenvVar02: \"B\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02).\n\t\t\t\t\tWithLiveTestExtra(func() bool { return false })\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.False(t, envTest.IsLiveTest())\n\t\t\t\tassert.Equal(t, \"A\", envTest.GetValue(envVar01))\n\t\t\t\tassert.Equal(t, \"B\", envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"WithLiveTestRequirements WithLiveTestExtra true\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar01: \"A\",\n\t\t\t\tenvVar02: \"B\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02).\n\t\t\t\t\tWithLiveTestRequirements(envVar02).\n\t\t\t\t\tWithLiveTestExtra(func() bool { return true })\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.True(t, envTest.IsLiveTest())\n\t\t\t\tassert.Equal(t, \"A\", envTest.GetValue(envVar01))\n\t\t\t\tassert.Equal(t, \"B\", envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"WithLiveTestRequirements WithLiveTestExtra false\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar01: \"A\",\n\t\t\t\tenvVar02: \"B\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02).\n\t\t\t\t\tWithLiveTestRequirements(envVar02).\n\t\t\t\t\tWithLiveTestExtra(func() bool { return false })\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.False(t, envTest.IsLiveTest())\n\t\t\t\tassert.Equal(t, \"A\", envTest.GetValue(envVar01))\n\t\t\t\tassert.Equal(t, \"B\", envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"WithLiveTestRequirements require env var missing WithLiveTestExtra true\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvVar01: \"A\",\n\t\t\t},\n\t\t\tenvTestSetup: func() *tester.EnvTest {\n\t\t\t\treturn tester.NewEnvTest(envVar01, envVar02).\n\t\t\t\t\tWithLiveTestRequirements(envVar02).\n\t\t\t\t\tWithLiveTestExtra(func() bool { return true })\n\t\t\t},\n\t\t\texpected: func(t *testing.T, envTest *tester.EnvTest) {\n\t\t\t\tassert.False(t, envTest.IsLiveTest())\n\t\t\t\tassert.Equal(t, \"A\", envTest.GetValue(envVar01))\n\t\t\t\tassert.Empty(t, envTest.GetValue(envVar02))\n\t\t\t\tassert.Empty(t, envTest.GetDomain())\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer clearEnv()\n\n\t\t\tapplyEnv(test.envVars)\n\n\t\t\tenvTest := test.envTestSetup()\n\n\t\t\ttest.expected(t, envTest)\n\t\t})\n\t}\n}\n\nfunc TestEnvTest_RestoreEnv(t *testing.T) {\n\tos.Setenv(envVar01, \"A\")\n\tos.Setenv(envVar02, \"B\")\n\n\tenvTest := tester.NewEnvTest(envVar01, envVar02)\n\n\tclearEnv()\n\n\tenvTest.RestoreEnv()\n\n\tassert.Equal(t, \"A\", os.Getenv(envVar01))\n\tassert.Equal(t, \"B\", os.Getenv(envVar02))\n}\n\nfunc TestEnvTest_ClearEnv(t *testing.T) {\n\tos.Setenv(envVar01, \"A\")\n\tos.Setenv(envVar02, \"B\")\n\tos.Setenv(\"EXTRA_LEGO_TEST\", \"X\")\n\n\tenvTest := tester.NewEnvTest(envVar01, envVar02)\n\n\tenvTest.ClearEnv()\n\n\tassert.Empty(t, os.Getenv(envVar01))\n\tassert.Empty(t, os.Getenv(envVar02))\n\tassert.Equal(t, \"X\", os.Getenv(\"EXTRA_LEGO_TEST\"))\n}\n"
  },
  {
    "path": "platform/tester/servermock/builder.go",
    "content": "package servermock\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Link represents a middleware interface, enabling middleware chaining.\ntype Link interface {\n\tBind(next http.Handler) http.Handler\n}\n\n// LinkFunc defines a function type [Link].\ntype LinkFunc func(next http.Handler) http.Handler\n\nfunc (f LinkFunc) Bind(next http.Handler) http.Handler {\n\treturn f(next)\n}\n\n// ClientBuilder defines a function type for creating a client of type T based on a httptest.Server instance.\ntype ClientBuilder[T any] func(server *httptest.Server) (T, error)\n\n// Builder is a type that facilitates the construction of testable HTTP clients and server.\n// It allows defining routes, attaching middleware, and creating custom HTTP clients.\ntype Builder[T any] struct {\n\tmux   *http.ServeMux\n\tchain []Link\n\n\tclientBuilder ClientBuilder[T]\n}\n\nfunc NewBuilder[T any](clientBuilder ClientBuilder[T], chain ...Link) *Builder[T] {\n\treturn &Builder[T]{\n\t\tmux:           http.NewServeMux(),\n\t\tchain:         chain,\n\t\tclientBuilder: clientBuilder,\n\t}\n}\n\nfunc (b *Builder[T]) Route(pattern string, handler http.Handler, chain ...Link) *Builder[T] {\n\tif handler == nil {\n\t\thandler = Noop()\n\t}\n\n\tfor _, link := range slices.Backward(b.chain) {\n\t\thandler = link.Bind(handler)\n\t}\n\n\tfor _, link := range slices.Backward(chain) {\n\t\thandler = link.Bind(handler)\n\t}\n\n\tb.mux.Handle(pattern, handler)\n\n\treturn b\n}\n\nfunc (b *Builder[T]) Build(t *testing.T) T {\n\tt.Helper()\n\n\tserver := httptest.NewServer(b.mux)\n\tt.Cleanup(server.Close)\n\n\tclient, err := b.clientBuilder(server)\n\trequire.NoError(t, err)\n\n\treturn client\n}\n\nfunc (b *Builder[T]) BuildHTTPS(t *testing.T) T {\n\tt.Helper()\n\n\tserver := httptest.NewTLSServer(b.mux)\n\tt.Cleanup(server.Close)\n\n\tclient, err := b.clientBuilder(server)\n\trequire.NoError(t, err)\n\n\treturn client\n}\n"
  },
  {
    "path": "platform/tester/servermock/handler_dump.go",
    "content": "package servermock\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n)\n\n// DumpRequest logs the full HTTP request to the console, including the body if present.\nfunc DumpRequest() http.HandlerFunc {\n\treturn func(rw http.ResponseWriter, req *http.Request) {\n\t\tdump, err := httputil.DumpRequest(req, true)\n\t\tif err != nil {\n\t\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Println(string(dump))\n\t}\n}\n"
  },
  {
    "path": "platform/tester/servermock/handler_file.go",
    "content": "package servermock\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n)\n\n// ResponseFromFileHandler handles HTTP responses using the content of a file.\ntype ResponseFromFileHandler struct {\n\tstatusCode int\n\theaders    http.Header\n\tfilename   string\n}\n\n// ResponseFromFile creates a [ResponseFromFileHandler] using a filename.\nfunc ResponseFromFile(filename string) *ResponseFromFileHandler {\n\treturn &ResponseFromFileHandler{\n\t\tstatusCode: http.StatusOK,\n\t\theaders:    http.Header{},\n\t\tfilename:   filename,\n\t}\n}\n\n// ResponseFromFixture creates a [ResponseFromFileHandler] using a filename from the `fixtures` directory.\nfunc ResponseFromFixture(filename string) *ResponseFromFileHandler {\n\treturn ResponseFromFile(filepath.Join(\"fixtures\", filename))\n}\n\n// ResponseFromInternal creates a [ResponseFromFileHandler] using a filename from the `internal/fixtures` directory.\nfunc ResponseFromInternal(filename string) *ResponseFromFileHandler {\n\treturn ResponseFromFile(filepath.Join(\"internal\", \"fixtures\", filename))\n}\n\nfunc (h *ResponseFromFileHandler) ServeHTTP(rw http.ResponseWriter, _ *http.Request) {\n\tfor k, values := range h.headers {\n\t\tfor _, v := range values {\n\t\t\trw.Header().Add(k, v)\n\t\t}\n\t}\n\n\tif h.filename == \"\" {\n\t\trw.WriteHeader(h.statusCode)\n\t\treturn\n\t}\n\n\tif filepath.Ext(h.filename) == \".json\" {\n\t\trw.Header().Set(contentTypeHeader, applicationJSONMimeType)\n\t}\n\n\tfile, err := os.Open(h.filename)\n\tif err != nil {\n\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdefer func() { _ = file.Close() }()\n\n\trw.WriteHeader(h.statusCode)\n\n\t_, err = io.Copy(rw, file)\n\tif err != nil {\n\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n}\n\nfunc (h *ResponseFromFileHandler) WithStatusCode(status int) *ResponseFromFileHandler {\n\tif h.statusCode >= http.StatusContinue {\n\t\th.statusCode = status\n\t}\n\n\treturn h\n}\n\nfunc (h *ResponseFromFileHandler) WithHeader(name, value string, values ...string) *ResponseFromFileHandler {\n\tfor _, v := range slices.Concat([]string{value}, values) {\n\t\th.headers.Add(name, v)\n\t}\n\n\treturn h\n}\n"
  },
  {
    "path": "platform/tester/servermock/handler_json.go",
    "content": "package servermock\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n)\n\n// JSONEncodeHandler is a handler that encodes data into JSON and writes it to an HTTP response.\ntype JSONEncodeHandler struct {\n\tdata       any\n\tstatusCode int\n}\n\nfunc JSONEncode(data any) *JSONEncodeHandler {\n\treturn &JSONEncodeHandler{\n\t\tdata:       data,\n\t\tstatusCode: http.StatusOK,\n\t}\n}\n\nfunc (h *JSONEncodeHandler) ServeHTTP(rw http.ResponseWriter, _ *http.Request) {\n\trw.Header().Set(contentTypeHeader, applicationJSONMimeType)\n\n\trw.WriteHeader(h.statusCode)\n\n\terr := json.NewEncoder(rw).Encode(h.data)\n\tif err != nil {\n\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n}\n\nfunc (h *JSONEncodeHandler) WithStatusCode(status int) *JSONEncodeHandler {\n\tif h.statusCode >= http.StatusContinue {\n\t\th.statusCode = status\n\t}\n\n\treturn h\n}\n"
  },
  {
    "path": "platform/tester/servermock/handler_noop.go",
    "content": "package servermock\n\nimport (\n\t\"net/http\"\n\t\"slices\"\n)\n\n// NoopHandler is a simple HTTP handler that responds without processing requests.\ntype NoopHandler struct {\n\tstatusCode int\n\theaders    http.Header\n}\n\nfunc Noop() *NoopHandler {\n\treturn &NoopHandler{\n\t\tstatusCode: http.StatusOK,\n\t\theaders:    http.Header{},\n\t}\n}\n\nfunc (h *NoopHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {\n\tfor k, values := range h.headers {\n\t\tfor _, v := range values {\n\t\t\trw.Header().Add(k, v)\n\t\t}\n\t}\n\n\trw.WriteHeader(h.statusCode)\n}\n\nfunc (h *NoopHandler) WithStatusCode(status int) *NoopHandler {\n\tif h.statusCode >= http.StatusContinue {\n\t\th.statusCode = status\n\t}\n\n\treturn h\n}\n\nfunc (h *NoopHandler) WithHeader(name, value string, values ...string) *NoopHandler {\n\tfor _, v := range slices.Concat([]string{value}, values) {\n\t\th.headers.Add(name, v)\n\t}\n\n\treturn h\n}\n"
  },
  {
    "path": "platform/tester/servermock/handler_raw.go",
    "content": "package servermock\n\nimport (\n\t\"net/http\"\n\t\"slices\"\n)\n\n// RawResponseHandler is a custom HTTP handler that serves raw response data.\ntype RawResponseHandler struct {\n\tstatusCode int\n\theaders    http.Header\n\tdata       []byte\n}\n\nfunc RawResponse(data []byte) *RawResponseHandler {\n\treturn &RawResponseHandler{\n\t\tstatusCode: http.StatusOK,\n\t\theaders:    http.Header{},\n\t\tdata:       data,\n\t}\n}\n\nfunc RawStringResponse(data string) *RawResponseHandler {\n\treturn RawResponse([]byte(data))\n}\n\nfunc (h *RawResponseHandler) ServeHTTP(rw http.ResponseWriter, _ *http.Request) {\n\tfor k, values := range h.headers {\n\t\tfor _, v := range values {\n\t\t\trw.Header().Add(k, v)\n\t\t}\n\t}\n\n\trw.WriteHeader(h.statusCode)\n\n\tif len(h.data) == 0 {\n\t\treturn\n\t}\n\n\t_, err := rw.Write(h.data)\n\tif err != nil {\n\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n}\n\nfunc (h *RawResponseHandler) WithStatusCode(status int) *RawResponseHandler {\n\tif h.statusCode >= http.StatusContinue {\n\t\th.statusCode = status\n\t}\n\n\treturn h\n}\n\nfunc (h *RawResponseHandler) WithHeader(name, value string, values ...string) *RawResponseHandler {\n\tfor _, v := range slices.Concat([]string{value}, values) {\n\t\th.headers.Add(name, v)\n\t}\n\n\treturn h\n}\n"
  },
  {
    "path": "platform/tester/servermock/link_form.go",
    "content": "package servermock\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"slices\"\n)\n\n// FormLink is a type used for validating and processing form data in HTTP requests.\n// It supports strict validation, predefined values, and regex-based checks to ensure form compliance.\ntype FormLink struct {\n\tvalues      url.Values\n\tregexes     map[string]*regexp.Regexp\n\tstrict      bool\n\tusePostForm bool\n\tstatusCode  int\n}\n\nfunc CheckForm() *FormLink {\n\treturn &FormLink{\n\t\tvalues:     url.Values{},\n\t\tregexes:    map[string]*regexp.Regexp{},\n\t\tstatusCode: http.StatusBadRequest,\n\t}\n}\n\nfunc (l *FormLink) Bind(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\terr := req.ParseForm()\n\t\tif err != nil {\n\t\t\thttp.Error(rw, err.Error(), l.statusCode)\n\t\t\treturn\n\t\t}\n\n\t\tform := req.Form\n\t\tif l.usePostForm {\n\t\t\tform = req.PostForm\n\t\t}\n\n\t\tif l.strict {\n\t\t\tif len(form) != len(l.values)+len(l.regexes) {\n\t\t\t\tmsg := fmt.Sprintf(\"invalid query parameters, got %v, want %v\", req.Form, l.values)\n\t\t\t\thttp.Error(rw, msg, l.statusCode)\n\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tfor k, v := range l.values {\n\t\t\tvalue := form[k]\n\t\t\tif !slices.Equal(v, value) {\n\t\t\t\tmsg := fmt.Sprintf(\"invalid %q form value, got %q, want %q\", k, value, v)\n\t\t\t\thttp.Error(rw, msg, l.statusCode)\n\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tfor k, exp := range l.regexes {\n\t\t\tvalue := form.Get(k)\n\t\t\tif !exp.MatchString(value) {\n\t\t\t\tmsg := fmt.Sprintf(\"invalid %q form value, %q doesn't match to %q\", k, value, exp)\n\t\t\t\thttp.Error(rw, msg, l.statusCode)\n\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tnext.ServeHTTP(rw, req)\n\t})\n}\n\nfunc (l *FormLink) Strict() *FormLink {\n\tl.strict = true\n\n\treturn l\n}\n\nfunc (l *FormLink) UsePostForm() *FormLink {\n\tl.usePostForm = true\n\n\treturn l\n}\n\nfunc (l *FormLink) With(name, value string) *FormLink {\n\tl.values.Set(name, value)\n\n\treturn l\n}\n\nfunc (l *FormLink) WithRegexp(name, exp string) *FormLink {\n\tl.regexes[name] = regexp.MustCompile(exp)\n\n\treturn l\n}\n"
  },
  {
    "path": "platform/tester/servermock/link_headers.go",
    "content": "package servermock\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"slices\"\n)\n\nconst (\n\tauthorizationHeader = \"Authorization\"\n\tcontentTypeHeader   = \"Content-Type\"\n\tacceptHeader        = \"Accept\"\n)\n\nconst (\n\tapplicationJSONMimeType = \"application/json\"\n\tapplicationFormMimeType = \"application/x-www-form-urlencoded\"\n)\n\ntype basicAuth struct {\n\tusername, password string\n}\n\n// HeaderLink validates HTTP request headers.\ntype HeaderLink struct {\n\tvalues     http.Header\n\tregexes    map[string]*regexp.Regexp\n\tjson       bool\n\tbasicAuth  *basicAuth\n\tstatusCode int\n}\n\nfunc CheckHeader() *HeaderLink {\n\treturn &HeaderLink{\n\t\tvalues:     http.Header{},\n\t\tregexes:    map[string]*regexp.Regexp{},\n\t\tstatusCode: http.StatusBadRequest,\n\t}\n}\n\nfunc (l *HeaderLink) Bind(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\tfor k, v := range l.values {\n\t\t\terr := checkHeader(req, k, v)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(rw, err.Error(), l.statusCode)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tfor k, exp := range l.regexes {\n\t\t\tvalue := req.Header.Get(k)\n\n\t\t\tif !exp.MatchString(value) {\n\t\t\t\tmsg := fmt.Sprintf(\"invalid %q header value, %q doesn't match to %q\", k, value, exp)\n\t\t\t\thttp.Error(rw, msg, l.statusCode)\n\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tif l.json && !l.checkJSONHeaders(rw, req) {\n\t\t\treturn\n\t\t}\n\n\t\tif l.basicAuth != nil && !l.checkBasicAuth(rw, req) {\n\t\t\treturn\n\t\t}\n\n\t\tnext.ServeHTTP(rw, req)\n\t})\n}\n\nfunc (l *HeaderLink) With(name, value string, values ...string) *HeaderLink {\n\tfor _, v := range slices.Concat([]string{value}, values) {\n\t\tl.values.Add(name, v)\n\t}\n\n\treturn l\n}\n\nfunc (l *HeaderLink) WithRegexp(name, exp string) *HeaderLink {\n\tl.regexes[name] = regexp.MustCompile(exp)\n\n\treturn l\n}\n\nfunc (l *HeaderLink) WithJSONHeaders() *HeaderLink {\n\tl.json = true\n\n\treturn l\n}\n\nfunc (l *HeaderLink) WithContentTypeFromURLEncoded() *HeaderLink {\n\tl.values.Set(contentTypeHeader, applicationFormMimeType)\n\n\treturn l\n}\n\nfunc (l *HeaderLink) WithContentType(value string) *HeaderLink {\n\tl.values.Set(contentTypeHeader, value)\n\n\treturn l\n}\n\nfunc (l *HeaderLink) WithAccept(value string) *HeaderLink {\n\tl.values.Set(acceptHeader, value)\n\n\treturn l\n}\n\nfunc (l *HeaderLink) WithAuthorization(value string) *HeaderLink {\n\tl.values.Set(authorizationHeader, value)\n\n\treturn l\n}\n\nfunc (l *HeaderLink) WithStatusCode(status int) *HeaderLink {\n\tif l.statusCode >= http.StatusContinue {\n\t\tl.statusCode = status\n\t}\n\n\treturn l\n}\n\nfunc (l *HeaderLink) WithBasicAuth(username, password string) *HeaderLink {\n\tl.basicAuth = &basicAuth{username: username, password: password}\n\n\treturn l\n}\n\nfunc (l *HeaderLink) checkBasicAuth(rw http.ResponseWriter, req *http.Request) bool {\n\tusr, pwd, ok := req.BasicAuth()\n\tif !ok {\n\t\thttp.Error(rw, \"missing Basic auth\", l.statusCode)\n\n\t\treturn false\n\t}\n\n\tif usr != l.basicAuth.username || pwd != l.basicAuth.password {\n\t\tmsg := fmt.Sprintf(\"invalid credentials: got [username: %q, password: %q], want [username: %q, password: %q]\",\n\t\t\tusr, pwd, l.basicAuth.username, l.basicAuth.password)\n\t\thttp.Error(rw, msg, l.statusCode)\n\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc (l *HeaderLink) checkJSONHeaders(rw http.ResponseWriter, req *http.Request) bool {\n\terr := checkHeader(req, acceptHeader, []string{applicationJSONMimeType})\n\tif err != nil {\n\t\thttp.Error(rw, err.Error(), l.statusCode)\n\n\t\treturn false\n\t}\n\n\tif req.ContentLength > 0 {\n\t\terr = checkHeader(req, contentTypeHeader, []string{applicationJSONMimeType})\n\t\tif err != nil {\n\t\t\thttp.Error(rw, err.Error(), l.statusCode)\n\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc checkHeader(req *http.Request, k string, v []string) error {\n\tif !slices.Equal(req.Header[k], v) {\n\t\treturn fmt.Errorf(\"invalid %q header value, got %q, want %q\", k, req.Header[k], v)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "platform/tester/servermock/link_query.go",
    "content": "package servermock\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n)\n\n// QueryParameterLink validates query parameters in HTTP requests.\n// The strict flag enforces exact matches with specified query parameters.\ntype QueryParameterLink struct {\n\tvalues     map[string]string\n\tregexes    map[string]*regexp.Regexp\n\tstrict     bool\n\tstatusCode int\n}\n\nfunc CheckQueryParameter() *QueryParameterLink {\n\treturn &QueryParameterLink{\n\t\tvalues:     map[string]string{},\n\t\tregexes:    map[string]*regexp.Regexp{},\n\t\tstatusCode: http.StatusBadRequest,\n\t}\n}\n\nfunc (l *QueryParameterLink) Bind(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\tquery := req.URL.Query()\n\n\t\tif l.strict {\n\t\t\tif len(query) != len(l.values)+len(l.regexes) {\n\t\t\t\tmsg := fmt.Sprintf(\"invalid query parameters, got %v, want %v\", query, l.values)\n\t\t\t\thttp.Error(rw, msg, l.statusCode)\n\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tfor k, v := range l.values {\n\t\t\tp := query.Get(k)\n\t\t\tif p != v {\n\t\t\t\tmsg := fmt.Sprintf(\"invalid %q query parameter value, got %q, want %q\", k, p, v)\n\t\t\t\thttp.Error(rw, msg, l.statusCode)\n\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tfor k, exp := range l.regexes {\n\t\t\tvalue := query.Get(k)\n\t\t\tif !exp.MatchString(value) {\n\t\t\t\tmsg := fmt.Sprintf(\"invalid %q query parameter value, %q doesn't match to %q\", k, value, exp)\n\t\t\t\thttp.Error(rw, msg, l.statusCode)\n\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tnext.ServeHTTP(rw, req)\n\t})\n}\n\nfunc (l *QueryParameterLink) Strict() *QueryParameterLink {\n\tl.strict = true\n\n\treturn l\n}\n\nfunc (l *QueryParameterLink) With(name, value string) *QueryParameterLink {\n\tl.values[name] = value\n\n\treturn l\n}\n\nfunc (l *QueryParameterLink) WithRegexp(name, exp string) *QueryParameterLink {\n\tl.regexes[name] = regexp.MustCompile(exp)\n\n\treturn l\n}\n\nfunc (l *QueryParameterLink) WithValues(values url.Values) *QueryParameterLink {\n\tfor k, v := range values {\n\t\tif len(v) != 1 {\n\t\t\tcontinue\n\t\t}\n\n\t\tl.values[k] = v[0]\n\t}\n\n\treturn l\n}\n\nfunc (l *QueryParameterLink) WithStatusCode(status int) *QueryParameterLink {\n\tif l.statusCode >= http.StatusContinue {\n\t\tl.statusCode = status\n\t}\n\n\treturn l\n}\n"
  },
  {
    "path": "platform/tester/servermock/link_request_body.go",
    "content": "package servermock\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n)\n\n// RequestBodyLink represents a handler utility to validate HTTP request bodies against a predefined byte slice.\ntype RequestBodyLink struct {\n\tbody             []byte\n\tfilename         string\n\tignoreWhitespace bool\n}\n\n// CheckRequestBody creates a [RequestBodyLink] initialized with the provided request body string.\nfunc CheckRequestBody(body string) *RequestBodyLink {\n\treturn &RequestBodyLink{body: []byte(body)}\n}\n\n// CheckRequestBodyFromFile creates a [RequestBodyLink] initialized with the provided request body file.\nfunc CheckRequestBodyFromFile(filename string) *RequestBodyLink {\n\treturn &RequestBodyLink{filename: filename}\n}\n\n// CheckRequestBodyFromFixture creates a [RequestBodyLink] initialized with the provided request body file from the `fixtures` directory.\nfunc CheckRequestBodyFromFixture(filename string) *RequestBodyLink {\n\treturn CheckRequestBodyFromFile(filepath.Join(\"fixtures\", filename))\n}\n\n// CheckRequestBodyFromInternal creates a [RequestBodyLink] initialized with the provided request body file from the `internal/fixtures directory.\nfunc CheckRequestBodyFromInternal(filename string) *RequestBodyLink {\n\treturn CheckRequestBodyFromFile(filepath.Join(\"internal\", \"fixtures\", filename))\n}\n\nfunc (l *RequestBodyLink) Bind(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\tif req.ContentLength == 0 {\n\t\t\thttp.Error(rw, fmt.Sprintf(\"%s: empty request body\", req.URL.Path), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tbody, err := io.ReadAll(req.Body)\n\t\tif err != nil {\n\t\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\t_ = req.Body.Close()\n\n\t\texpectedRaw := slices.Clone(l.body)\n\n\t\tif l.filename != \"\" {\n\t\t\texpectedRaw, err = os.ReadFile(l.filename)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tif len(expectedRaw) == 0 {\n\t\t\thttp.Error(rw, fmt.Sprintf(\"%s: empty expected request body\", req.URL.Path), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tif l.ignoreWhitespace {\n\t\t\tbody = trimLineSpace(body)\n\t\t\texpectedRaw = trimLineSpace(expectedRaw)\n\t\t}\n\n\t\tif !bytes.Equal(bytes.TrimSpace(expectedRaw), bytes.TrimSpace(body)) {\n\t\t\tmsg := fmt.Sprintf(\"%s: request body differences: got: %s, want: %s\", req.URL.Path,\n\t\t\t\tstring(bytes.TrimSpace(body)), string(bytes.TrimSpace(expectedRaw)))\n\t\t\thttp.Error(rw, msg, http.StatusBadRequest)\n\n\t\t\treturn\n\t\t}\n\n\t\tnext.ServeHTTP(rw, req)\n\t})\n}\n\nfunc (l *RequestBodyLink) IgnoreWhitespace() *RequestBodyLink {\n\tl.ignoreWhitespace = true\n\n\treturn l\n}\n\nfunc trimLineSpace(body []byte) []byte {\n\tbuf := bytes.NewBuffer(nil)\n\tfor line := range bytes.Lines(body) {\n\t\tbuf.Write(bytes.TrimSpace(line))\n\t}\n\n\treturn buf.Bytes()\n}\n"
  },
  {
    "path": "platform/tester/servermock/link_request_body_json.go",
    "content": "package servermock\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\n// RequestBodyJSONLink validates JSON request bodies.\ntype RequestBodyJSONLink struct {\n\tbody     []byte\n\tfilename string\n\tdata     any\n}\n\n// CheckRequestJSONBody creates a [RequestBodyJSONLink] initialized with a string.\nfunc CheckRequestJSONBody(body string) *RequestBodyJSONLink {\n\treturn &RequestBodyJSONLink{body: []byte(body)}\n}\n\n// CheckRequestJSONBodyFromStruct creates a [RequestBodyJSONLink] initialized with a struct.\nfunc CheckRequestJSONBodyFromStruct(data any) *RequestBodyJSONLink {\n\treturn &RequestBodyJSONLink{data: data}\n}\n\n// CheckRequestJSONBodyFromFile creates a [RequestBodyJSONLink] initialized with the provided request body file.\nfunc CheckRequestJSONBodyFromFile(filename string) *RequestBodyJSONLink {\n\treturn &RequestBodyJSONLink{\n\t\tfilename: filename,\n\t}\n}\n\n// CheckRequestJSONBodyFromFixture creates a [RequestBodyJSONLink] initialized with the provided request body file from the `fixtures` directory.\nfunc CheckRequestJSONBodyFromFixture(filename string) *RequestBodyJSONLink {\n\treturn CheckRequestJSONBodyFromFile(filepath.Join(\"fixtures\", filename))\n}\n\n// CheckRequestJSONBodyFromInternal creates a [RequestBodyJSONLink] initialized with the provided request body file from the `internal/fixtures` directory.\nfunc CheckRequestJSONBodyFromInternal(filename string) *RequestBodyJSONLink {\n\treturn CheckRequestJSONBodyFromFile(filepath.Join(\"internal\", \"fixtures\", filename))\n}\n\nfunc (l *RequestBodyJSONLink) Bind(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\tif req.ContentLength == 0 {\n\t\t\thttp.Error(rw, fmt.Sprintf(\"%s: empty request body\", req.URL.Path), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tbody, err := io.ReadAll(req.Body)\n\t\tif err != nil {\n\t\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\t_ = req.Body.Close()\n\n\t\tvar expected, actual any\n\n\t\texpectedRaw := slices.Clone(l.body)\n\n\t\tswitch {\n\t\tcase l.filename != \"\":\n\t\t\texpectedRaw, err = os.ReadFile(l.filename)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\tcase l.data != nil:\n\t\t\texpectedRaw, err = json.Marshal(l.data)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tif len(expectedRaw) == 0 {\n\t\t\thttp.Error(rw, fmt.Sprintf(\"%s: empty expected request body\", req.URL.Path), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\terr = json.Unmarshal(expectedRaw, &expected)\n\t\tif err != nil {\n\t\t\tmsg := fmt.Sprintf(\"%s: the expected request body is not valid JSON: %v\", req.URL.Path, err)\n\t\t\thttp.Error(rw, msg, http.StatusBadRequest)\n\n\t\t\treturn\n\t\t}\n\n\t\terr = json.Unmarshal(body, &actual)\n\t\tif err != nil {\n\t\t\tmsg := fmt.Sprintf(\"%s: request body is not valid JSON: %v\", req.URL.Path, err)\n\t\t\thttp.Error(rw, msg, http.StatusBadRequest)\n\n\t\t\treturn\n\t\t}\n\n\t\tif !cmp.Equal(actual, expected) {\n\t\t\tmsg := fmt.Sprintf(\"%s: request body differences: %s\", req.URL.Path, cmp.Diff(actual, expected))\n\t\t\thttp.Error(rw, msg, http.StatusBadRequest)\n\n\t\t\treturn\n\t\t}\n\n\t\tnext.ServeHTTP(rw, req)\n\t})\n}\n"
  },
  {
    "path": "platform/wait/wait.go",
    "content": "package wait\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/log\"\n)\n\n// For polls the given function 'f', once every 'interval', up to 'timeout'.\nfunc For(msg string, timeout, interval time.Duration, f func() (bool, error)) error {\n\tlog.Infof(\"Wait for %s [timeout: %s, interval: %s]\", msg, timeout, interval)\n\n\tvar lastErr error\n\n\ttimeUp := time.After(timeout)\n\n\tfor {\n\t\tselect {\n\t\tcase <-timeUp:\n\t\t\tif lastErr == nil {\n\t\t\t\treturn fmt.Errorf(\"%s: time limit exceeded\", msg)\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"%s: time limit exceeded: last error: %w\", msg, lastErr)\n\t\tdefault:\n\t\t}\n\n\t\tstop, err := f()\n\t\tif stop {\n\t\t\treturn err\n\t\t}\n\n\t\tif err != nil {\n\t\t\tlastErr = err\n\t\t}\n\n\t\ttime.Sleep(interval)\n\t}\n}\n\n// Retry retries the given operation until it succeeds or the context is canceled.\n// Similar to [backoff.Retry] but with a different signature.\nfunc Retry(ctx context.Context, operation func() error, opts ...backoff.RetryOption) error {\n\t_, err := backoff.Retry(ctx, func() (any, error) {\n\t\treturn nil, operation()\n\t}, opts...)\n\n\treturn err\n}\n"
  },
  {
    "path": "platform/wait/wait_test.go",
    "content": "package wait\n\nimport (\n\t\"errors\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TODO(ldez): rewrite those tests when upgrading to go1.25 as minimum Go version.\n\nfunc TestFor_timeout(t *testing.T) {\n\tvar io atomic.Int64\n\n\tc := make(chan error)\n\n\tgo func() {\n\t\tc <- For(\"test\", 3*time.Second, 1*time.Second, func() (bool, error) {\n\t\t\tio.Add(1)\n\n\t\t\tif io.Load() == 1 {\n\t\t\t\treturn false, nil\n\t\t\t}\n\n\t\t\treturn false, nil\n\t\t})\n\t}()\n\n\ttimeout := time.After(6 * time.Second)\n\n\tselect {\n\tcase <-timeout:\n\t\tt.Fatal(\"timeout exceeded\")\n\tcase err := <-c:\n\t\trequire.EqualError(t, err, \"test: time limit exceeded\")\n\t}\n\n\trequire.EqualValues(t, 3, io.Load())\n}\n\nfunc TestFor_timeout_with_error(t *testing.T) {\n\tvar io atomic.Int64\n\n\tc := make(chan error)\n\n\tgo func() {\n\t\tc <- For(\"test\", 3*time.Second, 1*time.Second, func() (bool, error) {\n\t\t\tio.Add(1)\n\n\t\t\t// This allows be sure that the latest previous error is returned.\n\t\t\tif io.Load() == 1 {\n\t\t\t\treturn false, errors.New(\"oops\")\n\t\t\t}\n\n\t\t\treturn false, nil\n\t\t})\n\t}()\n\n\ttimeout := time.After(6 * time.Second)\n\n\tselect {\n\tcase <-timeout:\n\t\tt.Fatal(\"timeout exceeded\")\n\tcase err := <-c:\n\t\trequire.EqualError(t, err, \"test: time limit exceeded: last error: oops\")\n\t}\n\n\trequire.EqualValues(t, 3, io.Load())\n}\n\nfunc TestFor_stop(t *testing.T) {\n\tvar io atomic.Int64\n\n\tc := make(chan error)\n\n\tgo func() {\n\t\tc <- For(\"test\", 3*time.Second, 1*time.Second, func() (bool, error) {\n\t\t\tio.Add(1)\n\n\t\t\treturn true, nil\n\t\t})\n\t}()\n\n\ttimeout := time.After(6 * time.Second)\n\n\tselect {\n\tcase <-timeout:\n\t\tt.Fatal(\"timeout exceeded\")\n\tcase err := <-c:\n\t\trequire.NoError(t, err)\n\t}\n\n\trequire.EqualValues(t, 1, io.Load())\n}\n\nfunc TestFor_stop_with_error(t *testing.T) {\n\tvar io atomic.Int64\n\n\tc := make(chan error)\n\n\tgo func() {\n\t\tc <- For(\"test\", 3*time.Second, 1*time.Second, func() (bool, error) {\n\t\t\tio.Add(1)\n\n\t\t\treturn true, errors.New(\"oops\")\n\t\t})\n\t}()\n\n\ttimeout := time.After(6 * time.Second)\n\n\tselect {\n\tcase <-timeout:\n\t\tt.Fatal(\"timeout exceeded\")\n\tcase err := <-c:\n\t\trequire.EqualError(t, err, \"oops\")\n\t}\n\n\trequire.EqualValues(t, 1, io.Load())\n}\n"
  },
  {
    "path": "providers/dns/acmedns/acmedns.go",
    "content": "// Package acmedns implements a DNS provider for solving DNS-01 challenges using Joohoi's acme-dns project.\n// For more information see the ACME-DNS homepage: https://github.com/joohoi/acme-dns\npackage acmedns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/acmedns/internal\"\n\t\"github.com/nrdcg/goacmedns\"\n\t\"github.com/nrdcg/goacmedns/storage\"\n)\n\nconst (\n\t// envNamespace is the prefix for ACME-DNS environment variables.\n\tenvNamespace = \"ACME_DNS_\"\n\n\t// EnvAPIBase is the environment variable name for the ACME-DNS API address.\n\t// (e.g. https://acmedns.your-domain.com).\n\tEnvAPIBase = envNamespace + \"API_BASE\"\n\n\t// EnvAllowList are source networks using CIDR notation,\n\t// e.g. \"192.168.100.1/24,1.2.3.4/32,2002:c0a8:2a00::0/40\".\n\tEnvAllowList = envNamespace + \"ALLOWLIST\"\n\n\t// EnvStoragePath is the environment variable name for the ACME-DNS JSON account data file.\n\t// A per-domain account will be registered/persisted to this file and used for TXT updates.\n\tEnvStoragePath = envNamespace + \"STORAGE_PATH\"\n\n\t// EnvStorageBaseURL  is the environment variable name for the ACME-DNS JSON account data.\n\t// The URL to the storage server.\n\tEnvStorageBaseURL = envNamespace + \"STORAGE_BASE_URL\"\n)\n\nvar _ challenge.Provider = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIBase        string\n\tAllowList      []string\n\tStoragePath    string\n\tStorageBaseURL string\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{}\n}\n\n// acmeDNSClient is an interface describing the goacmedns.Client functions the DNSProvider uses.\n// It makes it easier for tests to shim a mock Client into the DNSProvider.\ntype acmeDNSClient interface {\n\t// UpdateTXTRecord updates the provided account's TXT record\n\t// to the given value or returns an error.\n\tUpdateTXTRecord(ctx context.Context, account goacmedns.Account, value string) error\n\t// RegisterAccount registers and returns a new account\n\t// with the given allowFrom restriction or returns an error.\n\tRegisterAccount(ctx context.Context, allowFrom []string) (goacmedns.Account, error)\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig  *Config\n\tclient  acmeDNSClient\n\tstorage goacmedns.Storage\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Joohoi's acme-dns.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIBase)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"acme-dns: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIBase = values[EnvAPIBase]\n\tconfig.StoragePath = env.GetOrFile(EnvStoragePath)\n\tconfig.StorageBaseURL = env.GetOrFile(EnvStorageBaseURL)\n\n\tallowList := env.GetOrFile(EnvAllowList)\n\tif allowList != \"\" {\n\t\tconfig.AllowList = strings.Split(allowList, \",\")\n\t}\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Joohoi's acme-dns.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"acme-dns: the configuration of the DNS provider is nil\")\n\t}\n\n\tst, err := getStorage(config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"acme-dns: %w\", err)\n\t}\n\n\tclient, err := goacmedns.NewClient(config.APIBase)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"acme-dns: new client: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:  config,\n\t\tclient:  client,\n\t\tstorage: st,\n\t}, nil\n}\n\n// NewDNSProviderClient creates an ACME-DNS DNSProvider with the given acmeDNSClient and [goacmedns.Storage].\n//\n// Deprecated: use [NewDNSProviderConfig] instead.\nfunc NewDNSProviderClient(client acmeDNSClient, store goacmedns.Storage) (*DNSProvider, error) {\n\tif client == nil {\n\t\treturn nil, errors.New(\"acme-dns: Client must be not nil\")\n\t}\n\n\tif store == nil {\n\t\treturn nil, errors.New(\"acme-dns: Storage must be not nil\")\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:  NewDefaultConfig(),\n\t\tclient:  client,\n\t\tstorage: store,\n\t}, nil\n}\n\n// ErrCNAMERequired is returned by Present when the Domain indicated had no\n// existing ACME-DNS account in the Storage and additional setup is required.\n// The user must create a CNAME in the DNS zone for Domain that aliases FQDN\n// to Target in order to complete setup for the ACME-DNS account that was created.\ntype ErrCNAMERequired struct {\n\t// The Domain that is being issued for.\n\tDomain string\n\t// The alias of the CNAME (left hand DNS label).\n\tFQDN string\n\t// The RDATA of the CNAME (right hand side, canonical name).\n\tTarget string\n}\n\n// Error returns a descriptive message for the ErrCNAMERequired instance telling\n// the user that a CNAME needs to be added to the DNS zone of c.Domain before\n// the ACME-DNS hook will work.\n// The CNAME to be created should be of the form: {{ c.FQDN }} \tCNAME\t{{ c.Target }}.\nfunc (e ErrCNAMERequired) Error() string {\n\treturn fmt.Sprintf(\"acme-dns: new account created for %q. \"+\n\t\t\"To complete setup for %q you must provision the following \"+\n\t\t\"CNAME in your DNS zone and re-run this provider when it is \"+\n\t\t\"in place:\\n\"+\n\t\t\"%s CNAME %s.\",\n\t\te.Domain, e.Domain, e.FQDN, e.Target)\n}\n\n// Present creates a TXT record to fulfill the DNS-01 challenge.\n// If there is an existing account for the domain in the provider's storage\n// then it will be used to set the challenge response TXT record with the ACME-DNS server and issuance will continue.\n// If there is not an account for the given domain present in the DNSProvider storage\n// one will be created and registered with the ACME DNS server and an ErrCNAMERequired error is returned.\n// This will halt issuance and indicate to the user that a one-time manual setup is required for the domain.\nfunc (d *DNSProvider) Present(domain, _, keyAuth string) error {\n\tctx := context.Background()\n\n\t// Compute the challenge response FQDN and TXT value for the domain based on the keyAuth.\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// Check if credentials were previously saved for this domain.\n\taccount, err := d.storage.Fetch(ctx, domain)\n\tif err != nil {\n\t\tif !errors.Is(err, storage.ErrDomainNotFound) {\n\t\t\treturn err\n\t\t}\n\n\t\t// The account did not exist.\n\t\t// Create a new one and return an error indicating the required one-time manual CNAME setup.\n\t\taccount, err = d.register(ctx, domain, info.FQDN)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Update the acme-dns TXT record.\n\treturn d.client.UpdateTXTRecord(ctx, account, info.Value)\n}\n\n// CleanUp removes the record matching the specified parameters. It is not\n// implemented for the ACME-DNS provider.\nfunc (d *DNSProvider) CleanUp(_, _, _ string) error {\n\t// ACME-DNS doesn't support the notion of removing a record.\n\t// For users of ACME-DNS it is expected the stale records remain in-place.\n\treturn nil\n}\n\n// register creates a new ACME-DNS account for the given domain.\n// If account creation works as expected a ErrCNAMERequired error is returned describing\n// the one-time manual CNAME setup required to complete setup of the ACME-DNS hook for the domain.\n// If any other error occurs it is returned as-is.\nfunc (d *DNSProvider) register(ctx context.Context, domain, fqdn string) (goacmedns.Account, error) {\n\tnewAcct, err := d.client.RegisterAccount(ctx, d.config.AllowList)\n\tif err != nil {\n\t\treturn goacmedns.Account{}, err\n\t}\n\n\tvar cnameCreated bool\n\n\t// Store the new account in the storage and call save to persist the data.\n\terr = d.storage.Put(ctx, domain, newAcct)\n\tif err != nil {\n\t\tcnameCreated = errors.Is(err, internal.ErrCNAMEAlreadyCreated)\n\t\tif !cnameCreated {\n\t\t\treturn goacmedns.Account{}, err\n\t\t}\n\t}\n\n\terr = d.storage.Save(ctx)\n\tif err != nil {\n\t\treturn goacmedns.Account{}, err\n\t}\n\n\tif cnameCreated {\n\t\treturn newAcct, nil\n\t}\n\n\t// Stop issuance by returning an error.\n\t// The user needs to perform a manual one-time CNAME setup in their DNS zone\n\t// to complete the setup of the new account we created.\n\treturn goacmedns.Account{}, ErrCNAMERequired{\n\t\tDomain: domain,\n\t\tFQDN:   fqdn,\n\t\tTarget: newAcct.FullDomain,\n\t}\n}\n\nfunc getStorage(config *Config) (goacmedns.Storage, error) {\n\tif config.StoragePath == \"\" && config.StorageBaseURL == \"\" {\n\t\treturn nil, errors.New(\"storagePath or storageBaseURL is not set\")\n\t}\n\n\tif config.StoragePath != \"\" && config.StorageBaseURL != \"\" {\n\t\treturn nil, errors.New(\"storagePath and storageBaseURL cannot be used at the same time\")\n\t}\n\n\tif config.StoragePath != \"\" {\n\t\treturn storage.NewFile(config.StoragePath, 0o600), nil\n\t}\n\n\tst, err := internal.NewHTTPStorage(config.StorageBaseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"new HTTP storage: %w\", err)\n\t}\n\n\treturn st, nil\n}\n"
  },
  {
    "path": "providers/dns/acmedns/acmedns.toml",
    "content": "Name = \"Joohoi's ACME-DNS\"\nDescription = ''''''\nURL = \"https://github.com/joohoi/acme-dns\"\nCode = \"acme-dns\"\nAliases = [\"acmedns\"] # TODO(ldez): remove \"-\" in v5\nSince = \"v1.1.0\"\n\nExample = '''\nACME_DNS_API_BASE=http://10.0.0.8:4443 \\\nACME_DNS_STORAGE_PATH=/root/.lego-acme-dns-accounts.json \\\nlego --dns \"acme-dns\" -d '*.example.com' -d example.com run\n\n# or\n\nACME_DNS_API_BASE=http://10.0.0.8:4443 \\\nACME_DNS_STORAGE_BASE_URL=http://10.10.10.10:80 \\\nlego --dns \"acme-dns\" -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    ACME_DNS_API_BASE  = \"The ACME-DNS API address\"\n    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.\"\n    ACME_DNS_STORAGE_BASE_URL = \"The ACME-DNS JSON account data server.\"\n  [Configuration.Additional]\n    ACME_DNS_ALLOWLIST = \"Source networks using CIDR notation (multiple values should be separated with a comma).\"\n\n[Links]\n  API = \"https://github.com/joohoi/acme-dns#api\"\n  GoClient = \"https://github.com/nrdcg/goacmedns\"\n"
  },
  {
    "path": "providers/dns/acmedns/acmedns_test.go",
    "content": "package acmedns\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/nrdcg/goacmedns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tegDomain  = \"example.com\"\n\tegFQDN    = \"_acme-challenge.\" + egDomain + \".\"\n\tegKeyAuth = \"⚷\"\n)\n\nfunc TestPresent(t *testing.T) {\n\t// validAccountStorage is a mockStorage configured to return the egTestAccount.\n\tvalidAccountStorage := newMockStorage().WithAccount(egDomain, egTestAccount)\n\n\t// validUpdateClient is a mockClient configured with the egTestAccount that will track TXT updates in a map.\n\tvalidUpdateClient := newMockClient()\n\n\ttestCases := []struct {\n\t\tName          string\n\t\tClient        acmeDNSClient\n\t\tStorage       goacmedns.Storage\n\t\tExpectedError error\n\t}{\n\t\t{\n\t\t\tName:          \"present when client storage returns unexpected error\",\n\t\t\tClient:        newMockClient().WithRegisterAccount(egTestAccount),\n\t\t\tStorage:       newMockStorage().WithFetchError(errorStorageErr),\n\t\t\tExpectedError: errorStorageErr,\n\t\t},\n\t\t{\n\t\t\tName:   \"present when client storage returns ErrDomainNotFound\",\n\t\t\tClient: newMockClient().WithRegisterAccount(egTestAccount),\n\t\t\tExpectedError: ErrCNAMERequired{\n\t\t\t\tDomain: egDomain,\n\t\t\t\tFQDN:   egFQDN,\n\t\t\t\tTarget: egTestAccount.FullDomain,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:          \"present when client UpdateTXTRecord returns unexpected error\",\n\t\t\tClient:        newMockClient().WithUpdateTXTRecordError(errorClientErr),\n\t\t\tStorage:       validAccountStorage,\n\t\t\tExpectedError: errorClientErr,\n\t\t},\n\t\t{\n\t\t\tName:    \"present when everything works\",\n\t\t\tStorage: validAccountStorage,\n\t\t\tClient:  validUpdateClient,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.Name, func(t *testing.T) {\n\t\t\tp := &DNSProvider{\n\t\t\t\tconfig:  NewDefaultConfig(),\n\t\t\t\tclient:  test.Client,\n\t\t\t\tstorage: newMockStorage(),\n\t\t\t}\n\n\t\t\tif test.Storage != nil {\n\t\t\t\tp.storage = test.Storage\n\t\t\t}\n\n\t\t\terr := p.Present(egDomain, \"foo\", egKeyAuth)\n\t\t\tif test.ExpectedError != nil {\n\t\t\t\tassert.Equal(t, test.ExpectedError, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n\n\t// Check that the success test case set a record.\n\tassert.Len(t, validUpdateClient.records, 1)\n\n\t// Check that the success test case set the right record for the right account.\n\tassert.Len(t, validUpdateClient.records[egTestAccount], 43)\n}\n\nfunc TestRegister(t *testing.T) {\n\ttestCases := []struct {\n\t\tName          string\n\t\tClient        acmeDNSClient\n\t\tStorage       goacmedns.Storage\n\t\tExpectedError error\n\t}{\n\t\t{\n\t\t\tName:          \"register when acme-dns client returns an error\",\n\t\t\tClient:        newMockClient().WithRegisterAccountError(errorClientErr),\n\t\t\tExpectedError: errorClientErr,\n\t\t},\n\t\t{\n\t\t\tName:          \"register when acme-dns storage put returns an error\",\n\t\t\tClient:        newMockClient().WithRegisterAccount(egTestAccount),\n\t\t\tStorage:       newMockStorage().WithPutError(errorStorageErr),\n\t\t\tExpectedError: errorStorageErr,\n\t\t},\n\t\t{\n\t\t\tName:          \"register when acme-dns storage save returns an error\",\n\t\t\tClient:        newMockClient().WithRegisterAccount(egTestAccount),\n\t\t\tStorage:       newMockStorage().WithSaveError(errorStorageErr),\n\t\t\tExpectedError: errorStorageErr,\n\t\t},\n\t\t{\n\t\t\tName:   \"register when everything works\",\n\t\t\tClient: newMockClient().WithRegisterAccount(egTestAccount),\n\t\t\tExpectedError: ErrCNAMERequired{\n\t\t\t\tDomain: egDomain,\n\t\t\t\tFQDN:   egFQDN,\n\t\t\t\tTarget: egTestAccount.FullDomain,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.Name, func(t *testing.T) {\n\t\t\tp := &DNSProvider{\n\t\t\t\tconfig:  NewDefaultConfig(),\n\t\t\t\tclient:  test.Client,\n\t\t\t\tstorage: newMockStorage(),\n\t\t\t}\n\n\t\t\tif test.Storage != nil {\n\t\t\t\tp.storage = test.Storage\n\t\t\t}\n\n\t\t\tacc, err := p.register(t.Context(), egDomain, egFQDN)\n\t\t\tif test.ExpectedError != nil {\n\t\t\t\tassert.Equal(t, test.ExpectedError, err)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, goacmedns.Account{}, acc)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPresent_httpStorage(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tStatusCode    int\n\t\tExpectedError error\n\t}{\n\t\t{\n\t\t\tdesc:       \"the CNAME is not handled by the storage\",\n\t\t\tStatusCode: http.StatusOK,\n\t\t\tExpectedError: ErrCNAMERequired{\n\t\t\t\tDomain: egDomain,\n\t\t\t\tFQDN:   egFQDN,\n\t\t\t\tTarget: egTestAccount.FullDomain,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:       \"the CNAME is handled by the storage\",\n\t\t\tStatusCode: http.StatusCreated,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tprovider := servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {\n\t\t\t\tconfig := NewDefaultConfig()\n\t\t\t\tconfig.StorageBaseURL = server.URL\n\n\t\t\t\treturn NewDNSProviderConfig(config)\n\t\t\t}).\n\t\t\t\t// Fetch\n\t\t\t\tRoute(\"GET /example.com\", servermock.Noop().WithStatusCode(http.StatusNotFound)).\n\t\t\t\t// Put\n\t\t\t\tRoute(\"POST /example.com\", servermock.Noop().WithStatusCode(test.StatusCode)).\n\t\t\t\tBuild(t)\n\n\t\t\tclient := newMockClient().WithRegisterAccount(egTestAccount)\n\t\t\tprovider.client = client\n\n\t\t\terr := provider.Present(egDomain, \"foo\", egKeyAuth)\n\t\t\tif test.ExpectedError != nil {\n\t\t\t\tassert.EqualError(t, err, test.ExpectedError.Error())\n\t\t\t\tassert.True(t, client.registerAccountCalled)\n\t\t\t\tassert.False(t, client.updateTXTRecordCalled)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.True(t, client.registerAccountCalled)\n\t\t\t\tassert.True(t, client.updateTXTRecordCalled)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRegister_httpStorage(t *testing.T) {\n\ttestCases := []struct {\n\t\tName          string\n\t\tStatusCode    int\n\t\tExpectedError error\n\t}{\n\t\t{\n\t\t\tName:       \"status code 200\",\n\t\t\tStatusCode: http.StatusOK,\n\t\t\tExpectedError: ErrCNAMERequired{\n\t\t\t\tDomain: egDomain,\n\t\t\t\tFQDN:   egFQDN,\n\t\t\t\tTarget: egTestAccount.FullDomain,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:       \"status code 201\",\n\t\t\tStatusCode: http.StatusCreated,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.Name, func(t *testing.T) {\n\t\t\tprovider := servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {\n\t\t\t\tconfig := NewDefaultConfig()\n\t\t\t\tconfig.StorageBaseURL = server.URL\n\n\t\t\t\treturn NewDNSProviderConfig(config)\n\t\t\t}).\n\t\t\t\t// Put\n\t\t\t\tRoute(\"POST /example.com\", servermock.Noop().WithStatusCode(test.StatusCode)).\n\t\t\t\tBuild(t)\n\n\t\t\tprovider.client = newMockClient().WithRegisterAccount(egTestAccount)\n\n\t\t\tacc, err := provider.register(t.Context(), egDomain, egFQDN)\n\t\t\tif test.ExpectedError != nil {\n\t\t\t\tassert.Equal(t, test.ExpectedError, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, egTestAccount, acc)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/acmedns/internal/fixtures/error.json",
    "content": "{\n  \"message\": \"There is an error\"\n}\n"
  },
  {
    "path": "providers/dns/acmedns/internal/fixtures/fetch-request.json",
    "content": "{\n  \"fulldomain\": \"foo.example.com\",\n  \"subdomain\": \"foo\",\n  \"username\": \"user\",\n  \"password\": \"secret\",\n  \"server_url\": \"https://example.com\"\n}\n"
  },
  {
    "path": "providers/dns/acmedns/internal/fixtures/fetch.json",
    "content": "{\n  \"fulldomain\": \"foo.example.com\",\n  \"subdomain\": \"foo\",\n  \"username\": \"user\",\n  \"password\": \"secret\",\n  \"server_url\": \"https://example.com\"\n}\n"
  },
  {
    "path": "providers/dns/acmedns/internal/fixtures/fetch_all.json",
    "content": "{\n  \"a\": {\n    \"fulldomain\": \"foo.example.com\",\n    \"subdomain\": \"foo\",\n    \"username\": \"user\",\n    \"password\": \"secret\",\n    \"server_url\": \"https://example.com\"\n  },\n  \"b\": {\n    \"fulldomain\": \"bar.example.com\",\n    \"subdomain\": \"bar\",\n    \"username\": \"user\",\n    \"password\": \"secret\",\n    \"server_url\": \"https://example.com\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/acmedns/internal/http_storage.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/nrdcg/goacmedns\"\n\t\"github.com/nrdcg/goacmedns/storage\"\n)\n\nvar _ goacmedns.Storage = (*HTTPStorage)(nil)\n\nvar ErrCNAMEAlreadyCreated = errors.New(\"the CNAME has already been created\")\n\n// HTTPStorage is an implementation of [acmedns.Storage] over HTTP.\ntype HTTPStorage struct {\n\tclient  *http.Client\n\tbaseURL *url.URL\n}\n\n// NewHTTPStorage created a new [HTTPStorage].\nfunc NewHTTPStorage(baseURL string) (*HTTPStorage, error) {\n\tendpoint, err := url.Parse(baseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &HTTPStorage{\n\t\tclient:  &http.Client{Timeout: 2 * time.Minute},\n\t\tbaseURL: endpoint,\n\t}, nil\n}\n\nfunc (s *HTTPStorage) Save(_ context.Context) error {\n\treturn nil\n}\n\nfunc (s *HTTPStorage) Put(ctx context.Context, domain string, account goacmedns.Account) error {\n\treq, err := newJSONRequest(ctx, http.MethodPost, s.baseURL.JoinPath(domain), account)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treturn s.do(req, nil)\n}\n\nfunc (s *HTTPStorage) Fetch(ctx context.Context, domain string) (goacmedns.Account, error) {\n\treq, err := newJSONRequest(ctx, http.MethodGet, s.baseURL.JoinPath(domain), nil)\n\tif err != nil {\n\t\treturn goacmedns.Account{}, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tvar account goacmedns.Account\n\n\terr = s.do(req, &account)\n\tif err != nil {\n\t\treturn goacmedns.Account{}, err\n\t}\n\n\treturn account, nil\n}\n\nfunc (s *HTTPStorage) FetchAll(ctx context.Context) (map[string]goacmedns.Account, error) {\n\treq, err := newJSONRequest(ctx, http.MethodGet, s.baseURL, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar mapping map[string]goacmedns.Account\n\n\terr = s.do(req, &mapping)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn mapping, nil\n}\n\nfunc (s *HTTPStorage) do(req *http.Request, result any) error {\n\tresp, err := s.client.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode == http.StatusNotFound {\n\t\treturn storage.ErrDomainNotFound\n\t}\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\t// Hack related to `Put`.\n\t\tif resp.StatusCode == http.StatusCreated {\n\t\t\treturn ErrCNAMEAlreadyCreated\n\t\t}\n\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/acmedns/internal/http_storage_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/nrdcg/goacmedns\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*HTTPStorage] {\n\treturn servermock.NewBuilder[*HTTPStorage](\n\t\tfunc(server *httptest.Server) (*HTTPStorage, error) {\n\t\t\tstorage, err := NewHTTPStorage(server.URL)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tstorage.client = server.Client()\n\n\t\t\treturn storage, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders())\n}\n\nfunc TestHTTPStorage_Fetch(t *testing.T) {\n\tstorage := mockBuilder().\n\t\tRoute(\"GET /example.com\", servermock.ResponseFromFixture(\"fetch.json\")).\n\t\tBuild(t)\n\n\taccount, err := storage.Fetch(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := goacmedns.Account{\n\t\tFullDomain: \"foo.example.com\",\n\t\tSubDomain:  \"foo\",\n\t\tUsername:   \"user\",\n\t\tPassword:   \"secret\",\n\t\tServerURL:  \"https://example.com\",\n\t}\n\n\tassert.Equal(t, expected, account)\n}\n\nfunc TestHTTPStorage_Fetch_error(t *testing.T) {\n\tstorage := mockBuilder().\n\t\tRoute(\"GET /example.com\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusInternalServerError)).\n\t\tBuild(t)\n\n\t_, err := storage.Fetch(t.Context(), \"example.com\")\n\trequire.Error(t, err)\n}\n\nfunc TestHTTPStorage_FetchAll(t *testing.T) {\n\tstorage := mockBuilder().\n\t\tRoute(\"GET /\", servermock.ResponseFromFixture(\"fetch_all.json\")).\n\t\tBuild(t)\n\n\taccount, err := storage.FetchAll(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := map[string]goacmedns.Account{\n\t\t\"a\": {\n\t\t\tFullDomain: \"foo.example.com\",\n\t\t\tSubDomain:  \"foo\",\n\t\t\tUsername:   \"user\",\n\t\t\tPassword:   \"secret\",\n\t\t\tServerURL:  \"https://example.com\",\n\t\t},\n\t\t\"b\": {\n\t\t\tFullDomain: \"bar.example.com\",\n\t\t\tSubDomain:  \"bar\",\n\t\t\tUsername:   \"user\",\n\t\t\tPassword:   \"secret\",\n\t\t\tServerURL:  \"https://example.com\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, account)\n}\n\nfunc TestHTTPStorage_FetchAll_error(t *testing.T) {\n\tstorage := mockBuilder().\n\t\tRoute(\"GET /\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusInternalServerError)).\n\t\tBuild(t)\n\n\t_, err := storage.FetchAll(t.Context())\n\trequire.Error(t, err)\n}\n\nfunc TestHTTPStorage_Put(t *testing.T) {\n\tstorage := mockBuilder().\n\t\tRoute(\"POST /example.com\", nil,\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"fetch-request.json\")).\n\t\tBuild(t)\n\n\taccount := goacmedns.Account{\n\t\tFullDomain: \"foo.example.com\",\n\t\tSubDomain:  \"foo\",\n\t\tUsername:   \"user\",\n\t\tPassword:   \"secret\",\n\t\tServerURL:  \"https://example.com\",\n\t}\n\n\terr := storage.Put(t.Context(), \"example.com\", account)\n\trequire.NoError(t, err)\n}\n\nfunc TestHTTPStorage_Put_error(t *testing.T) {\n\tstorage := mockBuilder().\n\t\tRoute(\"POST /example.com\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusInternalServerError)).\n\t\tBuild(t)\n\n\taccount := goacmedns.Account{\n\t\tFullDomain: \"foo.example.com\",\n\t\tSubDomain:  \"foo\",\n\t\tUsername:   \"user\",\n\t\tPassword:   \"secret\",\n\t\tServerURL:  \"https://example.com\",\n\t}\n\n\terr := storage.Put(t.Context(), \"example.com\", account)\n\trequire.Error(t, err)\n}\n\nfunc TestHTTPStorage_Put_CNAME_created(t *testing.T) {\n\tstorage := mockBuilder().\n\t\tRoute(\"POST /example.com\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"fetch-request.json\")).\n\t\tBuild(t)\n\n\taccount := goacmedns.Account{\n\t\tFullDomain: \"foo.example.com\",\n\t\tSubDomain:  \"foo\",\n\t\tUsername:   \"user\",\n\t\tPassword:   \"secret\",\n\t\tServerURL:  \"https://example.com\",\n\t}\n\n\terr := storage.Put(t.Context(), \"example.com\", account)\n\trequire.ErrorIs(t, err, ErrCNAMEAlreadyCreated)\n}\n"
  },
  {
    "path": "providers/dns/acmedns/internal/readme.md",
    "content": "# HTTP Storage\n\n## Fetch\n\n### Request\n\nEndpoint: `GET <BaseURL>/<domain>`\n\n### Response\n\nResponse status code 200.\n\nResponse body (account):\n\n```json\n{\n  \"fulldomain\": \"foo.example.com\",\n  \"subdomain\": \"foo\",\n  \"username\": \"user\",\n  \"password\": \"secret\",\n  \"server_url\": \"https://example.com\"\n}\n```\n\n## Fetch All\n\n### Request\n\nEndpoint: `GET <BaseURL>`\n\n### Response\n\nResponse status code 200.\n\nResponse body (domain/account mapping):\n\n```json\n{\n  \"foo.example.com\": {\n    \"fulldomain\": \"foo.example.com\",\n    \"subdomain\": \"foo\",\n    \"username\": \"user\",\n    \"password\": \"secret\",\n    \"server_url\": \"https://example.com\"\n  },\n  \"bar.example.com\": {\n    \"fulldomain\": \"bar.example.com\",\n    \"subdomain\": \"bar\",\n    \"username\": \"user\",\n    \"password\": \"secret\",\n    \"server_url\": \"https://example.com\"\n  }\n}\n```\n\n## Put\n\n### Request\n\nEndpoint: `POST <BaseURL>/<domain>`\n\n### Response\n\nResponse status code:\n- 200: the process will be stopped to allow the user to create the CNAME.\n- 201: the process will continue without error (the CNAME should be created by the server)\n\nNo expected body.\n\n## Save\n\nNo dedicated endpoint.\n"
  },
  {
    "path": "providers/dns/acmedns/mock_test.go",
    "content": "package acmedns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/nrdcg/goacmedns\"\n\t\"github.com/nrdcg/goacmedns/storage\"\n)\n\nvar (\n\t// errorClientErr is used by the Client mocks that return an error.\n\terrorClientErr = errors.New(\"errorClient always errors\")\n\t// errorStorageErr is used by the Storage mocks that return an error.\n\terrorStorageErr = errors.New(\"errorStorage always errors\")\n)\n\nvar egTestAccount = goacmedns.Account{\n\tFullDomain: \"acme-dns.\" + egDomain,\n\tSubDomain:  \"random-looking-junk.\" + egDomain,\n\tUsername:   \"spooky.mulder\",\n\tPassword:   \"trustno1\",\n}\n\ntype mockClient struct {\n\trecords map[goacmedns.Account]string\n\n\tupdateTXTRecordCalled bool\n\tupdateTXTRecord       func(ctx context.Context, acct goacmedns.Account, value string) error\n\n\tregisterAccountCalled bool\n\tregisterAccount       func(ctx context.Context, allowFrom []string) (goacmedns.Account, error)\n}\n\nfunc newMockClient() *mockClient {\n\treturn &mockClient{\n\t\trecords: make(map[goacmedns.Account]string),\n\t\tupdateTXTRecord: func(_ context.Context, _ goacmedns.Account, _ string) error {\n\t\t\treturn nil\n\t\t},\n\t\tregisterAccount: func(_ context.Context, _ []string) (goacmedns.Account, error) {\n\t\t\treturn goacmedns.Account{}, nil\n\t\t},\n\t}\n}\n\nfunc (c *mockClient) UpdateTXTRecord(ctx context.Context, acct goacmedns.Account, value string) error {\n\tc.updateTXTRecordCalled = true\n\tc.records[acct] = value\n\n\treturn c.updateTXTRecord(ctx, acct, value)\n}\n\nfunc (c *mockClient) RegisterAccount(ctx context.Context, allowFrom []string) (goacmedns.Account, error) {\n\tc.registerAccountCalled = true\n\treturn c.registerAccount(ctx, allowFrom)\n}\n\nfunc (c *mockClient) WithUpdateTXTRecordError(err error) *mockClient {\n\tc.updateTXTRecord = func(_ context.Context, _ goacmedns.Account, _ string) error {\n\t\treturn err\n\t}\n\n\treturn c\n}\n\nfunc (c *mockClient) WithRegisterAccount(acct goacmedns.Account) *mockClient {\n\tc.registerAccount = func(_ context.Context, _ []string) (goacmedns.Account, error) {\n\t\treturn acct, nil\n\t}\n\n\treturn c\n}\n\nfunc (c *mockClient) WithRegisterAccountError(err error) *mockClient {\n\tc.registerAccount = func(_ context.Context, _ []string) (goacmedns.Account, error) {\n\t\treturn goacmedns.Account{}, err\n\t}\n\n\treturn c\n}\n\ntype mockStorage struct {\n\taccounts map[string]goacmedns.Account\n\tfetchAll func(ctx context.Context) (map[string]goacmedns.Account, error)\n\tfetch    func(ctx context.Context, domain string) (goacmedns.Account, error)\n\tput      func(ctx context.Context, domain string, acct goacmedns.Account) error\n\tsave     func(ctx context.Context) error\n}\n\nfunc newMockStorage() *mockStorage {\n\tm := &mockStorage{\n\t\taccounts: make(map[string]goacmedns.Account),\n\t\tput: func(_ context.Context, _ string, _ goacmedns.Account) error {\n\t\t\treturn nil\n\t\t},\n\t\tsave: func(_ context.Context) error {\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tm.fetchAll = func(ctx context.Context) (map[string]goacmedns.Account, error) {\n\t\treturn m.accounts, nil\n\t}\n\n\tm.fetch = func(_ context.Context, domain string) (goacmedns.Account, error) {\n\t\tif acct, ok := m.accounts[domain]; ok {\n\t\t\treturn acct, nil\n\t\t}\n\n\t\treturn goacmedns.Account{}, storage.ErrDomainNotFound\n\t}\n\n\treturn m\n}\n\nfunc (m *mockStorage) FetchAll(ctx context.Context) (map[string]goacmedns.Account, error) {\n\treturn m.fetchAll(ctx)\n}\n\nfunc (m *mockStorage) Fetch(ctx context.Context, domain string) (goacmedns.Account, error) {\n\treturn m.fetch(ctx, domain)\n}\n\nfunc (m *mockStorage) Put(ctx context.Context, domain string, account goacmedns.Account) error {\n\treturn m.put(ctx, domain, account)\n}\n\nfunc (m *mockStorage) Save(ctx context.Context) error {\n\treturn m.save(ctx)\n}\n\nfunc (m *mockStorage) WithAccount(domain string, acct goacmedns.Account) *mockStorage {\n\tm.accounts[domain] = acct\n\n\treturn m\n}\n\nfunc (m *mockStorage) WithFetchError(err error) *mockStorage {\n\tm.fetch = func(_ context.Context, _ string) (goacmedns.Account, error) {\n\t\treturn goacmedns.Account{}, err\n\t}\n\n\treturn m\n}\n\nfunc (m *mockStorage) WithPutError(err error) *mockStorage {\n\tm.put = func(_ context.Context, _ string, _ goacmedns.Account) error {\n\t\treturn err\n\t}\n\n\treturn m\n}\n\nfunc (m *mockStorage) WithSaveError(err error) *mockStorage {\n\tm.save = func(ctx context.Context) error {\n\t\treturn err\n\t}\n\n\treturn m\n}\n"
  },
  {
    "path": "providers/dns/active24/active24.go",
    "content": "// Package active24 implements a DNS provider for solving the DNS-01 challenge using Active24.\npackage active24\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/active24\"\n)\n\nconst baseAPIDomain = \"active24.cz\"\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"ACTIVE24_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\tEnvSecret = envNamespace + \"SECRET\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config = active24.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Active24.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey, EnvSecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"active24: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.Secret = values[EnvSecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Active24.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"active24: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := active24.NewDNSProviderConfig(config, baseAPIDomain)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"active24: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"active24: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"active24: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n"
  },
  {
    "path": "providers/dns/active24/active24.toml",
    "content": "Name = \"Active24\"\nDescription = ''''''\nURL = \"https://www.active24.cz\"\nCode = \"active24\"\nSince = \"v4.23.0\"\n\nExample = '''\nACTIVE24_API_KEY=\"xxx\" \\\nACTIVE24_SECRET=\"yyy\" \\\nlego --dns active24 -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    ACTIVE24_API_KEY = \"API key\"\n    ACTIVE24_SECRET = \"Secret\"\n  [Configuration.Additional]\n    ACTIVE24_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    ACTIVE24_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    ACTIVE24_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    ACTIVE24_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://rest.active24.cz/v2/docs\"\n  APIv1 = \"https://rest.active24.cz/docs/v1.service#services\"\n"
  },
  {
    "path": "providers/dns/active24/active24_test.go",
    "content": "package active24\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey, EnvSecret).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"user\",\n\t\t\t\tEnvSecret: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t\tEnvSecret: \"secret\",\n\t\t\t},\n\t\t\texpected: \"active24: some credentials information are missing: ACTIVE24_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"user\",\n\t\t\t\tEnvSecret: \"\",\n\t\t\t},\n\t\t\texpected: \"active24: some credentials information are missing: ACTIVE24_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"active24: some credentials information are missing: ACTIVE24_API_KEY,ACTIVE24_SECRET\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tsecret   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"user\",\n\t\t\tsecret: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\tapiKey:   \"\",\n\t\t\tsecret:   \"secret\",\n\t\t\texpected: \"active24: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret\",\n\t\t\tapiKey:   \"user\",\n\t\t\tsecret:   \"\",\n\t\t\texpected: \"active24: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"active24: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.Secret = test.secret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/alidns/alidns.go",
    "content": "// Package alidns implements a DNS provider for solving the DNS-01 challenge using Alibaba Cloud DNS.\npackage alidns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/aliyun/credentials-go/credentials\"\n\talidns \"github.com/go-acme/alidns-20150109/v4/client\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n\t\"golang.org/x/net/idna\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"ALICLOUD_\"\n\n\tEnvRAMRole       = envNamespace + \"RAM_ROLE\"\n\tEnvAccessKey     = envNamespace + \"ACCESS_KEY\"\n\tEnvSecretKey     = envNamespace + \"SECRET_KEY\"\n\tEnvSecurityToken = envNamespace + \"SECURITY_TOKEN\"\n\tEnvRegionID      = envNamespace + \"REGION_ID\"\n\tEnvLine          = envNamespace + \"LINE\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultRegionID = \"cn-hangzhou\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tRAMRole            string\n\tAPIKey             string\n\tSecretKey          string\n\tSecurityToken      string\n\tRegionID           string\n\tLine               string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *alidns.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Alibaba Cloud DNS.\n// - If you're using the instance RAM role, the RAM role environment variable must be passed in: ALICLOUD_RAM_ROLE.\n// - Other than that, credentials must be passed in the environment variables:\n// ALICLOUD_ACCESS_KEY, ALICLOUD_SECRET_KEY, and optionally ALICLOUD_SECURITY_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tconfig := NewDefaultConfig()\n\tconfig.RegionID = env.GetOrFile(EnvRegionID)\n\tconfig.Line = env.GetOrFile(EnvLine)\n\n\tvalues, err := env.Get(EnvRAMRole)\n\tif err == nil {\n\t\tconfig.RAMRole = values[EnvRAMRole]\n\t\treturn NewDNSProviderConfig(config)\n\t}\n\n\tvalues, err = env.Get(EnvAccessKey, EnvSecretKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"alicloud: %w\", err)\n\t}\n\n\tconfig.APIKey = values[EnvAccessKey]\n\tconfig.SecretKey = values[EnvSecretKey]\n\tconfig.SecurityToken = env.GetOrFile(EnvSecurityToken)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for alidns.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"alicloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.RegionID == \"\" {\n\t\tconfig.RegionID = defaultRegionID\n\t}\n\n\tcfg := new(openapi.Config).\n\t\tSetRegionId(config.RegionID).\n\t\tSetReadTimeout(int(config.HTTPTimeout.Milliseconds()))\n\n\tswitch {\n\tcase config.RAMRole != \"\":\n\t\t// https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance\n\t\tcredentialsCfg := new(credentials.Config).\n\t\t\tSetType(\"ecs_ram_role\").\n\t\t\tSetRoleName(config.RAMRole)\n\n\t\tcredentialClient, err := credentials.NewCredential(credentialsCfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"alicloud: new credential: %w\", err)\n\t\t}\n\n\t\tcfg = cfg.SetCredential(credentialClient)\n\n\tcase config.APIKey != \"\" && config.SecretKey != \"\" && config.SecurityToken != \"\":\n\t\tcfg = cfg.\n\t\t\tSetAccessKeyId(config.APIKey).\n\t\t\tSetAccessKeySecret(config.SecretKey).\n\t\t\tSetSecurityToken(config.SecurityToken)\n\n\tcase config.APIKey != \"\" && config.SecretKey != \"\":\n\t\tcfg = cfg.\n\t\t\tSetAccessKeyId(config.APIKey).\n\t\t\tSetAccessKeySecret(config.SecretKey)\n\n\tdefault:\n\t\treturn nil, errors.New(\"alicloud: ram role or credentials missing\")\n\t}\n\n\tclient, err := alidns.NewClient(cfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"alicloud: new client: %w\", err)\n\t}\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneName, err := d.getHostedZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"alicloud: %w\", err)\n\t}\n\n\trecordRequest, err := d.newTxtRecord(zoneName, info.EffectiveFQDN, info.Value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = alidns.AddDomainRecordWithContext(ctx, d.client, recordRequest, &dara.RuntimeOptions{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"alicloud: API call failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecords, err := d.findTxtRecords(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"alicloud: %w\", err)\n\t}\n\n\t_, err = d.getHostedZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"alicloud: %w\", err)\n\t}\n\n\tfor _, rec := range records {\n\t\trequest := &alidns.DeleteDomainRecordRequest{\n\t\t\tRecordId: rec.RecordId,\n\t\t}\n\n\t\t_, err = alidns.DeleteDomainRecordWithContext(ctx, d.client, request, &dara.RuntimeOptions{})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"alicloud: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (string, error) {\n\trequest := new(alidns.DescribeDomainsRequest)\n\n\tvar domains []*alidns.DescribeDomainsResponseBodyDomainsDomain\n\n\tvar startPage int64 = 1\n\n\tfor {\n\t\trequest.SetPageNumber(startPage)\n\n\t\tresponse, err := alidns.DescribeDomainsWithContext(ctx, d.client, request, &dara.RuntimeOptions{})\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"API call failed: %w\", err)\n\t\t}\n\n\t\tdomains = append(domains, response.Body.Domains.Domain...)\n\n\t\tif ptr.Deref(response.Body.PageNumber)*ptr.Deref(response.Body.PageSize) >= ptr.Deref(response.Body.TotalCount) {\n\t\t\tbreak\n\t\t}\n\n\t\tstartPage++\n\t}\n\n\tauthZone, err := dns01.FindZoneByFqdn(domain)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tvar hostedZone *alidns.DescribeDomainsResponseBodyDomainsDomain\n\n\tfor _, zone := range domains {\n\t\tif ptr.Deref(zone.DomainName) == dns01.UnFqdn(authZone) || ptr.Deref(zone.PunyCode) == dns01.UnFqdn(authZone) {\n\t\t\thostedZone = zone\n\t\t}\n\t}\n\n\tif hostedZone == nil || ptr.Deref(hostedZone.DomainId) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"zone %s not found in AliDNS for domain %s\", authZone, domain)\n\t}\n\n\treturn ptr.Deref(hostedZone.DomainName), nil\n}\n\nfunc (d *DNSProvider) newTxtRecord(zone, fqdn, value string) (*alidns.AddDomainRecordRequest, error) {\n\trr, err := extractRecordName(fqdn, zone)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tadrr := new(alidns.AddDomainRecordRequest).\n\t\tSetType(\"TXT\").\n\t\tSetDomainName(zone).\n\t\tSetRR(rr).\n\t\tSetValue(value).\n\t\tSetTTL(int64(d.config.TTL))\n\n\tif d.config.Line != \"\" {\n\t\tadrr.SetLine(d.config.Line)\n\t}\n\n\treturn adrr, nil\n}\n\nfunc (d *DNSProvider) findTxtRecords(ctx context.Context, fqdn string) ([]*alidns.DescribeDomainRecordsResponseBodyDomainRecordsRecord, error) {\n\tzoneName, err := d.getHostedZone(ctx, fqdn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trequest := new(alidns.DescribeDomainRecordsRequest).\n\t\tSetDomainName(zoneName).\n\t\tSetPageSize(500)\n\n\tvar records []*alidns.DescribeDomainRecordsResponseBodyDomainRecordsRecord\n\n\tresult, err := alidns.DescribeDomainRecordsWithContext(ctx, d.client, request, &dara.RuntimeOptions{})\n\tif err != nil {\n\t\treturn records, fmt.Errorf(\"API call has failed: %w\", err)\n\t}\n\n\trecordName, err := extractRecordName(fqdn, zoneName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, record := range result.Body.DomainRecords.Record {\n\t\tif ptr.Deref(record.RR) == recordName && ptr.Deref(record.Type) == \"TXT\" {\n\t\t\trecords = append(records, record)\n\t\t}\n\t}\n\n\treturn records, nil\n}\n\nfunc extractRecordName(fqdn, zone string) (string, error) {\n\tasciiDomain, err := idna.ToASCII(zone)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"fail to convert punycode: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, asciiDomain)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn subDomain, nil\n}\n"
  },
  {
    "path": "providers/dns/alidns/alidns.toml",
    "content": "Name = \"Alibaba Cloud DNS\"\nDescription = ''''''\nURL = \"https://www.alibabacloud.com/product/dns\"\nCode = \"alidns\"\nSince = \"v1.1.0\"\n\nExample = '''\n# Setup using instance RAM role\nALICLOUD_RAM_ROLE=lego \\\nlego --dns alidns -d '*.example.com' -d example.com run\n\n# Or, using credentials\nALICLOUD_ACCESS_KEY=abcdefghijklmnopqrstuvwx \\\nALICLOUD_SECRET_KEY=your-secret-key \\\nALICLOUD_SECURITY_TOKEN=your-sts-token \\\nlego --dns alidns - -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    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)\"\n    ALICLOUD_ACCESS_KEY = \"Access key ID\"\n    ALICLOUD_SECRET_KEY = \"Access Key secret\"\n    ALICLOUD_SECURITY_TOKEN = \"STS Security Token (optional)\"\n  [Configuration.Additional]\n    ALICLOUD_REGION_ID = \"Region ID (Default: cn-hangzhou)\"\n    ALICLOUD_LINE = \"Line (Default: default)\"\n    ALICLOUD_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    ALICLOUD_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    ALICLOUD_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)\"\n    ALICLOUD_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://www.alibabacloud.com/help/en/alibaba-cloud-dns/latest/api-alidns-2015-01-09-dir-parsing-records\"\n  GoClient = \"https://github.com/alibabacloud-go/alidns-20150109\"\n  GoClient2 = \"https://github.com/aliyun/alibabacloud-go-sdk/tree/HEAD/alidns-20150109\"\n"
  },
  {
    "path": "providers/dns/alidns/alidns_test.go",
    "content": "package alidns\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAccessKey,\n\tEnvSecretKey,\n\tEnvRAMRole).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKey: \"123\",\n\t\t\t\tEnvSecretKey: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success (RAM role)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvRAMRole: \"LegoInstanceRole\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKey: \"\",\n\t\t\t\tEnvSecretKey: \"\",\n\t\t\t},\n\t\t\texpected: \"alicloud: some credentials information are missing: ALICLOUD_ACCESS_KEY,ALICLOUD_SECRET_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKey: \"\",\n\t\t\t\tEnvSecretKey: \"456\",\n\t\t\t},\n\t\t\texpected: \"alicloud: some credentials information are missing: ALICLOUD_ACCESS_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKey: \"123\",\n\t\t\t\tEnvSecretKey: \"\",\n\t\t\t},\n\t\t\texpected: \"alicloud: some credentials information are missing: ALICLOUD_SECRET_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tramRole   string\n\t\tapiKey    string\n\t\tsecretKey string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tapiKey:    \"123\",\n\t\t\tsecretKey: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:    \"success\",\n\t\t\tramRole: \"LegoInstanceRole\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"alicloud: ram role or credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing api key\",\n\t\t\tsecretKey: \"456\",\n\t\t\texpected:  \"alicloud: ram role or credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret key\",\n\t\t\tapiKey:   \"123\",\n\t\t\texpected: \"alicloud: ram role or credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.SecretKey = test.secretKey\n\t\t\tconfig.RAMRole = test.ramRole\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/aliesa/aliesa.go",
    "content": "// Package aliesa implements a DNS provider for solving the DNS-01 challenge using AlibabaCloud ESA.\npackage aliesa\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\topenapi \"github.com/alibabacloud-go/darabonba-openapi/v2/client\"\n\t\"github.com/alibabacloud-go/tea/dara\"\n\t\"github.com/aliyun/credentials-go/credentials\"\n\tesa \"github.com/go-acme/esa-20240910/v2/client\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"ALIESA_\"\n\n\tEnvRAMRole       = envNamespace + \"RAM_ROLE\"\n\tEnvAccessKey     = envNamespace + \"ACCESS_KEY\"\n\tEnvSecretKey     = envNamespace + \"SECRET_KEY\"\n\tEnvSecurityToken = envNamespace + \"SECURITY_TOKEN\"\n\tEnvRegionID      = envNamespace + \"REGION_ID\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultRegionID = \"cn-hangzhou\"\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tRAMRole       string\n\tAPIKey        string\n\tSecretKey     string\n\tSecurityToken string\n\tRegionID      string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *esa.Client\n\n\trecordIDs   map[string]int64\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for AlibabaCloud ESA.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tconfig := NewDefaultConfig()\n\tconfig.RegionID = env.GetOrFile(EnvRegionID)\n\n\tvalues, err := env.Get(EnvRAMRole)\n\tif err == nil {\n\t\tconfig.RAMRole = values[EnvRAMRole]\n\t\treturn NewDNSProviderConfig(config)\n\t}\n\n\tvalues, err = env.Get(EnvAccessKey, EnvSecretKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"aliesa: %w\", err)\n\t}\n\n\tconfig.APIKey = values[EnvAccessKey]\n\tconfig.SecretKey = values[EnvSecretKey]\n\tconfig.SecurityToken = env.GetOrFile(EnvSecurityToken)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for AlibabaCloud ESA.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"aliesa: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.RegionID == \"\" {\n\t\tconfig.RegionID = defaultRegionID\n\t}\n\n\tcfg := new(openapi.Config).\n\t\tSetRegionId(config.RegionID).\n\t\tSetReadTimeout(int(config.HTTPTimeout.Milliseconds()))\n\n\tswitch {\n\tcase config.RAMRole != \"\":\n\t\t// https://www.alibabacloud.com/help/en/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance\n\t\tcredentialsCfg := new(credentials.Config).\n\t\t\tSetType(\"ecs_ram_role\").\n\t\t\tSetRoleName(config.RAMRole)\n\n\t\tcredentialClient, err := credentials.NewCredential(credentialsCfg)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"aliesa: new credential: %w\", err)\n\t\t}\n\n\t\tcfg = cfg.SetCredential(credentialClient)\n\n\tcase config.APIKey != \"\" && config.SecretKey != \"\" && config.SecurityToken != \"\":\n\t\tcfg = cfg.\n\t\t\tSetAccessKeyId(config.APIKey).\n\t\t\tSetAccessKeySecret(config.SecretKey).\n\t\t\tSetSecurityToken(config.SecurityToken)\n\n\tcase config.APIKey != \"\" && config.SecretKey != \"\":\n\t\tcfg = cfg.\n\t\t\tSetAccessKeyId(config.APIKey).\n\t\t\tSetAccessKeySecret(config.SecretKey)\n\n\tdefault:\n\t\treturn nil, errors.New(\"aliesa: ram role or credentials missing\")\n\t}\n\n\tclient, err := esa.NewClient(cfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"aliesa: new client: %w\", err)\n\t}\n\n\t// Workaround to get a regional URL.\n\t// https://github.com/alibabacloud-go/esa-20240910/blame/7660e3aab2045d4820e4b83427a154efe0c79319/client/client.go#L27\n\t// The `EndpointRule` is hardcoded with an empty string, so the region is ignored.\n\tclient.Endpoint = nil\n\tclient.EndpointRule = ptr.Pointer(\"regional\")\n\n\tclient.Endpoint, err = esa.GetEndpoint(client, dara.String(\"esa\"), client.RegionId, client.EndpointRule, client.Network, client.Suffix, client.EndpointMap, client.Endpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"aliesa: get endpoint: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]int64),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tsiteID, err := d.getSiteID(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"aliesa: %w\", err)\n\t}\n\n\tcrReq := new(esa.CreateRecordRequest).\n\t\tSetSiteId(siteID).\n\t\tSetType(\"TXT\").\n\t\tSetRecordName(dns01.UnFqdn(info.EffectiveFQDN)).\n\t\tSetTtl(int32(d.config.TTL)).\n\t\tSetData(new(esa.CreateRecordRequestData).SetValue(info.Value))\n\n\t// https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-createrecord\n\tcrResp, err := esa.CreateRecordWithContext(ctx, d.client, crReq, &dara.RuntimeOptions{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"aliesa: create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = ptr.Deref(crResp.Body.GetRecordId())\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// gets the record's unique ID\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"aliesa: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\tdrReq := new(esa.DeleteRecordRequest).\n\t\tSetRecordId(recordID)\n\n\t// https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-deleterecord\n\t_, err := esa.DeleteRecordWithContext(ctx, d.client, drReq, &dara.RuntimeOptions{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"aliesa: delete record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) getSiteID(ctx context.Context, fqdn string) (int64, error) {\n\tauthZone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"aliesa: could not find zone for domain %q: %w\", fqdn, err)\n\t}\n\n\tlsReq := new(esa.ListSitesRequest).\n\t\tSetSiteName(dns01.UnFqdn(authZone)).\n\t\tSetSiteSearchType(\"suffix\")\n\n\t// https://www.alibabacloud.com/help/en/edge-security-acceleration/esa/api-esa-2024-09-10-listsites\n\tlsResp, err := esa.ListSitesWithContext(ctx, d.client, lsReq, &dara.RuntimeOptions{})\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"list sites: %w\", err)\n\t}\n\n\tfor f := range dns01.UnFqdnDomainsSeq(fqdn) {\n\t\tdomain := dns01.UnFqdn(f)\n\n\t\tfor _, site := range lsResp.Body.GetSites() {\n\t\t\tif ptr.Deref(site.GetSiteName()) == domain {\n\t\t\t\treturn ptr.Deref(site.GetSiteId()), nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn 0, fmt.Errorf(\"site not found (fqdn: %q)\", fqdn)\n}\n"
  },
  {
    "path": "providers/dns/aliesa/aliesa.toml",
    "content": "Name = \"AlibabaCloud ESA\"\nDescription = ''''''\nURL = \"https://www.alibabacloud.com/en/product/esa\"\nCode = \"aliesa\"\nSince = \"v4.29.0\"\n\nExample = '''\n# Setup using instance RAM role\nALIESA_RAM_ROLE=lego \\\nlego --dns aliesa -d '*.example.com' -d example.com run\n\n# Or, using credentials\nALIESA_ACCESS_KEY=abcdefghijklmnopqrstuvwx \\\nALIESA_SECRET_KEY=your-secret-key \\\nALIESA_SECURITY_TOKEN=your-sts-token \\\nlego --dns aliesa - -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    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)\"\n    ALIESA_ACCESS_KEY = \"Access key ID\"\n    ALIESA_SECRET_KEY = \"Access Key secret\"\n    ALIESA_SECURITY_TOKEN = \"STS Security Token (optional)\"\n  [Configuration.Additional]\n    ALIESA_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    ALIESA_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    ALIESA_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    ALIESA_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  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\"\n  GoClient = \"https://github.com/alibabacloud-go/esa-20240910\"\n"
  },
  {
    "path": "providers/dns/aliesa/aliesa_test.go",
    "content": "package aliesa\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAccessKey,\n\tEnvSecretKey,\n\tEnvRAMRole).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKey: \"123\",\n\t\t\t\tEnvSecretKey: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success (RAM role)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvRAMRole: \"LegoInstanceRole\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKey: \"\",\n\t\t\t\tEnvSecretKey: \"\",\n\t\t\t},\n\t\t\texpected: \"aliesa: some credentials information are missing: ALIESA_ACCESS_KEY,ALIESA_SECRET_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKey: \"\",\n\t\t\t\tEnvSecretKey: \"456\",\n\t\t\t},\n\t\t\texpected: \"aliesa: some credentials information are missing: ALIESA_ACCESS_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKey: \"123\",\n\t\t\t\tEnvSecretKey: \"\",\n\t\t\t},\n\t\t\texpected: \"aliesa: some credentials information are missing: ALIESA_SECRET_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tramRole   string\n\t\tapiKey    string\n\t\tsecretKey string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tapiKey:    \"123\",\n\t\t\tsecretKey: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:    \"success\",\n\t\t\tramRole: \"LegoInstanceRole\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"aliesa: ram role or credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing api key\",\n\t\t\tsecretKey: \"456\",\n\t\t\texpected:  \"aliesa: ram role or credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret key\",\n\t\t\tapiKey:   \"123\",\n\t\t\texpected: \"aliesa: ram role or credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.SecretKey = test.secretKey\n\t\t\tconfig.RAMRole = test.ramRole\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/allinkl/allinkl.go",
    "content": "// Package allinkl implements a DNS provider for solving the DNS-01 challenge using all-inkl.\npackage allinkl\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/allinkl/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"ALL_INKL_\"\n\n\tEnvLogin    = envNamespace + \"LOGIN\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tLogin              string\n\tPassword           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\n\tidentifier *internal.Identifier\n\tclient     *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for all-inkl.\n// Credentials must be passed in the environment variable: ALL_INKL_LOGIN, ALL_INKL_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvLogin, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"allinkl: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Login = values[EnvLogin]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for all-inkl.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"allinkl: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Login == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"allinkl: missing credentials\")\n\t}\n\n\tidentifier := internal.NewIdentifier(config.Login, config.Password)\n\n\tif config.HTTPClient != nil {\n\t\tidentifier.HTTPClient = config.HTTPClient\n\t}\n\n\tidentifier.HTTPClient = clientdebug.Wrap(identifier.HTTPClient)\n\n\tclient := internal.NewClient(config.Login)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:     config,\n\t\tidentifier: identifier,\n\t\tclient:     client,\n\t\trecordIDs:  make(map[string]string),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tcredential, err := d.identifier.Authentication(ctx, 60, true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"allinkl: authentication: %w\", err)\n\t}\n\n\tctx = internal.WithContext(ctx, credential)\n\n\tauthZone, err := d.findZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"allinkl: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"allinkl: %w\", err)\n\t}\n\n\trecord := internal.DNSRequest{\n\t\tZoneHost:   authZone,\n\t\tRecordType: \"TXT\",\n\t\tRecordName: subDomain,\n\t\tRecordData: info.Value,\n\t}\n\n\trecordID, err := d.client.AddDNSSettings(ctx, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"allinkl: add DNS settings: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = recordID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tcredential, err := d.identifier.Authentication(ctx, 60, true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"allinkl: authentication: %w\", err)\n\t}\n\n\tctx = internal.WithContext(ctx, credential)\n\n\t// gets the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"allinkl: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\t_, err = d.client.DeleteDNSSettings(ctx, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"allinkl: delete DNS settings: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) findZone(ctx context.Context, fqdn string) (string, error) {\n\tfor z := range dns01.DomainsSeq(fqdn) {\n\t\t_, errG := d.client.GetDNSSettings(ctx, z, \"\")\n\t\tif errG != nil {\n\t\t\tlog.Infof(\"get DNS settings zone[%q] %v\", z, errG)\n\t\t\tcontinue\n\t\t}\n\n\t\treturn z, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"unable to find auth zone for '%s'\", fqdn)\n}\n"
  },
  {
    "path": "providers/dns/allinkl/allinkl.toml",
    "content": "Name = \"all-inkl\"\nDescription = ''''''\nURL = \"https://all-inkl.com\"\nCode = \"allinkl\"\nSince = \"v4.5.0\"\n\nExample = '''\nALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \\\nALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \\\nlego --dns allinkl -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    ALL_INKL_LOGIN = \"KAS login\"\n    ALL_INKL_PASSWORD = \"KAS password\"\n  [Configuration.Additional]\n    ALL_INKL_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    ALL_INKL_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    ALL_INKL_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://kasapi.kasserver.com/dokumentation/phpdoc/index.html\"\n  Guide = \"https://kasapi.kasserver.com/dokumentation/\"\n"
  },
  {
    "path": "providers/dns/allinkl/allinkl_test.go",
    "content": "package allinkl\n\nimport (\n\t\"encoding/json\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-acme/lego/v4/providers/dns/allinkl/internal\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvLogin, EnvPassword).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvLogin:    \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials: account name\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvLogin:    \"\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"allinkl: some credentials information are missing: ALL_INKL_LOGIN\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials: api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvLogin:    \"user\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"allinkl: some credentials information are missing: ALL_INKL_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials: all\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvLogin:    \"\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"allinkl: some credentials information are missing: ALL_INKL_LOGIN,ALL_INKL_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tlogin    string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tlogin:    \"user\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing account name\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"allinkl: missing credentials\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api key\",\n\t\t\tlogin:    \"user\",\n\t\t\texpected: \"allinkl: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Login = test.login\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Login = \"user\"\n\t\t\tconfig.Password = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\t\t\tp.identifier.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, err\n\t\t},\n\t).Route(\"POST /KasAuth.php\",\n\t\tservermock.ResponseFromInternal(\"auth.xml\"),\n\t\tservermock.CheckRequestBodyFromInternal(\"auth-request.xml\").\n\t\t\tIgnoreWhitespace(),\n\t)\n}\n\nfunc extractKasRequest(reader io.Reader) (*internal.KasRequest, error) {\n\ttype ReqEnvelope struct {\n\t\tXMLName xml.Name `xml:\"Envelope\"`\n\t\tBody    struct {\n\t\t\tKasAPI struct {\n\t\t\t\tParams string `xml:\"Params\"`\n\t\t\t} `xml:\"KasApi\"`\n\t\t} `xml:\"Body\"`\n\t}\n\n\traw, err := io.ReadAll(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treqEnvelope := ReqEnvelope{}\n\n\terr = xml.Unmarshal(raw, &reqEnvelope)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar kReq internal.KasRequest\n\n\terr = json.Unmarshal([]byte(reqEnvelope.Body.KasAPI.Params), &kReq)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &kReq, nil\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /KasApi.php\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\tkReq, err := extractKasRequest(req.Body)\n\t\t\t\tif err != nil {\n\t\t\t\t\thttp.Error(rw, err.Error(), http.StatusBadRequest)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tswitch kReq.Action {\n\t\t\t\tcase \"get_dns_settings\":\n\t\t\t\t\tparams := kReq.RequestParams.(map[string]any)\n\n\t\t\t\t\tif params[\"zone_host\"] == \"_acme-challenge.example.com.\" {\n\t\t\t\t\t\tservermock.ResponseFromInternal(\"get_dns_settings_not_found.xml\").ServeHTTP(rw, req)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tservermock.ResponseFromInternal(\"get_dns_settings.xml\").ServeHTTP(rw, req)\n\t\t\t\t\t}\n\n\t\t\t\tcase \"add_dns_settings\":\n\t\t\t\t\tservermock.ResponseFromInternal(\"add_dns_settings.xml\").ServeHTTP(rw, req)\n\n\t\t\t\tdefault:\n\t\t\t\t\thttp.Error(rw, fmt.Sprintf(\"unknown action: %v\", kReq.Action), http.StatusBadRequest)\n\t\t\t\t}\n\t\t\t}),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /KasApi.php\",\n\t\t\tservermock.ResponseFromInternal(\"delete_dns_settings.xml\"),\n\t\t\tservermock.CheckRequestBodyFromInternal(\"delete_dns_settings-request.xml\").\n\t\t\t\tIgnoreWhitespace()).\n\t\tBuild(t)\n\n\tprovider.recordIDs[\"abc\"] = \"57347450\"\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/allinkl/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-viper/mapstructure/v2\"\n)\n\nconst defaultBaseURL = \"https://kasapi.kasserver.com/soap/\"\n\nconst apiPath = \"KasApi.php\"\n\ntype Authentication interface {\n\tAuthentication(ctx context.Context, sessionLifetime int, sessionUpdateLifetime bool) (string, error)\n}\n\n// Client a KAS server client.\ntype Client struct {\n\tlogin string\n\n\tfloodTime   time.Time\n\tmuFloodTime sync.Mutex\n\n\tmaxElapsedTime time.Duration\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(login string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tlogin:          login,\n\t\tBaseURL:        baseURL,\n\t\tmaxElapsedTime: 3 * time.Minute,\n\t\tHTTPClient:     &http.Client{Timeout: 10 * time.Second},\n\t}\n}\n\n// GetDNSSettings Reading out the DNS settings of a zone.\n// - zone: host zone.\n// - recordID: the ID of the resource record (optional).\nfunc (c *Client) GetDNSSettings(ctx context.Context, zone, recordID string) ([]ReturnInfo, error) {\n\trequestParams := map[string]string{\"zone_host\": zone}\n\n\tif recordID != \"\" {\n\t\trequestParams[\"record_id\"] = recordID\n\t}\n\n\tvar g APIResponse[GetDNSSettingsResponse]\n\n\terr := c.doRequest(ctx, \"get_dns_settings\", requestParams, &g)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc.updateFloodTime(g.Response.KasFloodDelay)\n\n\treturn g.Response.ReturnInfo, nil\n}\n\n// AddDNSSettings Creation of a DNS resource record.\nfunc (c *Client) AddDNSSettings(ctx context.Context, record DNSRequest) (string, error) {\n\tvar g APIResponse[AddDNSSettingsResponse]\n\n\terr := c.doRequest(ctx, \"add_dns_settings\", record, &g)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tc.updateFloodTime(g.Response.KasFloodDelay)\n\n\treturn g.Response.ReturnInfo, nil\n}\n\n// DeleteDNSSettings Deleting a DNS Resource Record.\nfunc (c *Client) DeleteDNSSettings(ctx context.Context, recordID string) (string, error) {\n\trequestParams := map[string]string{\"record_id\": recordID}\n\n\tvar g APIResponse[DeleteDNSSettingsResponse]\n\n\terr := c.doRequest(ctx, \"delete_dns_settings\", requestParams, &g)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tc.updateFloodTime(g.Response.KasFloodDelay)\n\n\treturn g.Response.ReturnString, nil\n}\n\nfunc (c *Client) newRequest(ctx context.Context, action string, requestParams any) (*http.Request, error) {\n\tar := KasRequest{\n\t\tLogin:         c.login,\n\t\tAuthType:      \"session\",\n\t\tAuthData:      getToken(ctx),\n\t\tAction:        action,\n\t\tRequestParams: requestParams,\n\t}\n\n\tbody, err := json.Marshal(ar)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t}\n\n\tpayload := []byte(strings.TrimSpace(fmt.Sprintf(kasAPIEnvelope, body)))\n\n\tendpoint := c.BaseURL.JoinPath(apiPath)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(payload))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(ctx context.Context, action string, requestParams, result any) error {\n\treturn wait.Retry(ctx,\n\t\tfunc() error {\n\t\t\treq, err := c.newRequest(ctx, action, requestParams)\n\t\t\tif err != nil {\n\t\t\t\treturn backoff.Permanent(err)\n\t\t\t}\n\n\t\t\treturn c.do(req, result)\n\t\t},\n\t\tbackoff.WithBackOff(&backoff.ZeroBackOff{}),\n\t\tbackoff.WithMaxElapsedTime(c.maxElapsedTime),\n\t)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tc.muFloodTime.Lock()\n\ttime.Sleep(time.Until(c.floodTime))\n\tc.muFloodTime.Unlock()\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn backoff.Permanent(errutils.NewHTTPDoError(req, err))\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn backoff.Permanent(errutils.NewUnexpectedResponseStatusCodeError(req, resp))\n\t}\n\n\tenvlp, err := decodeXML[KasAPIResponseEnvelope](resp.Body)\n\tif err != nil {\n\t\treturn backoff.Permanent(err)\n\t}\n\n\tif envlp.Body.Fault != nil {\n\t\tif envlp.Body.Fault.Message == \"flood_protection\" {\n\t\t\tft, errP := strconv.ParseFloat(envlp.Body.Fault.Detail, 64)\n\t\t\tif errP != nil {\n\t\t\t\treturn fmt.Errorf(\"parse flood protection delay: %w\", envlp.Body.Fault)\n\t\t\t}\n\n\t\t\tc.updateFloodTime(ft)\n\n\t\t\treturn envlp.Body.Fault\n\t\t}\n\n\t\treturn backoff.Permanent(envlp.Body.Fault)\n\t}\n\n\traw := getValue(envlp.Body.KasAPIResponse.Return)\n\n\terr = mapstructure.Decode(raw, result)\n\tif err != nil {\n\t\treturn backoff.Permanent(fmt.Errorf(\"response struct decode: %w\", err))\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) updateFloodTime(delay float64) {\n\tc.muFloodTime.Lock()\n\tc.floodTime = time.Now().Add(time.Duration(delay * float64(time.Second)))\n\tc.muFloodTime.Unlock()\n}\n\nfunc getValue(item *Item) any {\n\tswitch {\n\tcase item.Raw != \"\":\n\t\tv, _ := strconv.ParseBool(item.Raw)\n\t\treturn v\n\n\tcase item.Text != \"\":\n\t\tswitch item.Type {\n\t\tcase \"xsd:string\":\n\t\t\treturn item.Text\n\t\tcase \"xsd:float\":\n\t\t\tv, _ := strconv.ParseFloat(item.Text, 64)\n\t\t\treturn v\n\t\tcase \"xsd:int\":\n\t\t\tv, _ := strconv.ParseInt(item.Text, 10, 64)\n\t\t\treturn v\n\t\tdefault:\n\t\t\treturn item.Text\n\t\t}\n\n\tcase item.Value != nil:\n\t\treturn getValue(item.Value)\n\n\tcase len(item.Items) > 0 && item.Type == \"SOAP-ENC:Array\":\n\t\tvar v []any\n\t\tfor _, i := range item.Items {\n\t\t\tv = append(v, getValue(i))\n\t\t}\n\n\t\treturn v\n\n\tcase len(item.Items) > 0:\n\t\tv := map[string]any{}\n\t\tfor _, i := range item.Items {\n\t\t\tv[getKey(i)] = getValue(i)\n\t\t}\n\n\t\treturn v\n\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc getKey(item *Item) string {\n\tif item.Key == nil {\n\t\treturn \"\"\n\t}\n\n\treturn item.Key.Text\n}\n"
  },
  {
    "path": "providers/dns/allinkl/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"user\")\n\tclient.BaseURL, _ = url.Parse(server.URL)\n\tclient.HTTPClient = server.Client()\n\n\tclient.maxElapsedTime = 1 * time.Second\n\n\treturn client, nil\n}\n\nfunc TestClient_GetDNSSettings(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /KasApi.php\", servermock.ResponseFromFixture(\"get_dns_settings.xml\"),\n\t\t\tservermock.CheckRequestBodyFromFixture(\"get_dns_settings-request.xml\").\n\t\t\t\tIgnoreWhitespace()).\n\t\tBuild(t)\n\n\trecords, err := client.GetDNSSettings(mockContext(t), \"example.com\", \"\")\n\trequire.NoError(t, err)\n\n\texpected := []ReturnInfo{\n\t\t{\n\t\t\tID:         \"57297429\",\n\t\t\tZone:       \"example.org\",\n\t\t\tName:       \"\",\n\t\t\tType:       \"A\",\n\t\t\tData:       \"10.0.0.1\",\n\t\t\tChangeable: \"Y\",\n\t\t\tAux:        0,\n\t\t},\n\t\t{\n\t\t\tID:         int64(0),\n\t\t\tZone:       \"example.org\",\n\t\t\tName:       \"\",\n\t\t\tType:       \"NS\",\n\t\t\tData:       \"ns5.kasserver.com.\",\n\t\t\tChangeable: \"N\",\n\t\t\tAux:        0,\n\t\t},\n\t\t{\n\t\t\tID:         int64(0),\n\t\t\tZone:       \"example.org\",\n\t\t\tName:       \"\",\n\t\t\tType:       \"NS\",\n\t\t\tData:       \"ns6.kasserver.com.\",\n\t\t\tChangeable: \"N\",\n\t\t\tAux:        0,\n\t\t},\n\t\t{\n\t\t\tID:         \"57297479\",\n\t\t\tZone:       \"example.org\",\n\t\t\tName:       \"*\",\n\t\t\tType:       \"A\",\n\t\t\tData:       \"10.0.0.1\",\n\t\t\tChangeable: \"Y\",\n\t\t\tAux:        0,\n\t\t},\n\t\t{\n\t\t\tID:         \"57297481\",\n\t\t\tZone:       \"example.org\",\n\t\t\tName:       \"\",\n\t\t\tType:       \"MX\",\n\t\t\tData:       \"user.kasserver.com.\",\n\t\t\tChangeable: \"Y\",\n\t\t\tAux:        10,\n\t\t},\n\t\t{\n\t\t\tID:         \"57297483\",\n\t\t\tZone:       \"example.org\",\n\t\t\tName:       \"\",\n\t\t\tType:       \"TXT\",\n\t\t\tData:       \"v=spf1 mx a ?all\",\n\t\t\tChangeable: \"Y\",\n\t\t\tAux:        0,\n\t\t},\n\t\t{\n\t\t\tID:         \"57297485\",\n\t\t\tZone:       \"example.org\",\n\t\t\tName:       \"_dmarc\",\n\t\t\tType:       \"TXT\",\n\t\t\tData:       \"v=DMARC1; p=none;\",\n\t\t\tChangeable: \"Y\",\n\t\t\tAux:        0,\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetDNSSettings_error_flood_protection(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /KasApi.php\",\n\t\t\tservermock.ResponseFromFixture(\"flood_protection.xml\"),\n\t\t).\n\t\tBuild(t)\n\n\tassert.Zero(t, client.floodTime)\n\n\t_, err := client.GetDNSSettings(mockContext(t), \"example.com\", \"\")\n\trequire.EqualError(t, err, \"KasApi: SOAP-ENV:Server: flood_protection: 0.0688529014587\")\n\n\tassert.NotZero(t, client.floodTime)\n}\n\nfunc TestClient_AddDNSSettings(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /KasApi.php\", servermock.ResponseFromFixture(\"add_dns_settings.xml\"),\n\t\t\tservermock.CheckRequestBodyFromFixture(\"add_dns_settings-request.xml\").\n\t\t\t\tIgnoreWhitespace()).\n\t\tBuild(t)\n\n\trecord := DNSRequest{\n\t\tZoneHost:   \"42cnc.de.\",\n\t\tRecordType: \"TXT\",\n\t\tRecordName: \"lego\",\n\t\tRecordData: \"abcdefgh\",\n\t}\n\n\trecordID, err := client.AddDNSSettings(mockContext(t), record)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"57347444\", recordID)\n}\n\nfunc TestClient_DeleteDNSSettings(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /KasApi.php\", servermock.ResponseFromFixture(\"delete_dns_settings.xml\"),\n\t\t\tservermock.CheckRequestBodyFromFixture(\"delete_dns_settings-request.xml\").\n\t\t\t\tIgnoreWhitespace()).\n\t\tBuild(t)\n\n\tr, err := client.DeleteDNSSettings(mockContext(t), \"57347450\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"TRUE\", r)\n}\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/add_dns_settings-request.xml",
    "content": "<Envelope xmlns=\"http://schemas.xmlsoap.org/soap/envelope/\">\n    <Body>\n        <KasApi xmlns=\"https://kasserver.com/\">\n            <Params>{\"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}}</Params>\n        </KasApi>\n    </Body>\n</Envelope>\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/add_dns_settings.json",
    "content": "{\n  \"Request\": {\n    \"KasRequestParams\": {\n      \"record_aux\": 0,\n      \"record_data\": \"abcdefgh\",\n      \"record_name\": \"lego\",\n      \"record_type\": \"TXT\",\n      \"zone_host\": \"example.org.\"\n    },\n    \"KasRequestTime\": 1625014992,\n    \"KasRequestType\": true\n  },\n  \"Response\": {\n    \"KasFloodDelay\": 0.5,\n    \"ReturnInfo\": \"57347444\",\n    \"ReturnString\": \"TRUE\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/add_dns_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"\n                   xmlns:ns1=\"https://kasapi.kasserver.com/soap/KasApi.php\"\n                   xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n                   xmlns:ns2=\"http://xml.apache.org/xml-soap\"\n                   SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\n    <SOAP-ENV:Body>\n        <ns1:KasApiResponse>\n            <return xsi:type=\"ns2:Map\">\n                <item>\n                    <key xsi:type=\"xsd:string\">Request</key>\n                    <value xsi:type=\"ns2:Map\">\n                        <item>\n                            <key xsi:type=\"xsd:string\">KasRequestTime</key>\n                            <value xsi:type=\"xsd:int\">1625014992</value>\n                        </item>\n                        <item>\n                            <key xsi:type=\"xsd:string\">KasRequestType</key>\n                            <value xsi:nil=\"true\"/>\n                        </item>\n                        <item>\n                            <key xsi:type=\"xsd:string\">KasRequestParams</key>\n                            <value xsi:type=\"ns2:Map\">\n                                <item>\n                                    <key xsi:type=\"xsd:string\">zone_host</key>\n                                    <value xsi:type=\"xsd:string\">example.org.</value>\n                                </item>\n                                <item>\n                                    <key xsi:type=\"xsd:string\">record_type</key>\n                                    <value xsi:type=\"xsd:string\">TXT</value>\n                                </item>\n                                <item>\n                                    <key xsi:type=\"xsd:string\">record_name</key>\n                                    <value xsi:type=\"xsd:string\">lego</value>\n                                </item>\n                                <item>\n                                    <key xsi:type=\"xsd:string\">record_data</key>\n                                    <value xsi:type=\"xsd:string\">abcdefgh</value>\n                                </item>\n                                <item>\n                                    <key xsi:type=\"xsd:string\">record_aux</key>\n                                    <value xsi:type=\"xsd:int\">0</value>\n                                </item>\n                            </value>\n                        </item>\n                    </value>\n                </item>\n                <item>\n                    <key xsi:type=\"xsd:string\">Response</key>\n                    <value xsi:type=\"ns2:Map\">\n                        <item>\n                            <key xsi:type=\"xsd:string\">KasFloodDelay</key>\n                            <value xsi:type=\"xsd:float\">0.5</value>\n                        </item>\n                        <item>\n                            <key xsi:type=\"xsd:string\">ReturnString</key>\n                            <value xsi:type=\"xsd:string\">TRUE</value>\n                        </item>\n                        <item>\n                            <key xsi:type=\"xsd:string\">ReturnInfo</key>\n                            <value xsi:type=\"xsd:string\">57347444</value>\n                        </item>\n                    </value>\n                </item>\n            </return>\n        </ns1:KasApiResponse>\n    </SOAP-ENV:Body>\n</SOAP-ENV:Envelope>\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/auth-request.xml",
    "content": "<Envelope xmlns=\"http://schemas.xmlsoap.org/soap/envelope/\">\n    <Body>\n        <KasAuth xmlns=\"https://kasserver.com/\">\n            <Params>{\"kas_login\":\"user\",\"kas_auth_data\":\"secret\",\"kas_auth_type\":\"plain\",\"session_lifetime\":60,\"session_update_lifetime\":\"Y\"}</Params>\n        </KasAuth>\n    </Body>\n</Envelope>\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/auth.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"\n                   xmlns:ns1=\"https://kasapi.kasserver.com/soap/KasAuth.php\"\n                   xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n                   SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\n    <SOAP-ENV:Body>\n        <ns1:KasAuthResponse>\n            <return xsi:type=\"xsd:string\">593959ca04f0de9689b586c6a647d15d</return>\n        </ns1:KasAuthResponse>\n    </SOAP-ENV:Body>\n</SOAP-ENV:Envelope>\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/auth_fault.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n    <SOAP-ENV:Body>\n        <SOAP-ENV:Fault>\n            <faultcode>SOAP-ENV:Client</faultcode>\n            <faultstring>kas_login_syntax_incorrect</faultstring>\n            <faultactor>KasAuth</faultactor>\n        </SOAP-ENV:Fault>\n    </SOAP-ENV:Body>\n</SOAP-ENV:Envelope>\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/delete_dns_settings-request.xml",
    "content": "<Envelope xmlns=\"http://schemas.xmlsoap.org/soap/envelope/\">\n    <Body>\n        <KasApi xmlns=\"https://kasserver.com/\">\n            <Params>{\"kas_login\":\"user\",\"kas_auth_type\":\"session\",\"kas_auth_data\":\"593959ca04f0de9689b586c6a647d15d\",\"kas_action\":\"delete_dns_settings\",\"KasRequestParams\":{\"record_id\":\"57347450\"}}</Params>\n        </KasApi>\n    </Body>\n</Envelope>\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/delete_dns_settings.json",
    "content": "{\n  \"Request\": {\n    \"KasRequestParams\": {\n      \"record_id\": \"57347444\"\n    },\n    \"KasRequestTime\": 1625016066,\n    \"KasRequestType\": true\n  },\n  \"Response\": {\n    \"KasFloodDelay\": 0.5,\n    \"ReturnInfo\": true,\n    \"ReturnString\": \"TRUE\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/delete_dns_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"\n                   xmlns:ns1=\"https://kasapi.kasserver.com/soap/KasApi.php\"\n                   xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n                   xmlns:ns2=\"http://xml.apache.org/xml-soap\"\n                   SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\n    <SOAP-ENV:Body>\n        <ns1:KasApiResponse>\n            <return xsi:type=\"ns2:Map\">\n                <item>\n                    <key xsi:type=\"xsd:string\">Request</key>\n                    <value xsi:type=\"ns2:Map\">\n                        <item>\n                            <key xsi:type=\"xsd:string\">KasRequestTime</key>\n                            <value xsi:type=\"xsd:int\">1625016066</value>\n                        </item>\n                        <item>\n                            <key xsi:type=\"xsd:string\">KasRequestType</key>\n                            <value xsi:nil=\"true\"/>\n                        </item>\n                        <item>\n                            <key xsi:type=\"xsd:string\">KasRequestParams</key>\n                            <value xsi:type=\"ns2:Map\">\n                                <item>\n                                    <key xsi:type=\"xsd:string\">record_id</key>\n                                    <value xsi:type=\"xsd:string\">57347444</value>\n                                </item>\n                            </value>\n                        </item>\n                    </value>\n                </item>\n                <item>\n                    <key xsi:type=\"xsd:string\">Response</key>\n                    <value xsi:type=\"ns2:Map\">\n                        <item>\n                            <key xsi:type=\"xsd:string\">KasFloodDelay</key>\n                            <value xsi:type=\"xsd:float\">0.5</value>\n                        </item>\n                        <item>\n                            <key xsi:type=\"xsd:string\">ReturnString</key>\n                            <value xsi:type=\"xsd:string\">TRUE</value>\n                        </item>\n                        <item>\n                            <key xsi:type=\"xsd:string\">ReturnInfo</key>\n                            <value xsi:nil=\"true\"/>\n                        </item>\n                    </value>\n                </item>\n            </return>\n        </ns1:KasApiResponse>\n    </SOAP-ENV:Body>\n</SOAP-ENV:Envelope>\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/flood_protection.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n    <SOAP-ENV:Body>\n        <SOAP-ENV:Fault>\n            <faultcode>SOAP-ENV:Server</faultcode>\n            <faultstring>flood_protection</faultstring>\n            <faultactor>KasApi</faultactor>\n            <detail>0.0688529014587</detail>\n        </SOAP-ENV:Fault>\n    </SOAP-ENV:Body>\n</SOAP-ENV:Envelope>\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/get_dns_settings-request.xml",
    "content": "<Envelope xmlns=\"http://schemas.xmlsoap.org/soap/envelope/\">\n    <Body>\n        <KasApi xmlns=\"https://kasserver.com/\">\n            <Params>{\"kas_login\":\"user\",\"kas_auth_type\":\"session\",\"kas_auth_data\":\"593959ca04f0de9689b586c6a647d15d\",\"kas_action\":\"get_dns_settings\",\"KasRequestParams\":{\"zone_host\":\"example.com\"}}</Params>\n        </KasApi>\n    </Body>\n</Envelope>\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_not_found.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n    <SOAP-ENV:Body>\n        <SOAP-ENV:Fault>\n            <faultcode>SOAP-ENV:Server</faultcode>\n            <faultstring>zone_not_found</faultstring>\n            <faultactor>KasApi</faultactor>\n            <detail>example.com</detail>\n        </SOAP-ENV:Fault>\n    </SOAP-ENV:Body>\n</SOAP-ENV:Envelope>\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/get_dns_settings-zone_syntax_incorrect.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n    <SOAP-ENV:Body>\n        <SOAP-ENV:Fault>\n            <faultcode>SOAP-ENV:Server</faultcode>\n            <faultstring>zone_syntax_incorrect</faultstring>\n            <faultactor>KasApi</faultactor>\n            <detail>_acme-challenge.example.com</detail>\n        </SOAP-ENV:Fault>\n    </SOAP-ENV:Body>\n</SOAP-ENV:Envelope>\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/get_dns_settings.json",
    "content": "{\n  \"Request\": {\n    \"KasRequestParams\": {\n      \"zone_host\": \"example.org\"\n    },\n    \"KasRequestTime\": 1625012975,\n    \"KasRequestType\": true\n  },\n  \"Response\": {\n    \"KasFloodDelay\": 0.5,\n    \"ReturnInfo\": [\n      {\n        \"record_aux\": 0,\n        \"record_changeable\": \"Y\",\n        \"record_data\": \"10.0.0.1\",\n        \"record_id\": \"57297429\",\n        \"record_name\": \"\",\n        \"record_type\": \"A\",\n        \"record_zone\": \"example.org\"\n      },\n      {\n        \"record_aux\": 0,\n        \"record_changeable\": \"N\",\n        \"record_data\": \"ns5.kasserver.com.\",\n        \"record_id\": 0,\n        \"record_name\": \"\",\n        \"record_type\": \"NS\",\n        \"record_zone\": \"example.org\"\n      },\n      {\n        \"record_aux\": 0,\n        \"record_changeable\": \"N\",\n        \"record_data\": \"ns6.kasserver.com.\",\n        \"record_id\": 0,\n        \"record_name\": \"\",\n        \"record_type\": \"NS\",\n        \"record_zone\": \"example.org\"\n      },\n      {\n        \"record_aux\": 0,\n        \"record_changeable\": \"Y\",\n        \"record_data\": \"10.0.0.1\",\n        \"record_id\": \"57297479\",\n        \"record_name\": \"*\",\n        \"record_type\": \"A\",\n        \"record_zone\": \"example.org\"\n      },\n      {\n        \"record_aux\": 10,\n        \"record_changeable\": \"Y\",\n        \"record_data\": \"user.kasserver.com.\",\n        \"record_id\": \"57297481\",\n        \"record_name\": \"\",\n        \"record_type\": \"MX\",\n        \"record_zone\": \"example.org\"\n      },\n      {\n        \"record_aux\": 0,\n        \"record_changeable\": \"Y\",\n        \"record_data\": \"v=spf1 mx a ?all\",\n        \"record_id\": \"57297483\",\n        \"record_name\": \"\",\n        \"record_type\": \"TXT\",\n        \"record_zone\": \"example.org\"\n      },\n      {\n        \"record_aux\": 0,\n        \"record_changeable\": \"Y\",\n        \"record_data\": \"v=DMARC1; p=none;\",\n        \"record_id\": \"57297485\",\n        \"record_name\": \"_dmarc\",\n        \"record_type\": \"TXT\",\n        \"record_zone\": \"example.org\"\n      }\n    ],\n    \"ReturnString\": \"TRUE\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/allinkl/internal/fixtures/get_dns_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"\n                   xmlns:ns1=\"https://kasapi.kasserver.com/soap/KasApi.php\"\n                   xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n                   xmlns:ns2=\"http://xml.apache.org/xml-soap\" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\"\n                   SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\n    <SOAP-ENV:Body>\n        <ns1:KasApiResponse>\n            <return xsi:type=\"ns2:Map\">\n                <item>\n                    <key xsi:type=\"xsd:string\">Request</key>\n                    <value xsi:type=\"ns2:Map\">\n                        <item>\n                            <key xsi:type=\"xsd:string\">KasRequestTime</key>\n                            <value xsi:type=\"xsd:int\">1624993260</value>\n                        </item>\n                        <item>\n                            <key xsi:type=\"xsd:string\">KasRequestType</key>\n                            <value xsi:nil=\"true\"/>\n                        </item>\n                        <item>\n                            <key xsi:type=\"xsd:string\">KasRequestParams</key>\n                            <value xsi:type=\"ns2:Map\">\n                                <item>\n                                    <key xsi:type=\"xsd:string\">zone_host</key>\n                                    <value xsi:type=\"xsd:string\">example.org</value>\n                                </item>\n                            </value>\n                        </item>\n                    </value>\n                </item>\n                <item>\n                    <key xsi:type=\"xsd:string\">Response</key>\n                    <value xsi:type=\"ns2:Map\">\n                        <item>\n                            <key xsi:type=\"xsd:string\">KasFloodDelay</key>\n                            <value xsi:type=\"xsd:float\">0.5</value>\n                        </item>\n                        <item>\n                            <key xsi:type=\"xsd:string\">ReturnString</key>\n                            <value xsi:type=\"xsd:string\">TRUE</value>\n                        </item>\n                        <item>\n                            <key xsi:type=\"xsd:string\">ReturnInfo</key>\n                            <value SOAP-ENC:arrayType=\"ns2:Map[7]\" xsi:type=\"SOAP-ENC:Array\">\n                                <item xsi:type=\"ns2:Map\">\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_zone</key>\n                                        <value xsi:type=\"xsd:string\">example.org</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_name</key>\n                                        <value xsi:type=\"xsd:string\"></value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_type</key>\n                                        <value xsi:type=\"xsd:string\">A</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_data</key>\n                                        <value xsi:type=\"xsd:string\">10.0.0.1</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_aux</key>\n                                        <value xsi:type=\"xsd:int\">0</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_id</key>\n                                        <value xsi:type=\"xsd:string\">57297429</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_changeable</key>\n                                        <value xsi:type=\"xsd:string\">Y</value>\n                                    </item>\n                                </item>\n                                <item xsi:type=\"ns2:Map\">\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_zone</key>\n                                        <value xsi:type=\"xsd:string\">example.org</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_name</key>\n                                        <value xsi:type=\"xsd:string\"></value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_type</key>\n                                        <value xsi:type=\"xsd:string\">NS</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_data</key>\n                                        <value xsi:type=\"xsd:string\">ns5.kasserver.com.</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_aux</key>\n                                        <value xsi:type=\"xsd:int\">0</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_id</key>\n                                        <value xsi:type=\"xsd:int\">0</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_changeable</key>\n                                        <value xsi:type=\"xsd:string\">N</value>\n                                    </item>\n                                </item>\n                                <item xsi:type=\"ns2:Map\">\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_zone</key>\n                                        <value xsi:type=\"xsd:string\">example.org</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_name</key>\n                                        <value xsi:type=\"xsd:string\"></value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_type</key>\n                                        <value xsi:type=\"xsd:string\">NS</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_data</key>\n                                        <value xsi:type=\"xsd:string\">ns6.kasserver.com.</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_aux</key>\n                                        <value xsi:type=\"xsd:int\">0</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_id</key>\n                                        <value xsi:type=\"xsd:int\">0</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_changeable</key>\n                                        <value xsi:type=\"xsd:string\">N</value>\n                                    </item>\n                                </item>\n                                <item xsi:type=\"ns2:Map\">\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_zone</key>\n                                        <value xsi:type=\"xsd:string\">example.org</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_name</key>\n                                        <value xsi:type=\"xsd:string\">*</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_type</key>\n                                        <value xsi:type=\"xsd:string\">A</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_data</key>\n                                        <value xsi:type=\"xsd:string\">10.0.0.1</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_aux</key>\n                                        <value xsi:type=\"xsd:int\">0</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_id</key>\n                                        <value xsi:type=\"xsd:string\">57297479</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_changeable</key>\n                                        <value xsi:type=\"xsd:string\">Y</value>\n                                    </item>\n                                </item>\n                                <item xsi:type=\"ns2:Map\">\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_zone</key>\n                                        <value xsi:type=\"xsd:string\">example.org</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_name</key>\n                                        <value xsi:type=\"xsd:string\"></value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_type</key>\n                                        <value xsi:type=\"xsd:string\">MX</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_data</key>\n                                        <value xsi:type=\"xsd:string\">user.kasserver.com.</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_aux</key>\n                                        <value xsi:type=\"xsd:int\">10</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_id</key>\n                                        <value xsi:type=\"xsd:string\">57297481</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_changeable</key>\n                                        <value xsi:type=\"xsd:string\">Y</value>\n                                    </item>\n                                </item>\n                                <item xsi:type=\"ns2:Map\">\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_zone</key>\n                                        <value xsi:type=\"xsd:string\">example.org</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_name</key>\n                                        <value xsi:type=\"xsd:string\"></value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_type</key>\n                                        <value xsi:type=\"xsd:string\">TXT</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_data</key>\n                                        <value xsi:type=\"xsd:string\">v=spf1 mx a ?all</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_aux</key>\n                                        <value xsi:type=\"xsd:int\">0</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_id</key>\n                                        <value xsi:type=\"xsd:string\">57297483</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_changeable</key>\n                                        <value xsi:type=\"xsd:string\">Y</value>\n                                    </item>\n                                </item>\n                                <item xsi:type=\"ns2:Map\">\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_zone</key>\n                                        <value xsi:type=\"xsd:string\">example.org</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_name</key>\n                                        <value xsi:type=\"xsd:string\">_dmarc</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_type</key>\n                                        <value xsi:type=\"xsd:string\">TXT</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_data</key>\n                                        <value xsi:type=\"xsd:string\">v=DMARC1; p=none;</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_aux</key>\n                                        <value xsi:type=\"xsd:int\">0</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_id</key>\n                                        <value xsi:type=\"xsd:string\">57297485</value>\n                                    </item>\n                                    <item>\n                                        <key xsi:type=\"xsd:string\">record_changeable</key>\n                                        <value xsi:type=\"xsd:string\">Y</value>\n                                    </item>\n                                </item>\n                            </value>\n                        </item>\n                    </value>\n                </item>\n            </return>\n        </ns1:KasApiResponse>\n    </SOAP-ENV:Body>\n</SOAP-ENV:Envelope>\n"
  },
  {
    "path": "providers/dns/allinkl/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst authPath = \"KasAuth.php\"\n\ntype token string\n\nconst tokenKey token = \"token\"\n\n// Identifier generates credential tokens.\ntype Identifier struct {\n\tlogin    string\n\tpassword string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewIdentifier creates a new Identifier.\nfunc NewIdentifier(login, password string) *Identifier {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Identifier{\n\t\tlogin:      login,\n\t\tpassword:   password,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}\n}\n\n// Authentication Creates a credential token.\n// - sessionLifetime: Validity of the token in seconds.\n// - sessionUpdateLifetime: with `true` the session is extended with every request.\nfunc (c *Identifier) Authentication(ctx context.Context, sessionLifetime int, sessionUpdateLifetime bool) (string, error) {\n\tsul := \"N\"\n\tif sessionUpdateLifetime {\n\t\tsul = \"Y\"\n\t}\n\n\tar := AuthRequest{\n\t\tLogin:                 c.login,\n\t\tAuthData:              c.password,\n\t\tAuthType:              \"plain\",\n\t\tSessionLifetime:       sessionLifetime,\n\t\tSessionUpdateLifetime: sul,\n\t}\n\n\tbody, err := json.Marshal(ar)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t}\n\n\tpayload := []byte(strings.TrimSpace(fmt.Sprintf(kasAuthEnvelope, body)))\n\n\tendpoint := c.BaseURL.JoinPath(authPath)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(payload))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tenvlp, err := decodeXML[KasAuthEnvelope](resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif envlp.Body.Fault != nil {\n\t\treturn \"\", envlp.Body.Fault\n\t}\n\n\treturn envlp.Body.KasAuthResponse.Return.Text, nil\n}\n\nfunc WithContext(ctx context.Context, credential string) context.Context {\n\treturn context.WithValue(ctx, tokenKey, credential)\n}\n\nfunc getToken(ctx context.Context) string {\n\tcredential, ok := ctx.Value(tokenKey).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn credential\n}\n"
  },
  {
    "path": "providers/dns/allinkl/internal/identity_test.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupIdentifierClient(server *httptest.Server) (*Identifier, error) {\n\tclient := NewIdentifier(\"user\", \"secret\")\n\tclient.BaseURL, _ = url.Parse(server.URL)\n\tclient.HTTPClient = server.Client()\n\n\treturn client, nil\n}\n\nfunc mockContext(t *testing.T) context.Context {\n\tt.Helper()\n\n\treturn context.WithValue(t.Context(), tokenKey, \"593959ca04f0de9689b586c6a647d15d\")\n}\n\nfunc TestIdentifier_Authentication(t *testing.T) {\n\tclient := servermock.NewBuilder[*Identifier](setupIdentifierClient).\n\t\tRoute(\"POST /KasAuth.php\",\n\t\t\tservermock.ResponseFromFixture(\"auth.xml\"),\n\t\t\tservermock.CheckRequestBodyFromFixture(\"auth-request.xml\").\n\t\t\t\tIgnoreWhitespace()).\n\t\tBuild(t)\n\n\tcredentialToken, err := client.Authentication(t.Context(), 60, true)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"593959ca04f0de9689b586c6a647d15d\", credentialToken)\n}\n\nfunc TestIdentifier_Authentication_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Identifier](setupIdentifierClient).\n\t\tRoute(\"POST /KasAuth.php\", servermock.ResponseFromFixture(\"auth_fault.xml\")).\n\t\tBuild(t)\n\n\t_, err := client.Authentication(t.Context(), 60, false)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "providers/dns/allinkl/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n)\n\n// Trimmer trim all XML fields.\ntype Trimmer struct {\n\tdecoder *xml.Decoder\n}\n\nfunc (tr Trimmer) Token() (xml.Token, error) {\n\tt, err := tr.decoder.Token()\n\tif cd, ok := t.(xml.CharData); ok {\n\t\tt = xml.CharData(bytes.TrimSpace(cd))\n\t}\n\n\treturn t, err\n}\n\n// Fault a SOAP fault.\ntype Fault struct {\n\tCode    string `xml:\"faultcode\"`\n\tMessage string `xml:\"faultstring\"`\n\tActor   string `xml:\"faultactor\"`\n\tDetail  string `xml:\"detail\"`\n}\n\nfunc (f *Fault) Error() string {\n\treturn fmt.Sprintf(\"%s: %s: %s: %s\", f.Actor, f.Code, f.Message, f.Detail)\n}\n\n// KasResponse a KAS SOAP response.\ntype KasResponse struct {\n\tReturn *Item `xml:\"return\"`\n}\n\n// Item an item of the KAS SOAP response.\ntype Item struct {\n\tText  string  `xml:\",chardata\" json:\"text,omitempty\"`\n\tType  string  `xml:\"type,attr\" json:\"type,omitempty\"`\n\tRaw   string  `xml:\"nil,attr\" json:\"raw,omitempty\"`\n\tKey   *Item   `xml:\"key\" json:\"key,omitempty\"`\n\tValue *Item   `xml:\"value\" json:\"value,omitempty\"`\n\tItems []*Item `xml:\"item\" json:\"item,omitempty\"`\n}\n\nfunc decodeXML[T any](reader io.Reader) (*T, error) {\n\traw, err := io.ReadAll(reader)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"read response body: %w\", err)\n\t}\n\n\tvar result T\n\n\terr = xml.NewTokenDecoder(Trimmer{decoder: xml.NewDecoder(bytes.NewReader(raw))}).Decode(&result)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"decode XML response: %w\", err)\n\t}\n\n\treturn &result, nil\n}\n"
  },
  {
    "path": "providers/dns/allinkl/internal/types_api.go",
    "content": "package internal\n\nimport \"encoding/xml\"\n\n// kasAPIEnvelope a KAS API request envelope.\nconst kasAPIEnvelope = `\n<Envelope xmlns=\"http://schemas.xmlsoap.org/soap/envelope/\">\n    <Body>\n        <KasApi xmlns=\"https://kasserver.com/\">\n            <Params>%s</Params>\n        </KasApi>\n    </Body>\n</Envelope>`\n\n// KasAPIResponseEnvelope a KAS envelope of the API response.\ntype KasAPIResponseEnvelope struct {\n\tXMLName xml.Name   `xml:\"Envelope\"`\n\tBody    KasAPIBody `xml:\"Body\"`\n}\n\ntype KasAPIBody struct {\n\tKasAPIResponse *KasResponse `xml:\"KasApiResponse\"`\n\tFault          *Fault       `xml:\"Fault\"`\n}\n\n// ---\n\ntype KasRequest struct {\n\t// Login the relevant KAS login.\n\tLogin string `json:\"kas_login,omitempty\"`\n\t// AuthType the authentication type.\n\tAuthType string `json:\"kas_auth_type,omitempty\"`\n\t// AuthData the authentication data.\n\tAuthData string `json:\"kas_auth_data,omitempty\"`\n\t// Action API function.\n\tAction string `json:\"kas_action,omitempty\"`\n\t// RequestParams Parameters to the API function.\n\tRequestParams any `json:\"KasRequestParams,omitempty\"`\n}\n\ntype DNSRequest struct {\n\t// ZoneHost the zone in question (must be a FQDN).\n\tZoneHost string `json:\"zone_host\"`\n\t// RecordType the TYPE of the resource record (MX, A, AAAA etc.).\n\tRecordType string `json:\"record_type\"`\n\t// RecordName the NAME of the resource record.\n\tRecordName string `json:\"record_name\"`\n\t// RecordData the DATA of the resource record.\n\tRecordData string `json:\"record_data\"`\n\t// RecordAux the AUX of the resource record.\n\tRecordAux int `json:\"record_aux\"`\n}\n\n// ---\n\ntype APIResponse[T any] struct {\n\tResponse T `json:\"Response\" mapstructure:\"Response\"`\n}\n\ntype GetDNSSettingsResponse struct {\n\tKasFloodDelay float64      `json:\"KasFloodDelay\" mapstructure:\"KasFloodDelay\"`\n\tReturnInfo    []ReturnInfo `json:\"ReturnInfo\" mapstructure:\"ReturnInfo\"`\n\tReturnString  string       `json:\"ReturnString\"`\n}\n\ntype ReturnInfo struct {\n\tID         any    `json:\"record_id,omitempty\" mapstructure:\"record_id\"`\n\tZone       string `json:\"record_zone,omitempty\" mapstructure:\"record_zone\"`\n\tName       string `json:\"record_name,omitempty\" mapstructure:\"record_name\"`\n\tType       string `json:\"record_type,omitempty\" mapstructure:\"record_type\"`\n\tData       string `json:\"record_data,omitempty\" mapstructure:\"record_data\"`\n\tChangeable string `json:\"record_changeable,omitempty\" mapstructure:\"record_changeable\"`\n\tAux        int    `json:\"record_aux,omitempty\" mapstructure:\"record_aux\"`\n}\n\ntype AddDNSSettingsResponse struct {\n\tKasFloodDelay float64 `json:\"KasFloodDelay\" mapstructure:\"KasFloodDelay\"`\n\tReturnInfo    string  `json:\"ReturnInfo\" mapstructure:\"ReturnInfo\"`\n\tReturnString  string  `json:\"ReturnString\" mapstructure:\"ReturnString\"`\n}\n\ntype DeleteDNSSettingsResponse struct {\n\tKasFloodDelay float64 `json:\"KasFloodDelay\"`\n\tReturnString  string  `json:\"ReturnString\"`\n\t// NOTE: ReturnInfo (!= ReturnString) doesn't seem to have a stable type\n}\n"
  },
  {
    "path": "providers/dns/allinkl/internal/types_auth.go",
    "content": "package internal\n\nimport \"encoding/xml\"\n\n// kasAuthEnvelope a KAS authentication request envelope.\nconst kasAuthEnvelope = `\n<Envelope xmlns=\"http://schemas.xmlsoap.org/soap/envelope/\">\n\t\t<Body>\n\t\t\t\t<KasAuth xmlns=\"https://kasserver.com/\">\n\t\t\t\t\t\t<Params>%s</Params>\n\t\t\t\t</KasAuth>\n\t\t</Body>\n</Envelope>`\n\n// KasAuthEnvelope a KAS envelope of the authentication response.\ntype KasAuthEnvelope struct {\n\tXMLName xml.Name    `xml:\"Envelope\"`\n\tBody    KasAuthBody `xml:\"Body\"`\n}\n\ntype KasAuthBody struct {\n\tKasAuthResponse *KasResponse `xml:\"KasAuthResponse\"`\n\tFault           *Fault       `xml:\"Fault\"`\n}\n\n// ---\n\ntype AuthRequest struct {\n\tLogin                 string `json:\"kas_login,omitempty\"`\n\tAuthData              string `json:\"kas_auth_data,omitempty\"`\n\tAuthType              string `json:\"kas_auth_type,omitempty\"`\n\tSessionLifetime       int    `json:\"session_lifetime,omitempty\"`\n\tSessionUpdateLifetime string `json:\"session_update_lifetime,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/alwaysdata/alwaysdata.go",
    "content": "// Package alwaysdata implements a DNS provider for solving the DNS-01 challenge using Alwaysdata.\npackage alwaysdata\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/alwaysdata/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"ALWAYSDATA_\"\n\n\tEnvAPIKey  = envNamespace + \"API_KEY\"\n\tEnvAccount = envNamespace + \"ACCOUNT\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey  string\n\tAccount string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Alwaysdata.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"alwaysdata: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.Account = env.GetOrFile(EnvAccount)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Alwaysdata.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"alwaysdata: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.APIKey, config.Account)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"alwaysdata: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.findZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"alwaysdata: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"alwaysdata: %w\", err)\n\t}\n\n\trecord := internal.RecordRequest{\n\t\tDomainID:   zone.ID,\n\t\tName:       subDomain,\n\t\tType:       \"TXT\",\n\t\tValue:      info.Value,\n\t\tTTL:        d.config.TTL,\n\t\tAnnotation: \"lego\",\n\t}\n\n\terr = d.client.AddRecord(ctx, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"alwaysdata: add TXT record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.findZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"alwaysdata: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"alwaysdata: %w\", err)\n\t}\n\n\trecords, err := d.client.ListRecords(ctx, zone.ID, subDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"alwaysdata: list records: %w\", err)\n\t}\n\n\tfor _, record := range records {\n\t\tif record.Type != \"TXT\" || record.Value != info.Value {\n\t\t\tcontinue\n\t\t}\n\n\t\terr = d.client.DeleteRecord(ctx, record.ID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"alwaysdata: delete TXT record: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*internal.Domain, error) {\n\tdomains, err := d.client.ListDomains(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"list domains: %w\", err)\n\t}\n\n\tfor a := range dns01.UnFqdnDomainsSeq(fqdn) {\n\t\tfor _, domain := range domains {\n\t\t\tif a == domain.Name {\n\t\t\t\treturn &domain, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"domain not found\")\n}\n"
  },
  {
    "path": "providers/dns/alwaysdata/alwaysdata.toml",
    "content": "Name = \"Alwaysdata\"\nDescription = ''''''\nURL = \"https://alwaysdata.com/\"\nCode = \"alwaysdata\"\nSince = \"v4.31.0\"\n\nExample = '''\nALWAYSDATA_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns alwaysdata -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    ALWAYSDATA_API_KEY = \"API Key\"\n  [Configuration.Additional]\n    ALWAYSDATA_ACCOUNT = \"Account name\"\n    ALWAYSDATA_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    ALWAYSDATA_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    ALWAYSDATA_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    ALWAYSDATA_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://help.alwaysdata.com/en/api/resources/\"\n  APIDocDomains = \"https://api.alwaysdata.com/v1/domain/doc/\"\n  APIDocRecords = \"https://api.alwaysdata.com/v1/record/doc/\"\n  APIExamples = \"https://help.alwaysdata.com/en/api/examples/\"\n"
  },
  {
    "path": "providers/dns/alwaysdata/alwaysdata_test.go",
    "content": "package alwaysdata\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey, EnvAccount).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success with an account\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:  \"secret\",\n\t\t\t\tEnvAccount: \"foo\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"alwaysdata: some credentials information are missing: ALWAYSDATA_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\taccount  string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:    \"success with an account\",\n\t\t\tapiKey:  \"secret\",\n\t\t\taccount: \"foo\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"alwaysdata: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.Account = test.account\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWithBasicAuth(\"secret\", \"\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /domain/\",\n\t\t\tservermock.ResponseFromInternal(\"domains.json\")).\n\t\tRoute(\"POST /record/\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"record_add-request.json\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /domain/\",\n\t\t\tservermock.ResponseFromInternal(\"domains.json\")).\n\t\tRoute(\"GET /record/\",\n\t\t\tservermock.ResponseFromInternal(\"records.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"domain\", \"132\").\n\t\t\t\tWith(\"name\", \"_acme-challenge\"),\n\t\t).\n\t\tRoute(\"DELETE /record/789/\",\n\t\t\tservermock.Noop()).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/alwaysdata/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\nconst defaultBaseURL = \"https://api.alwaysdata.com/v1\"\n\n// Client the Alwaysdata API client.\ntype Client struct {\n\tapiKey  string\n\taccount string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiKey, account string) (*Client, error) {\n\tif apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\taccount:    account,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domain\", \"/\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result []Domain\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\nfunc (c *Client) AddRecord(ctx context.Context, record RecordRequest) error {\n\tendpoint := c.BaseURL.JoinPath(\"record\", \"/\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, recordID int64) error {\n\tendpoint := c.BaseURL.JoinPath(\"record\", strconv.FormatInt(recordID, 10), \"/\")\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) ListRecords(ctx context.Context, domainID int64, name string) ([]Record, error) {\n\tendpoint := c.BaseURL.JoinPath(\"record\", \"/\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"domain\", strconv.FormatInt(domainID, 10))\n\tquery.Set(\"name\", name)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result []Record\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\tuser := c.apiKey\n\n\tif c.account != \"\" {\n\t\tuser += \"account=\" + c.account\n\t}\n\n\treq.SetBasicAuth(user, \"\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\traw, _ := io.ReadAll(resp.Body)\n\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/alwaysdata/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\", \"\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = clientdebug.Wrap(server.Client())\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWithBasicAuth(\"secret\", \"\"),\n\t)\n}\n\nfunc TestClient_ListDomains(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domain/\",\n\t\t\tservermock.ResponseFromFixture(\"domains.json\")).\n\t\tBuild(t)\n\n\tresult, err := client.ListDomains(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []Domain{\n\t\t{ID: 132, Name: \"example.com\", Annotation: \"test\"},\n\t\t{ID: 133, Name: \"example.net\", IsInternal: true},\n\t\t{ID: 134, Name: \"example.org\"},\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tt.Setenv(\"LEGO_DEBUG_DNS_API_HTTP_CLIENT\", \"true\")\n\n\tclient := mockBuilder().\n\t\tRoute(\"POST /record/\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"record_add-request.json\")).\n\t\tBuild(t)\n\n\trecord := RecordRequest{\n\t\tDomainID:   132,\n\t\tName:       \"_acme-challenge\",\n\t\tType:       \"TXT\",\n\t\tValue:      \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:        120,\n\t\tAnnotation: \"lego\",\n\t}\n\n\terr := client.AddRecord(t.Context(), record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /record/789/\",\n\t\t\tservermock.Noop()).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), 789)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_ListRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /record/\",\n\t\t\tservermock.ResponseFromFixture(\"records.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"domain\", \"132\").\n\t\t\t\tWith(\"name\", \"_acme-challenge\"),\n\t\t).\n\t\tBuild(t)\n\n\tresult, err := client.ListRecords(t.Context(), 132, \"_acme-challenge\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tID: 789,\n\t\t\tDomain: &Domain{\n\t\t\t\tHref: \"/v1/domain/132/\",\n\t\t\t},\n\t\t\tType:       \"TXT\",\n\t\t\tName:       \"_acme-challenge\",\n\t\t\tValue:      \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\t\tTTL:        120,\n\t\t\tAnnotation: \"lego\",\n\t\t},\n\t\t{\n\t\t\tID: 11619270,\n\t\t\tDomain: &Domain{\n\t\t\t\tHref: \"/v1/domain/118935/\",\n\t\t\t},\n\t\t\tName:          \"home\",\n\t\t\tType:          \"A\",\n\t\t\tValue:         \"149.202.90.65\",\n\t\t\tTTL:           300,\n\t\t\tIsUserDefined: true,\n\t\t\tIsActive:      true,\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n"
  },
  {
    "path": "providers/dns/alwaysdata/internal/fixtures/domains.json",
    "content": "[\n  {\n    \"id\": 132,\n    \"name\": \"example.com\",\n    \"annotation\": \"test\"\n  },\n  {\n    \"id\": 133,\n    \"name\": \"example.net\",\n    \"is_internal\": true\n  },\n  {\n    \"id\": 134,\n    \"name\": \"example.org\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/alwaysdata/internal/fixtures/record_add-request.json",
    "content": "{\n  \"domain\": 132,\n  \"name\": \"_acme-challenge\",\n  \"type\": \"TXT\",\n  \"value\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n  \"ttl\": 120,\n  \"annotation\": \"lego\"\n}\n"
  },
  {
    "path": "providers/dns/alwaysdata/internal/fixtures/records.json",
    "content": "[\n  {\n    \"id\": 789,\n    \"domain\": {\n      \"href\": \"/v1/domain/132/\"\n    },\n    \"name\": \"_acme-challenge\",\n    \"type\": \"TXT\",\n    \"value\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n    \"ttl\": 120,\n    \"annotation\": \"lego\"\n  },\n  {\n    \"id\": 11619270,\n    \"domain\": {\n      \"href\": \"/v1/domain/118935/\"\n    },\n    \"type\": \"A\",\n    \"name\": \"home\",\n    \"value\": \"149.202.90.65\",\n    \"priority\": null,\n    \"ttl\": 300,\n    \"href\": \"/v1/record/11619270/\",\n    \"annotation\": \"\",\n    \"is_user_defined\": true,\n    \"is_active\": true\n  }\n]\n"
  },
  {
    "path": "providers/dns/alwaysdata/internal/types.go",
    "content": "package internal\n\ntype RecordRequest struct {\n\tID            int64  `json:\"id,omitempty\"`\n\tDomainID      int64  `json:\"domain,omitempty\"`\n\tName          string `json:\"name,omitempty\"`\n\tType          string `json:\"type,omitempty\"`\n\tValue         string `json:\"value,omitempty\"`\n\tTTL           int    `json:\"ttl,omitempty\"`\n\tAnnotation    string `json:\"annotation,omitempty\"`\n\tIsUserDefined bool   `json:\"is_user_defined,omitempty\"`\n\tIsActive      bool   `json:\"is_active,omitempty\"`\n}\n\ntype Record struct {\n\tID            int64   `json:\"id,omitempty\"`\n\tDomain        *Domain `json:\"domain,omitempty\"`\n\tType          string  `json:\"type,omitempty\"`\n\tName          string  `json:\"name,omitempty\"`\n\tValue         string  `json:\"value,omitempty\"`\n\tTTL           int     `json:\"ttl,omitempty\"`\n\tAnnotation    string  `json:\"annotation,omitempty\"`\n\tIsUserDefined bool    `json:\"is_user_defined,omitempty\"`\n\tIsActive      bool    `json:\"is_active,omitempty\"`\n}\n\ntype Domain struct {\n\tID         int64  `json:\"id,omitempty\"`\n\tHref       string `json:\"href,omitempty\"`\n\tName       string `json:\"name,omitempty\"`\n\tIsInternal bool   `json:\"is_internal,omitempty\"`\n\tAnnotation string `json:\"annotation,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/anexia/anexia.go",
    "content": "// Package anexia implements a DNS provider for solving the DNS-01 challenge using Anexia CloudDNS.\npackage anexia\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/anexia/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"ANEXIA_\"\n\n\tEnvToken  = envNamespace + \"TOKEN\"\n\tEnvAPIURL = envNamespace + \"API_URL\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultTTL = 300\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken  string\n\tAPIURL string\n\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, defaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Anexia CloudDNS.\n// Credentials must be passed in the environment variable: ANEXIA_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"anexia: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvToken]\n\tconfig.APIURL = env.GetOrFile(EnvAPIURL)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Anexia CloudDNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"anexia: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"anexia: incomplete credentials, missing token\")\n\t}\n\n\tclient, err := internal.NewClient(config.Token)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"anexia: %w\", err)\n\t}\n\n\tif config.APIURL != \"\" {\n\t\tvar err error\n\n\t\tclient.BaseURL, err = url.Parse(config.APIURL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"anexia: %w\", err)\n\t\t}\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"anexia: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecordName, err := extractRecordName(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"anexia: %w\", err)\n\t}\n\n\tzoneName := dns01.UnFqdn(authZone)\n\n\trecordReq := internal.Record{\n\t\tName:  recordName,\n\t\tType:  \"TXT\",\n\t\tRData: info.Value,\n\t\tTTL:   d.config.TTL,\n\t}\n\n\t// Ignores returned zone, because of UUID unstability.\n\t// https://github.com/go-acme/lego/pull/2675#issuecomment-3418678194\n\t_, err = d.client.CreateRecord(ctx, zoneName, recordReq)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"anexia: new record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"anexia: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecordName, err := extractRecordName(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"anexia: %w\", err)\n\t}\n\n\trecordID, err := d.findRecordID(ctx, dns01.UnFqdn(authZone), recordName, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"anexia: %w\", err)\n\t}\n\n\terr = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"anexia: delete TXT record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// findRecordID attempts to find the record ID from the zone response.\n// If the record is not immediately available in the response, it retries by querying the zone.\nfunc (d *DNSProvider) findRecordID(ctx context.Context, zoneName, recordName, rdata string) (string, error) {\n\treturn backoff.Retry(ctx,\n\t\tfunc() (string, error) {\n\t\t\tcurrentZone, err := d.client.GetZone(ctx, zoneName)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", backoff.Permanent(fmt.Errorf(\"get zone: %w\", err))\n\t\t\t}\n\n\t\t\trecordID := findRecordIdentifier(currentZone, recordName, rdata)\n\t\t\tif recordID == \"\" {\n\t\t\t\treturn \"\", fmt.Errorf(\"get record identifier: %w\", err)\n\t\t\t}\n\n\t\t\treturn recordID, nil\n\t\t},\n\t\tbackoff.WithBackOff(backoff.NewConstantBackOff(5*time.Second)),\n\t\tbackoff.WithMaxElapsedTime(300*time.Second),\n\t)\n}\n\nfunc findRecordIdentifier(zone *internal.Zone, recordName, rdata string) string {\n\tif len(zone.Revisions) == 0 {\n\t\treturn \"\"\n\t}\n\n\t// Check the first revision (index 0) which should be the current one\n\n\tfor _, record := range zone.Revisions[0].Records {\n\t\tif record.Name != recordName || record.Type != \"TXT\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif record.RData == rdata || record.RData == strconv.Quote(rdata) {\n\t\t\treturn record.Identifier\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\nfunc extractRecordName(fqdn, authZone string) (string, error) {\n\tif dns01.UnFqdn(fqdn) == dns01.UnFqdn(authZone) {\n\t\t// \"@\" for the root domain instead of an empty string.\n\t\treturn \"@\", nil\n\t}\n\n\treturn dns01.ExtractSubDomain(fqdn, authZone)\n}\n"
  },
  {
    "path": "providers/dns/anexia/anexia.toml",
    "content": "Name = \"Anexia CloudDNS\"\nDescription = ''''''\nURL = \"https://www.anexia-it.com/\"\nCode = \"anexia\"\nSince = \"v4.28.0\"\n\nExample = '''\nANEXIA_TOKEN=xxx \\\nlego --dns anexia -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Description\n\nYou need to create an API token in the [Anexia Engine](https://engine.anexia-it.com/).\n\nThe token must have permissions to manage DNS zones and records.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    ANEXIA_TOKEN = \"API token for Anexia Engine\"\n  [Configuration.Additional]\n    ANEXIA_API_URL = \"API endpoint URL (default: https://engine.anexia-it.com)\"\n    ANEXIA_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    ANEXIA_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 300)\"\n    ANEXIA_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    ANEXIA_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://engine.anexia-it.com/docs/en/module/clouddns/api\"\n"
  },
  {
    "path": "providers/dns/anexia/anexia_test.go",
    "content": "package anexia\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvToken,\n\tEnvAPIURL).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success with token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"\",\n\t\t\t},\n\t\t\texpected: \"anexia: some credentials information are missing: ANEXIA_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success with token\",\n\t\t\ttoken: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing token\",\n\t\t\ttoken:    \"\",\n\t\t\texpected: \"anexia: incomplete credentials, missing token\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = \"secret\"\n\t\t\tconfig.APIURL = server.URL\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithAuthorization(\"Token secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /api/clouddns/v1/zone.json/example.com/records\",\n\t\t\tservermock.ResponseFromInternal(\"create_record.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentType(\"application/json; charset=utf-8\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /api/clouddns/v1/zone.json/example.com\",\n\t\t\tservermock.ResponseFromInternal(\"get_zone.json\")).\n\t\tRoute(\"DELETE /api/clouddns/v1/zone.json/example.com/records/12345678-1234-1234-1234-123456789abc\",\n\t\t\tservermock.Noop()).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/anexia/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\nconst defaultBaseURL = \"https://engine.anexia-it.com\"\n\n// Client the Anexia CloudDNS API client.\ntype Client struct {\n\ttoken string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(token string) (*Client, error) {\n\tif token == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\ttoken:      token,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) CreateRecord(ctx context.Context, zoneName string, record Record) (*Zone, error) {\n\tendpoint := c.BaseURL.JoinPath(\"api\", \"clouddns\", \"v1\", \"zone.json\", zoneName, \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar zone Zone\n\n\terr = c.do(req, &zone)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &zone, nil\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, zoneName, recordID string) error {\n\tendpoint := c.BaseURL.JoinPath(\"api\", \"clouddns\", \"v1\", \"zone.json\", zoneName, \"records\", recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) GetZone(ctx context.Context, zoneName string) (*Zone, error) {\n\tendpoint := c.BaseURL.JoinPath(\"api\", \"clouddns\", \"v1\", \"zone.json\", zoneName)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar zone Zone\n\n\terr = c.do(req, &zone)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &zone, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\treq.Header.Add(\"Authorization\", fmt.Sprintf(\"Token %s\", c.token))\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/anexia/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithAuthorization(\"Token secret\"),\n\t)\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /api/clouddns/v1/zone.json/example.com/records\",\n\t\t\tservermock.ResponseFromFixture(\"create_record.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentType(\"application/json; charset=utf-8\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:  \"_acme-challenge\",\n\t\tRData: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:   300,\n\t\tType:  \"TXT\",\n\t}\n\n\tzone, err := client.CreateRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n\n\texpected := &Zone{\n\t\tName:     \"example.com\",\n\t\tTTL:      86400,\n\t\tZoneName: \"example.com\",\n\t\tRevisions: []Revision{{\n\t\t\tIdentifier: \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n\t\t\tRecords: []Record{{\n\t\t\t\tIdentifier: \"12345678-1234-1234-1234-123456789abc\",\n\t\t\t\tName:       \"_acme-challenge\",\n\t\t\t\tRData:      \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\t\t\tTTL:        300,\n\t\t\t\tType:       \"TXT\",\n\t\t\t}},\n\t\t\tState: \"deployed\",\n\t\t}},\n\t}\n\n\tassert.Equal(t, expected, zone)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /api/clouddns/v1/zone.json/example.com/records/12345678-1234-1234-1234-123456789abc\",\n\t\t\tservermock.Noop()).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", \"12345678-1234-1234-1234-123456789abc\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /api/clouddns/v1/zone.json/example.com/records/12345678-1234-1234-1234-123456789abc\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", \"12345678-1234-1234-1234-123456789abc\")\n\trequire.EqualError(t, err, \"401: Unauthorized\")\n}\n\nfunc TestClient_GetZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/clouddns/v1/zone.json/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"get_zone.json\")).\n\t\tBuild(t)\n\n\tzone, err := client.GetZone(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := &Zone{\n\t\tIdentifier: \"fdb355ffd07c48aba3d4f6bf6a116296\",\n\t\tName:       \"example.com\",\n\t\tTTL:        3600,\n\t\tZoneName:   \"\",\n\t\tRevisions: []Revision{{\n\t\t\tIdentifier: \"eeed7e08-f1ad-442b-9e75-369a0958c7d8\",\n\t\t\tRecords: []Record{\n\t\t\t\t{\n\t\t\t\t\tIdentifier: \"5ced498b-c89d-4487-824d-c03ded84f849\",\n\t\t\t\t\tImmutable:  true,\n\t\t\t\t\tName:       \"@\",\n\t\t\t\t\tRData:      \"acns02.xaas.systems.\",\n\t\t\t\t\tRegion:     \"9a1609af9dae4ce1a4ef63f51d305321\",\n\t\t\t\t\tTTL:        3600,\n\t\t\t\t\tType:       \"NS\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tIdentifier: \"12345678-1234-1234-1234-123456789abc\",\n\t\t\t\t\tImmutable:  false,\n\t\t\t\t\tName:       \"_acme-challenge\",\n\t\t\t\t\tRData:      \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\t\t\t\tRegion:     \"\",\n\t\t\t\t\tTTL:        300,\n\t\t\t\t\tType:       \"TXT\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tState: \"active\",\n\t\t}},\n\t}\n\n\tassert.Equal(t, expected, zone)\n}\n"
  },
  {
    "path": "providers/dns/anexia/internal/fixtures/create_record-request.json",
    "content": "{\n  \"name\": \"_acme-challenge\",\n  \"type\": \"TXT\",\n  \"rdata\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n  \"region\": \"\",\n  \"ttl\": 300\n}\n"
  },
  {
    "path": "providers/dns/anexia/internal/fixtures/create_record.json",
    "content": "{\n  \"name\": \"example.com\",\n  \"zone_name\": \"example.com\",\n  \"master\": true,\n  \"dnssec_mode\": \"managed\",\n  \"admin_email\": \"admin@example.com\",\n  \"refresh\": 10800,\n  \"retry\": 3600,\n  \"expire\": 604800,\n  \"ttl\": 86400,\n  \"customer\": \"ANX12345\",\n  \"created_at\": \"0001-01-01T00:00:00Z\",\n  \"updated_at\": \"0001-01-01T00:00:00Z\",\n  \"published_at\": \"0001-01-01T00:00:00Z\",\n  \"is_editable\": true,\n  \"validation_level\": 0,\n  \"deployment_level\": 0,\n  \"revisions\": [\n    {\n      \"created_at\": \"0001-01-01T00:00:00Z\",\n      \"identifier\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n      \"modified_at\": \"0001-01-01T00:00:00Z\",\n      \"records\": [\n        {\n          \"identifier\": \"12345678-1234-1234-1234-123456789abc\",\n          \"immutable\": false,\n          \"name\": \"_acme-challenge\",\n          \"rdata\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n          \"region\": \"\",\n          \"ttl\": 300,\n          \"type\": \"TXT\"\n        }\n      ],\n      \"serial\": 1,\n      \"state\": \"deployed\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/anexia/internal/fixtures/create_record_incomplete.json",
    "content": "{\n  \"name\": \"example.com\",\n  \"zone_name\": \"example.com\",\n  \"master\": true,\n  \"dnssec_mode\": \"managed\",\n  \"admin_email\": \"admin@example.com\",\n  \"refresh\": 10800,\n  \"retry\": 3600,\n  \"expire\": 604800,\n  \"ttl\": 86400,\n  \"customer\": \"ANX12345\",\n  \"created_at\": \"0001-01-01T00:00:00Z\",\n  \"updated_at\": \"0001-01-01T00:00:00Z\",\n  \"published_at\": \"0001-01-01T00:00:00Z\",\n  \"is_editable\": true,\n  \"validation_level\": 0,\n  \"deployment_level\": 0,\n  \"revisions\": [\n    {\n      \"created_at\": \"0001-01-01T00:00:00Z\",\n      \"identifier\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n      \"modified_at\": \"0001-01-01T00:00:00Z\",\n      \"records\": [\n        {\n          \"immutable\": false,\n          \"name\": \"_acme-challenge\",\n          \"rdata\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n          \"region\": \"\",\n          \"ttl\": 300,\n          \"type\": \"TXT\"\n        }\n      ],\n      \"serial\": 1,\n      \"state\": \"deployed\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/anexia/internal/fixtures/error.json",
    "content": "{\n  \"error\": {\n    \"code\": 401,\n    \"message\": \"Unauthorized\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/anexia/internal/fixtures/get_zone.json",
    "content": "{\n  \"identifier\": \"fdb355ffd07c48aba3d4f6bf6a116296\",\n  \"admin_email\": \"admin@example.com\",\n  \"created_at\": \"2019-02-06T10:02:07.000Z\",\n  \"current_revision\": \"eeed7e08-f1ad-442b-9e75-369a0958c7d8\",\n  \"deployment_level\": 100,\n  \"dns_servers\": [\n    {\n      \"server\": \"acns01.xaas.systems\",\n      \"alias\": null\n    },\n    {\n      \"server\": \"acns04.xaas.systems\",\n      \"alias\": null\n    },\n    {\n      \"server\": \"acns02.xaas.systems\",\n      \"alias\": null\n    },\n    {\n      \"server\": \"acns03.xaas.systems\",\n      \"alias\": null\n    },\n    {\n      \"server\": \"acns05.xaas.systems\",\n      \"alias\": null\n    }\n  ],\n  \"dnsCluster\": null,\n  \"dnssec_ksk\": null,\n  \"dnssec_mode\": \"unvalidated\",\n  \"dnssec_sig_expires_at\": null,\n  \"dnssec_zsk\": null,\n  \"expire\": 604800,\n  \"inherit_ns_from\": null,\n  \"nameserver_set\": null,\n  \"master\": true,\n  \"master_ns\": \"acns02.xaas.systems.\",\n  \"name\": \"example.com\",\n  \"notify_allowed_ips\": [\n    \"127.0.0.1\"\n  ],\n  \"published_at\": \"2023-06-20T08:41:06.000Z\",\n  \"refresh\": 14400,\n  \"revisions\": [\n    {\n      \"created_at\": \"2023-06-20T08:41:06.000000Z\",\n      \"identifier\": \"eeed7e08-f1ad-442b-9e75-369a0958c7d8\",\n      \"modified_at\": \"2023-06-20T08:41:06.000000Z\",\n      \"records\": [\n        {\n          \"identifier\": \"5ced498b-c89d-4487-824d-c03ded84f849\",\n          \"immutable\": true,\n          \"name\": \"@\",\n          \"rdata\": \"acns02.xaas.systems.\",\n          \"region\": \"9a1609af9dae4ce1a4ef63f51d305321\",\n          \"ttl\": 3600,\n          \"type\": \"NS\",\n          \"options\": null\n        },\n        {\n          \"identifier\": \"12345678-1234-1234-1234-123456789abc\",\n          \"immutable\": false,\n          \"name\": \"_acme-challenge\",\n          \"rdata\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n          \"region\": \"\",\n          \"ttl\": 300,\n          \"Type\": \"TXT\"\n        }\n      ],\n      \"serial\": 14,\n      \"state\": \"active\"\n    }\n  ],\n  \"retry\": 3600,\n  \"ttl\": 3600,\n  \"updated_at\": \"2020-06-04T18:34:22.000Z\",\n  \"validation_level\": 100,\n  \"whitelabel_config\": null,\n  \"is_editable\": true,\n  \"deploy_zone\": \"49459f420f614eb2a979fc7e961f83e6\"\n}\n"
  },
  {
    "path": "providers/dns/anexia/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype APIError struct {\n\tDetails struct {\n\t\tCode    int    `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n}\n\nfunc (a *APIError) Error() string {\n\treturn fmt.Sprintf(\"%d: %s\", a.Details.Code, a.Details.Message)\n}\n\ntype Zone struct {\n\tIdentifier string     `json:\"identifier,omitempty\"`\n\tName       string     `json:\"name,omitempty\"`\n\tTTL        int        `json:\"ttl,omitempty\"`\n\tZoneName   string     `json:\"zone_name,omitempty\"`\n\tRevisions  []Revision `json:\"revisions,omitempty\"`\n}\n\ntype Revision struct {\n\tIdentifier string   `json:\"identifier,omitempty\"`\n\tRecords    []Record `json:\"records,omitempty\"`\n\tState      string   `json:\"state,omitempty\"`\n}\n\ntype Record struct {\n\tIdentifier string `json:\"identifier,omitempty\"`\n\tImmutable  bool   `json:\"immutable,omitempty\"`\n\tName       string `json:\"name,omitempty\"`\n\tRData      string `json:\"rdata,omitempty\"`\n\tRegion     string `json:\"region\"`\n\tTTL        int    `json:\"ttl,omitempty\"`\n\tType       string `json:\"type,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/artfiles/artfiles.go",
    "content": "// Package artfiles implements a DNS provider for solving the DNS-01 challenge using ArtFiles.\npackage artfiles\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/artfiles/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"ARTFILES_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername string\n\tPassword string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 6*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for ArtFiles.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"artfiles: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for ArtFiles.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"artfiles: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.Username, config.Password)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"artfiles: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.findZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"artfiles: %w\", err)\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"artfiles: get records: %w\", err)\n\t}\n\n\trv := internal.RecordValue{}\n\n\tif len(records[\"TXT\"]) > 0 {\n\t\tvar raw string\n\n\t\terr = json.Unmarshal(records[\"TXT\"], &raw)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"artfiles: unmarshal TXT records: %w\", err)\n\t\t}\n\n\t\trv = internal.ParseRecordValue(raw)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"artfiles: %w\", err)\n\t}\n\n\trv.Add(subDomain, info.Value)\n\n\terr = d.client.SetRecords(ctx, zone, \"TXT\", rv)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"artfiles: set TXT records: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.findZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"artfiles: %w\", err)\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"artfiles: get records: %w\", err)\n\t}\n\n\tvar raw string\n\n\terr = json.Unmarshal(records[\"TXT\"], &raw)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"artfiles: unmarshal TXT records: %w\", err)\n\t}\n\n\trv := internal.ParseRecordValue(raw)\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"artfiles: %w\", err)\n\t}\n\n\trv.RemoveValue(subDomain, info.Value)\n\n\terr = d.client.SetRecords(ctx, zone, \"TXT\", rv)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"artfiles: set TXT records: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) findZone(ctx context.Context, fqdn string) (string, error) {\n\tdomains, err := d.client.GetDomains(ctx)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"artfiles: get domains: %w\", err)\n\t}\n\n\tvar zone string\n\n\tfor s := range dns01.UnFqdnDomainsSeq(fqdn) {\n\t\tif slices.Contains(domains, s) {\n\t\t\tzone = s\n\t\t}\n\t}\n\n\tif zone == \"\" {\n\t\treturn \"\", fmt.Errorf(\"artfiles: could not find the zone for domain %q\", fqdn)\n\t}\n\n\treturn zone, nil\n}\n"
  },
  {
    "path": "providers/dns/artfiles/artfiles.toml",
    "content": "Name = \"ArtFiles\"\nDescription = ''''''\nURL = \"https://www.artfiles.de/extras/domains/\"\nCode = \"artfiles\"\nSince = \"v4.32.0\"\n\nExample = '''\nARTFILES_USERNAME=\"xxx\" \\\nARTFILES_PASSWORD=\"yyy\" \\\nlego --dns artfiles -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    ARTFILES_USERNAME = \"API username\"\n    ARTFILES_PASSWORD = \"API password\"\n  [Configuration.Additional]\n    ARTFILES_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    ARTFILES_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 360)\"\n    ARTFILES_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    ARTFILES_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://support.artfiles.de/DCP-API#dns\"\n"
  },
  {
    "path": "providers/dns/artfiles/artfiles_test.go",
    "content": "package artfiles\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"artfiles: some credentials information are missing: ARTFILES_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"artfiles: some credentials information are missing: ARTFILES_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"artfiles: some credentials information are missing: ARTFILES_USERNAME,ARTFILES_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"artfiles: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing Example\",\n\t\t\tusername: \"user\",\n\t\t\texpected: \"artfiles: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"artfiles: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = \"user\"\n\t\t\tconfig.Password = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithBasicAuth(\"user\", \"secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /domain/get_domains.html\",\n\t\t\tservermock.ResponseFromInternal(\"domains.txt\"),\n\t\t).\n\t\tRoute(\"GET /dns/get_dns.html\",\n\t\t\tservermock.ResponseFromInternal(\"get_dns.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"domain\", \"example.com\"),\n\t\t).\n\t\tRoute(\"POST /dns/set_dns.html\",\n\t\t\tservermock.ResponseFromInternal(\"set_dns.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"TXT\", `@ \"v=spf1 a mx ~all\"\n_acme-challenge \"TheAcmeChallenge\"\n_acme-challenge \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"\n_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\"\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\t\t\t\tWith(\"domain\", \"example.com\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /domain/get_domains.html\",\n\t\t\tservermock.ResponseFromInternal(\"domains.txt\"),\n\t\t).\n\t\tRoute(\"GET /dns/get_dns.html\",\n\t\t\tservermock.ResponseFromInternal(\"get_dns.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"domain\", \"example.com\"),\n\t\t).\n\t\tRoute(\"POST /dns/set_dns.html\",\n\t\t\tservermock.ResponseFromInternal(\"set_dns.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"TXT\", `@ \"v=spf1 a mx ~all\"\n_acme-challenge \"TheAcmeChallenge\"\n_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\"\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\t\t\t\tWith(\"domain\", \"example.com\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/artfiles/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\nconst defaultBaseURL = \"https://dcp.c.artfiles.de/api/\"\n\n// Client the ArtFiles API client.\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(username, password string) (*Client, error) {\n\tif username == \"\" || password == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) GetDomains(ctx context.Context) ([]string, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domain\", \"get_domains.html\")\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\traw, err := c.do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn parseDomains(string(raw))\n}\n\nfunc (c *Client) GetRecords(ctx context.Context, domain string) (map[string]json.RawMessage, error) {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", \"get_dns.html\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"domain\", domain)\n\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\traw, err := c.do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result Records\n\n\terr = json.Unmarshal(raw, &result)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, http.StatusOK, raw, err)\n\t}\n\n\treturn result.Data, nil\n}\n\nfunc (c *Client) SetRecords(ctx context.Context, domain, rType string, value RecordValue) error {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", \"set_dns.html\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"domain\", domain)\n\tquery.Set(rType, value.String())\n\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\t_, err = c.do(req)\n\n\treturn err\n}\n\nfunc (c *Client) do(req *http.Request) ([]byte, error) {\n\tuseragent.SetHeader(req.Header)\n\n\treq.SetBasicAuth(c.username, c.password)\n\n\tif req.Method == http.MethodPost {\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn nil, errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn raw, nil\n}\n"
  },
  {
    "path": "providers/dns/artfiles/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"user\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithBasicAuth(\"user\", \"secret\"),\n\t)\n}\n\nfunc TestClient_GetDomains(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domain/get_domains.html\",\n\t\t\tservermock.ResponseFromFixture(\"domains.txt\"),\n\t\t).\n\t\tBuild(t)\n\n\tzones, err := client.GetDomains(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []string{\"example.com\", \"example.org\", \"example.net\"}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/get_dns.html\",\n\t\t\tservermock.ResponseFromFixture(\"get_dns.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"domain\", \"example.com\"),\n\t\t).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := map[string]json.RawMessage{\n\t\t\"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\")),\n\t\t\"AAAA\":       json.RawMessage(strconv.Quote(\"\")),\n\t\t\"CAA\":        json.RawMessage(strconv.Quote(\"@ 128 iodef \\\"mailto:someone@example.tld\\\"\\n@ 128 issue \\\"letsencrypt.org\\\"\\n@ 128 issuewild \\\"letsencrypt.org\\\"\")),\n\t\t\"CName\":      json.RawMessage(strconv.Quote(\"some cname.to.example.tld.\")),\n\t\t\"MX\":         json.RawMessage(strconv.Quote(\"10 mail.example.tld.\")),\n\t\t\"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 .\")),\n\t\t\"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\")),\n\t\t\"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\\\"\")),\n\t\t\"TTL\":        json.RawMessage(\"3600\"),\n\t\t\"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\")),\n\t\t\"nameserver\": json.RawMessage(strconv.Quote(\"auth1.artfiles.de.\\nauth2.artfiles.de.\")),\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_SetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/set_dns.html\",\n\t\t\tservermock.ResponseFromFixture(\"set_dns.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"TXT\", \"a b\\nc \\\"d\\\"\").\n\t\t\t\tWith(\"domain\", \"example.com\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.SetRecords(t.Context(), \"example.com\", \"TXT\", RecordValue{\"c\": []string{`\"d\"`}, \"a\": []string{\"b\"}})\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/artfiles/internal/fixtures/domains.txt",
    "content": "example.com\tnormal\t\t2026-10-01\t2017-09-18\t163477\nexample.org\tnormal\t\t2026-08-01\t2016-07-07\t156216\nexample.net\tnormal\t\t2026-07-01\t2017-06-06\t162462\n"
  },
  {
    "path": "providers/dns/artfiles/internal/fixtures/get_dns.json",
    "content": "{\n  \"data\": {\n    \"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 .\",\n    \"AAAA\": \"\",\n    \"MX\": \"10 mail.example.tld.\",\n    \"CAA\": \"@ 128 iodef \\\"mailto:someone@example.tld\\\"\\n@ 128 issue \\\"letsencrypt.org\\\"\\n@ 128 issuewild \\\"letsencrypt.org\\\"\",\n    \"TTL\": 3600,\n    \"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\",\n    \"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\\\"\",\n    \"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\",\n    \"nameserver\": \"auth1.artfiles.de.\\nauth2.artfiles.de.\",\n    \"CName\": \"some cname.to.example.tld.\",\n    \"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\"\n  },\n  \"status\": \"OK\"\n}\n"
  },
  {
    "path": "providers/dns/artfiles/internal/fixtures/set_dns.json",
    "content": "{\n  \"status\": \"OK\",\n  \"error\": \"\"\n}\n"
  },
  {
    "path": "providers/dns/artfiles/internal/fixtures/txt_record-multiple.txt",
    "content": "_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 \"xxx\"\n_acme-challenge \"yyy\"\n"
  },
  {
    "path": "providers/dns/artfiles/internal/fixtures/txt_record.txt",
    "content": "_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\"\n"
  },
  {
    "path": "providers/dns/artfiles/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"encoding/csv\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"maps\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n)\n\ntype Records struct {\n\tData   map[string]json.RawMessage `json:\"data\"`\n\tStatus string                     `json:\"status\"`\n}\n\ntype RecordValue map[string][]string\n\nfunc (r RecordValue) Set(key, value string) {\n\tr[key] = []string{strconv.Quote(value)}\n}\n\nfunc (r RecordValue) Add(key, value string) {\n\tr[key] = append(r[key], strconv.Quote(value))\n}\n\nfunc (r RecordValue) Delete(key string) {\n\tdelete(r, key)\n}\n\nfunc (r RecordValue) RemoveValue(key, value string) {\n\tif len(r[key]) == 0 {\n\t\treturn\n\t}\n\n\tquotedValue := strconv.Quote(value)\n\n\tvar data []string\n\n\tfor _, s := range r[key] {\n\t\tif s != quotedValue {\n\t\t\tdata = append(data, s)\n\t\t}\n\t}\n\n\tr[key] = data\n\n\tif len(r[key]) == 0 {\n\t\tr.Delete(key)\n\t}\n}\n\nfunc (r RecordValue) String() string {\n\tvar parts []string\n\n\tfor _, key := range slices.Sorted(maps.Keys(r)) {\n\t\tfor _, s := range r[key] {\n\t\t\tparts = append(parts, key+\" \"+s)\n\t\t}\n\t}\n\n\treturn strings.Join(parts, \"\\n\")\n}\n\nfunc ParseRecordValue(lines string) RecordValue {\n\tdata := make(RecordValue)\n\n\tfor line := range strings.Lines(lines) {\n\t\tline = strings.TrimSpace(line)\n\n\t\tidx := strings.IndexFunc(line, unicode.IsSpace)\n\n\t\tdata[line[:idx]] = append(data[line[:idx]], line[idx+1:])\n\t}\n\n\treturn data\n}\n\nfunc parseDomains(input string) ([]string, error) {\n\treader := csv.NewReader(strings.NewReader(input))\n\treader.Comma = '\\t'\n\treader.TrimLeadingSpace = true\n\treader.LazyQuotes = true\n\n\tvar data []string\n\n\tfor {\n\t\trecord, err := reader.Read()\n\t\tif errors.Is(err, io.EOF) {\n\t\t\tbreak\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif len(record) < 1 {\n\t\t\t// Malformed line\n\t\t\tcontinue\n\t\t}\n\n\t\tdata = append(data, record[0])\n\t}\n\n\treturn data, nil\n}\n"
  },
  {
    "path": "providers/dns/artfiles/internal/types_test.go",
    "content": "package internal\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRecordValue_Set(t *testing.T) {\n\trv := make(RecordValue)\n\n\trv.Set(\"a\", \"1\")\n\trv.Set(\"b\", \"2\")\n\trv.Set(\"b\", \"3\")\n\n\tassert.Equal(t, \"a \\\"1\\\"\\nb \\\"3\\\"\", rv.String())\n}\n\nfunc TestRecordValue_Add(t *testing.T) {\n\trv := make(RecordValue)\n\n\trv.Add(\"a\", \"1\")\n\trv.Add(\"b\", \"2\")\n\trv.Add(\"b\", \"3\")\n\n\tassert.Equal(t, \"a \\\"1\\\"\\nb \\\"2\\\"\\nb \\\"3\\\"\", rv.String())\n}\n\nfunc TestRecordValue_Delete(t *testing.T) {\n\trv := make(RecordValue)\n\n\trv.Set(\"a\", \"1\")\n\trv.Add(\"b\", \"2\")\n\n\trv.Delete(\"b\")\n\n\tassert.Equal(t, \"a \\\"1\\\"\", rv.String())\n}\n\nfunc TestRecordValue_RemoveValue(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tdata     map[string][]string\n\t\ttoRemove map[string][]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"remove the only value\",\n\t\t\tdata: map[string][]string{\n\t\t\t\t\"a\": {\"1\"},\n\t\t\t},\n\t\t\ttoRemove: map[string][]string{\n\t\t\t\t\"a\": {\"1\"},\n\t\t\t},\n\t\t\texpected: ``,\n\t\t},\n\t\t{\n\t\t\tdesc: \"remove value in the middle\",\n\t\t\tdata: map[string][]string{\n\t\t\t\t\"a\": {\"1\", \"2\", \"3\"},\n\t\t\t},\n\t\t\ttoRemove: map[string][]string{\n\t\t\t\t\"a\": {\"2\"},\n\t\t\t},\n\t\t\texpected: \"a \\\"1\\\"\\na \\\"3\\\"\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"remove value at the beginning\",\n\t\t\tdata: map[string][]string{\n\t\t\t\t\"a\": {\"1\", \"2\", \"3\"},\n\t\t\t},\n\t\t\ttoRemove: map[string][]string{\n\t\t\t\t\"a\": {\"1\"},\n\t\t\t},\n\t\t\texpected: \"a \\\"2\\\"\\na \\\"3\\\"\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"remove value at the end\",\n\t\t\tdata: map[string][]string{\n\t\t\t\t\"a\": {\"1\", \"2\", \"3\"},\n\t\t\t},\n\t\t\ttoRemove: map[string][]string{\n\t\t\t\t\"a\": {\"3\"},\n\t\t\t},\n\t\t\texpected: \"a \\\"1\\\"\\na \\\"2\\\"\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"remove all (delete)\",\n\t\t\tdata: map[string][]string{\n\t\t\t\t\"a\": {\"1\", \"2\", \"3\"},\n\t\t\t},\n\t\t\ttoRemove: map[string][]string{\n\t\t\t\t\"a\": {\"1\", \"2\", \"3\"},\n\t\t\t},\n\t\t\texpected: ``,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\trv := make(RecordValue)\n\n\t\t\tfor k, values := range test.data {\n\t\t\t\tfor _, v := range values {\n\t\t\t\t\trv.Add(k, v)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor k, values := range test.toRemove {\n\t\t\t\tfor _, v := range values {\n\t\t\t\t\trv.RemoveValue(k, v)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.Equal(t, test.expected, rv.String())\n\t\t})\n\t}\n}\n\nfunc TestParseRecordValue(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tfilename string\n\t\texpected RecordValue\n\t}{\n\t\t{\n\t\t\tdesc:     \"simple\",\n\t\t\tfilename: \"txt_record.txt\",\n\t\t\texpected: RecordValue{\n\t\t\t\t\"@\":                      []string{\"\\\"v=spf1 a mx ~all\\\"\"},\n\t\t\t\t\"_acme-challenge\":        []string{\"\\\"TheAcmeChallenge\\\"\"},\n\t\t\t\t\"_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\\\"\"},\n\t\t\t\t\"_mta-sts\":               []string{\"\\\"v=STSv1;id=yyyymmddTHHMMSS;\\\"\"},\n\t\t\t\t\"_smtp._tls\":             []string{\"\\\"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\\\"\"},\n\t\t\t\t\"selector._domainkey\":    []string{\"\\\"v=DKIM1;k=rsa;p=Base64Stuff\\\" \\\"MoreBase64Stuff\\\" \\\"Even++MoreBase64Stuff\\\" \\\"YesMoreBase64Stuff\\\" \\\"And+Yes+Even+MoreBase64Stuff\\\" \\\"Sure++MoreBase64Stuff\\\" \\\"LastBase64Stuff\\\"\"},\n\t\t\t\t\"selectorecc._domainkey\": []string{\"\\\"v=DKIM1;k=ed25519;p=Base64Stuff\\\"\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"multiple values with the same key\",\n\t\t\tfilename: \"txt_record-multiple.txt\",\n\t\t\texpected: RecordValue{\n\t\t\t\t\"@\":                      []string{\"\\\"v=spf1 a mx ~all\\\"\"},\n\t\t\t\t\"_acme-challenge\":        []string{\"\\\"xxx\\\"\", \"\\\"yyy\\\"\"},\n\t\t\t\t\"_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\\\"\"},\n\t\t\t\t\"_mta-sts\":               []string{\"\\\"v=STSv1;id=yyyymmddTHHMMSS;\\\"\"},\n\t\t\t\t\"_smtp._tls\":             []string{\"\\\"v=TLSRPTv1;rua=mailto:someone@in.mailhardener.com\\\"\"},\n\t\t\t\t\"selector._domainkey\":    []string{\"\\\"v=DKIM1;k=rsa;p=Base64Stuff\\\" \\\"MoreBase64Stuff\\\" \\\"Even++MoreBase64Stuff\\\" \\\"YesMoreBase64Stuff\\\" \\\"And+Yes+Even+MoreBase64Stuff\\\" \\\"Sure++MoreBase64Stuff\\\" \\\"LastBase64Stuff\\\"\"},\n\t\t\t\t\"selectorecc._domainkey\": []string{\"\\\"v=DKIM1;k=ed25519;p=Base64Stuff\\\"\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tfile, err := os.ReadFile(filepath.Join(\"fixtures\", test.filename))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdata := ParseRecordValue(string(file))\n\n\t\t\tassert.Equal(t, test.expected, data)\n\t\t})\n\t}\n}\n\nfunc Test_parseDomains(t *testing.T) {\n\tfile, err := os.ReadFile(filepath.FromSlash(\"./fixtures/domains.txt\"))\n\trequire.NoError(t, err)\n\n\tdomains, err := parseDomains(string(file))\n\trequire.NoError(t, err)\n\n\texpected := []string{\"example.com\", \"example.org\", \"example.net\"}\n\n\tassert.Equal(t, expected, domains)\n}\n"
  },
  {
    "path": "providers/dns/arvancloud/arvancloud.go",
    "content": "// Package arvancloud implements a DNS provider for solving the DNS-01 challenge using ArvanCloud DNS.\npackage arvancloud\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/arvancloud/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"ARVANCLOUD_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst minTTL = 600\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for ArvanCloud.\n// Credentials must be passed in the environment variable: ARVANCLOUD_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"arvancloud: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for ArvanCloud.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"arvancloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"arvancloud: credentials missing\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"arvancloud: invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\tclient := internal.NewClient(config.APIKey)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"arvancloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"arvancloud: %w\", err)\n\t}\n\n\trecord := internal.DNSRecord{\n\t\tType:          \"txt\",\n\t\tName:          subDomain,\n\t\tValue:         internal.TXTRecordValue{Text: info.Value},\n\t\tTTL:           d.config.TTL,\n\t\tUpstreamHTTPS: \"default\",\n\t\tIPFilterMode: &internal.IPFilterMode{\n\t\t\tCount:     \"single\",\n\t\t\tGeoFilter: \"none\",\n\t\t\tOrder:     \"none\",\n\t\t},\n\t}\n\n\tnewRecord, err := d.client.CreateRecord(context.Background(), authZone, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"arvancloud: failed to add TXT record: fqdn=%s: %w\", info.EffectiveFQDN, err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = newRecord.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"arvancloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\t// gets the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"arvancloud: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\tif err := d.client.DeleteRecord(context.Background(), authZone, recordID); err != nil {\n\t\treturn fmt.Errorf(\"arvancloud: failed to delete TXT record: id=%s: %w\", recordID, err)\n\t}\n\n\t// deletes record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/arvancloud/arvancloud.toml",
    "content": "Name = \"ArvanCloud\"\nDescription = ''''''\nURL = \"https://arvancloud.ir\"\nCode = \"arvancloud\"\nSince = \"v3.8.0\"\n\nExample = '''\nARVANCLOUD_API_KEY=\"Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\" \\\nlego --dns arvancloud -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    ARVANCLOUD_API_KEY = \"API key\"\n  [Configuration.Additional]\n    ARVANCLOUD_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    ARVANCLOUD_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    ARVANCLOUD_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)\"\n    ARVANCLOUD_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.arvancloud.ir/docs/api/cdn/4.0\"\n"
  },
  {
    "path": "providers/dns/arvancloud/arvancloud_test.go",
    "content": "package arvancloud\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"arvancloud: some credentials information are missing: ARVANCLOUD_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tttl      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tttl:    minTTL,\n\t\t\tapiKey: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tttl:      minTTL,\n\t\t\texpected: \"arvancloud: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"invalid TTL\",\n\t\t\tapiKey:   \"123\",\n\t\t\tttl:      60,\n\t\t\texpected: \"arvancloud: invalid TTL, TTL (60) must be greater than 600\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.TTL = test.ttl\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/arvancloud/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// defaultBaseURL represents the API endpoint to call.\nconst defaultBaseURL = \"https://napi.arvancloud.ir\"\n\nconst authorizationHeader = \"Authorization\"\n\n// Client the ArvanCloud client.\ntype Client struct {\n\tapiKey string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(apiKey string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// GetTxtRecord gets a TXT record.\nfunc (c *Client) GetTxtRecord(ctx context.Context, domain, name, value string) (*DNSRecord, error) {\n\trecords, err := c.getRecords(ctx, domain, name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, record := range records {\n\t\tif equalsTXTRecord(record, name, value) {\n\t\t\treturn &record, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"could not find record: Domain: %s; Record: %s\", domain, name)\n}\n\n// https://www.arvancloud.ir/docs/api/cdn/4.0#operation/dns_records.list\nfunc (c *Client) getRecords(ctx context.Context, domain, search string) ([]DNSRecord, error) {\n\tendpoint := c.baseURL.JoinPath(\"cdn\", \"4.0\", \"domains\", domain, \"dns-records\")\n\n\tif search != \"\" {\n\t\tquery := endpoint.Query()\n\t\tquery.Set(\"search\", strings.ReplaceAll(search, \"_\", \"\"))\n\t\tendpoint.RawQuery = query.Encode()\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresponse := &apiResponse[[]DNSRecord]{}\n\n\terr = c.do(req, http.StatusOK, response)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not get records %s: Domain: %s: %w\", search, domain, err)\n\t}\n\n\treturn response.Data, nil\n}\n\n// CreateRecord creates a DNS record.\n// https://www.arvancloud.ir/docs/api/cdn/4.0#operation/dns_records.create\nfunc (c *Client) CreateRecord(ctx context.Context, domain string, record DNSRecord) (*DNSRecord, error) {\n\tendpoint := c.baseURL.JoinPath(\"cdn\", \"4.0\", \"domains\", domain, \"dns-records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresponse := &apiResponse[*DNSRecord]{}\n\n\terr = c.do(req, http.StatusCreated, response)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create record; Domain: %s: %w\", domain, err)\n\t}\n\n\treturn response.Data, nil\n}\n\n// DeleteRecord deletes a DNS record.\n// https://www.arvancloud.ir/docs/api/cdn/4.0#operation/dns_records.remove\nfunc (c *Client) DeleteRecord(ctx context.Context, domain, id string) error {\n\tendpoint := c.baseURL.JoinPath(\"cdn\", \"4.0\", \"domains\", domain, \"dns-records\", id)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, http.StatusOK, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not delete record %s; Domain: %s: %w\", id, domain, err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) do(req *http.Request, expectedStatus int, result any) error {\n\treq.Header.Set(authorizationHeader, c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != expectedStatus {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc equalsTXTRecord(record DNSRecord, name, value string) bool {\n\tif record.Type != \"txt\" {\n\t\treturn false\n\t}\n\n\tif record.Name != name {\n\t\treturn false\n\t}\n\n\tdata, ok := record.Value.(map[string]any)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn data[\"text\"] == value\n}\n"
  },
  {
    "path": "providers/dns/arvancloud/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder(apiKey string) *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(apiKey)\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(apiKey))\n}\n\nfunc TestClient_GetTxtRecord(t *testing.T) {\n\tconst apiKey = \"myKeyA\"\n\n\tconst domain = \"example.com\"\n\n\tclient := mockBuilder(apiKey).\n\t\tRoute(\"GET /cdn/4.0/domains/\"+domain+\"/dns-records\",\n\t\t\tservermock.ResponseFromFixture(\"get_txt_record.json\"),\n\t\t\tservermock.CheckQueryParameter().With(\"search\", \"acme-challenge\")).\n\t\tBuild(t)\n\n\t_, err := client.GetTxtRecord(t.Context(), domain, \"_acme-challenge\", \"txtxtxt\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tconst apiKey = \"myKeyB\"\n\n\tconst domain = \"example.com\"\n\n\tclient := mockBuilder(apiKey).\n\t\tRoute(\"POST /cdn/4.0/domains/\"+domain+\"/dns-records\",\n\t\t\tservermock.ResponseFromFixture(\"create_txt_record.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\trecord := DNSRecord{\n\t\tName:  \"_acme-challenge\",\n\t\tType:  \"txt\",\n\t\tValue: &TXTRecordValue{Text: \"txtxtxt\"},\n\t\tTTL:   600,\n\t}\n\n\tnewRecord, err := client.CreateRecord(t.Context(), domain, record)\n\trequire.NoError(t, err)\n\n\texpected := &DNSRecord{\n\t\tID:            \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\n\t\tType:          \"txt\",\n\t\tValue:         map[string]any{\"text\": \"txtxtxt\"},\n\t\tName:          \"_acme-challenge\",\n\t\tTTL:           120,\n\t\tUpstreamHTTPS: \"default\",\n\t\tIPFilterMode: &IPFilterMode{\n\t\t\tCount:     \"single\",\n\t\t\tOrder:     \"none\",\n\t\t\tGeoFilter: \"none\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, newRecord)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tconst apiKey = \"myKeyC\"\n\n\tconst (\n\t\tdomain   = \"example.com\"\n\t\trecordID = \"recordId\"\n\t)\n\n\tclient := mockBuilder(apiKey).\n\t\tRoute(\"DELETE /cdn/4.0/domains/\"+domain+\"/dns-records/\"+recordID, nil).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), domain, recordID)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/arvancloud/internal/fixtures/create_record-request.json",
    "content": "{\n  \"type\": \"txt\",\n  \"value\": {\n    \"text\": \"txtxtxt\"\n  },\n  \"name\": \"_acme-challenge\",\n  \"ttl\": 600\n}\n"
  },
  {
    "path": "providers/dns/arvancloud/internal/fixtures/create_txt_record.json",
    "content": "{\n  \"data\": {\n    \"id\": \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\n    \"type\": \"txt\",\n    \"name\": \"_acme-challenge\",\n    \"value\": {\n      \"text\": \"txtxtxt\"\n    },\n    \"ttl\": 120,\n    \"cloud\": false,\n    \"upstream_https\": \"default\",\n    \"ip_filter_mode\": {\n      \"count\": \"single\",\n      \"order\": \"none\",\n      \"geo_filter\": \"none\"\n    },\n    \"can_delete\": true,\n    \"health_check_status\": false,\n    \"health_check_setting\": {\n      \"protocol\": \"http\",\n      \"port\": \"\",\n      \"uri\": \"\"\n    },\n    \"created_at\": \"2020-05-27T23:57:02Z\",\n    \"updated_at\": \"2020-05-27T23:57:02Z\"\n  },\n  \"message\": \"DNS record created successfully\"\n}\n"
  },
  {
    "path": "providers/dns/arvancloud/internal/fixtures/get_txt_record.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\n      \"type\": \"a\",\n      \"name\": \"@\",\n      \"value\": [\n        {\n          \"ip\": \"xx.xxx.xxx.xxx\",\n          \"port\": null,\n          \"weight\": 1,\n          \"country\": \"\"\n        }\n      ],\n      \"ttl\": 120,\n      \"cloud\": true,\n      \"upstream_https\": \"default\",\n      \"ip_filter_mode\": {\n        \"count\": \"single\",\n        \"order\": \"none\",\n        \"geo_filter\": \"none\"\n      },\n      \"can_delete\": true,\n      \"health_check_status\": false,\n      \"health_check_setting\": {\n        \"protocol\": \"http\",\n        \"port\": \"\",\n        \"uri\": \"\"\n      },\n      \"created_at\": \"2020-05-19T15:05:12Z\",\n      \"updated_at\": \"2020-05-23T22:06:00Z\"\n    },\n    {\n      \"id\": \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\n      \"type\": \"a\",\n      \"name\": \"www\",\n      \"value\": [\n        {\n          \"ip\": \"xx.xxx.xxx.xxx\",\n          \"port\": null,\n          \"weight\": 1,\n          \"country\": \"\"\n        }\n      ],\n      \"ttl\": 120,\n      \"cloud\": true,\n      \"upstream_https\": \"default\",\n      \"ip_filter_mode\": {\n        \"count\": \"single\",\n        \"order\": \"none\",\n        \"geo_filter\": \"none\"\n      },\n      \"can_delete\": true,\n      \"health_check_status\": false,\n      \"health_check_setting\": {\n        \"protocol\": \"http\",\n        \"port\": \"\",\n        \"uri\": \"\"\n      },\n      \"created_at\": \"2020-05-19T15:05:12Z\",\n      \"updated_at\": \"2020-05-23T22:05:55Z\"\n    },\n    {\n      \"id\": \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\n      \"type\": \"a\",\n      \"name\": \"thatcher\",\n      \"value\": [\n        {\n          \"ip\": \"xx.xxx.xxx.xxx\",\n          \"port\": null,\n          \"weight\": 100,\n          \"country\": \"\"\n        }\n      ],\n      \"ttl\": 120,\n      \"cloud\": false,\n      \"upstream_https\": \"default\",\n      \"ip_filter_mode\": {\n        \"count\": \"single\",\n        \"order\": \"none\",\n        \"geo_filter\": \"none\"\n      },\n      \"can_delete\": true,\n      \"health_check_status\": false,\n      \"health_check_setting\": {\n        \"protocol\": \"http\",\n        \"port\": \"\",\n        \"uri\": \"\"\n      },\n      \"created_at\": \"2020-05-20T18:45:10Z\",\n      \"updated_at\": \"2020-05-21T13:19:46Z\"\n    },\n    {\n      \"id\": \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\n      \"type\": \"a\",\n      \"name\": \"api\",\n      \"value\": [\n        {\n          \"ip\": \"xx.xxx.xxx.xxx\",\n          \"port\": null,\n          \"weight\": 100,\n          \"country\": \"\"\n        }\n      ],\n      \"ttl\": 120,\n      \"cloud\": true,\n      \"upstream_https\": \"default\",\n      \"ip_filter_mode\": {\n        \"count\": \"single\",\n        \"order\": \"none\",\n        \"geo_filter\": \"none\"\n      },\n      \"can_delete\": true,\n      \"health_check_status\": false,\n      \"health_check_setting\": {\n        \"protocol\": \"http\",\n        \"port\": \"\",\n        \"uri\": \"\"\n      },\n      \"created_at\": \"2020-05-20T18:45:35Z\",\n      \"updated_at\": \"2020-05-22T20:22:27Z\"\n    },\n    {\n      \"id\": \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\n      \"type\": \"a\",\n      \"name\": \"rock\",\n      \"value\": [\n        {\n          \"ip\": \"xx.xxx.xxx.xxx\",\n          \"port\": null,\n          \"weight\": 100,\n          \"country\": \"\"\n        }\n      ],\n      \"ttl\": 120,\n      \"cloud\": true,\n      \"upstream_https\": \"default\",\n      \"ip_filter_mode\": {\n        \"count\": \"single\",\n        \"order\": \"none\",\n        \"geo_filter\": \"none\"\n      },\n      \"can_delete\": true,\n      \"health_check_status\": false,\n      \"health_check_setting\": {\n        \"protocol\": \"http\",\n        \"port\": \"\",\n        \"uri\": \"\"\n      },\n      \"created_at\": \"2020-05-22T10:29:27Z\",\n      \"updated_at\": \"2020-05-22T13:35:26Z\"\n    },\n    {\n      \"id\": \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\n      \"type\": \"ns\",\n      \"name\": \"@\",\n      \"value\": {\n        \"host\": \"z.ns.arvancdn.com.\"\n      },\n      \"ttl\": 7200,\n      \"cloud\": false,\n      \"upstream_https\": \"default\",\n      \"ip_filter_mode\": {\n        \"count\": \"single\",\n        \"order\": \"none\",\n        \"geo_filter\": \"none\"\n      },\n      \"can_delete\": false,\n      \"health_check_status\": false,\n      \"health_check_setting\": {\n        \"protocol\": \"http\",\n        \"port\": \"\",\n        \"uri\": \"\"\n      },\n      \"created_at\": \"2020-05-19T15:05:09Z\",\n      \"updated_at\": \"2020-05-19T15:05:09Z\"\n    },\n    {\n      \"id\": \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\n      \"type\": \"ns\",\n      \"name\": \"@\",\n      \"value\": {\n        \"host\": \"g.ns.arvancdn.com.\"\n      },\n      \"ttl\": 7200,\n      \"cloud\": false,\n      \"upstream_https\": \"default\",\n      \"ip_filter_mode\": {\n        \"count\": \"single\",\n        \"order\": \"none\",\n        \"geo_filter\": \"none\"\n      },\n      \"can_delete\": false,\n      \"health_check_status\": false,\n      \"health_check_setting\": {\n        \"protocol\": \"http\",\n        \"port\": \"\",\n        \"uri\": \"\"\n      },\n      \"created_at\": \"2020-05-19T15:05:12Z\",\n      \"updated_at\": \"2020-05-19T15:05:12Z\"\n    },\n    {\n      \"id\": \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\n      \"type\": \"txt\",\n      \"name\": \"_acme-challenge\",\n      \"value\": {\n        \"text\": \"txtxtxt\"\n      },\n      \"ttl\": 120,\n      \"cloud\": false,\n      \"upstream_https\": \"default\",\n      \"ip_filter_mode\": {\n        \"count\": \"single\",\n        \"order\": \"none\",\n        \"geo_filter\": \"none\"\n      },\n      \"can_delete\": true,\n      \"health_check_status\": false,\n      \"health_check_setting\": {\n        \"protocol\": \"http\",\n        \"port\": \"\",\n        \"uri\": \"\"\n      },\n      \"created_at\": \"2020-05-27T20:53:54Z\",\n      \"updated_at\": \"2020-05-27T20:53:54Z\"\n    }\n  ],\n  \"links\": {\n    \"first\": \"https://napi.arvancloud.ir/4.0/domains/example.ir/dns-records?page=1\",\n    \"last\": \"https://napi.arvancloud.ir/4.0/domains/example.ir/dns-records?page=1\",\n    \"prev\": null,\n    \"next\": null\n  },\n  \"meta\": {\n    \"current_page\": 1,\n    \"from\": 1,\n    \"last_page\": 1,\n    \"path\": \"https://napi.arvancloud.ir/4.0/domains/example.ir/dns-records\",\n    \"per_page\": 300,\n    \"to\": 8,\n    \"total\": 8\n  }\n}\n"
  },
  {
    "path": "providers/dns/arvancloud/internal/types.go",
    "content": "package internal\n\ntype apiResponse[T any] struct {\n\tMessage string `json:\"message\"`\n\tData    T      `json:\"data\"`\n}\n\n// DNSRecord a DNS record.\ntype DNSRecord struct {\n\tID            string        `json:\"id,omitempty\"`\n\tType          string        `json:\"type\"`\n\tValue         any           `json:\"value,omitempty\"`\n\tName          string        `json:\"name,omitempty\"`\n\tTTL           int           `json:\"ttl,omitempty\"`\n\tUpstreamHTTPS string        `json:\"upstream_https,omitempty\"`\n\tIPFilterMode  *IPFilterMode `json:\"ip_filter_mode,omitempty\"`\n}\n\n// TXTRecordValue represents a TXT record value.\ntype TXTRecordValue struct {\n\tText string `json:\"text,omitempty\"` // only for TXT Record.\n}\n\n// IPFilterMode a DNS ip_filter_mode.\ntype IPFilterMode struct {\n\tCount     string `json:\"count,omitempty\"`\n\tOrder     string `json:\"order,omitempty\"`\n\tGeoFilter string `json:\"geo_filter,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/auroradns/auroradns.go",
    "content": "// Package auroradns implements a DNS provider for solving the DNS-01 challenge using Aurora DNS.\npackage auroradns\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/nrdcg/auroradns\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"AURORA_\"\n\n\tEnvAPIKey   = envNamespace + \"API_KEY\"\n\tEnvSecret   = envNamespace + \"SECRET\"\n\tEnvEndpoint = envNamespace + \"ENDPOINT\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nconst defaultBaseURL = \"https://api.auroradns.eu\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL            string\n\tAPIKey             string\n\tSecret             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *auroradns.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for AuroraDNS.\n// Credentials must be passed in the environment variables:\n// AURORA_API_KEY and AURORA_SECRET.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey, EnvSecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"aurora: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.BaseURL = env.GetOrFile(EnvEndpoint)\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.Secret = values[EnvSecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for AuroraDNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"aurora: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" || config.Secret == \"\" {\n\t\treturn nil, errors.New(\"aurora: some credentials information are missing\")\n\t}\n\n\tif config.BaseURL == \"\" {\n\t\tconfig.BaseURL = defaultBaseURL\n\t}\n\n\ttr, err := auroradns.NewTokenTransport(config.APIKey, config.Secret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"aurora: %w\", err)\n\t}\n\n\tclient, err := auroradns.NewClient(clientdebug.Wrap(tr.Client()), auroradns.WithBaseURL(config.BaseURL))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"aurora: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"aurora: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// 1. Aurora will happily create the TXT record when it is provided a fqdn,\n\t//    but it will only appear in the control panel and will not be\n\t//    propagated to DNS servers. Extract and use subdomain instead.\n\t// 2. A trailing dot in the fqdn will cause Aurora to add a trailing dot to\n\t//    the subdomain, resulting in _acme-challenge..<domain> rather\n\t//    than _acme-challenge.<domain>\n\n\tsubdomain := info.EffectiveFQDN[0 : len(info.EffectiveFQDN)-len(authZone)-1]\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tzone, err := d.getZoneInformationByName(authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"aurora: could not create record: %w\", err)\n\t}\n\n\trecord := auroradns.Record{\n\t\tRecordType: \"TXT\",\n\t\tName:       subdomain,\n\t\tContent:    info.Value,\n\t\tTTL:        d.config.TTL,\n\t}\n\n\tnewRecord, _, err := d.client.CreateRecord(zone.ID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"aurora: could not create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = newRecord.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes a given record that was generated by Present.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"aurora: unknown recordID for %q\", info.EffectiveFQDN)\n\t}\n\n\tauthZone, err := dns01.FindZoneByFqdn(dns.Fqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"aurora: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tzone, err := d.getZoneInformationByName(authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"aurora: %w\", err)\n\t}\n\n\t_, _, err = d.client.DeleteRecord(zone.ID, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"aurora: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) getZoneInformationByName(name string) (auroradns.Zone, error) {\n\tzs, _, err := d.client.ListZones()\n\tif err != nil {\n\t\treturn auroradns.Zone{}, err\n\t}\n\n\tfor _, element := range zs {\n\t\tif element.Name == name {\n\t\t\treturn element, nil\n\t\t}\n\t}\n\n\treturn auroradns.Zone{}, errors.New(\"could not find Zone record\")\n}\n"
  },
  {
    "path": "providers/dns/auroradns/auroradns.toml",
    "content": "Name = \"Aurora DNS\"\nDescription = ''''''\nURL = \"https://www.pcextreme.com/dns-health-checks\"\nCode = \"auroradns\"\nSince = \"v0.4.0\"\n\nExample = '''\nAURORA_API_KEY=xxxxx \\\nAURORA_SECRET=yyyyyy \\\nlego --dns auroradns -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    AURORA_API_KEY = \"API key or username to used\"\n    AURORA_SECRET = \"Secret password to be used\"\n  [Configuration.Additional]\n    AURORA_ENDPOINT = \"API endpoint URL\"\n    AURORA_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    AURORA_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    AURORA_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n\n[Links]\n  API = \"https://libcloud.readthedocs.io/en/latest/dns/drivers/auroradns.html#api-docs\"\n  GoClient = \"https://github.com/nrdcg/auroradns\"\n"
  },
  {
    "path": "providers/dns/auroradns/auroradns_test.go",
    "content": "package auroradns\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/nrdcg/auroradns\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar envTest = tester.NewEnvTest(EnvAPIKey, EnvSecret)\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = \"asdf1234\"\n\t\t\tconfig.Secret = \"key\"\n\t\t\tconfig.BaseURL = server.URL\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithContentType(\"application/json\").\n\t\t\tWithRegexp(\"Authorization\", `AuroraDNSv1 .+`).\n\t\t\tWithRegexp(\"X-Auroradns-Date\", `[0-9TZ]+`))\n}\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t\tEnvSecret: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t\tEnvSecret: \"\",\n\t\t\t},\n\t\t\texpected: \"aurora: some credentials information are missing: AURORA_API_KEY,AURORA_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t\tEnvSecret: \"456\",\n\t\t\t},\n\t\t\texpected: \"aurora: some credentials information are missing: AURORA_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t\tEnvSecret: \"\",\n\t\t\t},\n\t\t\texpected: \"aurora: some credentials information are missing: AURORA_SECRET\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tsecret   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"123\",\n\t\t\tsecret: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tapiKey:   \"\",\n\t\t\tsecret:   \"\",\n\t\t\texpected: \"aurora: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing user id\",\n\t\t\tapiKey:   \"\",\n\t\t\tsecret:   \"456\",\n\t\t\texpected: \"aurora: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing key\",\n\t\t\tapiKey:   \"123\",\n\t\t\tsecret:   \"\",\n\t\t\texpected: \"aurora: some credentials information are missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.Secret = test.secret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /zones\",\n\t\t\tservermock.JSONEncode([]auroradns.Zone{{\n\t\t\t\tID:   \"c56a4180-65aa-42ec-a945-5fd21dec0538\",\n\t\t\t\tName: \"example.com\",\n\t\t\t}}).\n\t\t\t\tWithStatusCode(http.StatusCreated)).\n\t\tRoute(\"POST /zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records\",\n\t\t\tservermock.JSONEncode(auroradns.Record{\n\t\t\t\tID:         \"ec56a4180-65aa-42ec-a945-5fd21dec0538\",\n\t\t\t\tRecordType: \"TXT\",\n\t\t\t\tName:       \"_acme-challenge\",\n\t\t\t\tTTL:        300,\n\t\t\t}).\n\t\t\t\tWithStatusCode(http.StatusCreated)).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"\", \"foobar\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /zones\",\n\t\t\tservermock.JSONEncode([]auroradns.Zone{{\n\t\t\t\tID:   \"c56a4180-65aa-42ec-a945-5fd21dec0538\",\n\t\t\t\tName: \"example.com\",\n\t\t\t}}).\n\t\t\t\tWithStatusCode(http.StatusCreated)).\n\t\tRoute(\"POST /zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records\",\n\t\t\tservermock.JSONEncode(auroradns.Record{\n\t\t\t\tID:         \"ec56a4180-65aa-42ec-a945-5fd21dec0538\",\n\t\t\t\tRecordType: \"TXT\",\n\t\t\t\tName:       \"_acme-challenge\",\n\t\t\t\tTTL:        300,\n\t\t\t}).\n\t\t\t\tWithStatusCode(http.StatusCreated)).\n\t\tRoute(\"DELETE /zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records/ec56a4180-65aa-42ec-a945-5fd21dec0538\",\n\t\t\tservermock.RawStringResponse(\"{}\").\n\t\t\t\tWithStatusCode(http.StatusCreated)).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"\", \"foobar\")\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(\"example.com\", \"\", \"foobar\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/autodns/autodns.go",
    "content": "// Package autodns implements a DNS provider for solving the DNS-01 challenge using auto DNS.\npackage autodns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/autodns/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"AUTODNS_\"\n\n\tEnvAPIUser            = envNamespace + \"API_USER\"\n\tEnvAPIPassword        = envNamespace + \"API_PASSWORD\"\n\tEnvAPIEndpoint        = envNamespace + \"ENDPOINT\"\n\tEnvAPIEndpointContext = envNamespace + \"CONTEXT\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tEndpoint           *url.URL\n\tUsername           string\n\tPassword           string\n\tContext            int\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\tendpoint, _ := url.Parse(env.GetOrDefaultString(EnvAPIEndpoint, internal.DefaultEndpoint))\n\n\treturn &Config{\n\t\tEndpoint:           endpoint,\n\t\tContext:            env.GetOrDefaultInt(EnvAPIEndpointContext, internal.DefaultEndpointContext),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for autoDNS.\n// Credentials must be passed in the environment variables.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIUser, EnvAPIPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"autodns: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvAPIUser]\n\tconfig.Password = values[EnvAPIPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for autoDNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"autodns: config is nil\")\n\t}\n\n\tif config.Username == \"\" {\n\t\treturn nil, errors.New(\"autodns: missing user\")\n\t}\n\n\tif config.Password == \"\" {\n\t\treturn nil, errors.New(\"autodns: missing password\")\n\t}\n\n\tclient := internal.NewClient(config.Username, config.Password, config.Context)\n\n\tif config.Endpoint != nil {\n\t\tclient.BaseURL = config.Endpoint\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecords := []*internal.ResourceRecord{{\n\t\tName:  info.EffectiveFQDN,\n\t\tTTL:   int64(d.config.TTL),\n\t\tType:  \"TXT\",\n\t\tValue: info.Value,\n\t}}\n\n\t_, err := d.client.AddRecords(context.Background(), info.EffectiveFQDN, records)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"autodns: add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record previously created.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecords := []*internal.ResourceRecord{{\n\t\tName:  info.EffectiveFQDN,\n\t\tTTL:   int64(d.config.TTL),\n\t\tType:  \"TXT\",\n\t\tValue: info.Value,\n\t}}\n\n\t_, err := d.client.RemoveRecords(context.Background(), info.EffectiveFQDN, records)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"autodns: remove record: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/autodns/autodns.toml",
    "content": "Name = \"Autodns\"\nDescription = ''''''\nURL = \"https://www.internetx.com/domains/autodns/\"\nCode = \"autodns\"\nSince = \"v3.2.0\"\n\nExample = '''\nAUTODNS_API_USER=username \\\nAUTODNS_API_PASSWORD=supersecretpassword \\\nlego --dns autodns -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    AUTODNS_API_USER = \"Username\"\n    AUTODNS_API_PASSWORD = \"User Password\"\n  [Configuration.Additional]\n    AUTODNS_ENDPOINT = \"API endpoint URL, defaults to https://api.autodns.com/v1/\"\n    AUTODNS_CONTEXT = \"API context (4 for production, 1 for testing. Defaults to 4)\"\n    AUTODNS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)\"\n    AUTODNS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    AUTODNS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    AUTODNS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://help.internetx.com/display/APIJSONEN\"\n"
  },
  {
    "path": "providers/dns/autodns/autodns_test.go",
    "content": "package autodns\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIEndpoint,\n\tEnvAPIUser,\n\tEnvAPIPassword).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser:     \"123\",\n\t\t\t\tEnvAPIPassword: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser:     \"\",\n\t\t\t\tEnvAPIPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"autodns: some credentials information are missing: AUTODNS_API_USER,AUTODNS_API_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing user id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser:     \"\",\n\t\t\t\tEnvAPIPassword: \"456\",\n\t\t\t},\n\t\t\texpected: \"autodns: some credentials information are missing: AUTODNS_API_USER\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser:     \"123\",\n\t\t\t\tEnvAPIPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"autodns: some credentials information are missing: AUTODNS_API_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"123\",\n\t\t\tpassword: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tusername: \"\",\n\t\t\tpassword: \"\",\n\t\t\texpected: \"autodns: missing user\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing user id\",\n\t\t\tusername: \"\",\n\t\t\tpassword: \"456\",\n\t\t\texpected: \"autodns: missing user\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing key\",\n\t\t\tusername: \"123\",\n\t\t\tpassword: \"\",\n\t\t\texpected: \"autodns: missing password\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/autodns/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// DefaultEndpoint default API endpoint.\nconst DefaultEndpoint = \"https://api.autodns.com/v1/\"\n\n// DefaultEndpointContext default API endpoint context.\nconst DefaultEndpointContext int = 4\n\n// Client the Autodns API client.\ntype Client struct {\n\tusername string\n\tpassword string\n\tcontext  int\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(username, password string, clientContext int) *Client {\n\tbaseURL, _ := url.Parse(DefaultEndpoint)\n\n\treturn &Client{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\tcontext:    clientContext,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// AddRecords adds records.\nfunc (c *Client) AddRecords(ctx context.Context, domain string, records []*ResourceRecord) (*DataZoneResponse, error) {\n\tzoneStream := &ZoneStream{Adds: records}\n\n\treturn c.updateZone(ctx, domain, zoneStream)\n}\n\n// RemoveRecords removes records.\nfunc (c *Client) RemoveRecords(ctx context.Context, domain string, records []*ResourceRecord) (*DataZoneResponse, error) {\n\tzoneStream := &ZoneStream{Removes: records}\n\n\treturn c.updateZone(ctx, domain, zoneStream)\n}\n\n// https://github.com/InterNetX/domainrobot-api/blob/bdc8fe92a2f32fcbdb29e30bf6006ab446f81223/src/domainrobot.json#L21090\nfunc (c *Client) updateZone(ctx context.Context, domain string, zoneStream *ZoneStream) (*DataZoneResponse, error) {\n\tendpoint := c.BaseURL.JoinPath(\"zone\", domain, \"_stream\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, zoneStream)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar resp *DataZoneResponse\n\tif err := c.do(req, &resp); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(\"X-Domainrobot-Context\", strconv.Itoa(c.context))\n\treq.SetBasicAuth(c.username, c.password)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/autodns/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\", 123)\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithBasicAuth(\"user\", \"secret\").\n\t\t\tWithJSONHeaders())\n}\n\nfunc TestClient_AddRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zone/example.com/_stream\",\n\t\t\tservermock.ResponseFromFixture(\"add_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"add_record-request.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWith(\"X-Domainrobot-Context\", \"123\")).\n\t\tBuild(t)\n\n\trecords := []*ResourceRecord{{\n\t\tName:  \"example.com\",\n\t\tTTL:   600,\n\t\tType:  \"TXT\",\n\t\tValue: \"txtTXTtxt\",\n\t}}\n\n\tresp, err := client.AddRecords(t.Context(), \"example.com\", records)\n\trequire.NoError(t, err)\n\n\texpected := &DataZoneResponse{\n\t\tSTID: \"20251121-appf4923-126284\",\n\t\tCTID: \"\",\n\t\tMessages: []ResponseMessage{\n\t\t\t{\n\t\t\t\tText: \"string\",\n\t\t\t\tMessages: []string{\n\t\t\t\t\t\"string\",\n\t\t\t\t},\n\t\t\t\tObjects: []GenericObject{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  \"string\",\n\t\t\t\t\t\tValue: \"string\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCode:   \"string\",\n\t\t\t\tStatus: \"SUCCESS\",\n\t\t\t},\n\t\t},\n\t\tStatus: &ResponseStatus{\n\t\t\tCode: \"S0301\",\n\t\t\tText: \"Zone was updated successfully on the name server.\",\n\t\t\tType: \"SUCCESS\",\n\t\t},\n\t\tObject: nil,\n\t\tData: []Zone{\n\t\t\t{\n\t\t\t\tName: \"example.com\",\n\t\t\t\tResourceRecords: []ResourceRecord{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:  \"example.com\",\n\t\t\t\t\t\tTTL:   120,\n\t\t\t\t\t\tType:  \"TXT\",\n\t\t\t\t\t\tValue: \"txt\",\n\t\t\t\t\t\tPref:  1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAction:            \"xxx\",\n\t\t\t\tVirtualNameServer: \"yyy\",\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, resp)\n}\n\nfunc TestClient_AddRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zone/example.com/_stream\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\trecords := []*ResourceRecord{{\n\t\tName:  \"example.com\",\n\t\tTTL:   600,\n\t\tType:  \"TXT\",\n\t\tValue: \"txtTXTtxt\",\n\t}}\n\n\t_, err := client.AddRecords(t.Context(), \"example.com\", records)\n\trequire.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\"`)\n}\n\nfunc TestClient_RemoveRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zone/example.com/_stream\",\n\t\t\tservermock.ResponseFromFixture(\"remove_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"remove_record-request.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWith(\"X-Domainrobot-Context\", \"123\")).\n\t\tBuild(t)\n\n\trecords := []*ResourceRecord{{\n\t\tName:  \"example.com\",\n\t\tTTL:   600,\n\t\tType:  \"TXT\",\n\t\tValue: \"txtTXTtxt\",\n\t}}\n\n\tresp, err := client.RemoveRecords(t.Context(), \"example.com\", records)\n\trequire.NoError(t, err)\n\n\texpected := &DataZoneResponse{\n\t\tSTID: \"20251121-appf4923-126284\",\n\t\tCTID: \"\",\n\t\tMessages: []ResponseMessage{\n\t\t\t{\n\t\t\t\tText: \"string\",\n\t\t\t\tMessages: []string{\n\t\t\t\t\t\"string\",\n\t\t\t\t},\n\t\t\t\tObjects: []GenericObject{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  \"string\",\n\t\t\t\t\t\tValue: \"string\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCode:   \"string\",\n\t\t\t\tStatus: \"SUCCESS\",\n\t\t\t},\n\t\t},\n\t\tStatus: &ResponseStatus{\n\t\t\tCode: \"S0301\",\n\t\t\tText: \"Zone was updated successfully on the name server.\",\n\t\t\tType: \"SUCCESS\",\n\t\t},\n\t\tObject: nil,\n\t\tData: []Zone{\n\t\t\t{\n\t\t\t\tName: \"example.com\",\n\t\t\t\tResourceRecords: []ResourceRecord{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:  \"example.com\",\n\t\t\t\t\t\tTTL:   120,\n\t\t\t\t\t\tType:  \"TXT\",\n\t\t\t\t\t\tValue: \"txt\",\n\t\t\t\t\t\tPref:  1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAction:            \"xxx\",\n\t\t\t\tVirtualNameServer: \"yyy\",\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, resp)\n}\n"
  },
  {
    "path": "providers/dns/autodns/internal/fixtures/add_record-request.json",
    "content": "{\n  \"adds\": [\n    {\n      \"name\": \"example.com\",\n      \"ttl\": 600,\n      \"type\": \"TXT\",\n      \"value\": \"txtTXTtxt\"\n    }\n  ],\n  \"rems\": null\n}\n"
  },
  {
    "path": "providers/dns/autodns/internal/fixtures/add_record.json",
    "content": "{\n  \"stid\": \"20251121-appf4923-126284\",\n  \"messages\": [\n    {\n      \"text\": \"string\",\n      \"notice\": \"string\",\n      \"messages\": [\n        \"string\"\n      ],\n      \"objects\": [\n        {\n          \"type\": \"string\",\n          \"value\": \"string\"\n        }\n      ],\n      \"code\": \"string\",\n      \"status\": \"SUCCESS\"\n    }\n  ],\n  \"status\": {\n    \"code\": \"S0301\",\n    \"text\": \"Zone was updated successfully on the name server.\",\n    \"type\": \"SUCCESS\"\n  },\n  \"data\": [\n    {\n      \"origin\": \"example.com\",\n      \"resourceRecords\": [\n        {\n          \"name\": \"example.com\",\n          \"ttl\": 120,\n          \"type\": \"TXT\",\n          \"value\": \"txt\",\n          \"pref\": 1\n        }\n      ],\n      \"action\": \"xxx\",\n      \"virtualNameServer\": \"yyy\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/autodns/internal/fixtures/error.json",
    "content": "{\n  \"stid\": \"20251121-appf4923-126284\",\n  \"messages\": [\n    {\n      \"text\": \"Der Zusatzeintrag wurde doppelt eingetragen.\",\n      \"objects\": [\n        {\n          \"type\": \"OURDOMAIN.TLD@nsa7.schlundtech.de/rr[17]\",\n          \"value\": \"_acme-challenge.www.whoami.int.OURDOMAIN.TLD TXT \\\"rK2SJb_ZcrYefbfCKU6jZEANfEAJeOtSh1Fv8hkUoVc\\\"\"\n        }\n      ],\n      \"code\": \"EF02022\",\n      \"status\": \"ERROR\"\n    }\n  ],\n  \"status\": {\n    \"code\": \"E0202002\",\n    \"text\": \"Zone konnte auf dem Nameserver nicht aktualisiert werden.\",\n    \"type\": \"ERROR\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/autodns/internal/fixtures/remove_record-request.json",
    "content": "{\n  \"adds\": null,\n  \"rems\": [\n    {\n      \"name\": \"example.com\",\n      \"ttl\": 600,\n      \"type\": \"TXT\",\n      \"value\": \"txtTXTtxt\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/autodns/internal/fixtures/remove_record.json",
    "content": "{\n  \"stid\": \"20251121-appf4923-126284\",\n  \"messages\": [\n    {\n      \"text\": \"string\",\n      \"notice\": \"string\",\n      \"messages\": [\n        \"string\"\n      ],\n      \"objects\": [\n        {\n          \"type\": \"string\",\n          \"value\": \"string\"\n        }\n      ],\n      \"code\": \"string\",\n      \"status\": \"SUCCESS\"\n    }\n  ],\n  \"status\": {\n    \"code\": \"S0301\",\n    \"text\": \"Zone was updated successfully on the name server.\",\n    \"type\": \"SUCCESS\"\n  },\n  \"data\": [\n    {\n      \"origin\": \"example.com\",\n      \"resourceRecords\": [\n        {\n          \"name\": \"example.com\",\n          \"ttl\": 120,\n          \"type\": \"TXT\",\n          \"value\": \"txt\",\n          \"pref\": 1\n        }\n      ],\n      \"action\": \"xxx\",\n      \"virtualNameServer\": \"yyy\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/autodns/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype APIResponse[T any] struct {\n\tSTID     string            `json:\"stid\"`\n\tCTID     string            `json:\"ctid\"`\n\tMessages []ResponseMessage `json:\"messages\"`\n\tStatus   *ResponseStatus   `json:\"status\"`\n\tObject   *ResponseObject   `json:\"object\"`\n\tData     T                 `json:\"data\"`\n}\n\ntype APIError APIResponse[any]\n\nfunc (a *APIError) Error() string {\n\tvar parts []string\n\n\tif a.STID != \"\" {\n\t\tparts = append(parts, fmt.Sprintf(\"STID: %s\", a.STID))\n\t}\n\n\tif a.CTID != \"\" {\n\t\tparts = append(parts, fmt.Sprintf(\"CTID: %s\", a.CTID))\n\t}\n\n\tif a.Status != nil {\n\t\tparts = append(parts, \"status: \"+a.Status.String())\n\t}\n\n\tfor _, message := range a.Messages {\n\t\tparts = append(parts, \"message: \"+message.String())\n\t}\n\n\tif a.Object != nil {\n\t\tparts = append(parts, \"object: \"+a.Object.String())\n\t}\n\n\treturn strings.Join(parts, \", \")\n}\n\ntype DataZoneResponse APIResponse[[]Zone]\n\ntype ResponseMessage struct {\n\tText     string          `json:\"text\"`\n\tCode     string          `json:\"code\"`\n\tStatus   string          `json:\"status\"`\n\tMessages []string        `json:\"messages\"`\n\tObjects  []GenericObject `json:\"objects\"`\n}\n\nfunc (r ResponseMessage) String() string {\n\tvar parts []string\n\n\tif r.Code != \"\" {\n\t\tparts = append(parts, \"code: \"+r.Code)\n\t}\n\n\tif r.Text != \"\" {\n\t\tparts = append(parts, \"text: \"+r.Text)\n\t}\n\n\tif r.Status != \"\" {\n\t\tparts = append(parts, \"status: \"+r.Status)\n\t}\n\n\tif len(r.Messages) > 0 {\n\t\tparts = append(parts, \"messages: \"+strings.Join(r.Messages, \";\"))\n\t}\n\n\tfor _, object := range r.Objects {\n\t\tparts = append(parts, fmt.Sprintf(\"object: %s\", object))\n\t}\n\n\treturn strings.Join(parts, \", \")\n}\n\ntype GenericObject struct {\n\tType  string `json:\"type\"`\n\tValue string `json:\"value\"`\n}\n\nfunc (g GenericObject) String() string {\n\treturn g.Type + \": \" + g.Value\n}\n\ntype ResponseStatus struct {\n\tCode string `json:\"code\"`\n\tText string `json:\"text\"`\n\tType string `json:\"type\"` // SUCCESS, ERROR, NOTIFY, NOTICE, NICCOM_NOTIFY\n}\n\nfunc (r ResponseStatus) String() string {\n\treturn fmt.Sprintf(\"code: %s, text: %s, type: %s\", r.Code, r.Text, r.Type)\n}\n\ntype ResponseObject struct {\n\tType    string              `json:\"type\"`\n\tValue   string              `json:\"value\"`\n\tSummary int32               `json:\"summary\"`\n\tData    *ResponseObjectData `json:\"data\"`\n}\n\nfunc (r ResponseObject) String() string {\n\tvar parts []string\n\n\tif r.Type != \"\" {\n\t\tparts = append(parts, fmt.Sprintf(\"type: %s\", r.Type))\n\t}\n\n\tif r.Value != \"\" {\n\t\tparts = append(parts, fmt.Sprintf(\"value: %s\", r.Value))\n\t}\n\n\tif r.Summary != 0 {\n\t\tparts = append(parts, fmt.Sprintf(\"summary: %d\", r.Summary))\n\t}\n\n\tif r.Data != nil {\n\t\tparts = append(parts, fmt.Sprintf(\"data: %s\", r.Data.Description))\n\t}\n\n\treturn strings.Join(parts, \", \")\n}\n\ntype ResponseObjectData struct {\n\tDescription string `json:\"description\"`\n}\n\n// ResourceRecord holds a resource record.\n// https://help.internetx.com/display/APIXMLEN/Resource+Record+Object\ntype ResourceRecord struct {\n\tName  string `json:\"name\"`\n\tTTL   int64  `json:\"ttl\"`\n\tType  string `json:\"type\"`\n\tValue string `json:\"value\"`\n\tPref  int32  `json:\"pref,omitempty\"`\n}\n\n// Zone is an autodns zone record with all for us relevant fields.\n// https://help.internetx.com/display/APIXMLEN/Zone+Object\ntype Zone struct {\n\tName              string           `json:\"origin\"`\n\tResourceRecords   []ResourceRecord `json:\"resourceRecords\"`\n\tAction            string           `json:\"action\"`\n\tVirtualNameServer string           `json:\"virtualNameServer\"`\n}\n\n// ZoneStream body of the requests.\n// https://github.com/InterNetX/domainrobot-api/blob/bdc8fe92a2f32fcbdb29e30bf6006ab446f81223/src/domainrobot.json#L35914-L35932\ntype ZoneStream struct {\n\tAdds    []*ResourceRecord `json:\"adds\"`\n\tRemoves []*ResourceRecord `json:\"rems\"`\n}\n"
  },
  {
    "path": "providers/dns/axelname/axelname.go",
    "content": "// Package axelname implements a DNS provider for solving the DNS-01 challenge using Axelname.\npackage axelname\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/axelname/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"AXELNAME_\"\n\n\tEnvNickname = envNamespace + \"NICKNAME\"\n\tEnvToken    = envNamespace + \"TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tNickname string\n\tToken    string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Axelname.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvNickname, EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"axelname: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Nickname = values[EnvNickname]\n\tconfig.Token = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Axelname.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"axelname: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.Nickname, config.Token)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"axelname: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"axelname: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"axelname: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName:  subDomain,\n\t\tType:  \"TXT\",\n\t\tValue: info.Value,\n\t}\n\n\terr = d.client.AddRecord(context.Background(), dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"axelname: add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"axelname: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecords, err := d.client.ListRecords(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"axelname: list records: %w\", err)\n\t}\n\n\tfor _, record := range records {\n\t\tif record.Type != \"TXT\" || record.Value != info.Value {\n\t\t\tcontinue\n\t\t}\n\n\t\terr = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), record)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"axelname: delete record: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn errors.New(\"axelname: delete record: record not found\")\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/axelname/axelname.toml",
    "content": "Name = \"Axelname\"\nDescription = ''''''\nURL = \"https://axelname.ru\"\nCode = \"axelname\"\nSince = \"v4.23.0\"\n\nExample = '''\nAXELNAME_NICKNAME=\"yyy\" \\\nAXELNAME_TOKEN=\"xxx\" \\\nlego --dns axelname -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    AXELNAME_NICKNAME = \"Account nickname\"\n    AXELNAME_TOKEN = \"API token\"\n  [Configuration.Additional]\n    AXELNAME_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    AXELNAME_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    AXELNAME_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    AXELNAME_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://axelname.ru/static/content/files/axelname_api_rest_lite.pdf\"\n"
  },
  {
    "path": "providers/dns/axelname/axelname_test.go",
    "content": "package axelname\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvNickname, EnvToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvNickname: \"user\",\n\t\t\t\tEnvToken:    \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing nickname\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvNickname: \"\",\n\t\t\t\tEnvToken:    \"secret\",\n\t\t\t},\n\t\t\texpected: \"axelname: some credentials information are missing: AXELNAME_NICKNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvNickname: \"user\",\n\t\t\t\tEnvToken:    \"\",\n\t\t\t},\n\t\t\texpected: \"axelname: some credentials information are missing: AXELNAME_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"axelname: some credentials information are missing: AXELNAME_NICKNAME,AXELNAME_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\tnickname string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tnickname: \"user\",\n\t\t\ttoken:    \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing nickname\",\n\t\t\texpected: \"axelname: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing token\",\n\t\t\texpected: \"axelname: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"axelname: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.token\n\t\t\tconfig.Nickname = test.nickname\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/axelname/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\nconst statusSuccess = \"success\"\n\nconst defaultBaseURL = \"https://my.axelname.ru/rest/\"\n\n// Client the Axelname API client.\ntype Client struct {\n\tnickname string\n\ttoken    string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(nickname, token string) (*Client, error) {\n\tif token == \"\" || nickname == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tnickname:   nickname,\n\t\ttoken:      token,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) ListRecords(ctx context.Context, domain string) ([]Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns_list\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"domain\", domain)\n\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := c.newRequest(ctx, endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar results ListResponse\n\n\terr = c.do(req, &results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif results.Result != statusSuccess {\n\t\treturn nil, &results.APIError\n\t}\n\n\treturn results.List, nil\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, domain string, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"dns_delete\")\n\n\tvalues, err := querystring.Values(record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvalues.Set(\"domain\", domain)\n\n\tendpoint.RawQuery = values.Encode()\n\n\treq, err := c.newRequest(ctx, endpoint)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar results APIResponse\n\n\terr = c.do(req, &results)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif results.Result != statusSuccess {\n\t\treturn &results.APIError\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) AddRecord(ctx context.Context, domain string, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"dns_add\")\n\n\tvalues, err := querystring.Values(record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvalues.Set(\"domain\", domain)\n\n\tendpoint.RawQuery = values.Encode()\n\n\treq, err := c.newRequest(ctx, endpoint)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar results APIResponse\n\n\terr = c.do(req, &results)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif results.Result != statusSuccess {\n\t\treturn &results.APIError\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) newRequest(ctx context.Context, endpoint *url.URL) (*http.Request, error) {\n\tquery := endpoint.Query()\n\tquery.Set(\"token\", c.token)\n\tquery.Set(\"nichdl\", c.nickname)\n\n\tendpoint.RawQuery = query.Encode()\n\n\treturn http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/axelname/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient, err := NewClient(\"user\", \"secret\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.HTTPClient = server.Client()\n\tclient.baseURL, _ = url.Parse(server.URL)\n\n\treturn client, nil\n}\n\nfunc TestClient_ListRecords(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /dns_list\",\n\t\t\tservermock.ResponseFromFixture(\"dns_list.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\tWith(\"nichdl\", \"user\").\n\t\t\t\tWith(\"token\", \"secret\")).\n\t\tBuild(t)\n\n\trecords, err := client.ListRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{ID: \"74749\", Name: \"example.com\", Type: \"A\", Value: \"46.161.54.22\"},\n\t\t{ID: \"417\", Name: \"example.com\", Type: \"MX\", Value: \"mx.yandex.ru.\", Prio: \"10\"},\n\t\t{ID: \"419\", Name: \"mail.example.com\", Type: \"CNAME\", Value: \"mail.yandex.ru.\"},\n\t\t{ID: \"74750\", Name: \"www.example.com\", Type: \"A\", Value: \"46.161.54.22\"},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_ListRecords_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /dns_list\",\n\t\t\tservermock.ResponseFromFixture(\"dns_list_error.json\").\n\t\t\t\tWithStatusCode(http.StatusNotFound)).\n\t\tBuild(t)\n\n\t_, err := client.ListRecords(t.Context(), \"example.com\")\n\trequire.EqualError(t, err, \"error: Domain not found (1)\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /dns_delete\",\n\t\t\tservermock.ResponseFromFixture(\"dns_delete.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"id\", \"74749\").\n\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\tWith(\"nichdl\", \"user\").\n\t\t\t\tWith(\"token\", \"secret\")).\n\t\tBuild(t)\n\n\trecord := Record{ID: \"74749\"}\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /dns_delete\",\n\t\t\tservermock.ResponseFromFixture(\"dns_delete_error.json\").\n\t\t\t\tWithStatusCode(http.StatusNotFound)).\n\t\tBuild(t)\n\n\trecord := Record{ID: \"74749\"}\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", record)\n\trequire.EqualError(t, err, \"error: Domain not found (1)\")\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /dns_add\",\n\t\t\tservermock.ResponseFromFixture(\"dns_add.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"id\", \"74749\").\n\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\tWith(\"nichdl\", \"user\").\n\t\t\t\tWith(\"token\", \"secret\")).\n\t\tBuild(t)\n\n\trecord := Record{ID: \"74749\"}\n\n\terr := client.AddRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /dns_add\",\n\t\t\tservermock.ResponseFromFixture(\"dns_add_error.json\").\n\t\t\t\tWithStatusCode(http.StatusNotFound)).\n\t\tBuild(t)\n\n\trecord := Record{ID: \"74749\"}\n\n\terr := client.AddRecord(t.Context(), \"example.com\", record)\n\trequire.EqualError(t, err, \"error: Domain not found (1)\")\n}\n"
  },
  {
    "path": "providers/dns/axelname/internal/fixtures/dns_add.json",
    "content": "{\n  \"code\": \"OK\",\n  \"message\": \"DNS record added\",\n  \"result\": \"success\"\n}\n"
  },
  {
    "path": "providers/dns/axelname/internal/fixtures/dns_add_error.json",
    "content": "{\n  \"error_code\": \"1\",\n  \"error_text\": \"Domain not found\",\n  \"result\": \"error\"\n}\n"
  },
  {
    "path": "providers/dns/axelname/internal/fixtures/dns_delete.json",
    "content": "{\n  \"code\": \"OK\",\n  \"message\": \"DNS record deleted\",\n  \"result\": \"success\"\n}\n"
  },
  {
    "path": "providers/dns/axelname/internal/fixtures/dns_delete_error.json",
    "content": "{\n  \"error_code\": \"1\",\n  \"error_text\": \"Domain not found\",\n  \"result\": \"error\"\n}\n"
  },
  {
    "path": "providers/dns/axelname/internal/fixtures/dns_list.json",
    "content": "{\n  \"code\": \"OK\",\n  \"message\": \"DNS-records\",\n  \"count\": 4,\n  \"result\": \"success\",\n  \"list\": [\n    {\n      \"id\": \"74749\",\n      \"name\": \"example.com\",\n      \"type\": \"A\",\n      \"value\": \"46.161.54.22\"\n    },\n    {\n      \"id\": \"417\",\n      \"name\": \"example.com\",\n      \"type\": \"MX\",\n      \"value\": \"mx.yandex.ru.\",\n      \"prio\": \"10\"\n    },\n    {\n      \"id\": \"419\",\n      \"name\": \"mail.example.com\",\n      \"type\": \"CNAME\",\n      \"value\": \"mail.yandex.ru.\"\n    },\n    {\n      \"id\": \"74750\",\n      \"name\": \"www.example.com\",\n      \"type\": \"A\",\n      \"value\": \"46.161.54.22\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/axelname/internal/fixtures/dns_list_error.json",
    "content": "{\n  \"error_code\": \"1\",\n  \"error_text\": \"Domain not found\",\n  \"result\": \"error\"\n}\n"
  },
  {
    "path": "providers/dns/axelname/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype APIError struct {\n\tErrorCode string `json:\"error_code,omitempty\"`\n\tErrorText string `json:\"error_text,omitempty\"`\n\tResult    string `json:\"result,omitempty\"`\n}\n\nfunc (a *APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s (%s)\", a.Result, a.ErrorText, a.ErrorCode)\n}\n\ntype APIResponse struct {\n\tAPIError\n\n\tCode    string `json:\"code,omitempty\"`\n\tMessage string `json:\"message,omitempty\"`\n}\n\ntype ListResponse struct {\n\tAPIResponse\n\n\tCount int      `json:\"count,omitempty\"`\n\tList  []Record `json:\"list,omitempty\"`\n}\n\ntype Record struct {\n\tID    string `json:\"id,omitempty\" url:\"id,omitempty\"`\n\tName  string `json:\"name,omitempty\" url:\"name,omitempty\"`\n\tType  string `json:\"type,omitempty\" url:\"type,omitempty\"`\n\tValue string `json:\"value,omitempty\" url:\"value,omitempty\"`\n\tPrio  string `json:\"prio,omitempty\" url:\"prio,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/azion/azion.go",
    "content": "// Package azion implements a DNS provider for solving the DNS-01 challenge using Azion Edge DNS.\npackage azion\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/aziontech/azionapi-go-sdk/idns\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"AZION_\"\n\n\tEnvPersonalToken = envNamespace + \"PERSONAL_TOKEN\"\n\tEnvPageSize      = envNamespace + \"PAGE_SIZE\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tPersonalToken string\n\tPageSize      int\n\n\tPollingInterval    time.Duration\n\tPropagationTimeout time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPageSize:           env.GetOrDefaultInt(EnvPageSize, 50),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *idns.APIClient\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Azion.\n// Credentials must be passed in the environment variable: AZION_PERSONAL_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvPersonalToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"azion: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.PersonalToken = values[EnvPersonalToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Azion.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"azion: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.PersonalToken == \"\" {\n\t\treturn nil, errors.New(\"azion: missing credentials\")\n\t}\n\n\tclientConfig := idns.NewConfiguration()\n\tclientConfig.AddDefaultHeader(\"Accept\", \"application/json; version=3\")\n\tclientConfig.UserAgent = \"lego-dns/azion\"\n\n\tif config.HTTPClient != nil {\n\t\tclientConfig.HTTPClient = config.HTTPClient\n\t}\n\n\tclientConfig.HTTPClient = clientdebug.Wrap(clientConfig.HTTPClient)\n\n\tclient := idns.NewAPIClient(clientConfig)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctxAuth := authContext(context.Background(), d.config.PersonalToken)\n\n\tzone, err := d.findZone(ctxAuth, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azion: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := extractSubDomain(info, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azion: %w\", err)\n\t}\n\n\t// Check if a TXT record with the same name already exists\n\texistingRecord, err := d.findExistingTXTRecord(ctxAuth, zone.GetId(), subDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azion: check existing records: %w\", err)\n\t}\n\n\trecord := idns.NewRecordPostOrPut()\n\trecord.SetEntry(subDomain)\n\trecord.SetRecordType(\"TXT\")\n\trecord.SetTtl(int32(d.config.TTL))\n\n\tvar resp *idns.PostOrPutRecordResponse\n\n\tif existingRecord != nil {\n\t\t// Update existing record by adding the new value to the existing ones\n\t\trecord.SetAnswersList(append(existingRecord.GetAnswersList(), info.Value))\n\n\t\t// Use PUT to update the existing record\n\t\tresp, _, err = d.client.RecordsAPI.PutZoneRecord(ctxAuth, zone.GetId(), existingRecord.GetRecordId()).RecordPostOrPut(*record).Execute()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"azion: update existing record: %w\", err)\n\t\t}\n\t} else {\n\t\t// Create a new record\n\t\trecord.SetAnswersList([]string{info.Value})\n\n\t\tresp, _, err = d.client.RecordsAPI.PostZoneRecord(ctxAuth, zone.GetId()).RecordPostOrPut(*record).Execute()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"azion: create new zone record: %w\", err)\n\t\t}\n\t}\n\n\tif resp == nil || resp.Results == nil {\n\t\treturn errors.New(\"azion: create zone record error\")\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctxAuth := authContext(context.Background(), d.config.PersonalToken)\n\n\tzone, err := d.findZone(ctxAuth, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azion: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := extractSubDomain(info, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azion: %w\", err)\n\t}\n\n\texistingRecord, err := d.findExistingTXTRecord(ctxAuth, zone.GetId(), subDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azion: find existing record: %w\", err)\n\t}\n\n\tif existingRecord == nil {\n\t\treturn nil\n\t}\n\n\tcurrentAnswers := existingRecord.GetAnswersList()\n\n\tvar updatedAnswers []string\n\n\tfor _, answer := range currentAnswers {\n\t\tif answer != info.Value {\n\t\t\tupdatedAnswers = append(updatedAnswers, answer)\n\t\t}\n\t}\n\n\t// If no answers remain, delete the entire record\n\tif len(updatedAnswers) == 0 {\n\t\t_, resp, errDelete := d.client.RecordsAPI.DeleteZoneRecord(ctxAuth, zone.GetId(), existingRecord.GetRecordId()).Execute()\n\t\tif errDelete != nil {\n\t\t\t// If a record doesn't exist (404), consider cleanup successful\n\t\t\tif resp != nil && resp.StatusCode == http.StatusNotFound {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"azion: delete record: %w\", errDelete)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Update the record with remaining answers\n\trecord := idns.NewRecordPostOrPut()\n\trecord.SetEntry(subDomain)\n\trecord.SetRecordType(\"TXT\")\n\trecord.SetAnswersList(updatedAnswers)\n\trecord.SetTtl(existingRecord.GetTtl())\n\n\t_, _, err = d.client.RecordsAPI.PutZoneRecord(ctxAuth, zone.GetId(), existingRecord.GetRecordId()).RecordPostOrPut(*record).Execute()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azion: update record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*idns.Zone, error) {\n\tresp, _, err := d.client.ZonesAPI.GetZones(ctx).Execute()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get zones: %w\", err)\n\t}\n\n\tif resp == nil {\n\t\treturn nil, errors.New(\"get zones: no results\")\n\t}\n\n\tfor domain := range dns01.UnFqdnDomainsSeq(fqdn) {\n\t\tfor _, zone := range resp.GetResults() {\n\t\t\tif zone.GetDomain() == domain {\n\t\t\t\treturn &zone, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"zone not found (fqdn: %q)\", fqdn)\n}\n\n// findExistingTXTRecord searches for an existing TXT record with the given name in the specified zone.\n// It handles pagination to search through all pages of results.\nfunc (d *DNSProvider) findExistingTXTRecord(ctx context.Context, zoneID int32, recordName string) (*idns.RecordGet, error) {\n\tvar page int64 = 1\n\n\tfor {\n\t\tresp, _, err := d.client.RecordsAPI.GetZoneRecords(ctx, zoneID).Page(page).PageSize(int64(d.config.PageSize)).Execute()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"get zone records (page %d): %w\", page, err)\n\t\t}\n\n\t\tif resp == nil {\n\t\t\treturn nil, errors.New(\"get zone records: no results\")\n\t\t}\n\n\t\tresults, ok := resp.GetResultsOk()\n\t\tif !ok || results == nil {\n\t\t\treturn nil, errors.New(\"get zone records: empty\")\n\t\t}\n\n\t\t// Search for existing TXT record with the same name in current page\n\t\tfor _, record := range results.GetRecords() {\n\t\t\tif record.GetRecordType() == \"TXT\" && record.GetEntry() == recordName {\n\t\t\t\treturn &record, nil\n\t\t\t}\n\t\t}\n\n\t\t// Check if there are more pages to search\n\t\tif page >= int64(resp.GetTotalPages()) {\n\t\t\tbreak\n\t\t}\n\n\t\tpage++\n\t}\n\n\t// No existing record found in any page\n\treturn nil, nil\n}\n\nfunc authContext(ctx context.Context, key string) context.Context {\n\treturn context.WithValue(ctx, idns.ContextAPIKeys, map[string]idns.APIKey{\n\t\t\"tokenAuth\": {\n\t\t\tKey:    key,\n\t\t\tPrefix: \"Token\",\n\t\t},\n\t})\n}\n\nfunc extractSubDomain(info dns01.ChallengeInfo, zone *idns.Zone) (string, error) {\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.GetName())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif subDomain != \"\" {\n\t\treturn subDomain, nil\n\t}\n\n\treturn \"@\", nil\n}\n"
  },
  {
    "path": "providers/dns/azion/azion.toml",
    "content": "Name = \"Azion\"\nDescription = ''''''\nCode = \"azion\"\nSince = \"v4.24.0\"\nURL = \"https://www.azion.com/en/products/edge-dns/\"\n\nExample = '''\nAZION_PERSONAL_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns azion -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    AZION_PERSONAL_TOKEN = \"Your Azion personal token.\"\n  [Configuration.Additional]\n    AZION_PAGE_SIZE = \"The page size for the API request (Default: 50)\"\n    AZION_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    AZION_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    AZION_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    AZION_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api.azion.com/\"\n  GoClient = \"https://github.com/aziontech/azionapi-go-sdk\"\n"
  },
  {
    "path": "providers/dns/azion/azion_test.go",
    "content": "package azion\n\nimport (\n\t\"context\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/aziontech/azionapi-go-sdk/idns\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvPersonalToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPersonalToken: \"token\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPersonalToken: \"\",\n\t\t\t},\n\t\t\texpected: \"azion: some credentials information are missing: AZION_PERSONAL_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"token\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"azion: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.PersonalToken = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_findZone(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /intelligent_dns\", servermock.ResponseFromFixture(\"zones.json\")).\n\t\tBuild(t)\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tfqdn     string\n\t\texpected *idns.Zone\n\t}{\n\t\t{\n\t\t\tdesc: \"apex\",\n\t\t\tfqdn: \"example.com.\",\n\t\t\texpected: &idns.Zone{\n\t\t\t\tId:     idns.PtrInt32(1),\n\t\t\t\tDomain: idns.PtrString(\"example.com\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"sub domain\",\n\t\t\tfqdn: \"sub.example.com.\",\n\t\t\texpected: &idns.Zone{\n\t\t\t\tId:     idns.PtrInt32(2),\n\t\t\t\tDomain: idns.PtrString(\"sub.example.com\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"long sub domain\",\n\t\t\tfqdn: \"_acme-challenge.api.sub.example.com.\",\n\t\t\texpected: &idns.Zone{\n\t\t\t\tId:     idns.PtrInt32(2),\n\t\t\t\tDomain: idns.PtrString(\"sub.example.com\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"long sub domain, apex\",\n\t\t\tfqdn: \"_acme-challenge.test.example.com.\",\n\t\t\texpected: &idns.Zone{\n\t\t\t\tId:     idns.PtrInt32(1),\n\t\t\t\tDomain: idns.PtrString(\"example.com\"),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tzone, err := provider.findZone(context.Background(), test.fqdn)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.expected, zone)\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_findZone_error(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tfqdn     string\n\t\tresponse string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"no parent zone found\",\n\t\t\tfqdn:     \"_acme-challenge.example.org.\",\n\t\t\tresponse: \"zones.json\",\n\t\t\texpected: `zone not found (fqdn: \"_acme-challenge.example.org.\")`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"empty zones list\",\n\t\t\tfqdn:     \"example.com.\",\n\t\t\tresponse: \"zones_empty.json\",\n\t\t\texpected: `zone not found (fqdn: \"example.com.\")`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tprovider := mockBuilder().\n\t\t\t\tRoute(\"GET /intelligent_dns\", servermock.ResponseFromFixture(test.response)).\n\t\t\t\tBuild(t)\n\n\t\t\tzone, err := provider.findZone(context.Background(), test.fqdn)\n\t\t\trequire.EqualError(t, err, test.expected)\n\n\t\t\tassert.Nil(t, zone)\n\t\t})\n\t}\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.PersonalToken = \"secret\"\n\n\t\t\tprovider, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclientConfig := provider.client.GetConfig()\n\t\t\tclientConfig.HTTPClient = server.Client()\n\t\t\tclientConfig.Servers = idns.ServerConfigurations{{\n\t\t\t\tURL:         server.URL,\n\t\t\t\tDescription: \"Production\",\n\t\t\t}}\n\n\t\t\treturn provider, nil\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "providers/dns/azion/fixtures/zones.json",
    "content": "{\n  \"count\": 2,\n  \"links\": {\n    \"previous\": null,\n    \"next\": null\n  },\n  \"total_pages\": 1,\n  \"results\": [\n    {\n      \"id\": 1,\n      \"domain\": \"example.com\"\n    },\n    {\n      \"id\": 2,\n      \"domain\": \"sub.example.com\"\n    }\n  ],\n  \"schema_version\": 3\n}\n"
  },
  {
    "path": "providers/dns/azion/fixtures/zones_empty.json",
    "content": "{\n  \"count\": 0,\n  \"links\": {\n    \"previous\": null,\n    \"next\": null\n  },\n  \"total_pages\": 0,\n  \"results\": null,\n  \"schema_version\": 3\n}\n"
  },
  {
    "path": "providers/dns/azure/azure.go",
    "content": "// Package azure implements a DNS provider for solving the DNS-01 challenge using azure DNS.\n// Azure doesn't like trailing dots on domain names, most of the acme code does.\npackage azure\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Azure/go-autorest/autorest\"\n\taazure \"github.com/Azure/go-autorest/autorest/azure\"\n\t\"github.com/Azure/go-autorest/autorest/azure/auth\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"AZURE_\"\n\n\tEnvEnvironment      = envNamespace + \"ENVIRONMENT\"\n\tEnvMetadataEndpoint = envNamespace + \"METADATA_ENDPOINT\"\n\tEnvSubscriptionID   = envNamespace + \"SUBSCRIPTION_ID\"\n\tEnvResourceGroup    = envNamespace + \"RESOURCE_GROUP\"\n\tEnvTenantID         = envNamespace + \"TENANT_ID\"\n\tEnvClientID         = envNamespace + \"CLIENT_ID\"\n\tEnvClientSecret     = envNamespace + \"CLIENT_SECRET\"\n\tEnvZoneName         = envNamespace + \"ZONE_NAME\"\n\tEnvPrivateZone      = envNamespace + \"PRIVATE_ZONE\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nconst EnvLegoAzureBypassDeprecation = \"LEGO_AZURE_BYPASS_DEPRECATION\"\n\nconst defaultMetadataEndpoint = \"http://169.254.169.254\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tZoneName string\n\n\t// optional if using instance metadata service\n\tClientID     string\n\tClientSecret string\n\tTenantID     string\n\n\tSubscriptionID string\n\tResourceGroup  string\n\tPrivateZone    bool\n\n\tMetadataEndpoint        string\n\tResourceManagerEndpoint string\n\tActiveDirectoryEndpoint string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tZoneName:                env.GetOrFile(EnvZoneName),\n\t\tTTL:                     env.GetOrDefaultInt(EnvTTL, 60),\n\t\tPropagationTimeout:      env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:         env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second),\n\t\tMetadataEndpoint:        env.GetOrFile(EnvMetadataEndpoint),\n\t\tResourceManagerEndpoint: aazure.PublicCloud.ResourceManagerEndpoint,\n\t\tActiveDirectoryEndpoint: aazure.PublicCloud.ActiveDirectoryEndpoint,\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprovider challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for azure.\n// Credentials can be passed in the environment variables:\n// AZURE_ENVIRONMENT, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET,\n// AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_RESOURCE_GROUP\n// If the credentials are _not_ set via the environment,\n// then it will attempt to get a bearer token via the instance metadata service.\n// see: https://github.com/Azure/go-autorest/blob/v10.14.0/autorest/azure/auth/auth.go#L38-L42\n//\n// Deprecated: use azuredns instead.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tconfig := NewDefaultConfig()\n\n\tenvironmentName := env.GetOrFile(EnvEnvironment)\n\tif environmentName != \"\" {\n\t\tvar environment aazure.Environment\n\n\t\tswitch environmentName {\n\t\tcase \"china\":\n\t\t\tenvironment = aazure.ChinaCloud\n\t\tcase \"german\":\n\t\t\tenvironment = aazure.GermanCloud\n\t\tcase \"public\":\n\t\t\tenvironment = aazure.PublicCloud\n\t\tcase \"usgovernment\":\n\t\t\tenvironment = aazure.USGovernmentCloud\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"azure: unknown environment %s\", environmentName)\n\t\t}\n\n\t\tconfig.ResourceManagerEndpoint = environment.ResourceManagerEndpoint\n\t\tconfig.ActiveDirectoryEndpoint = environment.ActiveDirectoryEndpoint\n\t}\n\n\tconfig.SubscriptionID = env.GetOrFile(EnvSubscriptionID)\n\tconfig.ResourceGroup = env.GetOrFile(EnvResourceGroup)\n\tconfig.ClientSecret = env.GetOrFile(EnvClientSecret)\n\tconfig.ClientID = env.GetOrFile(EnvClientID)\n\tconfig.TenantID = env.GetOrFile(EnvTenantID)\n\tconfig.PrivateZone = env.GetOrDefaultBool(EnvPrivateZone, false)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Azure.\n//\n// Deprecated: use azuredns instead.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"azure: the configuration of the DNS provider is nil\")\n\t}\n\n\tif !env.GetOrDefaultBool(EnvLegoAzureBypassDeprecation, false) {\n\t\tvar msg strings.Builder\n\n\t\tmsg.WriteString(\"azure: \")\n\t\tmsg.WriteString(\"The `azure` provider has been deprecated since 2023, and replaced by `azuredns` provider. \")\n\t\tmsg.WriteString(\"It can be TEMPORARILY reactivated by using the environment variable `LEGO_AZURE_BYPASS_DEPRECATION=true`. \")\n\t\tmsg.WriteString(\"The `azure` provider will be removed in a future release, please migrate to the `azuredns` provider. \")\n\t\tmsg.WriteString(\"The documentation of the `azuredns` provider can be found at https://go-acme.github.io/lego/dns/azuredns/\")\n\n\t\treturn nil, errors.New(msg.String())\n\t}\n\n\tif config.HTTPClient == nil {\n\t\tconfig.HTTPClient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tauthorizer, err := getAuthorizer(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif config.SubscriptionID == \"\" {\n\t\tsubsID, err := getMetadata(config, \"subscriptionId\")\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"azure: %w\", err)\n\t\t}\n\n\t\tif subsID == \"\" {\n\t\t\treturn nil, errors.New(\"azure: SubscriptionID is missing\")\n\t\t}\n\n\t\tconfig.SubscriptionID = subsID\n\t}\n\n\tif config.ResourceGroup == \"\" {\n\t\tresGroup, err := getMetadata(config, \"resourceGroupName\")\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"azure: %w\", err)\n\t\t}\n\n\t\tif resGroup == \"\" {\n\t\t\treturn nil, errors.New(\"azure: ResourceGroup is missing\")\n\t\t}\n\n\t\tconfig.ResourceGroup = resGroup\n\t}\n\n\tif config.PrivateZone {\n\t\treturn &DNSProvider{provider: &dnsProviderPrivate{config: config, authorizer: authorizer}}, nil\n\t}\n\n\treturn &DNSProvider{provider: &dnsProviderPublic{config: config, authorizer: authorizer}}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.provider.Timeout()\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\treturn d.provider.Present(domain, token, keyAuth)\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\treturn d.provider.CleanUp(domain, token, keyAuth)\n}\n\nfunc getAuthorizer(config *Config) (autorest.Authorizer, error) {\n\tif config.ClientID != \"\" && config.ClientSecret != \"\" && config.TenantID != \"\" {\n\t\tcredentialsConfig := auth.ClientCredentialsConfig{\n\t\t\tClientID:     config.ClientID,\n\t\t\tClientSecret: config.ClientSecret,\n\t\t\tTenantID:     config.TenantID,\n\t\t\tResource:     config.ResourceManagerEndpoint,\n\t\t\tAADEndpoint:  config.ActiveDirectoryEndpoint,\n\t\t}\n\n\t\tspToken, err := credentialsConfig.ServicePrincipalToken()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get oauth token from client credentials: %w\", err)\n\t\t}\n\n\t\tspToken.SetSender(config.HTTPClient)\n\n\t\treturn autorest.NewBearerAuthorizer(spToken), nil\n\t}\n\n\treturn auth.NewAuthorizerFromEnvironment()\n}\n\n// Fetches metadata from environment or the instance metadata service.\n// borrowed from https://github.com/Microsoft/azureimds/blob/master/imdssample.go\nfunc getMetadata(config *Config, field string) (string, error) {\n\tmetadataEndpoint := config.MetadataEndpoint\n\tif metadataEndpoint == \"\" {\n\t\tmetadataEndpoint = defaultMetadataEndpoint\n\t}\n\n\tendpoint, err := url.JoinPath(metadataEndpoint, \"metadata\", \"instance\", \"compute\", field)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treq, err := http.NewRequest(http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treq.Header.Set(\"Metadata\", \"True\")\n\n\tq := req.URL.Query()\n\tq.Add(\"format\", \"text\")\n\tq.Add(\"api-version\", \"2017-12-01\")\n\treq.URL.RawQuery = q.Encode()\n\n\tresp, err := config.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\treturn string(raw), nil\n}\n"
  },
  {
    "path": "providers/dns/azure/azure.toml",
    "content": "Name = \"Azure (deprecated)\"\nDescription = ''''''\nURL = \"https://azure.microsoft.com/services/dns/\"\nCode = \"azure\"\nSince = \"v0.4.0\"\n\nExample = ''''''\n\n[Configuration]\n  [Configuration.Credentials]\n    AZURE_ENVIRONMENT = \"Azure environment, one of: public, usgovernment, german, and china\"\n    AZURE_CLIENT_ID = \"Client ID\"\n    AZURE_CLIENT_SECRET = \"Client secret\"\n    AZURE_SUBSCRIPTION_ID = \"Subscription ID\"\n    AZURE_TENANT_ID = \"Tenant ID\"\n    AZURE_RESOURCE_GROUP = \"Resource group\"\n    'instance metadata service' = \"If the credentials are **not** set via the environment, then it will attempt to get a bearer token via the [instance metadata service](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service).\"\n  [Configuration.Additional]\n    AZURE_METADATA_ENDPOINT = \"Metadata Service endpoint URL\"\n    AZURE_PRIVATE_ZONE = \"Set to true to use Azure Private DNS Zones and not public\"\n    AZURE_ZONE_NAME = \"Zone name to use inside Azure DNS service to add the TXT record in\"\n    AZURE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    AZURE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    AZURE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n\n[Links]\n  API = \"https://docs.microsoft.com/en-us/go/azure/\"\n  GoClient = \"https://github.com/Azure/azure-sdk-for-go\"\n"
  },
  {
    "path": "providers/dns/azure/azure_test.go",
    "content": "package azure\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvLegoAzureBypassDeprecation,\n\tEnvEnvironment,\n\tEnvClientID,\n\tEnvClientSecret,\n\tEnvSubscriptionID,\n\tEnvTenantID,\n\tEnvResourceGroup).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvClientID:       \"A\",\n\t\t\t\tEnvClientSecret:   \"B\",\n\t\t\t\tEnvTenantID:       \"C\",\n\t\t\t\tEnvSubscriptionID: \"D\",\n\t\t\t\tEnvResourceGroup:  \"E\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing client ID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvClientID:       \"\",\n\t\t\t\tEnvClientSecret:   \"B\",\n\t\t\t\tEnvTenantID:       \"C\",\n\t\t\t\tEnvSubscriptionID: \"D\",\n\t\t\t\tEnvResourceGroup:  \"E\",\n\t\t\t},\n\t\t\texpected: \"failed to get SPT from client credentials: parameter 'clientID' cannot be empty\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\ttest.envVars[EnvLegoAzureBypassDeprecation] = \"true\"\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, p)\n\t\t\trequire.NotNil(t, p.provider)\n\n\t\t\tassert.IsType(t, p.provider, new(dnsProviderPublic))\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc           string\n\t\tclientID       string\n\t\tclientSecret   string\n\t\tsubscriptionID string\n\t\ttenantID       string\n\t\tresourceGroup  string\n\t\tprivateZone    bool\n\t\thandler        func(w http.ResponseWriter, r *http.Request)\n\t\texpected       string\n\t}{\n\t\t{\n\t\t\tdesc:           \"success (public)\",\n\t\t\tclientID:       \"A\",\n\t\t\tclientSecret:   \"B\",\n\t\t\ttenantID:       \"C\",\n\t\t\tsubscriptionID: \"D\",\n\t\t\tresourceGroup:  \"E\",\n\t\t\tprivateZone:    false,\n\t\t},\n\t\t{\n\t\t\tdesc:           \"success (private)\",\n\t\t\tclientID:       \"A\",\n\t\t\tclientSecret:   \"B\",\n\t\t\ttenantID:       \"C\",\n\t\t\tsubscriptionID: \"D\",\n\t\t\tresourceGroup:  \"E\",\n\t\t\tprivateZone:    true,\n\t\t},\n\t\t{\n\t\t\tdesc:           \"SubscriptionID missing\",\n\t\t\tclientID:       \"A\",\n\t\t\tclientSecret:   \"B\",\n\t\t\ttenantID:       \"C\",\n\t\t\tsubscriptionID: \"\",\n\t\t\tresourceGroup:  \"\",\n\t\t\texpected:       \"azure: SubscriptionID is missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:           \"ResourceGroup missing\",\n\t\t\tclientID:       \"A\",\n\t\t\tclientSecret:   \"B\",\n\t\t\ttenantID:       \"C\",\n\t\t\tsubscriptionID: \"D\",\n\t\t\tresourceGroup:  \"\",\n\t\t\texpected:       \"azure: ResourceGroup is missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:           \"use metadata\",\n\t\t\tclientID:       \"A\",\n\t\t\tclientSecret:   \"B\",\n\t\t\ttenantID:       \"C\",\n\t\t\tsubscriptionID: \"\",\n\t\t\tresourceGroup:  \"\",\n\t\t\thandler: func(w http.ResponseWriter, _ *http.Request) {\n\t\t\t\t_, err := w.Write([]byte(\"foo\"))\n\t\t\t\tif err != nil {\n\t\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\n\tdefer envTest.RestoreEnv()\n\n\tenvTest.ClearEnv()\n\tenvTest.Apply(map[string]string{EnvLegoAzureBypassDeprecation: \"true\"})\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.ClientID = test.clientID\n\t\t\tconfig.ClientSecret = test.clientSecret\n\t\t\tconfig.SubscriptionID = test.subscriptionID\n\t\t\tconfig.TenantID = test.tenantID\n\t\t\tconfig.ResourceGroup = test.resourceGroup\n\t\t\tconfig.PrivateZone = test.privateZone\n\n\t\t\tmux := http.NewServeMux()\n\t\t\tserver := httptest.NewServer(mux)\n\t\t\tt.Cleanup(server.Close)\n\n\t\t\tif test.handler == nil {\n\t\t\t\tmux.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {})\n\t\t\t} else {\n\t\t\t\tmux.HandleFunc(\"/\", test.handler)\n\t\t\t}\n\n\t\t\tconfig.MetadataEndpoint = server.URL\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, p)\n\t\t\trequire.NotNil(t, p.provider)\n\n\t\t\tif test.privateZone {\n\t\t\t\tassert.IsType(t, p.provider, new(dnsProviderPrivate))\n\t\t\t} else {\n\t\t\t\tassert.IsType(t, p.provider, new(dnsProviderPublic))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/azure/private.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/Azure/azure-sdk-for-go/profiles/latest/privatedns/mgmt/privatedns\"\n\t\"github.com/Azure/go-autorest/autorest\"\n\t\"github.com/Azure/go-autorest/autorest/to\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n)\n\n// dnsProviderPrivate implements the challenge.Provider interface for Azure Private Zone DNS.\ntype dnsProviderPrivate struct {\n\tconfig     *Config\n\tauthorizer autorest.Authorizer\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *dnsProviderPrivate) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *dnsProviderPrivate) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azure: %w\", err)\n\t}\n\n\trsc := privatedns.NewRecordSetsClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)\n\trsc.Authorizer = d.authorizer\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azure: %w\", err)\n\t}\n\n\t// Get existing record set\n\trset, err := rsc.Get(ctx, d.config.ResourceGroup, zone, privatedns.TXT, subDomain)\n\tif err != nil {\n\t\tvar detailed autorest.DetailedError\n\t\tif !errors.As(err, &detailed) || detailed.StatusCode != http.StatusNotFound {\n\t\t\treturn fmt.Errorf(\"azure: %w\", err)\n\t\t}\n\t}\n\n\t// Construct unique TXT records using map\n\tuniqRecords := map[string]struct{}{info.Value: {}}\n\n\tif rset.RecordSetProperties != nil && rset.TxtRecords != nil {\n\t\tfor _, txtRecord := range *rset.TxtRecords {\n\t\t\t// Assume Value doesn't contain multiple strings\n\t\t\tvalues := to.StringSlice(txtRecord.Value)\n\t\t\tif len(values) > 0 {\n\t\t\t\tuniqRecords[values[0]] = struct{}{}\n\t\t\t}\n\t\t}\n\t}\n\n\tvar txtRecords []privatedns.TxtRecord\n\tfor txt := range uniqRecords {\n\t\ttxtRecords = append(txtRecords, privatedns.TxtRecord{Value: &[]string{txt}})\n\t}\n\n\trec := privatedns.RecordSet{\n\t\tName: &subDomain,\n\t\tRecordSetProperties: &privatedns.RecordSetProperties{\n\t\t\tTTL:        to.Int64Ptr(int64(d.config.TTL)),\n\t\t\tTxtRecords: &txtRecords,\n\t\t},\n\t}\n\n\t_, err = rsc.CreateOrUpdate(ctx, d.config.ResourceGroup, zone, privatedns.TXT, subDomain, rec, \"\", \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azure: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *dnsProviderPrivate) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azure: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azure: %w\", err)\n\t}\n\n\trsc := privatedns.NewRecordSetsClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)\n\trsc.Authorizer = d.authorizer\n\n\t_, err = rsc.Delete(ctx, d.config.ResourceGroup, zone, privatedns.TXT, subDomain, \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azure: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Checks that azure has a zone for this domain name.\nfunc (d *dnsProviderPrivate) getHostedZoneID(ctx context.Context, fqdn string) (string, error) {\n\tif d.config.ZoneName != \"\" {\n\t\treturn d.config.ZoneName, nil\n\t}\n\n\tauthZone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tdc := privatedns.NewPrivateZonesClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)\n\tdc.Authorizer = d.authorizer\n\n\tzone, err := dc.Get(ctx, d.config.ResourceGroup, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// zone.Name shouldn't have a trailing dot(.)\n\treturn to.String(zone.Name), nil\n}\n"
  },
  {
    "path": "providers/dns/azure/public.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/Azure/azure-sdk-for-go/profiles/latest/dns/mgmt/dns\"\n\t\"github.com/Azure/go-autorest/autorest\"\n\t\"github.com/Azure/go-autorest/autorest/to\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n)\n\n// dnsProviderPublic implements the challenge.Provider interface for Azure Public Zone DNS.\ntype dnsProviderPublic struct {\n\tconfig     *Config\n\tauthorizer autorest.Authorizer\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *dnsProviderPublic) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *dnsProviderPublic) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azure: %w\", err)\n\t}\n\n\trsc := dns.NewRecordSetsClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)\n\trsc.Authorizer = d.authorizer\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azure: %w\", err)\n\t}\n\n\t// Get existing record set\n\trset, err := rsc.Get(ctx, d.config.ResourceGroup, zone, subDomain, dns.TXT)\n\tif err != nil {\n\t\tvar detailed autorest.DetailedError\n\t\tif !errors.As(err, &detailed) || detailed.StatusCode != http.StatusNotFound {\n\t\t\treturn fmt.Errorf(\"azure: %w\", err)\n\t\t}\n\t}\n\n\t// Construct unique TXT records using map\n\tuniqRecords := map[string]struct{}{info.Value: {}}\n\n\tif rset.RecordSetProperties != nil && rset.TxtRecords != nil {\n\t\tfor _, txtRecord := range *rset.TxtRecords {\n\t\t\t// Assume Value doesn't contain multiple strings\n\t\t\tvalues := to.StringSlice(txtRecord.Value)\n\t\t\tif len(values) > 0 {\n\t\t\t\tuniqRecords[values[0]] = struct{}{}\n\t\t\t}\n\t\t}\n\t}\n\n\tvar txtRecords []dns.TxtRecord\n\tfor txt := range uniqRecords {\n\t\ttxtRecords = append(txtRecords, dns.TxtRecord{Value: &[]string{txt}})\n\t}\n\n\trec := dns.RecordSet{\n\t\tName: &subDomain,\n\t\tRecordSetProperties: &dns.RecordSetProperties{\n\t\t\tTTL:        to.Int64Ptr(int64(d.config.TTL)),\n\t\t\tTxtRecords: &txtRecords,\n\t\t},\n\t}\n\n\t_, err = rsc.CreateOrUpdate(ctx, d.config.ResourceGroup, zone, subDomain, dns.TXT, rec, \"\", \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azure: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *dnsProviderPublic) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azure: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azure: %w\", err)\n\t}\n\n\trsc := dns.NewRecordSetsClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)\n\trsc.Authorizer = d.authorizer\n\n\t_, err = rsc.Delete(ctx, d.config.ResourceGroup, zone, subDomain, dns.TXT, \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azure: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Checks that azure has a zone for this domain name.\nfunc (d *dnsProviderPublic) getHostedZoneID(ctx context.Context, fqdn string) (string, error) {\n\tif d.config.ZoneName != \"\" {\n\t\treturn d.config.ZoneName, nil\n\t}\n\n\tauthZone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tdc := dns.NewZonesClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID)\n\tdc.Authorizer = d.authorizer\n\n\tzone, err := dc.Get(ctx, d.config.ResourceGroup, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// zone.Name shouldn't have a trailing dot(.)\n\treturn to.String(zone.Name), nil\n}\n"
  },
  {
    "path": "providers/dns/azuredns/azuredns.go",
    "content": "// Package azuredns implements a DNS provider for solving the DNS-01 challenge using azure DNS.\n// Azure doesn't like trailing dots on domain names, most of the acme code does.\npackage azuredns\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"AZURE_\"\n\n\tEnvEnvironment    = envNamespace + \"ENVIRONMENT\"\n\tEnvSubscriptionID = envNamespace + \"SUBSCRIPTION_ID\"\n\tEnvResourceGroup  = envNamespace + \"RESOURCE_GROUP\"\n\tEnvZoneName       = envNamespace + \"ZONE_NAME\"\n\tEnvPrivateZone    = envNamespace + \"PRIVATE_ZONE\"\n\n\tEnvTenantID     = envNamespace + \"TENANT_ID\"\n\tEnvClientID     = envNamespace + \"CLIENT_ID\"\n\tEnvClientSecret = envNamespace + \"CLIENT_SECRET\"\n\n\tEnvOIDCToken              = envNamespace + \"OIDC_TOKEN\"\n\tEnvOIDCTokenFilePath      = envNamespace + \"OIDC_TOKEN_FILE_PATH\"\n\tEnvOIDCRequestURL         = envNamespace + \"OIDC_REQUEST_URL\"\n\tEnvGitHubOIDCRequestURL   = \"ACTIONS_ID_TOKEN_REQUEST_URL\"\n\taltEnvArmOIDCRequestURL   = \"ARM_OIDC_REQUEST_URL\"\n\tEnvOIDCRequestToken       = envNamespace + \"OIDC_REQUEST_TOKEN\"\n\tEnvGitHubOIDCRequestToken = \"ACTIONS_ID_TOKEN_REQUEST_TOKEN\"\n\taltEnvArmOIDCRequestToken = \"ARM_OIDC_REQUEST_TOKEN\"\n\n\tEnvServiceConnectionID                  = envNamespace + \"SERVICE_CONNECTION_ID\"\n\taltEnvServiceConnectionID               = \"SERVICE_CONNECTION_ID\"\n\taltEnvArmAdoPipelineServiceConnectionID = \"ARM_ADO_PIPELINE_SERVICE_CONNECTION_ID\"\n\taltEnvArmOIDCAzureServiceConnectionID   = \"ARM_OIDC_AZURE_SERVICE_CONNECTION_ID\"\n\tEnvSystemAccessToken                    = envNamespace + \"SYSTEM_ACCESS_TOKEN\"\n\taltEnvSystemAccessToken                 = \"SYSTEM_ACCESSTOKEN\"\n\n\tEnvAuthMethod     = envNamespace + \"AUTH_METHOD\"\n\tEnvAuthMSITimeout = envNamespace + \"AUTH_MSI_TIMEOUT\"\n\n\tEnvServiceDiscoveryFilter = envNamespace + \"SERVICEDISCOVERY_FILTER\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tZoneName string\n\n\tSubscriptionID string\n\tResourceGroup  string\n\tPrivateZone    bool\n\n\tEnvironment cloud.Configuration\n\n\t// optional if using default Azure credentials\n\tClientID     string\n\tClientSecret string\n\tTenantID     string\n\n\tOIDCToken         string\n\tOIDCTokenFilePath string\n\tOIDCRequestURL    string\n\tOIDCRequestToken  string\n\n\tServiceConnectionID string\n\tSystemAccessToken   string\n\n\tAuthMethod     string\n\tAuthMSITimeout time.Duration\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n\n\tServiceDiscoveryFilter string\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tZoneName:           env.GetOrFile(EnvZoneName),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 60),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second),\n\t\tEnvironment:        cloud.AzurePublic,\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprovider challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for azuredns.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tconfig := NewDefaultConfig()\n\n\tenvironmentName := env.GetOrFile(EnvEnvironment)\n\tif environmentName != \"\" {\n\t\tswitch environmentName {\n\t\tcase \"china\":\n\t\t\tconfig.Environment = cloud.AzureChina\n\t\tcase \"public\":\n\t\t\tconfig.Environment = cloud.AzurePublic\n\t\tcase \"usgovernment\":\n\t\t\tconfig.Environment = cloud.AzureGovernment\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"azuredns: unknown environment %s\", environmentName)\n\t\t}\n\t} else {\n\t\tconfig.Environment = cloud.AzurePublic\n\t}\n\n\tconfig.SubscriptionID = env.GetOrFile(EnvSubscriptionID)\n\tconfig.ResourceGroup = env.GetOrFile(EnvResourceGroup)\n\tconfig.PrivateZone = env.GetOrDefaultBool(EnvPrivateZone, false)\n\n\tconfig.ClientID = env.GetOrFile(EnvClientID)\n\tconfig.ClientSecret = env.GetOrFile(EnvClientSecret)\n\tconfig.TenantID = env.GetOrFile(EnvTenantID)\n\n\tconfig.OIDCToken = env.GetOrFile(EnvOIDCToken)\n\tconfig.OIDCTokenFilePath = env.GetOrFile(EnvOIDCTokenFilePath)\n\n\tconfig.ServiceDiscoveryFilter = env.GetOrFile(EnvServiceDiscoveryFilter)\n\n\toidcValues, _ := env.GetWithFallback(\n\t\t[]string{EnvOIDCRequestURL, EnvGitHubOIDCRequestURL, altEnvArmOIDCRequestURL},\n\t\t[]string{EnvOIDCRequestToken, EnvGitHubOIDCRequestToken, altEnvArmOIDCRequestToken},\n\t)\n\n\tconfig.OIDCRequestURL = oidcValues[EnvOIDCRequestURL]\n\tconfig.OIDCRequestToken = oidcValues[EnvOIDCRequestToken]\n\n\t// https://registry.terraform.io/providers/hashicorp/Azurerm/latest/docs/guides/service_principal_oidc\n\tpipelineValues, _ := env.GetWithFallback(\n\t\t[]string{EnvServiceConnectionID, altEnvServiceConnectionID, altEnvArmAdoPipelineServiceConnectionID, altEnvArmOIDCAzureServiceConnectionID},\n\t\t[]string{EnvSystemAccessToken, altEnvArmOIDCRequestToken, altEnvSystemAccessToken},\n\t)\n\n\tconfig.ServiceConnectionID = pipelineValues[EnvServiceConnectionID]\n\tconfig.SystemAccessToken = pipelineValues[EnvSystemAccessToken]\n\n\tconfig.AuthMethod = env.GetOrFile(EnvAuthMethod)\n\tconfig.AuthMSITimeout = env.GetOrDefaultSecond(EnvAuthMSITimeout, 2*time.Second)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Azure.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"azuredns: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.HTTPClient == nil {\n\t\tconfig.HTTPClient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tconfig.HTTPClient = clientdebug.Wrap(config.HTTPClient)\n\n\tcredentials, err := getCredentials(config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"azuredns: Unable to retrieve valid credentials: %w\", err)\n\t}\n\n\tvar dnsProvider challenge.ProviderTimeout\n\tif config.PrivateZone {\n\t\tdnsProvider, err = NewDNSProviderPrivate(config, credentials)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"azuredns: %w\", err)\n\t\t}\n\t} else {\n\t\tdnsProvider, err = NewDNSProviderPublic(config, credentials)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"azuredns: %w\", err)\n\t\t}\n\t}\n\n\treturn &DNSProvider{provider: dnsProvider}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.provider.Timeout()\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\treturn d.provider.Present(domain, token, keyAuth)\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\treturn d.provider.CleanUp(domain, token, keyAuth)\n}\n"
  },
  {
    "path": "providers/dns/azuredns/azuredns.toml",
    "content": "Name = \"Azure DNS\"\nDescription = ''''''\nURL = \"https://azure.microsoft.com/services/dns/\"\nCode = \"azuredns\"\nSince = \"v4.13.0\"\n\nExample = '''\n### Using client secret\n\nAZURE_CLIENT_ID=<your service principal client ID> \\\nAZURE_TENANT_ID=<your service principal tenant ID> \\\nAZURE_CLIENT_SECRET=<your service principal client secret> \\\nlego --dns azuredns -d '*.example.com' -d example.com run\n\n### Using client certificate\n\nAZURE_CLIENT_ID=<your service principal client ID> \\\nAZURE_TENANT_ID=<your service principal tenant ID> \\\nAZURE_CLIENT_CERTIFICATE_PATH=<your service principal certificate path> \\\nlego --dns azuredns -d '*.example.com' -d example.com run\n\n### Using Azure CLI\n\naz login \\\nlego --dns azuredns -d '*.example.com' -d example.com run\n\n### Using Managed Identity (Azure VM)\n\nAZURE_TENANT_ID=<your service principal tenant ID> \\\nAZURE_RESOURCE_GROUP=<your target zone resource group name> \\\nlego --dns azuredns -d '*.example.com' -d example.com run\n\n### Using Managed Identity (Azure Arc)\n\nAZURE_TENANT_ID=<your service principal tenant ID> \\\nIMDS_ENDPOINT=http://localhost:40342 \\\nIDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token \\\nlego --dns azuredns -d '*.example.com' -d example.com run\n\n'''\n\nAdditional = '''\n## Description\n\nSeveral authentication methods can be used to authenticate against Azure DNS API.\n\n### Default Azure Credentials (default option)\n\nDefault Azure Credentials automatically detects in the following locations and prioritized in the following order:\n\n1. Environment variables for client secret: `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_CLIENT_SECRET`\n2. Environment variables for client certificate: `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_CLIENT_CERTIFICATE_PATH`\n3. Workload identity for resources hosted in Azure environment (see below)\n4. Shared credentials (defaults to `~/.azure` folder), used by Azure CLI\n\nLink:\n- [Azure Authentication](https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication)\n\n### Environment variables\n\n#### Service Discovery\n\nLego automatically finds all visible Azure (private) DNS zones using [Azure ResourceGraph query](https://learn.microsoft.com/en-us/azure/governance/resource-graph/).\nThis can be limited by specifying environment variable `AZURE_SUBSCRIPTION_ID` and/or `AZURE_RESOURCE_GROUP` which limits the\nDNS zones to only a subscription or to one resourceGroup.\n\nAdditionally environment variable `AZURE_SERVICEDISCOVERY_FILTER` can be used to filter DNS zones with an addition Kusto filter eg:\n\n```\nresources\n| where type =~ \"microsoft.network/dnszones\"\n| ${AZURE_SERVICEDISCOVERY_FILTER}\n| project subscriptionId, resourceGroup, name\n```\n\n\n#### Client secret\n\nThe Azure Credentials can be configured using the following environment variables:\n* AZURE_CLIENT_ID = \"Client ID\"\n* AZURE_CLIENT_SECRET = \"Client secret\"\n* AZURE_TENANT_ID = \"Tenant ID\"\n\nThis authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `env`.\n\n#### Client certificate\n\nThe Azure Credentials can be configured using the following environment variables:\n* AZURE_CLIENT_ID = \"Client ID\"\n* AZURE_CLIENT_CERTIFICATE_PATH = \"Client certificate path\"\n* AZURE_TENANT_ID = \"Tenant ID\"\n\nThis authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `env`.\n\n### Workload identity\n\nWorkload identity allows workloads running Azure Kubernetes Services (AKS) clusters to authenticate as an Azure AD application identity using federated credentials.\n\nThis must be configured in kubernetes workload deployment in one hand and on the Azure AD application registration in the other hand.\n\nHere is a summary of the steps to follow to use it :\n* create a `ServiceAccount` resource, add following annotations to reference the targeted Azure AD application registration : `azure.workload.identity/client-id` and `azure.workload.identity/tenant-id`.\n* on the `Deployment` resource you must reference the previous `ServiceAccount` and add the following label : `azure.workload.identity/use: \"true\"`.\n* create a federated credentials of type `Kubernetes accessing Azure resources`, add the cluster issuer URL  and add the namespace and name of your kubernetes service account.\n\nLink :\n- [Azure AD Workload identity](https://azure.github.io/azure-workload-identity/docs/topics/service-account-labels-and-annotations.html)\n\nThis authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `wli`.\n\n### Azure Managed Identity\n\n#### Azure Managed Identity (with Azure workload)\n\nThe Azure Managed Identity service allows linking Azure AD identities to Azure resources, without needing to manually manage client IDs and secrets.\n\nWorkloads with a Managed Identity can manage their own certificates, with permissions on specific domain names set using IAM assignments.\nFor this to work, the Managed Identity requires the **Reader** role on the target DNS Zone,\nand the **DNS Zone Contributor** on the relevant `_acme-challenge` TXT records.\n\nFor example, to allow a Managed Identity to create a certificate for \"fw01.lab.example.com\", using Azure CLI:\n\n```bash\nexport AZURE_SUBSCRIPTION_ID=\"00000000-0000-0000-0000-000000000000\"\nexport AZURE_RESOURCE_GROUP=\"rg1\"\nexport SERVICE_PRINCIPAL_ID=\"00000000-0000-0000-0000-000000000000\"\n\nexport AZURE_DNS_ZONE=\"lab.example.com\"\nexport AZ_HOSTNAME=\"fw01\"\nexport AZ_RECORD_SET=\"_acme-challenge.${AZ_HOSTNAME}\"\n\naz role assignment create \\\n--assignee \"${SERVICE_PRINCIPAL_ID}\" \\\n--role \"Reader\" \\\n--scope \"/subscriptions/${AZURE_SUBSCRIPTION_ID}/resourceGroups/${AZURE_RESOURCE_GROUP}/providers/Microsoft.Network/dnszones/${AZURE_DNS_ZONE}\"\n\naz role assignment create \\\n--assignee \"${SERVICE_PRINCIPAL_ID}\" \\\n--role \"DNS Zone Contributor\" \\\n--scope \"/subscriptions/${AZURE_SUBSCRIPTION_ID}/resourceGroups/${AZURE_RESOURCE_GROUP}/providers/Microsoft.Network/dnszones/${AZURE_DNS_ZONE}/TXT/${AZ_RECORD_SET}\"\n```\n\nA timeout wrapper is configured for this authentication method.\nThe duration can be configured by setting the `AZURE_AUTH_MSI_TIMEOUT`.\nThe default timeout is 2 seconds.\nThis authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `msi`.\n\n#### Azure Managed Identity (with Azure Arc)\n\nThe Azure Arc agent provides the ability to use a Managed Identity on resources hosted outside of Azure\n(such as on-prem virtual machines, or VMs in another cloud provider).\n\nWhile the upstream `azidentity` SDK will try to automatically identify and use the Azure Arc metadata service,\nif you get `azuredns: DefaultAzureCredential: failed to acquire a token.` error messages,\nyou may need to set the environment variables:\n* `IMDS_ENDPOINT=http://localhost:40342`\n* `IDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token`\n\nA timeout wrapper is configured for this authentication method.\nThe duration can be configured by setting the `AZURE_AUTH_MSI_TIMEOUT`.\nThe default timeout is 2 seconds.\nThis authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `msi`.\n\n### Azure CLI\n\nThe Azure CLI is a command-line tool provided by Microsoft to interact with Azure resources.\nIt provides an easy way to authenticate by simply running `az login` command.\nThe generated token will be cached by default in the `~/.azure` folder.\n\nThis authentication method can be specifically used by setting the `AZURE_AUTH_METHOD` environment variable to `cli`.\n\n### Open ID Connect\n\nOpen ID Connect is a mechanism that establish a trust relationship between a running environment and the Azure AD identity provider.\nIt can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `oidc`.\n\n### Azure DevOps Pipelines\n\nIt can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `pipeline`.\n\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    AZURE_CLIENT_ID = \"Client ID\"\n    AZURE_CLIENT_SECRET = \"Client secret\"\n    AZURE_TENANT_ID = \"Tenant ID\"\n    AZURE_CLIENT_CERTIFICATE_PATH = \"Client certificate path\"\n  [Configuration.Additional]\n    AZURE_ENVIRONMENT = \"Azure environment, one of: public, usgovernment, and china\"\n    AZURE_SUBSCRIPTION_ID = \"DNS zone subscription ID\"\n    AZURE_RESOURCE_GROUP = \"DNS zone resource group\"\n    AZURE_SERVICEDISCOVERY_FILTER = \"Advanced ServiceDiscovery filter using Kusto query condition\"\n    AZURE_PRIVATE_ZONE = \"Set to true to use Azure Private DNS Zones and not public\"\n    AZURE_ZONE_NAME = \"Zone name to use inside Azure DNS service to add the TXT record in\"\n    AZURE_AUTH_METHOD = \"Specify which authentication method to use\"\n    AZURE_AUTH_MSI_TIMEOUT = \"Managed Identity timeout duration\"\n    AZURE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    AZURE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    AZURE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n\n[Links]\n  API = \"https://docs.microsoft.com/en-us/go/azure/\"\n  GoClient = \"https://github.com/Azure/azure-sdk-for-go\"\n"
  },
  {
    "path": "providers/dns/azuredns/azuredns_test.go",
    "content": "package azuredns\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvEnvironment,\n\tEnvSubscriptionID,\n\tEnvResourceGroup).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"unknown environment\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEnvironment: \"test\",\n\t\t\t},\n\t\t\texpected: \"azuredns: unknown environment test\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, p)\n\t\t\trequire.NotNil(t, p.provider)\n\n\t\t\tassert.IsType(t, p.provider, new(DNSProviderPublic))\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/azuredns/credentials.go",
    "content": "package azuredns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azidentity\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n)\n\nconst (\n\tauthMethodEnv      = \"env\"\n\tauthMethodWLI      = \"wli\"\n\tauthMethodMSI      = \"msi\"\n\tauthMethodCLI      = \"cli\"\n\tauthMethodOIDC     = \"oidc\"\n\tauthMethodPipeline = \"pipeline\"\n)\n\n//nolint:gocyclo // The complexity is related to the number of possible configurations.\nfunc getCredentials(config *Config) (azcore.TokenCredential, error) {\n\tclientOptions := azcore.ClientOptions{Cloud: config.Environment}\n\n\tswitch strings.ToLower(config.AuthMethod) {\n\tcase authMethodEnv:\n\t\tif config.ClientID != \"\" && config.ClientSecret != \"\" && config.TenantID != \"\" {\n\t\t\treturn azidentity.NewClientSecretCredential(config.TenantID, config.ClientID, config.ClientSecret,\n\t\t\t\t&azidentity.ClientSecretCredentialOptions{ClientOptions: clientOptions})\n\t\t}\n\n\t\treturn azidentity.NewEnvironmentCredential(&azidentity.EnvironmentCredentialOptions{ClientOptions: clientOptions})\n\n\tcase authMethodWLI:\n\t\treturn azidentity.NewWorkloadIdentityCredential(&azidentity.WorkloadIdentityCredentialOptions{ClientOptions: clientOptions})\n\n\tcase authMethodMSI:\n\t\tcred, err := azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ClientOptions: clientOptions})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &timeoutTokenCredential{cred: cred, timeout: config.AuthMSITimeout}, nil\n\n\tcase authMethodCLI:\n\t\tvar credOptions *azidentity.AzureCLICredentialOptions\n\t\tif config.TenantID != \"\" {\n\t\t\tcredOptions = &azidentity.AzureCLICredentialOptions{TenantID: config.TenantID}\n\t\t}\n\n\t\treturn azidentity.NewAzureCLICredential(credOptions)\n\n\tcase authMethodOIDC:\n\t\terr := checkOIDCConfig(config)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn azidentity.NewClientAssertionCredential(config.TenantID, config.ClientID, getOIDCAssertion(config), &azidentity.ClientAssertionCredentialOptions{ClientOptions: clientOptions})\n\n\tcase authMethodPipeline:\n\t\terr := checkPipelineConfig(config)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Uses the env var `SYSTEM_OIDCREQUESTURI`,\n\t\t// but the constant is not exported,\n\t\t// and there is no way to set it programmatically.\n\t\t// https://github.com/Azure/azure-sdk-for-go/blob/aae2fb75ffccafc669db72bebc3c1a66332f48d7/sdk/azidentity/azure_pipelines_credential.go#L22\n\t\t// https://github.com/Azure/azure-sdk-for-go/blob/aae2fb75ffccafc669db72bebc3c1a66332f48d7/sdk/azidentity/azure_pipelines_credential.go#L79\n\n\t\treturn azidentity.NewAzurePipelinesCredential(config.TenantID, config.ClientID, config.ServiceConnectionID, config.SystemAccessToken, &azidentity.AzurePipelinesCredentialOptions{ClientOptions: clientOptions})\n\n\tdefault:\n\t\treturn azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{ClientOptions: clientOptions})\n\t}\n}\n\n// timeoutTokenCredential wraps a TokenCredential to add a timeout.\ntype timeoutTokenCredential struct {\n\tcred    azcore.TokenCredential\n\ttimeout time.Duration\n}\n\n// GetToken implements the azcore.TokenCredential interface.\nfunc (w *timeoutTokenCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {\n\tif w.timeout <= 0 {\n\t\treturn w.cred.GetToken(ctx, opts)\n\t}\n\n\tctxTimeout, cancel := context.WithTimeout(ctx, w.timeout)\n\tdefer cancel()\n\n\ttk, err := w.cred.GetToken(ctxTimeout, opts)\n\tif ce := ctxTimeout.Err(); errors.Is(ce, context.DeadlineExceeded) {\n\t\treturn tk, azidentity.NewCredentialUnavailableError(\"managed identity timed out\")\n\t}\n\n\tw.timeout = 0\n\n\treturn tk, err\n}\n\nfunc getZoneName(config *Config, fqdn string) (string, error) {\n\tif config.ZoneName != \"\" {\n\t\treturn config.ZoneName, nil\n\t}\n\n\tauthZone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not find zone for %s: %w\", fqdn, err)\n\t}\n\n\tif authZone == \"\" {\n\t\treturn \"\", errors.New(\"empty zone name\")\n\t}\n\n\treturn authZone, nil\n}\n\nfunc checkPipelineConfig(config *Config) error {\n\tif config.ServiceConnectionID == \"\" {\n\t\treturn errors.New(\"azuredns: ServiceConnectionID is missing\")\n\t}\n\n\tif config.SystemAccessToken == \"\" {\n\t\treturn errors.New(\"azuredns: SystemAccessToken is missing\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/azuredns/oidc.go",
    "content": "package azuredns\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n)\n\nfunc checkOIDCConfig(config *Config) error {\n\tif config.TenantID == \"\" {\n\t\treturn errors.New(\"azuredns: TenantID is missing\")\n\t}\n\n\tif config.ClientID == \"\" {\n\t\treturn errors.New(\"azuredns: ClientID is missing\")\n\t}\n\n\tif config.OIDCToken == \"\" && config.OIDCTokenFilePath == \"\" && (config.OIDCRequestURL == \"\" || config.OIDCRequestToken == \"\") {\n\t\treturn errors.New(\"azuredns: OIDCToken, OIDCTokenFilePath or OIDCRequestURL and OIDCRequestToken must be set\")\n\t}\n\n\treturn nil\n}\n\nfunc getOIDCAssertion(config *Config) func(ctx context.Context) (string, error) {\n\treturn func(ctx context.Context) (string, error) {\n\t\tvar token string\n\t\tif config.OIDCToken != \"\" {\n\t\t\ttoken = strings.TrimSpace(config.OIDCToken)\n\t\t}\n\n\t\tif config.OIDCTokenFilePath != \"\" {\n\t\t\tfileTokenRaw, err := os.ReadFile(config.OIDCTokenFilePath)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"azuredns: error retrieving token file with path %s: %w\", config.OIDCTokenFilePath, err)\n\t\t\t}\n\n\t\t\tfileToken := strings.TrimSpace(string(fileTokenRaw))\n\t\t\tif config.OIDCToken != fileToken {\n\t\t\t\treturn \"\", fmt.Errorf(\"azuredns: token file with path %s does not match token from environment variable\", config.OIDCTokenFilePath)\n\t\t\t}\n\n\t\t\ttoken = fileToken\n\t\t}\n\n\t\tif token == \"\" && config.OIDCRequestURL != \"\" && config.OIDCRequestToken != \"\" {\n\t\t\treturn getOIDCToken(config)\n\t\t}\n\n\t\treturn token, nil\n\t}\n}\n\nfunc getOIDCToken(config *Config) (string, error) {\n\treq, err := http.NewRequest(http.MethodGet, config.OIDCRequestURL, http.NoBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"azuredns: failed to build OIDC request: %w\", err)\n\t}\n\n\tquery, err := url.ParseQuery(req.URL.RawQuery)\n\tif err != nil {\n\t\treturn \"\", errors.New(\"azuredns: cannot parse OIDC request URL query\")\n\t}\n\n\tif query.Get(\"audience\") == \"\" {\n\t\tquery.Set(\"audience\", \"api://AzureADTokenExchange\")\n\t\treq.URL.RawQuery = query.Encode()\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", config.OIDCRequestToken))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := config.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"azuredns: cannot request OIDC token: %w\", err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tbody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"azuredns: cannot parse OIDC token response: %w\", err)\n\t}\n\n\tif resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusNoContent {\n\t\treturn \"\", fmt.Errorf(\"azuredns: OIDC token request received HTTP status %d with response: %s\", resp.StatusCode, body)\n\t}\n\n\tvar returnedToken struct {\n\t\tCount int    `json:\"count\"`\n\t\tValue string `json:\"value\"`\n\t}\n\tif err := json.Unmarshal(body, &returnedToken); err != nil {\n\t\treturn \"\", fmt.Errorf(\"azuredns: cannot unmarshal OIDC token response: %w\", err)\n\t}\n\n\treturn returnedToken.Value, nil\n}\n"
  },
  {
    "path": "providers/dns/azuredns/private.go",
    "content": "package azuredns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/to\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProviderPrivate)(nil)\n\n// DNSProviderPrivate implements the challenge.Provider interface for Azure Private Zone DNS.\ntype DNSProviderPrivate struct {\n\tconfig                *Config\n\tcredentials           azcore.TokenCredential\n\tserviceDiscoveryZones map[string]ServiceDiscoveryZone\n}\n\n// NewDNSProviderPrivate creates a DNSProviderPrivate structure.\nfunc NewDNSProviderPrivate(config *Config, credentials azcore.TokenCredential) (*DNSProviderPrivate, error) {\n\tzones, err := discoverDNSZones(context.Background(), config, credentials)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"discover DNS zones: %w\", err)\n\t}\n\n\treturn &DNSProviderPrivate{\n\t\tconfig:                config,\n\t\tcredentials:           credentials,\n\t\tserviceDiscoveryZones: zones,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProviderPrivate) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProviderPrivate) Present(domain, _, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getHostedZone(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\tclient, err := newPrivateZoneClient(zone, d.credentials, d.config.Environment)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\t// Get existing record set\n\tresp, err := client.Get(ctx, subDomain)\n\tif err != nil {\n\t\tvar respErr *azcore.ResponseError\n\t\tif !errors.As(err, &respErr) || respErr.StatusCode != http.StatusNotFound {\n\t\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t\t}\n\t}\n\n\t// Construct unique TXT records using map\n\tuniqRecords := privateUniqueRecords(resp.RecordSet, info.Value)\n\n\tvar txtRecords []*armprivatedns.TxtRecord\n\tfor txt := range uniqRecords {\n\t\ttxtRecords = append(txtRecords, &armprivatedns.TxtRecord{Value: to.SliceOfPtrs(txt)})\n\t}\n\n\trec := armprivatedns.RecordSet{\n\t\tName: &subDomain,\n\t\tProperties: &armprivatedns.RecordSetProperties{\n\t\t\tTTL:        to.Ptr(int64(d.config.TTL)),\n\t\t\tTxtRecords: txtRecords,\n\t\t},\n\t}\n\n\t_, err = client.CreateOrUpdate(ctx, subDomain, rec)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProviderPrivate) CleanUp(domain, _, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getHostedZone(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\tclient, err := newPrivateZoneClient(zone, d.credentials, d.config.Environment)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\t_, err = client.Delete(ctx, subDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Checks that azure has a zone for this domain name.\nfunc (d *DNSProviderPrivate) getHostedZone(fqdn string) (ServiceDiscoveryZone, error) {\n\tauthZone, err := getZoneName(d.config, fqdn)\n\tif err != nil {\n\t\treturn ServiceDiscoveryZone{}, err\n\t}\n\n\tazureZone, exists := d.serviceDiscoveryZones[dns01.UnFqdn(authZone)]\n\tif !exists {\n\t\treturn ServiceDiscoveryZone{}, fmt.Errorf(\"could not find zone (from discovery): %s\", authZone)\n\t}\n\n\treturn azureZone, nil\n}\n\n// privateZoneClient provides Azure client for one DNS zone.\ntype privateZoneClient struct {\n\tzone         ServiceDiscoveryZone\n\trecordClient *armprivatedns.RecordSetsClient\n}\n\n// newPrivateZoneClient creates privateZoneClient structure with initialized Azure client.\nfunc newPrivateZoneClient(zone ServiceDiscoveryZone, credential azcore.TokenCredential, environment cloud.Configuration) (*privateZoneClient, error) {\n\toptions := &arm.ClientOptions{\n\t\tClientOptions: azcore.ClientOptions{\n\t\t\tCloud: environment,\n\t\t},\n\t}\n\n\trecordClient, err := armprivatedns.NewRecordSetsClient(zone.SubscriptionID, credential, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &privateZoneClient{\n\t\tzone:         zone,\n\t\trecordClient: recordClient,\n\t}, nil\n}\n\nfunc (c privateZoneClient) Get(ctx context.Context, subDomain string) (armprivatedns.RecordSetsClientGetResponse, error) {\n\treturn c.recordClient.Get(ctx, c.zone.ResourceGroup, c.zone.Name, armprivatedns.RecordTypeTXT, subDomain, nil)\n}\n\nfunc (c privateZoneClient) CreateOrUpdate(ctx context.Context, subDomain string, rec armprivatedns.RecordSet) (armprivatedns.RecordSetsClientCreateOrUpdateResponse, error) {\n\treturn c.recordClient.CreateOrUpdate(ctx, c.zone.ResourceGroup, c.zone.Name, armprivatedns.RecordTypeTXT, subDomain, rec, nil)\n}\n\nfunc (c privateZoneClient) Delete(ctx context.Context, subDomain string) (armprivatedns.RecordSetsClientDeleteResponse, error) {\n\treturn c.recordClient.Delete(ctx, c.zone.ResourceGroup, c.zone.Name, armprivatedns.RecordTypeTXT, subDomain, nil)\n}\n\nfunc privateUniqueRecords(recordSet armprivatedns.RecordSet, value string) map[string]struct{} {\n\tuniqRecords := map[string]struct{}{value: {}}\n\n\tif recordSet.Properties != nil && recordSet.Properties.TxtRecords != nil {\n\t\tfor _, txtRecord := range recordSet.Properties.TxtRecords {\n\t\t\t// Assume Value doesn't contain multiple strings\n\t\t\tif len(txtRecord.Value) > 0 {\n\t\t\t\tuniqRecords[ptr.Deref(txtRecord.Value[0])] = struct{}{}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn uniqRecords\n}\n"
  },
  {
    "path": "providers/dns/azuredns/public.go",
    "content": "package azuredns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/to\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProviderPublic)(nil)\n\n// DNSProviderPublic implements the challenge.Provider interface for Azure Public Zone DNS.\ntype DNSProviderPublic struct {\n\tconfig                *Config\n\tcredentials           azcore.TokenCredential\n\tserviceDiscoveryZones map[string]ServiceDiscoveryZone\n}\n\n// NewDNSProviderPublic creates a DNSProviderPublic structure.\nfunc NewDNSProviderPublic(config *Config, credentials azcore.TokenCredential) (*DNSProviderPublic, error) {\n\tzones, err := discoverDNSZones(context.Background(), config, credentials)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"discover DNS zones: %w\", err)\n\t}\n\n\treturn &DNSProviderPublic{\n\t\tconfig:                config,\n\t\tcredentials:           credentials,\n\t\tserviceDiscoveryZones: zones,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProviderPublic) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProviderPublic) Present(domain, _, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getHostedZone(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\tclient, err := newPublicZoneClient(zone, d.credentials, d.config.Environment)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\t// Get existing record set\n\tresp, err := client.Get(ctx, subDomain)\n\tif err != nil {\n\t\tvar respErr *azcore.ResponseError\n\t\tif !errors.As(err, &respErr) || respErr.StatusCode != http.StatusNotFound {\n\t\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t\t}\n\t}\n\n\tuniqRecords := publicUniqueRecords(resp.RecordSet, info.Value)\n\n\tvar txtRecords []*armdns.TxtRecord\n\tfor txt := range uniqRecords {\n\t\ttxtRecords = append(txtRecords, &armdns.TxtRecord{Value: to.SliceOfPtrs(txt)})\n\t}\n\n\trec := armdns.RecordSet{\n\t\tName: &subDomain,\n\t\tProperties: &armdns.RecordSetProperties{\n\t\t\tTTL:        to.Ptr(int64(d.config.TTL)),\n\t\t\tTxtRecords: txtRecords,\n\t\t},\n\t}\n\n\t_, err = client.CreateOrUpdate(ctx, subDomain, rec)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProviderPublic) CleanUp(domain, _, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getHostedZone(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\tclient, err := newPublicZoneClient(zone, d.credentials, d.config.Environment)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\t_, err = client.Delete(ctx, subDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"azuredns: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Checks that azure has a zone for this domain name.\nfunc (d *DNSProviderPublic) getHostedZone(fqdn string) (ServiceDiscoveryZone, error) {\n\tauthZone, err := getZoneName(d.config, fqdn)\n\tif err != nil {\n\t\treturn ServiceDiscoveryZone{}, err\n\t}\n\n\tazureZone, exists := d.serviceDiscoveryZones[dns01.UnFqdn(authZone)]\n\tif !exists {\n\t\treturn ServiceDiscoveryZone{}, fmt.Errorf(\"could not find zone (from discovery): %s\", authZone)\n\t}\n\n\treturn azureZone, nil\n}\n\ntype publicZoneClient struct {\n\tzone         ServiceDiscoveryZone\n\trecordClient *armdns.RecordSetsClient\n}\n\n// newPublicZoneClient creates publicZoneClient structure with initialized Azure client.\nfunc newPublicZoneClient(zone ServiceDiscoveryZone, credential azcore.TokenCredential, environment cloud.Configuration) (*publicZoneClient, error) {\n\toptions := &arm.ClientOptions{\n\t\tClientOptions: azcore.ClientOptions{\n\t\t\tCloud: environment,\n\t\t},\n\t}\n\n\trecordClient, err := armdns.NewRecordSetsClient(zone.SubscriptionID, credential, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &publicZoneClient{\n\t\tzone:         zone,\n\t\trecordClient: recordClient,\n\t}, nil\n}\n\nfunc (c publicZoneClient) Get(ctx context.Context, subDomain string) (armdns.RecordSetsClientGetResponse, error) {\n\treturn c.recordClient.Get(ctx, c.zone.ResourceGroup, c.zone.Name, subDomain, armdns.RecordTypeTXT, nil)\n}\n\nfunc (c publicZoneClient) CreateOrUpdate(ctx context.Context, subDomain string, rec armdns.RecordSet) (armdns.RecordSetsClientCreateOrUpdateResponse, error) {\n\treturn c.recordClient.CreateOrUpdate(ctx, c.zone.ResourceGroup, c.zone.Name, subDomain, armdns.RecordTypeTXT, rec, nil)\n}\n\nfunc (c publicZoneClient) Delete(ctx context.Context, subDomain string) (armdns.RecordSetsClientDeleteResponse, error) {\n\treturn c.recordClient.Delete(ctx, c.zone.ResourceGroup, c.zone.Name, subDomain, armdns.RecordTypeTXT, nil)\n}\n\nfunc publicUniqueRecords(recordSet armdns.RecordSet, value string) map[string]struct{} {\n\tuniqRecords := map[string]struct{}{value: {}}\n\n\tif recordSet.Properties != nil && recordSet.Properties.TxtRecords != nil {\n\t\tfor _, txtRecord := range recordSet.Properties.TxtRecords {\n\t\t\t// Assume Value doesn't contain multiple strings\n\t\t\tif len(txtRecord.Value) > 0 {\n\t\t\t\tuniqRecords[ptr.Deref(txtRecord.Value[0])] = struct{}{}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn uniqRecords\n}\n"
  },
  {
    "path": "providers/dns/azuredns/servicediscovery.go",
    "content": "package azuredns\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/to\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n)\n\ntype ServiceDiscoveryZone struct {\n\tName           string\n\tSubscriptionID string\n\tResourceGroup  string\n}\n\nconst (\n\tResourceGraphTypePublicDNSZone  = \"microsoft.network/dnszones\"\n\tResourceGraphTypePrivateDNSZone = \"microsoft.network/privatednszones\"\n)\n\nconst ResourceGraphQueryOptionsTop int32 = 1000\n\n// discoverDNSZones finds all visible Azure DNS zones based on optional subscriptionID, resourceGroup and serviceDiscovery filter using Kusto query.\nfunc discoverDNSZones(ctx context.Context, config *Config, credentials azcore.TokenCredential) (map[string]ServiceDiscoveryZone, error) {\n\toptions := &arm.ClientOptions{\n\t\tClientOptions: azcore.ClientOptions{\n\t\t\tCloud: config.Environment,\n\t\t},\n\t}\n\n\tclient, err := armresourcegraph.NewClient(credentials, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set options\n\trequestOptions := &armresourcegraph.QueryRequestOptions{\n\t\tResultFormat: to.Ptr(armresourcegraph.ResultFormatObjectArray),\n\t\tTop:          to.Ptr(ResourceGraphQueryOptionsTop),\n\t\tSkip:         to.Ptr[int32](0),\n\t}\n\n\tzones := map[string]ServiceDiscoveryZone{}\n\n\tfor {\n\t\t// create the query request\n\t\trequest := armresourcegraph.QueryRequest{\n\t\t\tQuery:   to.Ptr(createGraphQuery(config)),\n\t\t\tOptions: requestOptions,\n\t\t}\n\n\t\tresult, err := client.Resources(ctx, request, nil)\n\t\tif err != nil {\n\t\t\treturn zones, err\n\t\t}\n\n\t\tresultList, ok := result.Data.([]any)\n\t\tif !ok {\n\t\t\t// got invalid or empty data, skipping\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, row := range resultList {\n\t\t\trowData, ok := row.(map[string]any)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tzoneName, ok := rowData[\"name\"].(string)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif _, exists := zones[zoneName]; exists {\n\t\t\t\treturn zones, fmt.Errorf(`found duplicate dns zone \"%s\"`, zoneName)\n\t\t\t}\n\n\t\t\tzones[zoneName] = ServiceDiscoveryZone{\n\t\t\t\tName:           zoneName,\n\t\t\t\tResourceGroup:  rowData[\"resourceGroup\"].(string),\n\t\t\t\tSubscriptionID: rowData[\"subscriptionId\"].(string),\n\t\t\t}\n\t\t}\n\n\t\t*requestOptions.Skip += ResourceGraphQueryOptionsTop\n\n\t\tif result.TotalRecords != nil {\n\t\t\tif int64(ptr.Deref(requestOptions.Skip)) >= ptr.Deref(result.TotalRecords) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn zones, nil\n}\n\nfunc createGraphQuery(config *Config) string {\n\tbuf := new(bytes.Buffer)\n\tbuf.WriteString(\"\\nresources\\n\")\n\n\tresourceType := ResourceGraphTypePublicDNSZone\n\tif config.PrivateZone {\n\t\tresourceType = ResourceGraphTypePrivateDNSZone\n\t}\n\n\t_, _ = fmt.Fprintf(buf, \"| where type =~ %q\\n\", resourceType)\n\n\tif config.SubscriptionID != \"\" {\n\t\t_, _ = fmt.Fprintf(buf, \"| where subscriptionId =~ %q\\n\", config.SubscriptionID)\n\t}\n\n\tif config.ResourceGroup != \"\" {\n\t\t_, _ = fmt.Fprintf(buf, \"| where resourceGroup =~ %q\\n\", config.ResourceGroup)\n\t}\n\n\tif config.ServiceDiscoveryFilter != \"\" {\n\t\t_, _ = fmt.Fprintf(buf, \"| %s\\n\", config.ServiceDiscoveryFilter)\n\t}\n\n\tbuf.WriteString(\"| project subscriptionId, resourceGroup, name\")\n\n\treturn buf.String()\n}\n"
  },
  {
    "path": "providers/dns/azuredns/servicediscovery_test.go",
    "content": "package azuredns\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_createGraphQuery(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tcfg      *Config\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"empty configuration (public)\",\n\t\t\tcfg:  &Config{},\n\t\t\texpected: `\nresources\n| where type =~ \"microsoft.network/dnszones\"\n| project subscriptionId, resourceGroup, name`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"SubscriptionID (public)\",\n\t\t\tcfg: &Config{\n\t\t\t\tSubscriptionID: \"123\",\n\t\t\t},\n\t\t\texpected: `\nresources\n| where type =~ \"microsoft.network/dnszones\"\n| where subscriptionId =~ \"123\"\n| project subscriptionId, resourceGroup, name`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"ResourceGroup (public)\",\n\t\t\tcfg: &Config{\n\t\t\t\tResourceGroup: \"123\",\n\t\t\t},\n\t\t\texpected: `\nresources\n| where type =~ \"microsoft.network/dnszones\"\n| where resourceGroup =~ \"123\"\n| project subscriptionId, resourceGroup, name`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"ServiceDiscoveryFilter (public)\",\n\t\t\tcfg: &Config{\n\t\t\t\tServiceDiscoveryFilter: \"123\",\n\t\t\t},\n\t\t\texpected: `\nresources\n| where type =~ \"microsoft.network/dnszones\"\n| 123\n| project subscriptionId, resourceGroup, name`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"empty configuration (private)\",\n\t\t\tcfg: &Config{\n\t\t\t\tPrivateZone: true,\n\t\t\t},\n\t\t\texpected: `\nresources\n| where type =~ \"microsoft.network/privatednszones\"\n| project subscriptionId, resourceGroup, name`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"SubscriptionID (private)\",\n\t\t\tcfg: &Config{\n\t\t\t\tSubscriptionID: \"123\",\n\t\t\t\tPrivateZone:    true,\n\t\t\t},\n\t\t\texpected: `\nresources\n| where type =~ \"microsoft.network/privatednszones\"\n| where subscriptionId =~ \"123\"\n| project subscriptionId, resourceGroup, name`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"ResourceGroup (private)\",\n\t\t\tcfg: &Config{\n\t\t\t\tResourceGroup: \"123\",\n\t\t\t\tPrivateZone:   true,\n\t\t\t},\n\t\t\texpected: `\nresources\n| where type =~ \"microsoft.network/privatednszones\"\n| where resourceGroup =~ \"123\"\n| project subscriptionId, resourceGroup, name`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"ServiceDiscoveryFilter (private)\",\n\t\t\tcfg: &Config{\n\t\t\t\tServiceDiscoveryFilter: \"123\",\n\t\t\t\tPrivateZone:            true,\n\t\t\t},\n\t\t\texpected: `\nresources\n| where type =~ \"microsoft.network/privatednszones\"\n| 123\n| project subscriptionId, resourceGroup, name`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"all (private)\",\n\t\t\tcfg: &Config{\n\t\t\t\tSubscriptionID:         \"123\",\n\t\t\t\tResourceGroup:          \"456\",\n\t\t\t\tServiceDiscoveryFilter: \"789\",\n\t\t\t\tPrivateZone:            true,\n\t\t\t},\n\t\t\texpected: `\nresources\n| where type =~ \"microsoft.network/privatednszones\"\n| where subscriptionId =~ \"123\"\n| where resourceGroup =~ \"456\"\n| 789\n| project subscriptionId, resourceGroup, name`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tquery := createGraphQuery(test.cfg)\n\t\t\tassert.Equal(t, strings.ReplaceAll(test.expected, \"\\r\", \"\"), query)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/baiducloud/baiducloud.go",
    "content": "// Package baiducloud implements a DNS provider for solving the DNS-01 challenge using Baidu Cloud.\npackage baiducloud\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\tbaidudns \"github.com/baidubce/bce-sdk-go/services/dns\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"BAIDUCLOUD_\"\n\n\tEnvAccessKeyID     = envNamespace + \"ACCESS_KEY_ID\"\n\tEnvSecretAccessKey = envNamespace + \"SECRET_ACCESS_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\n// 300 is the minimum TTL for free users.\nconst defaultTTL = 300\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAccessKeyID     string\n\tSecretAccessKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, defaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *baidudns.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Baidu Cloud.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"baiducloud: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AccessKeyID = values[EnvAccessKeyID]\n\tconfig.SecretAccessKey = values[EnvSecretAccessKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Baidu Cloud.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"baiducloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.AccessKeyID == \"\" && config.SecretAccessKey == \"\" {\n\t\treturn nil, errors.New(\"baiducloud: credentials missing\")\n\t}\n\n\tclient, err := baidudns.NewClient(config.AccessKeyID, config.SecretAccessKey, \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"baiducloud: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"baiducloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"baiducloud: %w\", err)\n\t}\n\n\tcrr := &baidudns.CreateRecordRequest{\n\t\tDescription: ptr.Pointer(\"lego\"),\n\t\tRr:          subDomain,\n\t\tType:        \"TXT\",\n\t\tValue:       info.Value,\n\t\tTtl:         ptr.Pointer(int32(d.config.TTL)),\n\t}\n\n\terr = d.client.CreateRecord(dns01.UnFqdn(authZone), crr, \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"baiducloud: create record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"baiducloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecordID, err := d.findRecordID(dns01.UnFqdn(authZone), info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"baiducloud: find record: %w\", err)\n\t}\n\n\terr = d.client.DeleteRecord(dns01.UnFqdn(authZone), recordID, \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"baiducloud: delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) findRecordID(zoneName, tokenValue string) (string, error) {\n\tlrr := &baidudns.ListRecordRequest{}\n\n\tfor {\n\t\trecordResponse, err := d.client.ListRecord(zoneName, lrr)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"baiducloud: list record: %w\", err)\n\t\t}\n\n\t\tfor _, record := range recordResponse.Records {\n\t\t\tif record.Type == \"TXT\" && record.Value == tokenValue {\n\t\t\t\treturn record.Id, nil\n\t\t\t}\n\t\t}\n\n\t\tif !recordResponse.IsTruncated {\n\t\t\tbreak\n\t\t}\n\n\t\tlrr.Marker = recordResponse.NextMarker\n\t}\n\n\treturn \"\", errors.New(\"record not found\")\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/baiducloud/baiducloud.toml",
    "content": "Name = \"Baidu Cloud\"\nDescription = ''''''\nURL = \"https://cloud.baidu.com\"\nCode = \"baiducloud\"\nSince = \"v4.23.0\"\n\nExample = '''\nBAIDUCLOUD_ACCESS_KEY_ID=\"xxx\" \\\nBAIDUCLOUD_SECRET_ACCESS_KEY=\"yyy\" \\\nlego --dns baiducloud -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    BAIDUCLOUD_ACCESS_KEY_ID = \"Access key\"\n    BAIDUCLOUD_SECRET_ACCESS_KEY = \"Secret access key\"\n  [Configuration.Additional]\n    BAIDUCLOUD_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    BAIDUCLOUD_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    BAIDUCLOUD_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n\n[Links]\n  API = \"https://cloud.baidu.com/doc/DNS/s/El4s7lssr\"\n  GoClient = \"https://github.com/baidubce/bce-sdk-go\"\n"
  },
  {
    "path": "providers/dns/baiducloud/baiducloud_test.go",
    "content": "package baiducloud\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAccessKeyID, EnvSecretAccessKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKeyID:     \"key\",\n\t\t\t\tEnvSecretAccessKey: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access key ID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKeyID: \"key\",\n\t\t\t},\n\t\t\texpected: \"baiducloud: some credentials information are missing: BAIDUCLOUD_SECRET_ACCESS_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret access key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretAccessKey: \"secret\",\n\t\t\t},\n\t\t\texpected: \"baiducloud: some credentials information are missing: BAIDUCLOUD_ACCESS_KEY_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"baiducloud: some credentials information are missing: BAIDUCLOUD_ACCESS_KEY_ID,BAIDUCLOUD_SECRET_ACCESS_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc            string\n\t\taccessKeyID     string\n\t\tsecretAccessKey string\n\t\texpected        string\n\t}{\n\t\t{\n\t\t\tdesc:            \"success\",\n\t\t\taccessKeyID:     \"key\",\n\t\t\tsecretAccessKey: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:            \"missing access key ID\",\n\t\t\taccessKeyID:     \"\",\n\t\t\tsecretAccessKey: \"secret\",\n\t\t\texpected:        \"baiducloud: accessKeyId should not be empty\",\n\t\t},\n\t\t{\n\t\t\tdesc:            \"missing secret access key\",\n\t\t\taccessKeyID:     \"key\",\n\t\t\tsecretAccessKey: \"\",\n\t\t\texpected:        \"baiducloud: secretKey should not be empty\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"baiducloud: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AccessKeyID = test.accessKeyID\n\t\t\tconfig.SecretAccessKey = test.secretAccessKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/beget/beget.go",
    "content": "// Package beget implements a DNS provider for solving the DNS-01 challenge using beget.com DNS.\npackage beget\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/beget/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"BEGET_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername string\n\tPassword string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 30*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for beget.com.\n// Credentials must be passed in the environment variables:\n// BEGET_USERNAME and BEGET_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"beget: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for beget.com.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"beget: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Username == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"beget: incomplete credentials, missing username and/or password\")\n\t}\n\n\tclient := internal.NewClient(config.Username, config.Password)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecords, err := d.client.GetTXTRecords(ctx, dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"beget: get TXT records: %w\", err)\n\t}\n\n\trecords = append(records, internal.Record{\n\t\tValue:    info.Value,\n\t\tData:     \"\", // NOTE: there are 2 fields in the API for the same thing.\n\t\tPriority: 10,\n\t\tTTL:      d.config.TTL,\n\t})\n\n\terr = d.client.ChangeTXTRecord(ctx, dns01.UnFqdn(info.EffectiveFQDN), records)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"beget: failed to create TXT records [domain: %s]: %w\",\n\t\t\tdns01.UnFqdn(info.EffectiveFQDN), err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecords, err := d.client.GetTXTRecords(ctx, dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"beget: get TXT records: %w\", err)\n\t}\n\n\tif len(records) == 0 {\n\t\treturn nil\n\t}\n\n\tvar updatedRecords []internal.Record\n\n\tfor _, record := range records {\n\t\tif record.Data == info.Value {\n\t\t\tcontinue\n\t\t}\n\n\t\tupdatedRecords = append(updatedRecords, record)\n\t}\n\n\terr = d.client.ChangeTXTRecord(ctx, dns01.UnFqdn(info.EffectiveFQDN), updatedRecords)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"beget: failed to remove TXT records [domain: %s]: %w\",\n\t\t\tdns01.UnFqdn(info.EffectiveFQDN), err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/beget/beget.toml",
    "content": "Name = \"Beget.com\"\nDescription = ''''''\nURL = \"https://beget.com/\"\nCode = \"beget\"\nSince = \"v4.27.0\"\n\nExample = '''\nBEGET_USERNAME=xxxxxx \\\nBEGET_PASSWORD=yyyyyy \\\nlego --dns beget -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    BEGET_USERNAME = \"API username\"\n    BEGET_PASSWORD = \"API password\"\n  [Configuration.Additional]\n    BEGET_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 30)\"\n    BEGET_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 300)\"\n    BEGET_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    BEGET_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://beget.com/ru/kb/api/funkczii-upravleniya-dns\"\n"
  },
  {
    "path": "providers/dns/beget/beget_test.go",
    "content": "package beget\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvPassword: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"beget: some credentials information are missing: BEGET_USERNAME,BEGET_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"456\",\n\t\t\t},\n\t\t\texpected: \"beget: some credentials information are missing: BEGET_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"beget: some credentials information are missing: BEGET_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"123\",\n\t\t\tpassword: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tusername: \"\",\n\t\t\tpassword: \"\",\n\t\t\texpected: \"beget: incomplete credentials, missing username and/or password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tusername: \"\",\n\t\t\tpassword: \"456\",\n\t\t\texpected: \"beget: incomplete credentials, missing username and/or password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tusername: \"123\",\n\t\t\tpassword: \"\",\n\t\t\texpected: \"beget: incomplete credentials, missing username and/or password\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\tassert.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\tassert.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\tassert.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\tassert.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = \"user\"\n\t\t\tconfig.Password = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckQueryParameter().\n\t\t\tWith(\"login\", \"user\").\n\t\t\tWith(\"passwd\", \"secret\").\n\t\t\tWith(\"input_format\", \"json\").\n\t\t\tWith(\"output_format\", \"json\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /dns/getData\",\n\t\t\tservermock.ResponseFromInternal(\"getData-real.json\"),\n\t\t\tservermock.CheckQueryParameter().\n\t\t\t\tWith(\"input_data\", `{\"fqdn\":\"_acme-challenge.example.com\"}`),\n\t\t).\n\t\tRoute(\"GET /dns/changeRecords\",\n\t\t\tservermock.ResponseFromInternal(\"changeRecords-doc.json\"),\n\t\t\tservermock.CheckQueryParameter().\n\t\t\t\tWith(\"input_data\", `{\"fqdn\":\"_acme-challenge.example.com\",\"records\":{\"TXT\":[{\"txtdata\":\"v=spf1 redirect=beget.com\",\"ttl\":300},{\"value\":\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\"priority\":10,\"ttl\":300}]}}`),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /dns/getData\",\n\t\t\tservermock.ResponseFromInternal(\"getData.json\"),\n\t\t\tservermock.CheckQueryParameter().\n\t\t\t\tWith(\"input_data\", `{\"fqdn\":\"_acme-challenge.example.com\"}`),\n\t\t).\n\t\tRoute(\"GET /dns/changeRecords\",\n\t\t\tservermock.ResponseFromInternal(\"changeRecords-doc.json\"),\n\t\t\tservermock.CheckQueryParameter().\n\t\t\t\tWith(\"input_data\", `{\"fqdn\":\"_acme-challenge.example.com\",\"records\":{\"TXT\":[{\"txtdata\":\"foo\",\"ttl\":300}]}}`),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp_empty(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /dns/getData\",\n\t\t\tservermock.ResponseFromInternal(\"getData_empty.json\"),\n\t\t\tservermock.CheckQueryParameter().\n\t\t\t\tWith(\"input_data\", `{\"fqdn\":\"_acme-challenge.example.com\"}`),\n\t\t).\n\t\tRoute(\"/\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusInternalServerError)).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/beget/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api.beget.com/api/\"\n\n// Client the beget.com client.\ntype Client struct {\n\tlogin    string\n\tpassword string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a beget.com client.\nfunc NewClient(login, password string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tlogin:      login,\n\t\tpassword:   password,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// GetTXTRecords returns TXT records.\n// https://beget.com/ru/kb/api/funkczii-upravleniya-dns#getdata\nfunc (c *Client) GetTXTRecords(ctx context.Context, domain string) ([]Record, error) {\n\trequest := GetRecordsRequest{Fqdn: domain}\n\n\tresp, err := c.doRequest(ctx, request, \"dns\", \"getData\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = resp.HasError()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := GetRecordsResult{}\n\n\terr = json.Unmarshal(resp.Answer.Result, &result)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unmarshal result: %s: %w\", string(resp.Answer.Result), err)\n\t}\n\n\treturn result.Records.TXT, nil\n}\n\n// ChangeTXTRecord changes TXT records.\n// https://beget.com/ru/kb/api/funkczii-upravleniya-dns#changerecords\nfunc (c *Client) ChangeTXTRecord(ctx context.Context, domain string, records []Record) error {\n\trequest := ChangeRecordsRequest{\n\t\tFqdn:    domain,\n\t\tRecords: RecordList{TXT: records},\n\t}\n\n\tresp, err := c.doRequest(ctx, request, \"dns\", \"changeRecords\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn resp.HasError()\n}\n\nfunc (c *Client) doRequest(ctx context.Context, data any, fragments ...string) (*APIResponse, error) {\n\tendpoint := c.BaseURL.JoinPath(fragments...)\n\n\tinputData, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to mashall input data: %w\", err)\n\t}\n\n\tquery := endpoint.Query()\n\tquery.Add(\"input_data\", string(inputData))\n\tquery.Add(\"login\", c.login)\n\tquery.Add(\"passwd\", c.password)\n\tquery.Add(\"input_format\", \"json\")\n\tquery.Add(\"output_format\", \"json\")\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn nil, parseError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar apiResp APIResponse\n\n\terr = json.Unmarshal(raw, &apiResp)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn &apiResp, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar apiResp APIResponse\n\n\terr := json.Unmarshal(raw, &apiResp)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code %d] %w\", resp.StatusCode, apiResp)\n}\n"
  },
  {
    "path": "providers/dns/beget/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\")\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckQueryParameter().\n\t\t\tWith(\"login\", \"user\").\n\t\t\tWith(\"passwd\", \"secret\").\n\t\t\tWith(\"input_format\", \"json\").\n\t\t\tWith(\"output_format\", \"json\"),\n\t)\n}\n\nfunc TestClient_GetTXTRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/getData\",\n\t\t\tservermock.ResponseFromFixture(\"getData-real.json\"),\n\t\t\tservermock.CheckQueryParameter().\n\t\t\t\tWith(\"input_data\", `{\"fqdn\":\"example.com\"}`),\n\t\t).\n\t\tBuild(t)\n\n\tdata, err := client.GetTXTRecords(context.Background(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{{Data: \"v=spf1 redirect=beget.com\", TTL: 300}}\n\n\tassert.Equal(t, expected, data)\n}\n\nfunc TestClient_ChangeTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/changeRecords\",\n\t\t\tservermock.ResponseFromFixture(\"changeRecords-doc.json\"),\n\t\t\tservermock.CheckQueryParameter().\n\t\t\t\tWith(\"input_data\", `{\"fqdn\":\"sub.example.com\",\"records\":{\"TXT\":[{\"value\":\"txtTXTtxt\",\"priority\":10,\"ttl\":300}]}}`),\n\t\t).\n\t\tBuild(t)\n\n\trecords := []Record{{Value: \"txtTXTtxt\", TTL: 300, Priority: 10}}\n\n\terr := client.ChangeTXTRecord(context.Background(), \"sub.example.com\", records)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_ChangeTXTRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/changeRecords\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\trecords := []Record{{Data: \"txtTXTtxt\", TTL: 300}}\n\n\terr := client.ChangeTXTRecord(context.Background(), \"sub.example.com\", records)\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"API error: NO_SUCH_METHOD: No such method\")\n}\n\nfunc TestClient_ChangeTXTRecord_answer_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/changeRecords\",\n\t\t\tservermock.ResponseFromFixture(\"answer_error.json\")).\n\t\tBuild(t)\n\n\trecords := []Record{{Data: \"txtTXTtxt\", TTL: 300}}\n\n\terr := client.ChangeTXTRecord(context.Background(), \"sub.example.com\", records)\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"API answer error: INVALID_DATA: Login length cannot be greater than 12 characters\")\n}\n\nfunc TestClient_ChangeTXTRecord_remove(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/changeRecords\",\n\t\t\tservermock.ResponseFromFixture(\"changeRecords-doc.json\"),\n\t\t\tservermock.CheckQueryParameter().\n\t\t\t\tWith(\"input_data\", `{\"fqdn\":\"sub.example.com\",\"records\":{}}`),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.ChangeTXTRecord(context.Background(), \"sub.example.com\", nil)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/beget/internal/fixtures/answer_error.json",
    "content": "{\n  \"status\": \"success\",\n  \"answer\": {\n    \"status\": \"error\",\n    \"errors\": [\n      {\n        \"error_code\": \"INVALID_DATA\",\n        \"error_text\": \"Login length cannot be greater than 12 characters\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/beget/internal/fixtures/changeRecords-doc.json",
    "content": "{\n  \"status\": \"success\",\n  \"answer\": {\n    \"status\": \"success\",\n    \"result\": {\n      \"A\": [\n        {\n          \"priority\": 10,\n          \"value\": \"127.0.0.1\"\n        }\n      ],\n      \"MX\": [\n        {\n          \"priority\": 10,\n          \"value\": \"mx1.beget.ru\"\n        },\n        {\n          \"priority\": 20,\n          \"value\": \"mx2.beget.ru\"\n        }\n      ],\n      \"TXT\": [\n        {\n          \"priority\": 10,\n          \"value\": \"TXT record\"\n        }\n      ]\n    }\n  }\n}\n\n"
  },
  {
    "path": "providers/dns/beget/internal/fixtures/error.json",
    "content": "{\n  \"status\": \"error\",\n  \"error_text\": \"No such method\",\n  \"error_code\": \"NO_SUCH_METHOD\"\n}\n"
  },
  {
    "path": "providers/dns/beget/internal/fixtures/getData-doc.json",
    "content": "{\n  \"status\": \"success\",\n  \"answer\": {\n    \"status\": \"success\",\n    \"result\": {\n      \"is_under_control\": 1,\n      \"is_beget_dns\": 1,\n      \"is_subdomain\": 0,\n      \"fqdn\": \"beget.ru\",\n      \"records\": {\n        \"DNS\": [\n          {\n            \"value\": \"ns1.beget.ru\",\n            \"priority\": 10\n          },\n          {\n            \"value\": \"ns2.beget.ru\",\n            \"priority\": 20\n          }\n        ],\n        \"DNS_IP\": [\n          {\n            \"value\": null,\n            \"priority\": 10\n          },\n          {\n            \"value\": null,\n            \"priority\": 20\n          }\n        ],\n        \"A\": [\n          {\n            \"value\": \"91.106.201.65\",\n            \"priority\": \"0\"\n          }\n        ],\n        \"MX\": [\n          {\n            \"value\": \"mx1.beget.ru\",\n            \"priority\": \"10\"\n          },\n          {\n            \"value\": \"mx2.beget.ru\",\n            \"priority\": \"20\"\n          }\n        ],\n        \"TXT\": [\n          {\n            \"value\": \"\",\n            \"priority\": 0\n          }\n        ]\n      },\n      \"set_type\": 1\n    }\n  }\n}\n\n"
  },
  {
    "path": "providers/dns/beget/internal/fixtures/getData-real.json",
    "content": "{\n  \"status\": \"success\",\n  \"answer\": {\n    \"status\": \"success\",\n    \"result\": {\n      \"is_under_control\": true,\n      \"is_beget_dns\": true,\n      \"is_subdomain\": false,\n      \"fqdn\": \"example.com\",\n      \"records\": {\n        \"MX\": [\n          {\n            \"ttl\": 300,\n            \"exchange\": \"mx2.beget.com.\",\n            \"preference\": 20\n          },\n          {\n            \"ttl\": 300,\n            \"exchange\": \"mx1.beget.com.\",\n            \"preference\": 10\n          }\n        ],\n        \"TXT\": [\n          {\n            \"ttl\": 300,\n            \"txtdata\": \"v=spf1 redirect=beget.com\"\n          }\n        ],\n        \"A\": [\n          {\n            \"ttl\": 300,\n            \"address\": \"1.2.3.4\"\n          }\n        ],\n        \"DNS\": [\n          {\n            \"value\": \"ns1.beget.pro\"\n          },\n          {\n            \"value\": \"ns2.beget.pro\"\n          },\n          {\n            \"value\": \"ns1.beget.com\"\n          },\n          {\n            \"value\": \"ns2.beget.com\"\n          }\n        ],\n        \"DNS_IP\": [\n          {\n            \"value\": \"\"\n          },\n          {\n            \"value\": \"\"\n          },\n          {\n            \"value\": \"\"\n          },\n          {\n            \"value\": \"\"\n          }\n        ]\n      },\n      \"set_type\": 1\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/beget/internal/fixtures/getData.json",
    "content": "{\n  \"status\": \"success\",\n  \"answer\": {\n    \"status\": \"success\",\n    \"result\": {\n      \"is_under_control\": true,\n      \"is_beget_dns\": true,\n      \"is_subdomain\": false,\n      \"fqdn\": \"_acme-challenge.example.com\",\n      \"records\": {\n        \"MX\": [\n          {\n            \"ttl\": 300,\n            \"exchange\": \"mx2.beget.com.\",\n            \"preference\": 20\n          },\n          {\n            \"ttl\": 300,\n            \"exchange\": \"mx1.beget.com.\",\n            \"preference\": 10\n          }\n        ],\n        \"TXT\": [\n          {\n            \"ttl\": 300,\n            \"txtdata\": \"foo\"\n          }\n        ],\n        \"A\": [\n          {\n            \"ttl\": 300,\n            \"address\": \"1.2.3.4\"\n          }\n        ],\n        \"DNS\": [\n          {\n            \"value\": \"ns1.beget.pro\"\n          },\n          {\n            \"value\": \"ns2.beget.pro\"\n          },\n          {\n            \"value\": \"ns1.beget.com\"\n          },\n          {\n            \"value\": \"ns2.beget.com\"\n          }\n        ],\n        \"DNS_IP\": [\n          {\n            \"value\": \"\"\n          },\n          {\n            \"value\": \"\"\n          },\n          {\n            \"value\": \"\"\n          },\n          {\n            \"value\": \"\"\n          }\n        ]\n      },\n      \"set_type\": 1\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/beget/internal/fixtures/getData_empty.json",
    "content": "{\n  \"status\": \"success\",\n  \"answer\": {\n    \"status\": \"success\",\n    \"result\": {\n      \"is_under_control\": true,\n      \"is_beget_dns\": true,\n      \"is_subdomain\": false,\n      \"fqdn\": \"_acme-challenge.example.com\",\n      \"set_type\": 1\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/beget/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nconst successResult = \"success\"\n\n// APIResponse is the representation of an API response.\ntype APIResponse struct {\n\tStatus string `json:\"status\"`\n\n\tAnswer *Answer `json:\"answer,omitempty\"`\n\n\tErrorCode string `json:\"error_code,omitempty\"`\n\tErrorText string `json:\"error_text,omitempty\"`\n}\n\nfunc (a APIResponse) Error() string {\n\treturn fmt.Sprintf(\"API %s: %s: %s\", a.Status, a.ErrorCode, a.ErrorText)\n}\n\n// HasError returns an error is the response contains an error.\nfunc (a APIResponse) HasError() error {\n\tif a.Status != successResult {\n\t\treturn a\n\t}\n\n\tif a.Answer == nil || a.Status != successResult || a.Answer.Status != successResult {\n\t\treturn a.Answer\n\t}\n\n\treturn nil\n}\n\n// Answer is the representation of an API response answer.\ntype Answer struct {\n\tStatus string          `json:\"status,omitempty\"`\n\tResult json.RawMessage `json:\"result,omitempty\"`\n\n\tErrors    []AnswerError `json:\"errors,omitempty\"`\n\tErrorCode string        `json:\"error_code,omitempty\"`\n\tErrorText string        `json:\"error_text,omitempty\"`\n}\n\ntype AnswerError struct {\n\tErrorCode string `json:\"error_code,omitempty\"`\n\tErrorText string `json:\"error_text,omitempty\"`\n}\n\nfunc (a Answer) Error() string {\n\tparts := []string{fmt.Sprintf(\"API answer %s\", a.Status)}\n\n\tif a.ErrorCode != \"\" {\n\t\tparts = append(parts, a.ErrorCode)\n\t}\n\n\tif a.ErrorText != \"\" {\n\t\tparts = append(parts, a.ErrorText)\n\t}\n\n\tif len(a.Errors) > 0 {\n\t\tfor _, e := range a.Errors {\n\t\t\tparts = append(parts, e.ErrorCode, e.ErrorText)\n\t\t}\n\t}\n\n\treturn strings.Join(parts, \": \")\n}\n\n// GetRecordsRequest data representation for data get request.\ntype GetRecordsRequest struct {\n\tFqdn string `json:\"fqdn,omitempty\"`\n}\n\n// ChangeRecordsRequest data representation for data change request.\ntype ChangeRecordsRequest struct {\n\tFqdn    string     `json:\"fqdn,omitempty\"`\n\tRecords RecordList `json:\"records\"`\n}\n\n// RecordList List of entries (in this case only described TXT).\ntype RecordList struct {\n\tTXT []Record `json:\"TXT,omitempty\"`\n}\n\n// Record data representation for TXT record.\ntype Record struct {\n\tValue    string `json:\"value,omitempty\"`\n\tData     string `json:\"txtdata,omitempty\"`\n\tPriority int    `json:\"priority,omitempty\"`\n\tTTL      int    `json:\"ttl,omitempty\"`\n}\n\ntype GetRecordsResult struct {\n\tFqdn    string     `json:\"fqdn\"`\n\tRecords RecordList `json:\"records\"`\n}\n"
  },
  {
    "path": "providers/dns/binarylane/binarylane.go",
    "content": "// Package binarylane implements a DNS provider for solving the DNS-01 challenge using Binary Lane.\npackage binarylane\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/binarylane/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"BINARYLANE_\"\n\n\tEnvAPIToken = envNamespace + \"API_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 3600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]int64\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Binary Lane.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"binarylane: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIToken = values[EnvAPIToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Binary Lane.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"binarylane: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.APIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"binarylane: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]int64),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"binarylane: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"binarylane: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tType: \"TXT\",\n\t\tName: subDomain,\n\t\tData: info.Value,\n\t\tTTL:  d.config.TTL,\n\t}\n\n\tresponse, err := d.client.CreateRecord(context.Background(), dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"binarylane: create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = response.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"binarylane: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// get the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"binarylane: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\terr = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"binarylane: delete record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/binarylane/binarylane.toml",
    "content": "Name = \"Binary Lane\"\nDescription = ''''''\nURL = \"https://www.binarylane.com.au/\"\nCode = \"binarylane\"\nSince = \"v4.26.0\"\n\nExample = '''\nBINARYLANE_API_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns binarylane -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    BINARYLANE_API_TOKEN = \"API token\"\n  [Configuration.Additional]\n    BINARYLANE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    BINARYLANE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    BINARYLANE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    BINARYLANE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api.binarylane.com.au/reference/#tag/Domains\"\n"
  },
  {
    "path": "providers/dns/binarylane/binarylane_test.go",
    "content": "package binarylane\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"\",\n\t\t\t},\n\t\t\texpected: \"binarylane: some credentials information are missing: BINARYLANE_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiToken string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiToken: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API token\",\n\t\t\texpected: \"binarylane: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = test.apiToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/binarylane/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api.binarylane.com.au/v2/\"\n\nconst authorizationHeader = \"Authorization\"\n\n// Client the Binary Lane API client.\ntype Client struct {\n\tapiToken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiToken string) (*Client, error) {\n\tif apiToken == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiToken:   apiToken,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// CreateRecord Creates a new domain record.\n// https://api.binarylane.com.au/reference/#tag/Domains/paths/~1v2~1domains~1%7Bdomain_name%7D~1records/post\nfunc (c *Client) CreateRecord(ctx context.Context, domain string, record Record) (*Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"domains\", domain, \"records\")\n\n\tif record.Name == \"\" {\n\t\trecord.Name = \"@\"\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result APIResponse\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.DomainRecord, nil\n}\n\n// DeleteRecord Deletes an existing domain record.\n// https://api.binarylane.com.au/reference/#tag/Domains/paths/~1v2~1domains~1%7Bdomain_name%7D~1records~1%7Brecord_id%7D/delete\nfunc (c *Client) DeleteRecord(ctx context.Context, domainName string, recordID int64) error {\n\tendpoint := c.baseURL.JoinPath(\"domains\", domainName, \"records\", strconv.FormatInt(recordID, 10))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(authorizationHeader, \"Bearer \"+c.apiToken)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/binarylane/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer secret\"),\n\t)\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/example.com/records\",\n\t\t\tservermock.ResponseFromFixture(\"create_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType: \"TXT\",\n\t\tName: \"foo\",\n\t\tData: \"txtTXTtxt\",\n\t\tTTL:  300,\n\t}\n\n\trec, err := client.CreateRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tID:   123,\n\t\tType: \"TXT\",\n\t\tName: \"foo\",\n\t\tData: \"txtTXTtxt\",\n\t\tTTL:  300,\n\t}\n\n\trequire.Equal(t, expected, rec)\n}\n\nfunc TestClient_CreateRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/example.com/records\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType: \"TXT\",\n\t\tName: \"foo\",\n\t\tData: \"txtTXTtxt\",\n\t\tTTL:  300,\n\t}\n\n\t_, err := client.CreateRecord(t.Context(), \"example.com\", record)\n\trequire.EqualError(t, err, \"400: type: title: detail: instance: property1: a\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domains/example.com/records/123\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", 123)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domains/example.com/records/123\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", 123)\n\trequire.EqualError(t, err, \"400: type: title: detail: instance: property1: a\")\n}\n"
  },
  {
    "path": "providers/dns/binarylane/internal/fixtures/create_record-request.json",
    "content": "{\n  \"type\": \"TXT\",\n  \"name\": \"foo\",\n  \"data\": \"txtTXTtxt\",\n  \"ttl\": 300\n}\n"
  },
  {
    "path": "providers/dns/binarylane/internal/fixtures/create_record.json",
    "content": "{\n  \"domain_record\": {\n    \"id\": 123,\n    \"type\": \"TXT\",\n    \"name\": \"foo\",\n    \"data\": \"txtTXTtxt\",\n    \"ttl\": 300\n  }\n}\n"
  },
  {
    "path": "providers/dns/binarylane/internal/fixtures/error.json",
    "content": "{\n  \"type\": \"type\",\n  \"title\": \"title\",\n  \"status\": 400,\n  \"detail\": \"detail\",\n  \"instance\": \"instance\",\n  \"errors\": {\n    \"property1\": [\n      \"a\"\n    ]\n  },\n  \"property1\": null,\n  \"property2\": null\n}\n"
  },
  {
    "path": "providers/dns/binarylane/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype APIError struct {\n\tType     string              `json:\"type\"`\n\tTitle    string              `json:\"title\"`\n\tStatus   int                 `json:\"status\"`\n\tDetail   string              `json:\"detail\"`\n\tInstance string              `json:\"instance\"`\n\tErrors   map[string][]string `json:\"errors\"`\n}\n\nfunc (a *APIError) Error() string {\n\tmsg := new(strings.Builder)\n\n\t_, _ = fmt.Fprintf(msg, \"%d: %s: %s: %s: %s\", a.Status, a.Type, a.Title, a.Detail, a.Instance)\n\n\tfor s, values := range a.Errors {\n\t\t_, _ = fmt.Fprintf(msg, \": %s: %s\", s, strings.Join(values, \", \"))\n\t}\n\n\treturn msg.String()\n}\n\ntype Record struct {\n\tID       int64  `json:\"id,omitempty\"`\n\tType     string `json:\"type,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tData     string `json:\"data,omitempty\"`\n\tPriority int    `json:\"priority,omitempty\"`\n\tPort     int    `json:\"port,omitempty\"`\n\tTTL      int    `json:\"ttl,omitempty\"`\n\tWeight   int    `json:\"weight,omitempty\"`\n\tFlags    int    `json:\"flags,omitempty\"`\n\tTag      string `json:\"tag,omitempty\"`\n}\n\ntype APIResponse struct {\n\tDomainRecord *Record `json:\"domain_record\"`\n}\n"
  },
  {
    "path": "providers/dns/bindman/bindman.go",
    "content": "// Package bindman implements a DNS provider for solving the DNS-01 challenge.\npackage bindman\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\tbindman \"github.com/labbsr0x/bindman-dns-webhook/src/client\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"BINDMAN_\"\n\n\tEnvManagerAddress = envNamespace + \"MANAGER_ADDRESS\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tBaseURL            string\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, time.Minute),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *bindman.DNSWebhookClient\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Bindman.\n// BINDMAN_MANAGER_ADDRESS should have the scheme, hostname, and port (if required) of the authoritative Bindman Manager server.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvManagerAddress)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"bindman: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.BaseURL = values[EnvManagerAddress]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Bindman.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"bindman: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.BaseURL == \"\" {\n\t\treturn nil, errors.New(\"bindman: bindman manager address missing\")\n\t}\n\n\t// Because the client.New uses the http.DefaultClient.\n\tif config.HTTPClient == nil {\n\t\tconfig.HTTPClient = &http.Client{Timeout: time.Minute}\n\t}\n\n\tclient, err := bindman.New(config.BaseURL, clientdebug.Wrap(config.HTTPClient))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"bindman: %w\", err)\n\t}\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\n// This will *not* create a subzone to contain the TXT record,\n// so make sure the FQDN specified is within an extant zone.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tif err := d.client.AddRecord(info.EffectiveFQDN, \"TXT\", info.Value); err != nil {\n\t\treturn fmt.Errorf(\"bindman: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tif err := d.client.RemoveRecord(info.EffectiveFQDN, \"TXT\"); err != nil {\n\t\treturn fmt.Errorf(\"bindman: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/bindman/bindman.toml",
    "content": "Name = \"Bindman\"\nDescription = ''''''\nURL = \"https://github.com/labbsr0x/bindman-dns-webhook\"\nCode = \"bindman\"\nSince = \"v2.6.0\"\n\nExample = '''\nBINDMAN_MANAGER_ADDRESS=<your bindman manager address> \\\nlego --dns bindman -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    BINDMAN_MANAGER_ADDRESS = \"The server URL, should have scheme, hostname, and port (if required) of the Bindman-DNS Manager server\"\n  [Configuration.Additional]\n    BINDMAN_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    BINDMAN_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    BINDMAN_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 60)\"\n\n[Links]\n  API = \"https://gitlab.isc.org/isc-projects/bind9\"\n  GoClient = \"https://github.com/labbsr0x/bindman-dns-webhook\"\n"
  },
  {
    "path": "providers/dns/bindman/bindman_test.go",
    "content": "package bindman\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvManagerAddress).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvManagerAddress: \"http://localhost\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing bindman manager address\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvManagerAddress: \"\",\n\t\t\t},\n\t\t\texpected: \"bindman: some credentials information are missing: BINDMAN_MANAGER_ADDRESS\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"empty bindman manager address\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvManagerAddress: \"  \",\n\t\t\t},\n\t\t\texpected: \"bindman: managerAddress parameter must be a non-empty string\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tconfig   *Config\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tconfig: &Config{BaseURL: \"http://localhost\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing base URL\",\n\t\t\tconfig:   &Config{BaseURL: \"\"},\n\t\t\texpected: \"bindman: bindman manager address missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing base URL\",\n\t\t\tconfig:   &Config{BaseURL: \"  \"},\n\t\t\texpected: \"bindman: managerAddress parameter must be a non-empty string\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing config\",\n\t\t\texpected: \"bindman: the configuration of the DNS provider is nil\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tp, err := NewDNSProviderConfig(test.config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.BaseURL = server.URL\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(\"User-Agent\", \"bindman-dns-webhook-client\"))\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\ttestCases := []struct {\n\t\tname        string\n\t\tmock        *servermock.Builder[*DNSProvider]\n\t\tdomain      string\n\t\ttoken       string\n\t\tkeyAuth     string\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname: \"success when add record function return no error\",\n\t\t\tmock: mockBuilder().\n\t\t\t\tRoute(\"POST /records\",\n\t\t\t\t\tservermock.Noop().WithStatusCode(http.StatusNoContent),\n\t\t\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"add_record-request.json\"),\n\t\t\t\t),\n\t\t\tdomain:      \"example.com\",\n\t\t\tkeyAuth:     \"szDTG4zmM0GsKG91QAGO2M4UYOJMwU8oFpWOP7eTjCw\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"error when add record function return an error\",\n\t\t\tmock: mockBuilder().\n\t\t\t\tRoute(\"POST /records\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"error.json\"),\n\t\t\t\t),\n\t\t\tdomain:      \"example.com\",\n\t\t\tkeyAuth:     \"szDTG4zmM0GsKG91QAGO2M4UYOJMwU8oFpWOP7eTjCw\",\n\t\t\texpectError: true,\n\t\t},\n\t}\n\tfor _, test := range testCases {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tprovider := test.mock.Build(t)\n\n\t\t\terr := provider.Present(test.domain, test.token, test.keyAuth)\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\ttestCases := []struct {\n\t\tname        string\n\t\tmock        *servermock.Builder[*DNSProvider]\n\t\tdomain      string\n\t\ttoken       string\n\t\tkeyAuth     string\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname: \"success when remove record function return no error\",\n\t\t\tmock: mockBuilder().\n\t\t\t\tRoute(\"DELETE /records/_acme-challenge.example.com./TXT\",\n\t\t\t\t\tservermock.Noop().WithStatusCode(http.StatusNoContent),\n\t\t\t\t),\n\t\t\tdomain:      \"example.com\",\n\t\t\tkeyAuth:     \"szDTG4zmM0GsKG91QAGO2M4UYOJMwU8oFpWOP7eTjCw\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"error when remove record function return an error\",\n\t\t\tmock: mockBuilder().\n\t\t\t\tRoute(\"DELETE /records/_acme-challenge.example.com./TXT\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"error.json\"),\n\t\t\t\t),\n\t\t\tdomain:      \"example.com\",\n\t\t\tkeyAuth:     \"szDTG4zmM0GsKG91QAGO2M4UYOJMwU8oFpWOP7eTjCw\",\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tprovider := test.mock.Build(t)\n\n\t\t\terr := provider.CleanUp(test.domain, test.token, test.keyAuth)\n\t\t\tif test.expectError {\n\t\t\t\trequire.ErrorContains(t, err, \"bindman: ERROR (400): bar; \")\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/bindman/fixtures/add_record-request.json",
    "content": "{\n  \"name\": \"_acme-challenge.example.com.\",\n  \"value\": \"_EYMkjukXEMcXbnvpT6WLESzfYhxH190NKTBo3cpu-E\",\n  \"type\": \"TXT\"\n}\n"
  },
  {
    "path": "providers/dns/bindman/fixtures/error.json",
    "content": "{\n  \"message\": \"bar\",\n  \"code\": 400,\n  \"details\": [\"foo\"]\n}\n"
  },
  {
    "path": "providers/dns/bluecat/bluecat.go",
    "content": "// Package bluecat implements a DNS provider for solving the DNS-01 challenge using a self-hosted Bluecat Address Manager.\npackage bluecat\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/bluecat/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"BLUECAT_\"\n\n\tEnvServerURL  = envNamespace + \"SERVER_URL\"\n\tEnvUserName   = envNamespace + \"USER_NAME\"\n\tEnvPassword   = envNamespace + \"PASSWORD\"\n\tEnvConfigName = envNamespace + \"CONFIG_NAME\"\n\tEnvDNSView    = envNamespace + \"DNS_VIEW\"\n\tEnvDebug      = envNamespace + \"DEBUG\"\n\tEnvSkipDeploy = envNamespace + \"SKIP_DEPLOY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL            string\n\tUserName           string\n\tPassword           string\n\tConfigName         string\n\tDNSView            string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n\tDebug              bool\n\tSkipDeploy         bool\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t\tDebug:      env.GetOrDefaultBool(EnvDebug, false),\n\t\tSkipDeploy: env.GetOrDefaultBool(EnvSkipDeploy, false),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Bluecat DNS.\n// Credentials must be passed in the environment variables:\n//   - BLUECAT_SERVER_URL\n//     It should have the scheme, hostname, and port (if required) of the authoritative Bluecat BAM server.\n//     The REST endpoint will be appended.\n//   - BLUECAT_USER_NAME and BLUECAT_PASSWORD\n//   - BLUECAT_CONFIG_NAME (the Configuration name)\n//   - BLUECAT_DNS_VIEW (external DNS View Name)\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvServerURL, EnvUserName, EnvPassword, EnvConfigName, EnvDNSView)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"bluecat: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.BaseURL = values[EnvServerURL]\n\tconfig.UserName = values[EnvUserName]\n\tconfig.Password = values[EnvPassword]\n\tconfig.ConfigName = values[EnvConfigName]\n\tconfig.DNSView = values[EnvDNSView]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Bluecat DNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"bluecat: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.BaseURL == \"\" || config.UserName == \"\" || config.Password == \"\" || config.ConfigName == \"\" || config.DNSView == \"\" {\n\t\treturn nil, errors.New(\"bluecat: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(config.BaseURL, config.UserName, config.Password)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record using the specified parameters\n// This will *not* create a sub-zone to contain the TXT record,\n// so make sure the FQDN specified is within an existent zone.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecat: login: %w\", err)\n\t}\n\n\tviewID, err := d.client.LookupViewID(ctx, d.config.ConfigName, d.config.DNSView)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecat: lookupViewID: %w\", err)\n\t}\n\n\tparentZoneID, name, err := d.client.LookupParentZoneID(ctx, viewID, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecat: lookupParentZoneID: %w\", err)\n\t}\n\n\tif d.config.Debug {\n\t\tlog.Infof(\"fqdn: %s; viewID: %d; ZoneID: %d; zone: %s\", info.EffectiveFQDN, viewID, parentZoneID, name)\n\t}\n\n\ttxtRecord := internal.Entity{\n\t\tName:       name,\n\t\tType:       internal.TXTType,\n\t\tProperties: fmt.Sprintf(\"ttl=%d|absoluteName=%s|txt=%s|\", d.config.TTL, info.EffectiveFQDN, info.Value),\n\t}\n\n\t_, err = d.client.AddEntity(ctx, parentZoneID, txtRecord)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecat: add TXT record: %w\", err)\n\t}\n\n\tif !d.config.SkipDeploy {\n\t\terr = d.client.Deploy(ctx, parentZoneID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"bluecat: deploy: %w\", err)\n\t\t}\n\t}\n\n\terr = d.client.Logout(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecat: logout: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecat: login: %w\", err)\n\t}\n\n\tviewID, err := d.client.LookupViewID(ctx, d.config.ConfigName, d.config.DNSView)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecat: lookupViewID: %w\", err)\n\t}\n\n\tparentZoneID, name, err := d.client.LookupParentZoneID(ctx, viewID, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecat: lookupParentZoneID: %w\", err)\n\t}\n\n\ttxtRecord, err := d.client.GetEntityByName(ctx, parentZoneID, name, internal.TXTType)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecat: get TXT record: %w\", err)\n\t}\n\n\terr = d.client.Delete(ctx, txtRecord.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecat: delete TXT record: %w\", err)\n\t}\n\n\tif !d.config.SkipDeploy {\n\t\terr = d.client.Deploy(ctx, parentZoneID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"bluecat: deploy: %w\", err)\n\t\t}\n\t}\n\n\terr = d.client.Logout(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecat: logout: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/bluecat/bluecat.toml",
    "content": "Name = \"Bluecat\"\nDescription = ''''''\nURL = \"https://www.bluecatnetworks.com\"\nCode = \"bluecat\"\nSince = \"v0.5.0\"\n\nExample = '''\nBLUECAT_PASSWORD=mypassword \\\nBLUECAT_DNS_VIEW=myview \\\nBLUECAT_USER_NAME=myusername \\\nBLUECAT_CONFIG_NAME=myconfig \\\nBLUECAT_SERVER_URL=https://bam.example.com \\\nBLUECAT_TTL=30 \\\nlego --dns bluecat -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    BLUECAT_SERVER_URL = \"The server URL, should have scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve\"\n    BLUECAT_USER_NAME = \"API username\"\n    BLUECAT_PASSWORD = \"API password\"\n    BLUECAT_CONFIG_NAME = \"Configuration name\"\n    BLUECAT_DNS_VIEW = \"External DNS View Name\"\n  [Configuration.Additional]\n    BLUECAT_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    BLUECAT_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    BLUECAT_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    BLUECAT_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n    BLUECAT_SKIP_DEPLOY = \"Skip deployements\"\n\n[Links]\n  API = \"https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/REST-API/9.1.0\"\n"
  },
  {
    "path": "providers/dns/bluecat/bluecat_test.go",
    "content": "package bluecat\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvServerURL,\n\tEnvUserName,\n\tEnvPassword,\n\tEnvConfigName,\n\tEnvDNSView).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL:  \"http://localhost\",\n\t\t\t\tEnvUserName:   \"A\",\n\t\t\t\tEnvPassword:   \"B\",\n\t\t\t\tEnvConfigName: \"C\",\n\t\t\t\tEnvDNSView:    \"D\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL:  \"\",\n\t\t\t\tEnvUserName:   \"\",\n\t\t\t\tEnvPassword:   \"\",\n\t\t\t\tEnvConfigName: \"\",\n\t\t\t\tEnvDNSView:    \"\",\n\t\t\t},\n\t\t\texpected: \"bluecat: some credentials information are missing: BLUECAT_SERVER_URL,BLUECAT_USER_NAME,BLUECAT_PASSWORD,BLUECAT_CONFIG_NAME,BLUECAT_DNS_VIEW\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing server url\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL:  \"\",\n\t\t\t\tEnvUserName:   \"A\",\n\t\t\t\tEnvPassword:   \"B\",\n\t\t\t\tEnvConfigName: \"C\",\n\t\t\t\tEnvDNSView:    \"D\",\n\t\t\t},\n\t\t\texpected: \"bluecat: some credentials information are missing: BLUECAT_SERVER_URL\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL:  \"http://localhost\",\n\t\t\t\tEnvUserName:   \"\",\n\t\t\t\tEnvPassword:   \"B\",\n\t\t\t\tEnvConfigName: \"C\",\n\t\t\t\tEnvDNSView:    \"D\",\n\t\t\t},\n\t\t\texpected: \"bluecat: some credentials information are missing: BLUECAT_USER_NAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL:  \"http://localhost\",\n\t\t\t\tEnvUserName:   \"A\",\n\t\t\t\tEnvPassword:   \"\",\n\t\t\t\tEnvConfigName: \"C\",\n\t\t\t\tEnvDNSView:    \"D\",\n\t\t\t},\n\t\t\texpected: \"bluecat: some credentials information are missing: BLUECAT_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing config name\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL:  \"http://localhost\",\n\t\t\t\tEnvUserName:   \"A\",\n\t\t\t\tEnvPassword:   \"B\",\n\t\t\t\tEnvConfigName: \"\",\n\t\t\t\tEnvDNSView:    \"D\",\n\t\t\t},\n\t\t\texpected: \"bluecat: some credentials information are missing: BLUECAT_CONFIG_NAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing DNS view\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL:  \"http://localhost\",\n\t\t\t\tEnvUserName:   \"A\",\n\t\t\t\tEnvPassword:   \"B\",\n\t\t\t\tEnvConfigName: \"C\",\n\t\t\t\tEnvDNSView:    \"\",\n\t\t\t},\n\t\t\texpected: \"bluecat: some credentials information are missing: BLUECAT_DNS_VIEW\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc       string\n\t\tbaseURL    string\n\t\tuserName   string\n\t\tpassword   string\n\t\tconfigName string\n\t\tdnsView    string\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tdesc:       \"success\",\n\t\t\tbaseURL:    \"http://localhost\",\n\t\t\tuserName:   \"A\",\n\t\t\tpassword:   \"B\",\n\t\t\tconfigName: \"C\",\n\t\t\tdnsView:    \"D\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"bluecat: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"missing base URL\",\n\t\t\tbaseURL:    \"\",\n\t\t\tuserName:   \"A\",\n\t\t\tpassword:   \"B\",\n\t\t\tconfigName: \"C\",\n\t\t\tdnsView:    \"D\",\n\t\t\texpected:   \"bluecat: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"missing username\",\n\t\t\tbaseURL:    \"http://localhost\",\n\t\t\tuserName:   \"\",\n\t\t\tpassword:   \"B\",\n\t\t\tconfigName: \"C\",\n\t\t\tdnsView:    \"D\",\n\t\t\texpected:   \"bluecat: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"missing password\",\n\t\t\tbaseURL:    \"http://localhost\",\n\t\t\tuserName:   \"A\",\n\t\t\tpassword:   \"\",\n\t\t\tconfigName: \"C\",\n\t\t\tdnsView:    \"D\",\n\t\t\texpected:   \"bluecat: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"missing config name\",\n\t\t\tbaseURL:    \"http://localhost\",\n\t\t\tuserName:   \"A\",\n\t\t\tpassword:   \"B\",\n\t\t\tconfigName: \"\",\n\t\t\tdnsView:    \"D\",\n\t\t\texpected:   \"bluecat: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"missing DNS view\",\n\t\t\tbaseURL:    \"http://localhost\",\n\t\t\tuserName:   \"A\",\n\t\t\tpassword:   \"B\",\n\t\t\tconfigName: \"C\",\n\t\t\tdnsView:    \"\",\n\t\t\texpected:   \"bluecat: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.BaseURL = test.baseURL\n\t\t\tconfig.UserName = test.userName\n\t\t\tconfig.Password = test.password\n\t\t\tconfig.ConfigName = test.configName\n\t\t\tconfig.DNSView = test.dnsView\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(time.Second * 1)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/bluecat/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// Object types.\nconst (\n\tConfigType = \"Configuration\"\n\tViewType   = \"View\"\n\tZoneType   = \"Zone\"\n\tTXTType    = \"TXTRecord\"\n)\n\nconst authorizationHeader = \"Authorization\"\n\ntype Client struct {\n\tusername string\n\tpassword string\n\n\ttokenExp *regexp.Regexp\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(baseURL, username, password string) *Client {\n\tbu, _ := url.Parse(baseURL)\n\n\treturn &Client{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\ttokenExp:   regexp.MustCompile(\"BAMAuthToken: [^ ]+\"),\n\t\tbaseURL:    bu,\n\t\tHTTPClient: &http.Client{Timeout: 30 * time.Second},\n\t}\n}\n\n// Deploy the DNS config for the specified entity to the authoritative servers.\n// https://docs.bluecatnetworks.com/r/Address-Manager-Legacy-v1-API-Guide/POST/v1/quickDeploy/9.5.0\nfunc (c *Client) Deploy(ctx context.Context, entityID uint) error {\n\tendpoint := c.createEndpoint(\"quickDeploy\")\n\n\tq := endpoint.Query()\n\tq.Set(\"entityId\", strconv.FormatUint(uint64(entityID), 10))\n\tendpoint.RawQuery = q.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := c.doAuthenticated(ctx, req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\t// The API doc says that 201 is expected but in the reality 200 is return.\n\tif resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\treturn nil\n}\n\n// AddEntity A generic method for adding configurations, DNS zones, and DNS resource records.\n// https://docs.bluecatnetworks.com/r/Address-Manager-Legacy-v1-API-Guide/POST/v1/addEntity/9.5.0\nfunc (c *Client) AddEntity(ctx context.Context, parentID uint, entity Entity) (uint64, error) {\n\tendpoint := c.createEndpoint(\"addEntity\")\n\n\tq := endpoint.Query()\n\tq.Set(\"parentId\", strconv.FormatUint(uint64(parentID), 10))\n\tendpoint.RawQuery = q.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, entity)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tresp, err := c.doAuthenticated(ctx, req)\n\tif err != nil {\n\t\treturn 0, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn 0, errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, _ := io.ReadAll(resp.Body)\n\n\t// addEntity responds only with body text containing the ID of the created record\n\taddTxtResp := string(raw)\n\n\tid, err := strconv.ParseUint(addTxtResp, 10, 64)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"addEntity request failed: %s\", addTxtResp)\n\t}\n\n\treturn id, nil\n}\n\n// GetEntityByName Returns objects from the database referenced by their database ID and with its properties fields populated.\n// https://docs.bluecatnetworks.com/r/Address-Manager-Legacy-v1-API-Guide/GET/v1/getEntityById/9.5.0\nfunc (c *Client) GetEntityByName(ctx context.Context, parentID uint, name, objType string) (*EntityResponse, error) {\n\tendpoint := c.createEndpoint(\"getEntityByName\")\n\n\tq := endpoint.Query()\n\tq.Set(\"parentId\", strconv.FormatUint(uint64(parentID), 10))\n\tq.Set(\"name\", name)\n\tq.Set(\"type\", objType)\n\tendpoint.RawQuery = q.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := c.doAuthenticated(ctx, req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar entity EntityResponse\n\n\terr = json.Unmarshal(raw, &entity)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn &entity, nil\n}\n\n// Delete Deletes an object using the generic delete method.\n// https://docs.bluecatnetworks.com/r/Address-Manager-Legacy-v1-API-Guide/DELETE/v1/delete/9.5.0\nfunc (c *Client) Delete(ctx context.Context, objectID uint) error {\n\tendpoint := c.createEndpoint(\"delete\")\n\n\tq := endpoint.Query()\n\tq.Set(\"objectId\", strconv.FormatUint(uint64(objectID), 10))\n\tendpoint.RawQuery = q.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := c.doAuthenticated(ctx, req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\t// The API doc says that 204 is expected but in the reality 200 is returned.\n\tif resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\treturn nil\n}\n\n// LookupViewID Find the DNS view with the given name within.\nfunc (c *Client) LookupViewID(ctx context.Context, configName, viewName string) (uint, error) {\n\t// Lookup the entity ID of the configuration named in our properties.\n\tconf, err := c.GetEntityByName(ctx, 0, configName, ConfigType)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tview, err := c.GetEntityByName(ctx, conf.ID, viewName, ViewType)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn view.ID, nil\n}\n\n// LookupParentZoneID returns the entityId of the parent zone by iterating through the root labels.\n// Also return the simple name of the host.\nfunc (c *Client) LookupParentZoneID(ctx context.Context, viewID uint, fqdn string) (uint, string, error) {\n\tif fqdn == \"\" {\n\t\treturn viewID, \"\", nil\n\t}\n\n\tzones := strings.Split(strings.Trim(fqdn, \".\"), \".\")\n\n\tname := zones[0]\n\tparentViewID := viewID\n\n\tfor i := len(zones) - 1; i > -1; i-- {\n\t\tzone, err := c.GetEntityByName(ctx, parentViewID, zones[i], ZoneType)\n\t\tif err != nil {\n\t\t\treturn 0, \"\", fmt.Errorf(\"could not find zone named %s: %w\", name, err)\n\t\t}\n\n\t\tif zone == nil || zone.ID == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tif i > 0 {\n\t\t\tname = strings.Join(zones[0:i], \".\")\n\t\t}\n\n\t\tparentViewID = zone.ID\n\t}\n\n\treturn parentViewID, name, nil\n}\n\nfunc (c *Client) createEndpoint(resource string) *url.URL {\n\treturn c.baseURL.JoinPath(\"Services\", \"REST\", \"v1\", resource)\n}\n\nfunc (c *Client) doAuthenticated(ctx context.Context, req *http.Request) (*http.Response, error) {\n\ttok := getToken(ctx)\n\tif tok != \"\" {\n\t\treq.Header.Set(authorizationHeader, tok)\n\t}\n\n\treturn c.HTTPClient.Do(req)\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/bluecat/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(server.URL, \"user\", \"secret\")\n\tclient.HTTPClient = server.Client()\n\n\treturn client, nil\n}\n\nfunc TestClient_LookupParentZoneID(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /Services/REST/v1/getEntityByName\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\tquery := req.URL.Query()\n\n\t\t\t\tif query.Get(\"name\") == \"com\" {\n\t\t\t\t\t_ = json.NewEncoder(rw).Encode(EntityResponse{\n\t\t\t\t\t\tID:         2,\n\t\t\t\t\t\tName:       \"com\",\n\t\t\t\t\t\tType:       ZoneType,\n\t\t\t\t\t\tProperties: \"test\",\n\t\t\t\t\t})\n\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t_, _ = rw.Write([]byte(`{}`))\n\t\t\t})).\n\t\tBuild(t)\n\n\tparentID, name, err := client.LookupParentZoneID(t.Context(), 2, \"foo.example.com\")\n\trequire.NoError(t, err)\n\n\tassert.EqualValues(t, 2, parentID)\n\tassert.Equal(t, \"foo.example\", name)\n}\n"
  },
  {
    "path": "providers/dns/bluecat/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\ntype token string\n\nconst tokenKey token = \"token\"\n\n// login Logs in as API user.\n// Authenticates and receives a token to be used in for subsequent requests.\n// https://docs.bluecatnetworks.com/r/Address-Manager-Legacy-v1-API-Guide/GET/v1/login/9.5.0\nfunc (c *Client) login(ctx context.Context) (string, error) {\n\tendpoint := c.createEndpoint(\"login\")\n\n\tq := endpoint.Query()\n\tq.Set(\"username\", c.username)\n\tq.Set(\"password\", c.password)\n\tendpoint.RawQuery = q.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tauthResp := string(raw)\n\tif strings.Contains(authResp, \"Authentication Error\") {\n\t\treturn \"\", fmt.Errorf(\"request failed: %s\", strings.Trim(authResp, `\"`))\n\t}\n\n\t// Upon success, API responds with \"Session Token-> BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM= <- for User : username\"\n\ttok := c.tokenExp.FindString(authResp)\n\n\treturn tok, nil\n}\n\n// Logout Logs out of the current API session.\n// https://docs.bluecatnetworks.com/r/Address-Manager-Legacy-v1-API-Guide/GET/v1/logout/9.5.0\nfunc (c *Client) Logout(ctx context.Context) error {\n\tif getToken(ctx) == \"\" {\n\t\t// nothing to do\n\t\treturn nil\n\t}\n\n\tendpoint := c.createEndpoint(\"logout\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := c.doAuthenticated(ctx, req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tauthResp := string(raw)\n\tif !strings.Contains(authResp, \"successfully\") {\n\t\treturn fmt.Errorf(\"request failed to delete session: %s\", strings.Trim(authResp, `\"`))\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) {\n\ttok, err := c.login(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn context.WithValue(ctx, tokenKey, tok), nil\n}\n\nfunc getToken(ctx context.Context) string {\n\ttok, ok := ctx.Value(tokenKey).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn tok\n}\n"
  },
  {
    "path": "providers/dns/bluecat/internal/identity_test.go",
    "content": "package internal\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst fakeToken = \"BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM=\"\n\nfunc TestClient_CreateAuthenticatedContext(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /Services/REST/v1/login\",\n\t\t\tservermock.RawStringResponse(fakeToken),\n\t\t\tservermock.CheckQueryParameter().\n\t\t\t\tWith(\"username\", \"user\").\n\t\t\t\tWith(\"password\", \"secret\")).\n\t\tRoute(\"DELETE /Services/REST/v1/delete\", nil,\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(fakeToken)).\n\t\tBuild(t)\n\n\tctx, err := client.CreateAuthenticatedContext(t.Context())\n\trequire.NoError(t, err)\n\n\tat := getToken(ctx)\n\tassert.Equal(t, fakeToken, at)\n\n\terr = client.Delete(ctx, 123)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/bluecat/internal/types.go",
    "content": "package internal\n\n// Entity JSON body for Bluecat entity requests.\ntype Entity struct {\n\tID         string `json:\"id,omitempty\"`\n\tName       string `json:\"name\"`\n\tType       string `json:\"type\"`\n\tProperties string `json:\"properties\"`\n}\n\n// EntityResponse JSON body for Bluecat entity responses.\ntype EntityResponse struct {\n\tID         uint   `json:\"id\"`\n\tName       string `json:\"name\"`\n\tType       string `json:\"type\"`\n\tProperties string `json:\"properties\"`\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/bluecatv2.go",
    "content": "// Package bluecatv2 implements a DNS provider for solving the DNS-01 challenge using Bluecat v2.\npackage bluecatv2\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/bluecatv2/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"BLUECATV2_\"\n\n\tEnvServerURL  = envNamespace + \"SERVER_URL\"\n\tEnvUsername   = envNamespace + \"USERNAME\"\n\tEnvPassword   = envNamespace + \"PASSWORD\"\n\tEnvConfigName = envNamespace + \"CONFIG_NAME\"\n\tEnvViewName   = envNamespace + \"VIEW_NAME\"\n\tEnvSkipDeploy = envNamespace + \"SKIP_DEPLOY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tServerURL  string\n\tUsername   string\n\tPassword   string\n\tConfigName string\n\tViewName   string\n\tSkipDeploy bool\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tSkipDeploy: env.GetOrDefaultBool(EnvSkipDeploy, false),\n\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\tzoneIDs     map[string]int64\n\trecordIDs   map[string]int64\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Bluecat v2.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvServerURL, EnvUsername, EnvPassword, EnvConfigName, EnvViewName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"bluecatv2: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.ServerURL = values[EnvServerURL]\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\tconfig.ConfigName = values[EnvConfigName]\n\tconfig.ViewName = values[EnvViewName]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Bluecat v2.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"bluecatv2: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.ServerURL == \"\" {\n\t\treturn nil, errors.New(\"bluecatv2: missing server URL\")\n\t}\n\n\tif config.ConfigName == \"\" {\n\t\treturn nil, errors.New(\"bluecatv2: missing configuration name\")\n\t}\n\n\tif config.ViewName == \"\" {\n\t\treturn nil, errors.New(\"bluecatv2: missing view name\")\n\t}\n\n\tclient, err := internal.NewClient(config.ServerURL, config.Username, config.Password)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"bluecatv2: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]int64),\n\t\tzoneIDs:   make(map[string]int64),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecatv2: %w\", err)\n\t}\n\n\tzone, err := d.findZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecatv2: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.AbsoluteName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecatv2: %w\", err)\n\t}\n\n\trecord := internal.RecordTXT{\n\t\tCommonResource: internal.CommonResource{\n\t\t\tType: \"TXTRecord\",\n\t\t\tName: subDomain,\n\t\t},\n\t\tText:       info.Value,\n\t\tTTL:        d.config.TTL,\n\t\tRecordType: \"TXT\",\n\t}\n\n\tnewRecord, err := d.client.CreateZoneResourceRecord(ctx, zone.ID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecatv2: create resource record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.zoneIDs[token] = zone.ID\n\td.recordIDs[token] = newRecord.ID\n\td.recordIDsMu.Unlock()\n\n\tif d.config.SkipDeploy {\n\t\treturn nil\n\t}\n\n\t_, err = d.client.CreateZoneDeployment(ctx, zone.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecat: deploy zone: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordIDsMu.Lock()\n\trecordID, recordOK := d.recordIDs[token]\n\tzoneID, zoneOK := d.zoneIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !recordOK {\n\t\treturn fmt.Errorf(\"bluecatv2: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\tif !zoneOK {\n\t\treturn fmt.Errorf(\"bluecatv2: unknown zone ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecatv2: %w\", err)\n\t}\n\n\terr = d.client.DeleteResourceRecord(ctx, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecatv2: delete resource record: %w\", err)\n\t}\n\n\tif d.config.SkipDeploy {\n\t\treturn nil\n\t}\n\n\t_, err = d.client.CreateZoneDeployment(ctx, zoneID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bluecat: deploy zone: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*internal.ZoneResource, error) {\n\tfor name := range dns01.UnFqdnDomainsSeq(fqdn) {\n\t\topts := &internal.CollectionOptions{\n\t\t\tFields: \"id,absoluteName,configuration.id,configuration.name,view.id,view.name\",\n\t\t\tFilter: internal.And(\n\t\t\t\tinternal.Eq(\"absoluteName\", name),\n\t\t\t\tinternal.Eq(\"configuration.name\", d.config.ConfigName),\n\t\t\t\tinternal.Eq(\"view.name\", d.config.ViewName),\n\t\t\t).String(),\n\t\t}\n\n\t\tzones, err := d.client.RetrieveZones(ctx, opts)\n\t\tif err != nil {\n\t\t\t// TODO(ldez) maybe add a log in v5.\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, zone := range zones {\n\t\t\tif zone.AbsoluteName == name {\n\t\t\t\treturn &zone, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"no zone found for fqdn: %s\", fqdn)\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/bluecatv2.toml",
    "content": "Name = \"Bluecat v2\"\nDescription = ''''''\nURL = \"https://www.bluecatnetworks.com\"\nCode = \"bluecatv2\"\nSince = \"v4.32.0\"\n\nExample = '''\nBLUECATV2_SERVER_URL=\"https://example.com\" \\\nBLUECATV2_USERNAME=\"xxx\" \\\nBLUECATV2_PASSWORD=\"yyy\" \\\nBLUECATV2_CONFIG_NAME=\"myConfiguration\" \\\nBLUECATV2_VIEW_NAME=\"myView\" \\\nlego --dns bluecatv2 -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    BLUECAT_SERVER_URL = \"The server URL: it should have a scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve\"\n    BLUECATV2_USERNAME = \"API username\"\n    BLUECATV2_PASSWORD = \"API password\"\n    BLUECATV2_CONFIG_NAME = \"Configuration name\"\n    BLUECATV2_VIEW_NAME = \"DNS View Name\"\n  [Configuration.Additional]\n    BLUECATV2_SKIP_DEPLOY = \"Skip quick deployements\"\n    BLUECATV2_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    BLUECATV2_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    BLUECATV2_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    BLUECATV2_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Introduction/9.6.0\"\n  Swagger = \"http://{Address_Manager_IP}/api/openapi.json\"\n  SwaggerDump = \"https://github.com/go-acme/lego/discussions/2218#discussioncomment-13060545\"\n"
  },
  {
    "path": "providers/dns/bluecatv2/bluecatv2_test.go",
    "content": "package bluecatv2\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-acme/lego/v4/providers/dns/bluecatv2/internal\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvServerURL,\n\tEnvUsername,\n\tEnvPassword,\n\tEnvConfigName,\n\tEnvViewName,\n\tEnvSkipDeploy,\n).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL:  \"https://example.com/\",\n\t\t\t\tEnvUsername:   \"userA\",\n\t\t\t\tEnvPassword:   \"secret\",\n\t\t\t\tEnvConfigName: \"myConfig\",\n\t\t\t\tEnvViewName:   \"myView\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing server URL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL:  \"\",\n\t\t\t\tEnvUsername:   \"userA\",\n\t\t\t\tEnvPassword:   \"secret\",\n\t\t\t\tEnvConfigName: \"myConfig\",\n\t\t\t\tEnvViewName:   \"myView\",\n\t\t\t},\n\t\t\texpected: \"bluecatv2: some credentials information are missing: BLUECATV2_SERVER_URL\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL:  \"https://example.com/\",\n\t\t\t\tEnvUsername:   \"\",\n\t\t\t\tEnvPassword:   \"secret\",\n\t\t\t\tEnvConfigName: \"myConfig\",\n\t\t\t\tEnvViewName:   \"myView\",\n\t\t\t},\n\t\t\texpected: \"bluecatv2: some credentials information are missing: BLUECATV2_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL:  \"https://example.com/\",\n\t\t\t\tEnvUsername:   \"userA\",\n\t\t\t\tEnvPassword:   \"\",\n\t\t\t\tEnvConfigName: \"myConfig\",\n\t\t\t\tEnvViewName:   \"myView\",\n\t\t\t},\n\t\t\texpected: \"bluecatv2: some credentials information are missing: BLUECATV2_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing configuration name\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL:  \"https://example.com/\",\n\t\t\t\tEnvUsername:   \"userA\",\n\t\t\t\tEnvPassword:   \"secret\",\n\t\t\t\tEnvConfigName: \"\",\n\t\t\t\tEnvViewName:   \"myView\",\n\t\t\t},\n\t\t\texpected: \"bluecatv2: some credentials information are missing: BLUECATV2_CONFIG_NAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing view name\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL:  \"https://example.com/\",\n\t\t\t\tEnvUsername:   \"userA\",\n\t\t\t\tEnvPassword:   \"secret\",\n\t\t\t\tEnvConfigName: \"myConfig\",\n\t\t\t\tEnvViewName:   \"\",\n\t\t\t},\n\t\t\texpected: \"bluecatv2: some credentials information are missing: BLUECATV2_VIEW_NAME\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"bluecatv2: some credentials information are missing: BLUECATV2_SERVER_URL,BLUECATV2_USERNAME,BLUECATV2_PASSWORD,BLUECATV2_CONFIG_NAME,BLUECATV2_VIEW_NAME\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc       string\n\t\tserverURL  string\n\t\tusername   string\n\t\tpassword   string\n\t\tconfigName string\n\t\tviewName   string\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tdesc:       \"success\",\n\t\t\tserverURL:  \"https://example.com/\",\n\t\t\tusername:   \"userA\",\n\t\t\tpassword:   \"secret\",\n\t\t\tconfigName: \"myConfig\",\n\t\t\tviewName:   \"myView\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"missing server URL\",\n\t\t\tusername:   \"userA\",\n\t\t\tpassword:   \"secret\",\n\t\t\tconfigName: \"myConfig\",\n\t\t\tviewName:   \"myView\",\n\t\t\texpected:   \"bluecatv2: missing server URL\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"missing username\",\n\t\t\tserverURL:  \"https://example.com/\",\n\t\t\tpassword:   \"secret\",\n\t\t\tconfigName: \"myConfig\",\n\t\t\tviewName:   \"myView\",\n\t\t\texpected:   \"bluecatv2: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"missing password\",\n\t\t\tserverURL:  \"https://example.com/\",\n\t\t\tusername:   \"userA\",\n\t\t\tconfigName: \"myConfig\",\n\t\t\tviewName:   \"myView\",\n\t\t\texpected:   \"bluecatv2: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing configuration name\",\n\t\t\tserverURL: \"https://example.com/\",\n\t\t\tusername:  \"userA\",\n\t\t\tpassword:  \"secret\",\n\t\t\tviewName:  \"myView\",\n\t\t\texpected:  \"bluecatv2: missing configuration name\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"missing view name\",\n\t\t\tserverURL:  \"https://example.com/\",\n\t\t\tusername:   \"userA\",\n\t\t\tpassword:   \"secret\",\n\t\t\tconfigName: \"myConfig\",\n\t\t\texpected:   \"bluecatv2: missing view name\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"bluecatv2: missing server URL\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.ServerURL = test.serverURL\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\t\t\tconfig.ConfigName = test.configName\n\t\t\tconfig.ViewName = test.viewName\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\n\t\t\tconfig.ServerURL = server.URL\n\t\t\tconfig.Username = \"userA\"\n\t\t\tconfig.Password = \"secret\"\n\t\t\tconfig.ConfigName = \"myConfiguration\"\n\t\t\tconfig.ViewName = \"myView\"\n\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders(),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /api/v2/sessions\",\n\t\t\tservermock.ResponseFromInternal(\"postSession.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"postSession-request.json\"),\n\t\t).\n\t\tRoute(\"GET /api/v2/configurations\",\n\t\t\tservermock.ResponseFromInternal(\"configurations.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"filter\", \"name:eq('myConfiguration')\"),\n\t\t).\n\t\tRoute(\"GET /api/v2/configurations/12345/views\",\n\t\t\tservermock.ResponseFromInternal(\"views.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"filter\", \"name:eq('myView')\"),\n\t\t).\n\t\tRoute(\"GET /api/v2/zones\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\tfilter := req.URL.Query().Get(\"filter\")\n\n\t\t\t\tif strings.Contains(filter, internal.Eq(\"absoluteName\", \"example.com\").String()) {\n\t\t\t\t\tservermock.ResponseFromInternal(\"zones.json\").ServeHTTP(rw, req)\n\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tservermock.ResponseFromInternal(\"error.json\").\n\t\t\t\t\tWithStatusCode(http.StatusNotFound).ServeHTTP(rw, req)\n\t\t\t}),\n\t\t).\n\t\tRoute(\"POST /api/v2/zones/12345/resourceRecords\",\n\t\t\tservermock.ResponseFromInternal(\"postZoneResourceRecord.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"postZoneResourceRecord-request.json\"),\n\t\t).\n\t\tRoute(\"POST /api/v2/zones/12345/deployments\",\n\t\t\tservermock.ResponseFromInternal(\"postZoneDeployment.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"postZoneDeployment-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_Present_skipDeploy(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\tenvTest.ClearEnv()\n\n\tenvTest.Apply(map[string]string{\n\t\tEnvSkipDeploy: \"true\",\n\t})\n\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /api/v2/sessions\",\n\t\t\tservermock.ResponseFromInternal(\"postSession.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"postSession-request.json\"),\n\t\t).\n\t\tRoute(\"GET /api/v2/configurations\",\n\t\t\tservermock.ResponseFromInternal(\"configurations.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"filter\", \"name:eq('myConfiguration')\"),\n\t\t).\n\t\tRoute(\"GET /api/v2/configurations/12345/views\",\n\t\t\tservermock.ResponseFromInternal(\"views.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"filter\", \"name:eq('myView')\"),\n\t\t).\n\t\tRoute(\"GET /api/v2/zones\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\tfilter := req.URL.Query().Get(\"filter\")\n\n\t\t\t\tif strings.Contains(filter, internal.Eq(\"absoluteName\", \"example.com\").String()) {\n\t\t\t\t\tservermock.ResponseFromInternal(\"zones.json\").ServeHTTP(rw, req)\n\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tservermock.ResponseFromInternal(\"error.json\").\n\t\t\t\t\tWithStatusCode(http.StatusNotFound).ServeHTTP(rw, req)\n\t\t\t}),\n\t\t).\n\t\tRoute(\"POST /api/v2/zones/12345/resourceRecords\",\n\t\t\tservermock.ResponseFromInternal(\"postZoneResourceRecord.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"postZoneResourceRecord-request.json\"),\n\t\t).\n\t\tRoute(\"POST /api/v2/zones/456789/deployments\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusUnauthorized),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /api/v2/sessions\",\n\t\t\tservermock.ResponseFromInternal(\"postSession.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"postSession-request.json\"),\n\t\t).\n\t\tRoute(\"DELETE /api/v2/resourceRecords/12345\",\n\t\t\tservermock.ResponseFromInternal(\"deleteResourceRecord.json\"),\n\t\t).\n\t\tRoute(\"POST /api/v2/zones/456789/deployments\",\n\t\t\tservermock.ResponseFromInternal(\"postZoneDeployment.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"postZoneDeployment-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\tprovider.zoneIDs[\"abc\"] = 456789\n\tprovider.recordIDs[\"abc\"] = 12345\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp_skipDeploy(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\tenvTest.ClearEnv()\n\n\tenvTest.Apply(map[string]string{\n\t\tEnvSkipDeploy: \"true\",\n\t})\n\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /api/v2/sessions\",\n\t\t\tservermock.ResponseFromInternal(\"postSession.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"postSession-request.json\"),\n\t\t).\n\t\tRoute(\"DELETE /api/v2/resourceRecords/12345\",\n\t\t\tservermock.ResponseFromInternal(\"deleteResourceRecord.json\"),\n\t\t).\n\t\tRoute(\"POST /api/v2/zones/456789/deployments\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusUnauthorized),\n\t\t).\n\t\tBuild(t)\n\n\tprovider.zoneIDs[\"abc\"] = 456789\n\tprovider.recordIDs[\"abc\"] = 12345\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\n// Client the Bluecat v2 API client.\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(serverURL, username, password string) (*Client, error) {\n\tif serverURL == \"\" {\n\t\treturn nil, errors.New(\"server URL missing\")\n\t}\n\n\tif username == \"\" || password == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, err := url.Parse(serverURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// RetrieveZones retrieves all zones.\nfunc (c *Client) RetrieveZones(ctx context.Context, opts *CollectionOptions) ([]ZoneResource, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v2\", \"zones\")\n\n\tcollection, err := retrieveCollection[ZoneResource](ctx, c, endpoint, opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn collection.Data, nil\n}\n\n// RetrieveZoneDeployments retrieves all deployments for a zone.\nfunc (c *Client) RetrieveZoneDeployments(ctx context.Context, zoneID int64, opts *CollectionOptions) ([]QuickDeployment, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v2\", \"zones\", strconv.FormatInt(zoneID, 10), \"deployments\")\n\n\tcollection, err := retrieveCollection[QuickDeployment](ctx, c, endpoint, opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn collection.Data, nil\n}\n\n// CreateZoneDeployment creates a new deployment for a zone.\nfunc (c *Client) CreateZoneDeployment(ctx context.Context, zoneID int64) (*QuickDeployment, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v2\", \"zones\", strconv.FormatInt(zoneID, 10), \"deployments\")\n\n\tpayload := CommonResource{\n\t\tType: \"QuickDeployment\",\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := new(QuickDeployment)\n\n\terr = c.doAuthenticated(ctx, req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// CreateZoneResourceRecord creates a new TXT record in a zone.\nfunc (c *Client) CreateZoneResourceRecord(ctx context.Context, zoneID int64, record RecordTXT) (*RecordTXT, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v2\", \"zones\", strconv.FormatInt(zoneID, 10), \"resourceRecords\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := new(RecordTXT)\n\n\terr = c.doAuthenticated(ctx, req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// DeleteResourceRecord deletes a resource record.\nfunc (c *Client) DeleteResourceRecord(ctx context.Context, recordID int64) error {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v2\", \"resourceRecords\", strconv.FormatInt(recordID, 10))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.doAuthenticated(ctx, req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc retrieveCollection[T any](ctx context.Context, client *Client, endpoint *url.URL, opts *CollectionOptions) (*Collection[T], error) {\n\tif opts != nil {\n\t\tvalues, err := querystring.Values(opts)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tendpoint.RawQuery = values.Encode()\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &Collection[T]{}\n\n\terr = client.doAuthenticated(ctx, req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilderAuthenticated() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(server.URL, \"userA\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders(),\n\t\tservermock.CheckHeader().\n\t\t\tWithAuthorization(\"Basic secretToken\"),\n\t)\n}\n\nfunc TestClient_RetrieveZones(t *testing.T) {\n\tclient := mockBuilderAuthenticated().\n\t\tRoute(\"GET /api/v2/zones\",\n\t\t\tservermock.ResponseFromFixture(\"zones.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\n\t\t\t\t\t\"filter\",\n\t\t\t\t\t\"absoluteName:eq('example.com') and configuration.name:eq('myConfiguration') and view.name:eq('myView')\",\n\t\t\t\t),\n\t\t).\n\t\tBuild(t)\n\n\topts := &CollectionOptions{\n\t\tFilter: And(\n\t\t\tEq(\"absoluteName\", \"example.com\"),\n\t\t\tEq(\"configuration.name\", \"myConfiguration\"),\n\t\t\tEq(\"view.name\", \"myView\"),\n\t\t).String(),\n\t}\n\n\tresult, err := client.RetrieveZones(mockToken(t.Context()), opts)\n\trequire.NoError(t, err)\n\n\texpected := []ZoneResource{\n\t\t{\n\t\t\tCommonResource: CommonResource{ID: 12345, Type: \"ENUMZone\", Name: \"5678\"},\n\t\t\tAbsoluteName:   \"string\",\n\t\t},\n\t\t{\n\t\t\tCommonResource: CommonResource{ID: 12345, Type: \"ExternalHostsZone\", Name: \"name\"},\n\t\t},\n\t\t{\n\t\t\tCommonResource: CommonResource{ID: 12345, Type: \"InternalRootZone\", Name: \"name\"},\n\t\t},\n\t\t{\n\t\t\tCommonResource: CommonResource{ID: 12345, Type: \"ResponsePolicyZone\", Name: \"name\"},\n\t\t},\n\t\t{\n\t\t\tCommonResource: CommonResource{ID: 12345, Type: \"Zone\", Name: \"example.com\"},\n\t\t\tAbsoluteName:   \"example.com\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_RetrieveZones_error(t *testing.T) {\n\tclient := mockBuilderAuthenticated().\n\t\tRoute(\"GET /api/v2/zones\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized),\n\t\t).\n\t\tBuild(t)\n\n\topts := &CollectionOptions{\n\t\tFilter: And(\n\t\t\tEq(\"absoluteName\", \"example.com\"),\n\t\t\tEq(\"configuration.name\", \"myConfiguration\"),\n\t\t\tEq(\"view.name\", \"myView\"),\n\t\t).String(),\n\t}\n\n\t_, err := client.RetrieveZones(mockToken(t.Context()), opts)\n\trequire.EqualError(t, err, \"401: Unauthorized: InvalidAuthorizationToken: The provided authorization token is invalid\")\n}\n\nfunc TestClient_RetrieveZoneDeployments(t *testing.T) {\n\tclient := mockBuilderAuthenticated().\n\t\tRoute(\"GET /api/v2/zones/456789/deployments\",\n\t\t\tservermock.ResponseFromFixture(\"getZoneDeployments.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"filter\", \"id:eq('12345')\"),\n\t\t).\n\t\tBuild(t)\n\n\topts := &CollectionOptions{\n\t\tFilter: Eq(\"id\", \"12345\").String(),\n\t}\n\n\tresult, err := client.RetrieveZoneDeployments(mockToken(t.Context()), 456789, opts)\n\trequire.NoError(t, err)\n\n\texpected := []QuickDeployment{\n\t\t{\n\t\t\tCommonResource:     CommonResource{ID: 12345, Type: \"QuickDeployment\", Name: \"\"},\n\t\t\tState:              \"PENDING\",\n\t\t\tStatus:             \"CANCEL\",\n\t\t\tMessage:            \"string\",\n\t\t\tPercentComplete:    50,\n\t\t\tCreationDateTime:   time.Date(2022, time.November, 23, 2, 53, 0, 0, time.UTC),\n\t\t\tStartDateTime:      time.Date(2022, time.November, 23, 2, 53, 3, 0, time.UTC),\n\t\t\tCompletionDateTime: time.Date(2022, time.November, 23, 2, 54, 5, 0, time.UTC),\n\t\t\tMethod:             \"SCHEDULED\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_CreateZoneDeployment(t *testing.T) {\n\tclient := mockBuilderAuthenticated().\n\t\tRoute(\"POST /api/v2/zones/12345/deployments\",\n\t\t\tservermock.ResponseFromFixture(\"postZoneDeployment.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"postZoneDeployment-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\tquickDeployment, err := client.CreateZoneDeployment(mockToken(t.Context()), 12345)\n\trequire.NoError(t, err)\n\n\texpected := &QuickDeployment{\n\t\tCommonResource:     CommonResource{ID: 12345, Type: \"QuickDeployment\"},\n\t\tState:              \"PENDING\",\n\t\tStatus:             \"CANCEL\",\n\t\tMessage:            \"string\",\n\t\tPercentComplete:    50,\n\t\tCreationDateTime:   time.Date(2022, time.November, 23, 2, 53, 0, 0, time.UTC),\n\t\tStartDateTime:      time.Date(2022, time.November, 23, 2, 53, 3, 0, time.UTC),\n\t\tCompletionDateTime: time.Date(2022, time.November, 23, 2, 54, 5, 0, time.UTC),\n\t\tMethod:             \"SCHEDULED\",\n\t}\n\n\tassert.Equal(t, expected, quickDeployment)\n}\n\nfunc TestClient_CreateZoneResourceRecord(t *testing.T) {\n\tclient := mockBuilderAuthenticated().\n\t\tRoute(\"POST /api/v2/zones/12345/resourceRecords\",\n\t\t\tservermock.ResponseFromFixture(\"postZoneResourceRecord.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"postZoneResourceRecord-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := RecordTXT{\n\t\tCommonResource: CommonResource{\n\t\t\tType: \"TXTRecord\",\n\t\t\tName: \"_acme-challenge\",\n\t\t},\n\t\tText:       \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:        120,\n\t\tRecordType: \"TXT\",\n\t}\n\n\tresult, err := client.CreateZoneResourceRecord(mockToken(t.Context()), 12345, record)\n\trequire.NoError(t, err)\n\n\texpected := &RecordTXT{\n\t\tCommonResource: CommonResource{\n\t\t\tID:   12345,\n\t\t\tType: \"ResourceRecord\",\n\t\t\tName: \"name\",\n\t\t},\n\t\tTTL:          3600,\n\t\tAbsoluteName: \"host1.example.com\",\n\t\tComment:      \"Sample comment.\",\n\t\tDynamic:      true,\n\t\tRecordType:   \"CNAME\",\n\t\tText:         \"\",\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_DeleteResourceRecord(t *testing.T) {\n\tclient := mockBuilderAuthenticated().\n\t\tRoute(\"DELETE /api/v2/resourceRecords/12345\",\n\t\t\tservermock.ResponseFromFixture(\"deleteResourceRecord.json\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.DeleteResourceRecord(mockToken(t.Context()), 12345)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/fixtures/deleteResourceRecord.json",
    "content": "{\n  \"id\": 12345,\n  \"type\": \"WorkflowRequest\",\n  \"state\": \"APPROVED\",\n  \"operation\": \"ADD_ALIAS_RECORD\",\n  \"creator\": {\n    \"id\": 103307,\n    \"type\": \"User\",\n    \"name\": \"admin\",\n    \"userDefinedFields\": {\n      \"udf1\": \"value1\",\n      \"udf2\": \"value2\"\n    },\n    \"authenticator\": {\n      \"id\": 12345,\n      \"type\": \"Authenticator\",\n      \"name\": \"LDAP authenticator\"\n    },\n    \"email\": \"user@example.com\",\n    \"phoneNumber\": \"555-1234\",\n    \"securityPrivilege\": \"NO_ACCESS\",\n    \"historyPrivilege\": \"HIDE\",\n    \"accessType\": \"GUI\",\n    \"passwordResetRequired\": true,\n    \"accountLocked\": true,\n    \"x509Required\": true,\n    \"administrativeAccessRights\": [\n      {\n        \"resourceType\": \"Event\",\n        \"accessLevel\": \"HIDE\"\n      }\n    ]\n  },\n  \"resourceId\": 0,\n  \"resourceType\": \"ACL\",\n  \"fieldUpdates\": [\n    {\n      \"name\": \"string\",\n      \"value\": {},\n      \"previousValue\": {}\n    }\n  ],\n  \"dependentRequest\": \"string\",\n  \"modifier\": {\n    \"id\": 103307,\n    \"type\": \"User\",\n    \"name\": \"admin\",\n    \"userDefinedFields\": {\n      \"udf1\": \"value1\",\n      \"udf2\": \"value2\"\n    },\n    \"authenticator\": {\n      \"id\": 12345,\n      \"type\": \"Authenticator\",\n      \"name\": \"LDAP authenticator\"\n    },\n    \"email\": \"user@example.com\",\n    \"phoneNumber\": \"555-1234\",\n    \"securityPrivilege\": \"NO_ACCESS\",\n    \"historyPrivilege\": \"HIDE\",\n    \"accessType\": \"GUI\",\n    \"passwordResetRequired\": true,\n    \"accountLocked\": true,\n    \"x509Required\": true,\n    \"administrativeAccessRights\": [\n      {\n        \"resourceType\": \"Event\",\n        \"accessLevel\": \"HIDE\"\n      }\n    ]\n  },\n  \"creationDateTime\": \"2022-10-17T19:11:45Z\",\n  \"modificationDateTime\": \"2022-10-18T19:11:45Z\",\n  \"comment\": \"Sample comment.\"\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/fixtures/error.json",
    "content": "{\n  \"status\": 401,\n  \"reason\": \"Unauthorized\",\n  \"code\": \"InvalidAuthorizationToken\",\n  \"message\": \"The provided authorization token is invalid\"\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/fixtures/getZoneDeployments.json",
    "content": "{\n  \"count\": 0,\n  \"totalCount\": 0,\n  \"data\": [\n    {\n      \"id\": 12345,\n      \"type\": \"QuickDeployment\",\n      \"state\": \"PENDING\",\n      \"status\": \"CANCEL\",\n      \"message\": \"string\",\n      \"percentComplete\": 50,\n      \"creationDateTime\": \"2022-11-23T02:53:00Z\",\n      \"startDateTime\": \"2022-11-23T02:53:03Z\",\n      \"completionDateTime\": \"2022-11-23T02:54:05Z\",\n      \"user\": {\n        \"id\": 103307,\n        \"type\": \"User\",\n        \"name\": \"admin\",\n        \"userDefinedFields\": {\n          \"udf1\": \"value1\",\n          \"udf2\": \"value2\"\n        },\n        \"authenticator\": {\n          \"id\": 12345,\n          \"type\": \"Authenticator\",\n          \"name\": \"LDAP authenticator\"\n        },\n        \"email\": \"user@example.com\",\n        \"phoneNumber\": \"555-1234\",\n        \"securityPrivilege\": \"NO_ACCESS\",\n        \"historyPrivilege\": \"HIDE\",\n        \"accessType\": \"GUI\",\n        \"passwordResetRequired\": true,\n        \"accountLocked\": true,\n        \"x509Required\": true,\n        \"administrativeAccessRights\": [\n          {\n            \"resourceType\": \"Event\",\n            \"accessLevel\": \"HIDE\"\n          }\n        ]\n      },\n      \"method\": \"SCHEDULED\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/fixtures/postSession-request.json",
    "content": "{\n  \"username\": \"userA\",\n  \"password\": \"secret\"\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/fixtures/postSession.json",
    "content": "{\n  \"id\": 12345,\n  \"type\": \"UserSession\",\n  \"apiToken\": \"VZoO2Z0BjBaJyvuhE4vNJRWqI9upwDHk70UNi0Ez\",\n  \"apiTokenExpirationDateTime\": \"2022-09-15T17:52:07Z\",\n  \"basicAuthenticationCredentials\": \"YXBpOlQ0OExOT3VIRGhDcnVBNEo1bGFES3JuS3hTZC9QK3pjczZXTzBJMDY=\",\n  \"remoteAddress\": \"192.168.1.1\",\n  \"readOnly\": true,\n  \"loginDateTime\": \"2022-09-14T17:45:03Z\",\n  \"logoutDateTime\": \"2022-09-14T19:45:03Z\",\n  \"state\": \"LOGGED_IN\",\n  \"response\": \"Authentication Error: Ensure that your username and password are correct.\",\n  \"user\": {\n    \"id\": 103307,\n    \"type\": \"User\",\n    \"name\": \"admin\",\n    \"userDefinedFields\": {\n      \"udf1\": \"value1\",\n      \"udf2\": \"value2\"\n    },\n    \"authenticator\": {\n      \"id\": 12345,\n      \"type\": \"Authenticator\",\n      \"name\": \"LDAP authenticator\"\n    },\n    \"email\": \"user@example.com\",\n    \"phoneNumber\": \"555-1234\",\n    \"securityPrivilege\": \"NO_ACCESS\",\n    \"historyPrivilege\": \"HIDE\",\n    \"accessType\": \"GUI\",\n    \"passwordResetRequired\": true,\n    \"accountLocked\": true,\n    \"x509Required\": true,\n    \"administrativeAccessRights\": [\n      {\n        \"resourceType\": \"Event\",\n        \"accessLevel\": \"HIDE\"\n      }\n    ]\n  },\n  \"authenticator\": {\n    \"id\": 12345,\n    \"type\": \"Authenticator\",\n    \"name\": \"LDAP authenticator\",\n    \"userDefinedFields\": {\n      \"udf1\": \"value1\",\n      \"udf2\": \"value2\"\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/fixtures/postZoneDeployment-request.json",
    "content": "{\n  \"type\": \"QuickDeployment\"\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/fixtures/postZoneDeployment.json",
    "content": "{\n  \"id\": 12345,\n  \"type\": \"QuickDeployment\",\n  \"state\": \"PENDING\",\n  \"status\": \"CANCEL\",\n  \"message\": \"string\",\n  \"percentComplete\": 50,\n  \"creationDateTime\": \"2022-11-23T02:53:00Z\",\n  \"startDateTime\": \"2022-11-23T02:53:03Z\",\n  \"completionDateTime\": \"2022-11-23T02:54:05Z\",\n  \"user\": {\n    \"id\": 103307,\n    \"type\": \"User\",\n    \"name\": \"admin\",\n    \"userDefinedFields\": {\n      \"udf1\": \"value1\",\n      \"udf2\": \"value2\"\n    },\n    \"authenticator\": {\n      \"id\": 12345,\n      \"type\": \"Authenticator\",\n      \"name\": \"LDAP authenticator\"\n    },\n    \"email\": \"user@example.com\",\n    \"phoneNumber\": \"555-1234\",\n    \"securityPrivilege\": \"NO_ACCESS\",\n    \"historyPrivilege\": \"HIDE\",\n    \"accessType\": \"GUI\",\n    \"passwordResetRequired\": true,\n    \"accountLocked\": true,\n    \"x509Required\": true,\n    \"administrativeAccessRights\": [\n      {\n        \"resourceType\": \"Event\",\n        \"accessLevel\": \"HIDE\"\n      }\n    ]\n  },\n  \"method\": \"SCHEDULED\"\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord-request.json",
    "content": "{\n  \"type\": \"TXTRecord\",\n  \"name\": \"_acme-challenge\",\n  \"ttl\": 120,\n  \"recordType\": \"TXT\",\n  \"text\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/fixtures/postZoneResourceRecord.json",
    "content": "{\n  \"id\": 12345,\n  \"type\": \"ResourceRecord\",\n  \"name\": \"name\",\n  \"userDefinedFields\": {\n    \"udf1\": \"value1\",\n    \"udf2\": \"value2\"\n  },\n  \"configuration\": {\n    \"id\": 12345,\n    \"type\": \"Configuration\",\n    \"name\": \"name\"\n  },\n  \"ttl\": 3600,\n  \"absoluteName\": \"host1.example.com\",\n  \"comment\": \"Sample comment.\",\n  \"dynamic\": true,\n  \"recordType\": \"CNAME\",\n  \"linkedRecord\": {\n    \"id\": 12345,\n    \"type\": \"ResourceRecord\",\n    \"name\": \"name\",\n    \"absoluteName\": \"host1.example.com\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/fixtures/zones.json",
    "content": "{\n  \"count\": 0,\n  \"totalCount\": 0,\n  \"data\": [\n    {\n      \"id\": 12345,\n      \"type\": \"ENUMZone\",\n      \"name\": \"5678\",\n      \"userDefinedFields\": {\n        \"udf1\": \"value1\",\n        \"udf2\": \"value2\"\n      },\n      \"configuration\": {\n        \"id\": 12345,\n        \"type\": \"Configuration\",\n        \"name\": \"name\"\n      },\n      \"view\": {\n        \"id\": 12345,\n        \"type\": \"View\",\n        \"name\": \"default\",\n        \"userDefinedFields\": {\n          \"udf1\": \"value1\",\n          \"udf2\": \"value2\"\n        },\n        \"configuration\": {\n          \"id\": 12345,\n          \"type\": \"Configuration\",\n          \"name\": \"name\"\n        },\n        \"deviceRegistrationEnabled\": true,\n        \"deviceRegistrationPortalAddress\": \"10.10.10.10\"\n      },\n      \"deploymentEnabled\": true,\n      \"absoluteName\": \"string\"\n    },\n    {\n      \"id\": 12345,\n      \"type\": \"ExternalHostsZone\",\n      \"name\": \"name\",\n      \"userDefinedFields\": {\n        \"udf1\": \"value1\",\n        \"udf2\": \"value2\"\n      },\n      \"configuration\": {\n        \"id\": 12345,\n        \"type\": \"Configuration\",\n        \"name\": \"name\"\n      },\n      \"view\": {\n        \"id\": 12345,\n        \"type\": \"View\",\n        \"name\": \"default\",\n        \"userDefinedFields\": {\n          \"udf1\": \"value1\",\n          \"udf2\": \"value2\"\n        },\n        \"configuration\": {\n          \"id\": 12345,\n          \"type\": \"Configuration\",\n          \"name\": \"name\"\n        },\n        \"deviceRegistrationEnabled\": true,\n        \"deviceRegistrationPortalAddress\": \"10.10.10.10\"\n      }\n    },\n    {\n      \"id\": 12345,\n      \"type\": \"InternalRootZone\",\n      \"name\": \"name\",\n      \"userDefinedFields\": {\n        \"udf1\": \"value1\",\n        \"udf2\": \"value2\"\n      },\n      \"configuration\": {\n        \"id\": 12345,\n        \"type\": \"Configuration\",\n        \"name\": \"name\"\n      },\n      \"view\": {\n        \"id\": 12345,\n        \"type\": \"View\",\n        \"name\": \"default\",\n        \"userDefinedFields\": {\n          \"udf1\": \"value1\",\n          \"udf2\": \"value2\"\n        },\n        \"configuration\": {\n          \"id\": 12345,\n          \"type\": \"Configuration\",\n          \"name\": \"name\"\n        },\n        \"deviceRegistrationEnabled\": true,\n        \"deviceRegistrationPortalAddress\": \"10.10.10.10\"\n      },\n      \"deploymentEnabled\": true\n    },\n    {\n      \"id\": 12345,\n      \"type\": \"ResponsePolicyZone\",\n      \"name\": \"name\",\n      \"userDefinedFields\": {\n        \"udf1\": \"value1\",\n        \"udf2\": \"value2\"\n      },\n      \"configuration\": {\n        \"id\": 12345,\n        \"type\": \"Configuration\",\n        \"name\": \"name\"\n      },\n      \"view\": {\n        \"id\": 12345,\n        \"type\": \"View\",\n        \"name\": \"default\",\n        \"userDefinedFields\": {\n          \"udf1\": \"value1\",\n          \"udf2\": \"value2\"\n        },\n        \"configuration\": {\n          \"id\": 12345,\n          \"type\": \"Configuration\",\n          \"name\": \"name\"\n        },\n        \"deviceRegistrationEnabled\": true,\n        \"deviceRegistrationPortalAddress\": \"10.10.10.10\"\n      },\n      \"responsePolicyZoneType\": \"LOCAL\",\n      \"responsePolicy\": {\n        \"id\": 12345,\n        \"type\": \"ResponsePolicy\",\n        \"name\": \"Block Response Policy\"\n      },\n      \"overridePolicyType\": \"ALLOWLIST\",\n      \"overrideRefreshTime\": \"string\",\n      \"redirectTarget\": \"string\",\n      \"feedCategories\": [\n        \"string\"\n      ]\n    },\n    {\n      \"id\": 12345,\n      \"type\": \"Zone\",\n      \"name\": \"example.com\",\n      \"userDefinedFields\": {\n        \"udf1\": \"value1\",\n        \"udf2\": \"value2\"\n      },\n      \"configuration\": {\n        \"id\": 12345,\n        \"type\": \"Configuration\",\n        \"name\": \"name\"\n      },\n      \"view\": {\n        \"id\": 12345,\n        \"type\": \"View\",\n        \"name\": \"default\",\n        \"userDefinedFields\": {\n          \"udf1\": \"value1\",\n          \"udf2\": \"value2\"\n        },\n        \"configuration\": {\n          \"id\": 12345,\n          \"type\": \"Configuration\",\n          \"name\": \"name\"\n        },\n        \"deviceRegistrationEnabled\": true,\n        \"deviceRegistrationPortalAddress\": \"10.10.10.10\"\n      },\n      \"deploymentEnabled\": true,\n      \"dynamicUpdateEnabled\": true,\n      \"template\": {\n        \"id\": 12345,\n        \"type\": \"ZoneTemplate\",\n        \"name\": \"name\"\n      },\n      \"signed\": true,\n      \"signingPolicy\": {\n        \"id\": 12345,\n        \"type\": \"DNSSECSigningPolicy\",\n        \"name\": \"name\"\n      },\n      \"absoluteName\": \"example.com\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\ntype token string\n\nconst tokenKey token = \"token\"\n\nconst authorizationHeader = \"Authorization\"\n\n// CreateSession creates a new session.\nfunc (c *Client) CreateSession(ctx context.Context, info LoginInfo) (*Session, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v2\", \"sessions\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, info)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := new(Session)\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// CreateAuthenticatedContext creates a new authenticated context.\nfunc (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) {\n\ttok, err := c.CreateSession(ctx, LoginInfo{Username: c.username, Password: c.password})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create session: %w\", err)\n\t}\n\n\treturn context.WithValue(ctx, tokenKey, tok.BasicAuthenticationCredentials), nil\n}\n\nfunc (c *Client) doAuthenticated(ctx context.Context, req *http.Request, result any) error {\n\ttok := getToken(ctx)\n\tif tok != \"\" {\n\t\treq.Header.Set(authorizationHeader, \"Basic \"+tok)\n\t}\n\n\treturn c.do(req, result)\n}\n\nfunc getToken(ctx context.Context) string {\n\ttok, ok := ctx.Value(tokenKey).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn tok\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/identity_test.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(server.URL, \"userA\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders(),\n\t)\n}\n\nfunc mockToken(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, tokenKey, \"secretToken\")\n}\n\nfunc TestClient_CreateSession(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /api/v2/sessions\",\n\t\t\tservermock.ResponseFromFixture(\"postSession.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"postSession-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\tinfo := LoginInfo{\n\t\tUsername: \"userA\",\n\t\tPassword: \"secret\",\n\t}\n\n\tresult, err := client.CreateSession(mockToken(t.Context()), info)\n\trequire.NoError(t, err)\n\n\texpected := &Session{\n\t\tID:                             12345,\n\t\tType:                           \"UserSession\",\n\t\tAPIToken:                       \"VZoO2Z0BjBaJyvuhE4vNJRWqI9upwDHk70UNi0Ez\",\n\t\tAPITokenExpirationDateTime:     time.Date(2022, time.September, 15, 17, 52, 7, 0, time.UTC),\n\t\tBasicAuthenticationCredentials: \"YXBpOlQ0OExOT3VIRGhDcnVBNEo1bGFES3JuS3hTZC9QK3pjczZXTzBJMDY=\",\n\t\tRemoteAddress:                  \"192.168.1.1\",\n\t\tReadOnly:                       true,\n\t\tLoginDateTime:                  time.Date(2022, time.September, 14, 17, 45, 3, 0, time.UTC),\n\t\tLogoutDateTime:                 time.Date(2022, time.September, 14, 19, 45, 3, 0, time.UTC),\n\t\tState:                          \"LOGGED_IN\",\n\t\tResponse:                       \"Authentication Error: Ensure that your username and password are correct.\",\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_CreateAuthenticatedContext(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /api/v2/sessions\",\n\t\t\tservermock.ResponseFromFixture(\"postSession.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"postSession-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\tctx, err := client.CreateAuthenticatedContext(t.Context())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"YXBpOlQ0OExOT3VIRGhDcnVBNEo1bGFES3JuS3hTZC9QK3pjczZXTzBJMDY=\", getToken(ctx))\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/predicates.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype Predicate struct {\n\tfield    string\n\toperator string\n\tvalues   []string\n}\n\nfunc (p *Predicate) String() string {\n\tvar values []string\n\tfor _, v := range p.values {\n\t\tvalues = append(values, fmt.Sprintf(\"'%s'\", v))\n\t}\n\n\treturn fmt.Sprintf(\"%s:%s(%s)\", p.field, p.operator, strings.Join(values, \", \"))\n}\n\nfunc Eq(field, value string) *Predicate {\n\treturn &Predicate{field: field, operator: \"eq\", values: []string{value}}\n}\n\nfunc Contains(field, value string) *Predicate {\n\treturn &Predicate{field: field, operator: \"contains\", values: []string{value}}\n}\n\nfunc StartsWith(field, value string) *Predicate {\n\treturn &Predicate{field: field, operator: \"startsWith\", values: []string{value}}\n}\n\nfunc EndsWith(field, value string) *Predicate {\n\treturn &Predicate{field: field, operator: \"endsWith\", values: []string{value}}\n}\n\nfunc In(field string, values ...string) *Predicate {\n\treturn &Predicate{field: field, operator: \"in\", values: values}\n}\n\ntype Combined struct {\n\tpredicates []*Predicate\n\toperator   string\n}\n\nfunc (o *Combined) String() string {\n\tvar parts []string\n\n\tfor _, predicate := range o.predicates {\n\t\tparts = append(parts, predicate.String())\n\t}\n\n\treturn strings.Join(parts, \" \"+o.operator+\" \")\n}\n\nfunc And(predicates ...*Predicate) *Combined {\n\treturn &Combined{predicates: predicates, operator: \"and\"}\n}\n\nfunc Or(predicates ...*Predicate) *Combined {\n\treturn &Combined{predicates: predicates, operator: \"or\"}\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/predicates_test.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPredicate(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tpredicate fmt.Stringer\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"Equals\",\n\t\t\tpredicate: Eq(\"foo\", \"bar\"),\n\t\t\texpected:  \"foo:eq('bar')\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"Contains\",\n\t\t\tpredicate: Contains(\"foo\", \"bar\"),\n\t\t\texpected:  \"foo:contains('bar')\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"Starts with\",\n\t\t\tpredicate: StartsWith(\"foo\", \"bar\"),\n\t\t\texpected:  \"foo:startsWith('bar')\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"Ends with\",\n\t\t\tpredicate: EndsWith(\"foo\", \"bar\"),\n\t\t\texpected:  \"foo:endsWith('bar')\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"Match a list of values\",\n\t\t\tpredicate: In(\"foo\", \"bar\", \"bir\"),\n\t\t\texpected:  \"foo:in('bar', 'bir')\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"Combined: and\",\n\t\t\tpredicate: And(Eq(\"foo\", \"bar\"), Eq(\"fii\", \"bir\")),\n\t\t\texpected:  \"foo:eq('bar') and fii:eq('bir')\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"Combined: multiple and\",\n\t\t\tpredicate: And(\n\t\t\t\tEq(\"foo\", \"bar\"),\n\t\t\t\tEq(\"fii\", \"bir\"),\n\t\t\t\tEq(\"fuu\", \"bur\"),\n\t\t\t),\n\t\t\texpected: \"foo:eq('bar') and fii:eq('bir') and fuu:eq('bur')\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"Combined: or\",\n\t\t\tpredicate: Or(Eq(\"foo\", \"bar\"), Eq(\"foo\", \"bir\")),\n\t\t\texpected:  \"foo:eq('bar') or foo:eq('bir')\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"Combined: multiple or\",\n\t\t\tpredicate: Or(\n\t\t\t\tEq(\"foo\", \"bar\"),\n\t\t\t\tEq(\"foo\", \"bir\"),\n\t\t\t\tEq(\"foo\", \"bur\"),\n\t\t\t),\n\t\t\texpected: \"foo:eq('bar') or foo:eq('bir') or foo:eq('bur')\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassert.Equal(t, test.expected, test.predicate.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/bluecatv2/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\n// Quick deployment states.\n//\n//nolint:misspell // US vs UK\nconst (\n\tQDStatePending               = \"PENDING\"\n\tQDStateQueued                = \"QUEUED\"\n\tQDStateRunning               = \"RUNNING\"\n\tQDStateCancelled             = \"CANCELLED\"\n\tQDStateCancelling            = \"CANCELLING\"\n\tQDStateCompleted             = \"COMPLETED\"\n\tQDStateCompletedWithErrors   = \"COMPLETED_WITH_ERRORS\"\n\tQDStateCompletedWithWarnings = \"COMPLETED_WITH_WARNINGS\"\n\tQDStateFailed                = \"FAILED\"\n\tQDStateUnknown               = \"UNKNOWN\"\n)\n\n// APIError represents an error.\n// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Errors/9.6.0\ntype APIError struct {\n\tStatus  int    `json:\"status\"`\n\tReason  string `json:\"reason\"`\n\tCode    string `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\nfunc (a *APIError) Error() string {\n\treturn fmt.Sprintf(\"%d: %s: %s: %s\", a.Status, a.Reason, a.Code, a.Message)\n}\n\n// CommonResource represents the common resource fields.\n// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Resources/9.6.0\ntype CommonResource struct {\n\tID   int64  `json:\"id,omitempty\"`\n\tType string `json:\"type,omitempty\"`\n\tName string `json:\"name,omitempty\"`\n}\n\n// Collection represents a collection of resources.\n// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Collections/9.6.0\ntype Collection[T any] struct {\n\tCount      int64 `json:\"count\"`\n\tTotalCount int64 `json:\"totalCount\"`\n\tData       []T   `json:\"data\"`\n}\n\ntype CollectionOptions struct {\n\t// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Fields/9.6.0\n\tFields string `url:\"fields,omitempty\"`\n\n\t// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Pagination/9.6.0\n\tLimit  int `url:\"limit,omitempty\"`\n\tOffset int `url:\"offset,omitempty\"`\n\n\t// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Filter/9.6.0\n\tFilter string `url:\"filter,omitempty\"`\n\n\t// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Ordering/9.6.0\n\tOrderBy string `url:\"orderBy,omitempty\"`\n\n\t// Should return or not the total number of resources matching the query.\n\tTotal bool `url:\"total,omitempty\"`\n}\n\ntype RecordTXT struct {\n\tCommonResource\n\n\tTTL          int    `json:\"ttl,omitempty\"`\n\tAbsoluteName string `json:\"absoluteName,omitempty\"`\n\tComment      string `json:\"comment,omitempty\"`\n\tDynamic      bool   `json:\"dynamic,omitempty\"`\n\tRecordType   string `json:\"recordType,omitempty\"`\n\tText         string `json:\"text,omitempty\"`\n}\n\ntype ZoneResource struct {\n\tCommonResource\n\n\tAbsoluteName string `json:\"absoluteName,omitempty\"`\n}\n\ntype QuickDeployment struct {\n\tCommonResource\n\n\tState              string    `json:\"state,omitempty\"`\n\tStatus             string    `json:\"status,omitempty\"`\n\tMessage            string    `json:\"message,omitempty\"`\n\tPercentComplete    int       `json:\"percentComplete,omitempty\"`\n\tCreationDateTime   time.Time `json:\"creationDateTime,omitzero\"`\n\tStartDateTime      time.Time `json:\"startDateTime,omitzero\"`\n\tCompletionDateTime time.Time `json:\"completionDateTime,omitzero\"`\n\tMethod             string    `json:\"method,omitempty\"`\n}\n\n// LoginInfo represents the login information.\n// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Creating-an-API-session/9.6.0\ntype LoginInfo struct {\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n}\n\n// Session represents the session.\n// https://docs.bluecatnetworks.com/r/Address-Manager-RESTful-v2-API-Guide/Creating-an-API-session/9.6.0\ntype Session struct {\n\tID                             int       `json:\"id\"`\n\tType                           string    `json:\"type\"`\n\tAPIToken                       string    `json:\"apiToken\"`\n\tAPITokenExpirationDateTime     time.Time `json:\"apiTokenExpirationDateTime\"`\n\tBasicAuthenticationCredentials string    `json:\"basicAuthenticationCredentials\"`\n\tRemoteAddress                  string    `json:\"remoteAddress\"`\n\tReadOnly                       bool      `json:\"readOnly\"`\n\tLoginDateTime                  time.Time `json:\"loginDateTime\"`\n\tLogoutDateTime                 time.Time `json:\"logoutDateTime\"`\n\tState                          string    `json:\"state\"`\n\tResponse                       string    `json:\"response\"`\n}\n"
  },
  {
    "path": "providers/dns/bookmyname/bookmyname.go",
    "content": "// Package bookmyname implements a DNS provider for solving the DNS-01 challenge using BookMyName.\npackage bookmyname\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/bookmyname/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"BOOKMYNAME_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername string\n\tPassword string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for BookMyName.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"bookmyname: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for BookMyName.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"bookmyname: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.Username, config.Password)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"bookmyname: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecord := internal.Record{\n\t\tHostname: dns01.UnFqdn(info.EffectiveFQDN),\n\t\tType:     \"txt\",\n\t\tTTL:      d.config.TTL,\n\t\tValue:    info.Value,\n\t}\n\n\terr := d.client.AddRecord(context.Background(), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bookmyname: add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecord := internal.Record{\n\t\tHostname: dns01.UnFqdn(info.EffectiveFQDN),\n\t\tType:     \"txt\",\n\t\tTTL:      d.config.TTL,\n\t\tValue:    info.Value,\n\t}\n\n\terr := d.client.RemoveRecord(context.Background(), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bookmyname: add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/bookmyname/bookmyname.toml",
    "content": "Name = \"BookMyName\"\nDescription = ''''''\nURL = \"https://www.bookmyname.com/\"\nCode = \"bookmyname\"\nSince = \"v4.23.0\"\n\nExample = '''\nBOOKMYNAME_USERNAME=\"xxx\" \\\nBOOKMYNAME_PASSWORD=\"yyy\" \\\nlego --dns bookmyname -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    BOOKMYNAME_USERNAME = \"Username\"\n    BOOKMYNAME_PASSWORD = \"Password\"\n  [Configuration.Additional]\n    BOOKMYNAME_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    BOOKMYNAME_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    BOOKMYNAME_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    BOOKMYNAME_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://fr.faqs.bookmyname.com/frfaqs/dyndns\"\n"
  },
  {
    "path": "providers/dns/bookmyname/bookmyname_test.go",
    "content": "package bookmyname\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"bookmyname: some credentials information are missing: BOOKMYNAME_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing paswword\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"bookmyname: some credentials information are missing: BOOKMYNAME_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"bookmyname: some credentials information are missing: BOOKMYNAME_USERNAME,BOOKMYNAME_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"bookmyname: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tusername: \"user\",\n\t\t\texpected: \"bookmyname: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"bookmyname: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/bookmyname/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\nconst defaultBaseURL = \"https://www.bookmyname.com/dyndns/\"\n\n// Client the BookMyName API client.\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tbaseURL    string\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(username, password string) (*Client, error) {\n\tif username == \"\" || password == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\treturn &Client{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\tbaseURL:    defaultBaseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) AddRecord(ctx context.Context, record Record) error {\n\tendpoint, err := c.createEndpoint(record, \"add\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(ctx, endpoint)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) RemoveRecord(ctx context.Context, record Record) error {\n\tendpoint, err := c.createEndpoint(record, \"remove\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(ctx, endpoint)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) createEndpoint(record Record, action string) (*url.URL, error) {\n\tendpoint, err := url.Parse(c.baseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parse URL:  %w\", err)\n\t}\n\n\tvalues, err := querystring.Values(record)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"query parameters: %w\", err)\n\t}\n\n\tvalues.Set(\"do\", action)\n\n\tendpoint.RawQuery = values.Encode()\n\n\treturn endpoint, nil\n}\n\nfunc (c *Client) do(ctx context.Context, endpoint *url.URL) error {\n\tendpoint.User = url.UserPassword(c.username, c.password)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\tif !strings.HasPrefix(string(raw), \"good: update done\") && !strings.HasPrefix(string(raw), \"good: remove done\") {\n\t\treturn fmt.Errorf(\"unexpected response: %s\", string(bytes.TrimSpace(raw)))\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/bookmyname/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"user\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL = server.URL\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithBasicAuth(\"user\", \"secret\"))\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /\",\n\t\t\tservermock.ResponseFromFixture(\"add_success.txt\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"do\", \"add\").\n\t\t\t\tWith(\"hostname\", \"_acme-challenge.sub.example.com.\").\n\t\t\t\tWith(\"type\", \"txt\").\n\t\t\t\tWith(\"value\", \"test\").\n\t\t\t\tWith(\"ttl\", \"300\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tHostname: \"_acme-challenge.sub.example.com.\",\n\t\tType:     \"txt\",\n\t\tTTL:      300,\n\t\tValue:    \"test\",\n\t}\n\n\terr := client.AddRecord(t.Context(), record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /\",\n\t\t\tservermock.ResponseFromFixture(\"error.txt\"),\n\t\t\tservermock.CheckQueryParameter().\n\t\t\t\tWith(\"do\", \"add\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tHostname: \"_acme-challenge.sub.example.com.\",\n\t\tType:     \"txt\",\n\t\tTTL:      300,\n\t\tValue:    \"test\",\n\t}\n\n\terr := client.AddRecord(t.Context(), record)\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"unexpected response: notfqdn: Host _acme-challenge.sub.example.com. malformed / vhn\")\n}\n\nfunc TestClient_RemoveRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /\",\n\t\t\tservermock.ResponseFromFixture(\"remove_success.txt\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"do\", \"remove\").\n\t\t\t\tWith(\"hostname\", \"_acme-challenge.sub.example.com.\").\n\t\t\t\tWith(\"type\", \"txt\").\n\t\t\t\tWith(\"value\", \"test\").\n\t\t\t\tWith(\"ttl\", \"300\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tHostname: \"_acme-challenge.sub.example.com.\",\n\t\tType:     \"txt\",\n\t\tTTL:      300,\n\t\tValue:    \"test\",\n\t}\n\n\terr := client.RemoveRecord(t.Context(), record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_RemoveRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /\",\n\t\t\tservermock.ResponseFromFixture(\"error.txt\"),\n\t\t\tservermock.CheckQueryParameter().\n\t\t\t\tWith(\"do\", \"remove\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tHostname: \"_acme-challenge.sub.example.com.\",\n\t\tType:     \"txt\",\n\t\tTTL:      300,\n\t\tValue:    \"test\",\n\t}\n\n\terr := client.RemoveRecord(t.Context(), record)\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"unexpected response: notfqdn: Host _acme-challenge.sub.example.com. malformed / vhn\")\n}\n"
  },
  {
    "path": "providers/dns/bookmyname/internal/fixtures/add_success.txt",
    "content": "good: update done, cid 123, domain id 456, type txt, ip xxx\n"
  },
  {
    "path": "providers/dns/bookmyname/internal/fixtures/error.txt",
    "content": "notfqdn: Host _acme-challenge.sub.example.com. malformed / vhn\n"
  },
  {
    "path": "providers/dns/bookmyname/internal/fixtures/remove_success.txt",
    "content": "good: remove done 1, cid 123, domain id 456, ttl 300, type txt, ip xxx\n"
  },
  {
    "path": "providers/dns/bookmyname/internal/types.go",
    "content": "package internal\n\ntype Record struct {\n\tHostname string `url:\"hostname\"`\n\tType     string `url:\"type\"`\n\tTTL      int    `url:\"ttl\"`\n\tValue    string `url:\"value\"`\n}\n"
  },
  {
    "path": "providers/dns/brandit/brandit.go",
    "content": "package brandit\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/brandit/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"BRANDIT_\"\n\n\tEnvAPIKey      = envNamespace + \"API_KEY\"\n\tEnvAPIUsername = envNamespace + \"API_USERNAME\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey      string\n\tAPIUsername string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecords   map[string]string\n\trecordsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for BrandIT.\n// Credentials must be passed in the environment variables: BRANDIT_API_KEY, BRANDIT_API_USERNAME.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey, EnvAPIUsername)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"brandit: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.APIUsername = values[EnvAPIUsername]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for BrandIT.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"brandit: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.APIUsername, config.APIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"brandit: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:  config,\n\t\tclient:  client,\n\t\trecords: make(map[string]string),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"brandit: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"brandit: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\trecord := internal.Record{\n\t\tType:    \"TXT\",\n\t\tName:    subDomain,\n\t\tContent: info.Value,\n\t\tTTL:     d.config.TTL,\n\t}\n\n\t// find the account associated with the domain\n\taccount, err := d.client.StatusDomain(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"brandit: status domain: %w\", err)\n\t}\n\n\t// Find the next record id\n\trecordID, err := d.client.ListRecords(ctx, account.Registrar[0], dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"brandit: list records: %w\", err)\n\t}\n\n\tresult, err := d.client.AddRecord(ctx, dns01.UnFqdn(authZone), account.Registrar[0], strconv.Itoa(recordID.Total[0]), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"brandit: add record: %w\", err)\n\t}\n\n\td.recordsMu.Lock()\n\td.records[token] = result.Record\n\td.recordsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"brandit: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// gets the record's unique ID\n\td.recordsMu.Lock()\n\tdnsRecord, ok := d.records[token]\n\td.recordsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"brandit: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\tctx := context.Background()\n\n\t// find the account associated with the domain\n\taccount, err := d.client.StatusDomain(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"brandit: status domain: %w\", err)\n\t}\n\n\trecords, err := d.client.ListRecords(ctx, account.Registrar[0], dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"brandit: list records: %w\", err)\n\t}\n\n\tvar recordID int\n\n\tfor i, r := range records.RR {\n\t\tif r == dnsRecord {\n\t\t\trecordID = i\n\t\t}\n\t}\n\n\terr = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), account.Registrar[0], dnsRecord, strconv.Itoa(recordID))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"brandit: delete record: %w\", err)\n\t}\n\n\t// deletes record ID from map\n\td.recordsMu.Lock()\n\tdelete(d.records, token)\n\td.recordsMu.Unlock()\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/brandit/brandit.toml",
    "content": "Name = \"Brandit (deprecated)\"\nDescription = '''\nBrandit has been acquired by Abion.\nAbion has a different API.\n\nIf you are a Brandit/Albion user, you can try the PR https://github.com/go-acme/lego/pull/2112.\n'''\nURL = \"https://www.brandit.com/\"\nCode = \"brandit\"\nSince = \"v4.11.0\"\n\nExample = '''\nBRANDIT_API_KEY=xxxxxxxxxxxxxxxxxxxxx \\\nBRANDIT_API_USERNAME=yyyyyyyyyyyyyyyyyyyy \\\nlego --dns brandit -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    BRANDIT_API_KEY = \"The API key\"\n    BRANDIT_API_USERNAME = \"The API username\"\n  [Configuration.Additional]\n    BRANDIT_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    BRANDIT_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 600)\"\n    BRANDIT_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)\"\n    BRANDIT_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://portal.brandit.com/apidocv3\"\n"
  },
  {
    "path": "providers/dns/brandit/brandit_test.go",
    "content": "package brandit\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey, EnvAPIUsername).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:      \"key\",\n\t\t\t\tEnvAPIUsername: \"test_user\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUsername: \"test_user\",\n\t\t\t},\n\t\t\texpected: \"brandit: some credentials information are missing: BRANDIT_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"key\",\n\t\t\t},\n\t\t\texpected: \"brandit: some credentials information are missing: BRANDIT_API_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"brandit: some credentials information are missing: BRANDIT_API_KEY,BRANDIT_API_USERNAME\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tuser     string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"key\",\n\t\t\tuser:   \"test_user\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\tuser:     \"test_user\",\n\t\t\texpected: \"brandit: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret\",\n\t\t\tapiKey:   \"key\",\n\t\t\texpected: \"brandit: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"brandit: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.APIUsername = test.user\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/brandit/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://portal.brandit.com/api/v3/\"\n\n// Client a BrandIT DNS API client.\ntype Client struct {\n\tapiUsername string\n\tapiKey      string\n\n\tbaseURL    string\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiUsername, apiKey string) (*Client, error) {\n\tif apiKey == \"\" || apiUsername == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\treturn &Client{\n\t\tapiUsername: apiUsername,\n\t\tapiKey:      apiKey,\n\t\tbaseURL:     defaultBaseURL,\n\t\tHTTPClient:  &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// ListRecords lists all records.\n// https://portal.brandit.com/apidocv3#listDNSRR\nfunc (c *Client) ListRecords(ctx context.Context, account, dnsZone string) (*ListRecordsResponse, error) {\n\tquery := url.Values{}\n\tquery.Add(\"command\", \"listDNSRR\")\n\tquery.Add(\"account\", account)\n\tquery.Add(\"dnszone\", dnsZone)\n\n\tresult := &Response[*ListRecordsResponse]{}\n\n\terr := c.do(ctx, query, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor len(result.Response.RR) < result.Response.Total[0] {\n\t\tquery.Add(\"first\", strconv.Itoa(result.Response.Last[0]+1))\n\n\t\ttmp := &Response[*ListRecordsResponse]{}\n\n\t\terr := c.do(ctx, query, tmp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresult.Response.RR = append(result.Response.RR, tmp.Response.RR...)\n\t\tresult.Response.Last = tmp.Response.Last\n\t}\n\n\treturn result.Response, nil\n}\n\n// AddRecord adds a DNS record.\n// https://portal.brandit.com/apidocv3#addDNSRR\nfunc (c *Client) AddRecord(ctx context.Context, domainName, account, newRecordID string, record Record) (*AddRecord, error) {\n\tvalue := strings.Join([]string{record.Name, strconv.Itoa(record.TTL), \"IN\", record.Type, record.Content}, \" \")\n\n\tquery := url.Values{}\n\tquery.Add(\"command\", \"addDNSRR\")\n\tquery.Add(\"account\", account)\n\tquery.Add(\"dnszone\", domainName)\n\tquery.Add(\"rrdata\", value)\n\tquery.Add(\"key\", newRecordID)\n\n\tresult := &AddRecord{}\n\n\terr := c.do(ctx, query, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult.Record = value\n\n\treturn result, nil\n}\n\n// DeleteRecord deletes a DNS record.\n// https://portal.brandit.com/apidocv3#deleteDNSRR\nfunc (c *Client) DeleteRecord(ctx context.Context, domainName, account, dnsRecord, recordID string) error {\n\tquery := url.Values{}\n\tquery.Add(\"command\", \"deleteDNSRR\")\n\tquery.Add(\"account\", account)\n\tquery.Add(\"dnszone\", domainName)\n\tquery.Add(\"rrdata\", dnsRecord)\n\tquery.Add(\"key\", recordID)\n\n\treturn c.do(ctx, query, nil)\n}\n\n// StatusDomain returns the status of a domain and account associated with it.\n// https://portal.brandit.com/apidocv3#statusDomain\nfunc (c *Client) StatusDomain(ctx context.Context, domain string) (*StatusResponse, error) {\n\tquery := url.Values{}\n\n\tquery.Add(\"command\", \"statusDomain\")\n\tquery.Add(\"domain\", domain)\n\n\tresult := &Response[*StatusResponse]{}\n\n\terr := c.do(ctx, query, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Response, nil\n}\n\nfunc (c *Client) do(ctx context.Context, query url.Values, result any) error {\n\tvalues, err := sign(c.apiUsername, c.apiKey, query)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, strings.NewReader(values.Encode()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\t//  Unmarshal the error response, because the API returns a 200 OK even if there is an error.\n\tvar apiError APIError\n\n\terr = json.Unmarshal(raw, &apiError)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tif apiError.Code > 299 || apiError.Status != \"success\" {\n\t\treturn apiError\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc sign(apiUsername, apiKey string, query url.Values) (url.Values, error) {\n\ttimestamp := time.Now().UTC().Format(\"2006-01-02T15:04:05Z\")\n\n\tcanonicalRequest := fmt.Sprintf(\"%s%s%s\", apiUsername, timestamp, defaultBaseURL)\n\n\tmac := hmac.New(sha256.New, []byte(apiKey))\n\n\t_, err := mac.Write([]byte(canonicalRequest))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thashed := mac.Sum(nil)\n\tsignature := hex.EncodeToString(hashed)\n\n\tquery.Add(\"user\", apiUsername)\n\tquery.Add(\"timestamp\", timestamp)\n\tquery.Add(\"signature\", signature)\n\n\treturn query, nil\n}\n"
  },
  {
    "path": "providers/dns/brandit/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"user\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL = server.URL\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded())\n}\n\nfunc TestClient_StatusDomain(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"status-domain.json\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWithRegexp(\"signature\", \"[a-z0-9]+\").\n\t\t\t\tWithRegexp(\"timestamp\", `\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z`).\n\t\t\t\tWith(\"command\", \"statusDomain\").\n\t\t\t\tWith(\"user\", \"user\").\n\t\t\t\tWith(\"domain\", \"example.com\"),\n\t\t).\n\t\tBuild(t)\n\n\tdomain, err := client.StatusDomain(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := &StatusResponse{\n\t\tRenewalMode:                []string{\"DEFAULT\"},\n\t\tStatus:                     []string{\"clientTransferProhibited\"},\n\t\tTransferLock:               []int{1},\n\t\tRegistrar:                  []string{\"brandit\"},\n\t\tPaidUntilDate:              []string{\"2021-12-15 05:00:00.0\"},\n\t\tNameserver:                 []string{\"NS1.RRPPROXY.NET\", \"NS2.RRPPROXY.NET\"},\n\t\tRegistrationExpirationDate: []string{\"2021-12-15 05:00:00.0\"},\n\t\tDomain:                     []string{\"example.com\"},\n\t\tRenewalDate:                []string{\"2024-01-19 05:00:00.0\"},\n\t\tUpdatedDate:                []string{\"2022-12-16 08:01:27.0\"},\n\t\tBillingContact:             []string{\"example\"},\n\t\tXDomainRoID:                []string{\"example\"},\n\t\tAdminContact:               []string{\"example\"},\n\t\tTechContact:                []string{\"example\"},\n\t\tDomainIDN:                  []string{\"example.com\"},\n\t\tCreatedDate:                []string{\"2016-12-16 05:00:00.0\"},\n\t\tRegistrarTransferDate:      []string{\"2021-12-09 05:17:42.0\"},\n\t\tZone:                       []string{\"com\"},\n\t\tAuth:                       []string{\"example\"},\n\t\tUpdatedBy:                  []string{\"example\"},\n\t\tRoID:                       []string{\"example\"},\n\t\tOwnerContact:               []string{\"example\"},\n\t\tCreatedBy:                  []string{\"example\"},\n\t\tTransferMode:               []string{\"auto\"},\n\t}\n\n\tassert.Equal(t, expected, domain)\n}\n\nfunc TestClient_StatusDomain_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\t_, err := client.StatusDomain(t.Context(), \"example.com\")\n\trequire.ErrorIs(t, err, APIError{Code: 402, Status: \"error\", Message: \"Invalid user.\"})\n}\n\nfunc TestClient_ListRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"list-records.json\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWithRegexp(\"signature\", \"[a-z0-9]+\").\n\t\t\t\tWithRegexp(\"timestamp\", `\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z`).\n\t\t\t\tWith(\"account\", \"example\").\n\t\t\t\tWith(\"command\", \"listDNSRR\").\n\t\t\t\tWith(\"user\", \"user\").\n\t\t\t\tWith(\"dnszone\", \"example.com\"),\n\t\t).\n\t\tBuild(t)\n\n\tresp, err := client.ListRecords(t.Context(), \"example\", \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := &ListRecordsResponse{\n\t\tLimit:  []int{100},\n\t\tColumn: []string{\"rr\"},\n\t\tCount:  []int{1},\n\t\tFirst:  []int{0},\n\t\tTotal:  []int{1},\n\t\tRR:     []string{\"example.com. 600 IN TXT txttxttxt\"},\n\t\tLast:   []int{0},\n\t}\n\n\tassert.Equal(t, expected, resp)\n}\n\nfunc TestClient_ListRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\t_, err := client.ListRecords(t.Context(), \"example\", \"example.com\")\n\trequire.ErrorIs(t, err, APIError{Code: 402, Status: \"error\", Message: \"Invalid user.\"})\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"add-record.json\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWithRegexp(\"signature\", \"[a-z0-9]+\").\n\t\t\t\tWithRegexp(\"timestamp\", `\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z`).\n\t\t\t\tWith(\"account\", \"test\").\n\t\t\t\tWith(\"command\", \"addDNSRR\").\n\t\t\t\tWith(\"key\", \"2565\").\n\t\t\t\tWith(\"user\", \"user\").\n\t\t\t\tWith(\"rrdata\", \"example.com 600 IN TXT txttxttxt\").\n\t\t\t\tWith(\"dnszone\", \"example.com\"),\n\t\t).\n\t\tBuild(t)\n\n\ttestRecord := Record{\n\t\tID:      2565,\n\t\tType:    \"TXT\",\n\t\tName:    \"example.com\",\n\t\tContent: \"txttxttxt\",\n\t\tTTL:     600,\n\t}\n\tresp, err := client.AddRecord(t.Context(), \"example.com\", \"test\", \"2565\", testRecord)\n\trequire.NoError(t, err)\n\n\texpected := &AddRecord{\n\t\tResponse: AddRecordResponse{\n\t\t\tZoneType: []string{\"com\"},\n\t\t\tSigned:   []int{1},\n\t\t},\n\t\tRecord: \"example.com 600 IN TXT txttxttxt\",\n\t\tCode:   200,\n\t\tStatus: \"success\",\n\t\tError:  \"\",\n\t}\n\n\tassert.Equal(t, expected, resp)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\ttestRecord := Record{\n\t\tID:      2565,\n\t\tType:    \"TXT\",\n\t\tName:    \"example.com\",\n\t\tContent: \"txttxttxt\",\n\t\tTTL:     600,\n\t}\n\n\t_, err := client.AddRecord(t.Context(), \"example.com\", \"test\", \"2565\", testRecord)\n\trequire.ErrorIs(t, err, APIError{Code: 402, Status: \"error\", Message: \"Invalid user.\"})\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"delete-record.json\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWithRegexp(\"signature\", \"[a-z0-9]+\").\n\t\t\t\tWithRegexp(\"timestamp\", `\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z`).\n\t\t\t\tWith(\"account\", \"test\").\n\t\t\t\tWith(\"command\", \"deleteDNSRR\").\n\t\t\t\tWith(\"key\", \"2374\").\n\t\t\t\tWith(\"user\", \"user\").\n\t\t\t\tWith(\"rrdata\", \"example.com 600 IN TXT txttxttxt\").\n\t\t\t\tWith(\"dnszone\", \"example.com\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", \"test\", \"example.com 600 IN TXT txttxttxt\", \"2374\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", \"test\", \"example.com 600 IN TXT txttxttxt\", \"2374\")\n\trequire.ErrorIs(t, err, APIError{Code: 402, Status: \"error\", Message: \"Invalid user.\"})\n}\n"
  },
  {
    "path": "providers/dns/brandit/internal/fixtures/add-record.json",
    "content": "{\n  \"response\": {\n    \"zonetype\": [\n      \"com\"\n    ],\n    \"signed\": [\n      1\n    ]\n  },\n  \"record\": \"example.com. 600 IN TXT txttxttxt\",\n  \"code\": 200,\n  \"status\": \"success\",\n  \"error\": \"\"\n}"
  },
  {
    "path": "providers/dns/brandit/internal/fixtures/delete-record.json",
    "content": "{\n  \"code\": 200,\n  \"status\": \"success\",\n  \"error\": \"\"\n}"
  },
  {
    "path": "providers/dns/brandit/internal/fixtures/error.json",
    "content": "{\n  \"code\": 402,\n  \"status\": \"error\",\n  \"error\": \"Invalid user.\"\n}\n"
  },
  {
    "path": "providers/dns/brandit/internal/fixtures/list-records.json",
    "content": "{\n  \"response\": {\n    \"limit\": [\n      100\n    ],\n    \"column\": [\n      \"rr\"\n    ],\n    \"count\": [\n      1\n    ],\n    \"first\": [\n      0\n    ],\n    \"total\": [\n      1\n    ],\n    \"rr\": [\n      \"example.com. 600 IN TXT txttxttxt\"\n    ],\n    \"last\": [\n      0\n    ]\n  },\n  \"code\": 200,\n  \"status\": \"success\",\n  \"error\": \"\"\n}"
  },
  {
    "path": "providers/dns/brandit/internal/fixtures/status-domain.json",
    "content": "{\n  \"response\": {\n    \"renewalmode\": [\n      \"DEFAULT\"\n    ],\n    \"status\": [\n      \"clientTransferProhibited\"\n    ],\n    \"transferlock\": [\n      1\n    ],\n    \"registrar\": [\n      \"brandit\"\n    ],\n    \"paiduntildate\": [\n      \"2021-12-15 05:00:00.0\"\n    ],\n    \"nameserver\": [\n      \"NS1.RRPPROXY.NET\",\n      \"NS2.RRPPROXY.NET\"\n    ],\n    \"registrationexpirationdate\": [\n      \"2021-12-15 05:00:00.0\"\n    ],\n    \"domain\": [\n      \"example.com\"\n    ],\n    \"renewaldate\": [\n      \"2024-01-19 05:00:00.0\"\n    ],\n    \"updateddate\": [\n      \"2022-12-16 08:01:27.0\"\n    ],\n    \"billingcontact\": [\n      \"example\"\n    ],\n    \"x-domain-roid\": [\n      \"example\"\n    ],\n    \"admincontact\": [\n      \"example\"\n    ],\n    \"techcontact\": [\n      \"example\"\n    ],\n    \"domainidn\": [\n      \"example.com\"\n    ],\n    \"createddate\": [\n      \"2016-12-16 05:00:00.0\"\n    ],\n    \"registrartransferdate\": [\n      \"2021-12-09 05:17:42.0\"\n    ],\n    \"zone\": [\n      \"com\"\n    ],\n    \"auth\": [\n      \"example\"\n    ],\n    \"updatedby\": [\n      \"example\"\n    ],\n    \"roid\": [\n      \"example\"\n    ],\n    \"ownercontact\": [\n      \"example\"\n    ],\n    \"createdby\": [\n      \"example\"\n    ],\n    \"transfermode\": [\n      \"auto\"\n    ]\n  },\n  \"code\": 200,\n  \"status\": \"success\",\n  \"error\": \"\"\n}"
  },
  {
    "path": "providers/dns/brandit/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype Response[T any] struct {\n\tResponse T      `json:\"response,omitempty\"`\n\tCode     int    `json:\"code\"`\n\tStatus   string `json:\"status\"`\n\tError    string `json:\"error\"`\n}\n\ntype StatusResponse struct {\n\tRenewalMode                []string `json:\"renewalmode\"`\n\tStatus                     []string `json:\"status\"`\n\tTransferLock               []int    `json:\"transferlock\"`\n\tRegistrar                  []string `json:\"registrar\"`\n\tPaidUntilDate              []string `json:\"paiduntildate\"`\n\tNameserver                 []string `json:\"nameserver\"`\n\tRegistrationExpirationDate []string `json:\"registrationexpirationdate\"`\n\tDomain                     []string `json:\"domain\"`\n\tRenewalDate                []string `json:\"renewaldate\"`\n\tUpdatedDate                []string `json:\"updateddate\"`\n\tBillingContact             []string `json:\"billingcontact\"`\n\tXDomainRoID                []string `json:\"x-domain-roid\"`\n\tAdminContact               []string `json:\"admincontact\"`\n\tTechContact                []string `json:\"techcontact\"`\n\tDomainIDN                  []string `json:\"domainidn\"`\n\tCreatedDate                []string `json:\"createddate\"`\n\tRegistrarTransferDate      []string `json:\"registrartransferdate\"`\n\tZone                       []string `json:\"zone\"`\n\tAuth                       []string `json:\"auth\"`\n\tUpdatedBy                  []string `json:\"updatedby\"`\n\tRoID                       []string `json:\"roid\"`\n\tOwnerContact               []string `json:\"ownercontact\"`\n\tCreatedBy                  []string `json:\"createdby\"`\n\tTransferMode               []string `json:\"transfermode\"`\n}\n\ntype ListRecordsResponse struct {\n\tLimit  []int    `json:\"limit,omitempty\"`\n\tColumn []string `json:\"column,omitempty\"`\n\tCount  []int    `json:\"count,omitempty\"`\n\tFirst  []int    `json:\"first,omitempty\"`\n\tTotal  []int    `json:\"total,omitempty\"`\n\tRR     []string `json:\"rr,omitempty\"`\n\tLast   []int    `json:\"last,omitempty\"`\n}\n\ntype APIError struct {\n\tCode    int    `json:\"code\"`\n\tStatus  string `json:\"status\"`\n\tMessage string `json:\"error\"`\n}\n\nfunc (a APIError) Error() string {\n\treturn fmt.Sprintf(\"code: %d, status: %s, message: %s\", a.Code, a.Status, a.Message)\n}\n\ntype AddRecord struct {\n\tResponse AddRecordResponse `json:\"response\"`\n\tRecord   string            `json:\"record\"`\n\tCode     int               `json:\"code\"`\n\tStatus   string            `json:\"status\"`\n\tError    string            `json:\"error\"`\n}\n\ntype AddRecordResponse struct {\n\tZoneType []string `json:\"zonetype\"`\n\tSigned   []int    `json:\"signed\"`\n}\n\ntype Record struct {\n\tID      int    `json:\"id,omitempty\"`\n\tType    string `json:\"type,omitempty\"`\n\tName    string `json:\"name,omitempty\"` // subdomain name or @ if you don't want subdomain\n\tContent string `json:\"content,omitempty\"`\n\tTTL     int    `json:\"ttl,omitempty\"` // default 600\n}\n"
  },
  {
    "path": "providers/dns/bunny/bunny.go",
    "content": "// Package bunny implements a DNS provider for solving the DNS-01 challenge using Bunny DNS.\npackage bunny\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\t\"github.com/nrdcg/bunny-go\"\n\t\"golang.org/x/net/publicsuffix\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"BUNNY_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst minTTL = 60\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *bunny.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for bunny.\n// Credentials must be passed in the environment variable: BUNNY_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"bunny: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for bunny.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"bunny: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"bunny: credentials missing\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"bunny: invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\tif config.HTTPClient == nil {\n\t\tconfig.HTTPClient = &http.Client{Timeout: 30 * time.Second}\n\t}\n\n\tconfig.HTTPClient = clientdebug.Wrap(config.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: bunny.NewClient(config.APIKey,\n\t\t\tbunny.WithUserAgent(useragent.Get()),\n\t\t\tbunny.WithHTTPClient(config.HTTPClient),\n\t\t),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzone, err := d.findZone(ctx, dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bunny: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, ptr.Deref(zone.Domain))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bunny: %w\", err)\n\t}\n\n\trecord := &bunny.AddOrUpdateDNSRecordOptions{\n\t\tType:  ptr.Pointer(bunny.DNSRecordTypeTXT),\n\t\tName:  ptr.Pointer(subDomain),\n\t\tValue: ptr.Pointer(info.Value),\n\t\tTTL:   ptr.Pointer(int32(d.config.TTL)),\n\t}\n\n\tif _, err := d.client.DNSZone.AddDNSRecord(ctx, ptr.Deref(zone.ID), record); err != nil {\n\t\treturn fmt.Errorf(\"bunny: failed to add TXT record: fqdn=%s, zoneID=%d: %w\", info.EffectiveFQDN, ptr.Deref(zone.ID), err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzone, err := d.findZone(ctx, dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bunny: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, ptr.Deref(zone.Domain))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bunny: %w\", err)\n\t}\n\n\tvar record *bunny.DNSRecord\n\n\tfor _, r := range zone.Records {\n\t\tif ptr.Deref(r.Name) == subDomain && ptr.Deref(r.Type) == bunny.DNSRecordTypeTXT {\n\t\t\tr := r\n\t\t\trecord = &r\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif record == nil {\n\t\treturn fmt.Errorf(\"bunny: could not find TXT record zone=%d, subdomain=%s\", ptr.Deref(zone.ID), subDomain)\n\t}\n\n\tif err := d.client.DNSZone.DeleteDNSRecord(ctx, ptr.Deref(zone.ID), ptr.Deref(record.ID)); err != nil {\n\t\treturn fmt.Errorf(\"bunny: failed to delete TXT record: id=%d, name=%s: %w\", ptr.Deref(record.ID), ptr.Deref(record.Name), err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) findZone(ctx context.Context, authZone string) (*bunny.DNSZone, error) {\n\tzones, err := d.client.DNSZone.List(ctx, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tzone := findZone(zones, authZone)\n\tif zone == nil {\n\t\treturn nil, fmt.Errorf(\"could not find DNSZone domain=%s\", authZone)\n\t}\n\n\treturn zone, nil\n}\n\nfunc findZone(zones *bunny.DNSZones, domain string) *bunny.DNSZone {\n\tdomains := possibleDomains(domain)\n\n\tvar domainLength int\n\n\tvar zone *bunny.DNSZone\n\n\tfor _, item := range zones.Items {\n\t\tif item == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tcurr := ptr.Deref(item.Domain)\n\n\t\tif slices.Contains(domains, curr) && domainLength < len(curr) {\n\t\t\tdomainLength = len(curr)\n\n\t\t\tzone = item\n\t\t}\n\t}\n\n\treturn zone\n}\n\nfunc possibleDomains(domain string) []string {\n\tvar domains []string\n\n\ttld, _ := publicsuffix.PublicSuffix(domain)\n\tfor d := range dns01.DomainsSeq(domain) {\n\t\tif tld == d {\n\t\t\t// skip the TLD\n\t\t\tbreak\n\t\t}\n\n\t\tdomains = append(domains, dns01.UnFqdn(d))\n\t}\n\n\treturn domains\n}\n"
  },
  {
    "path": "providers/dns/bunny/bunny.toml",
    "content": "Name = \"Bunny\"\nDescription = ''''''\nURL = \"https://bunny.net\"\nCode = \"bunny\"\nSince = \"v4.11.0\"\n\nExample = '''\nBUNNY_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \\\nlego --dns bunny -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    BUNNY_API_KEY = \"API key\"\n  [Configuration.Additional]\n    BUNNY_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    BUNNY_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    BUNNY_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    BUNNY_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://docs.bunny.net/reference/dnszonepublic_index\"\n  bunny-go = \"https://github.com/nrdcg/bunny-go\"\n"
  },
  {
    "path": "providers/dns/bunny/bunny_test.go",
    "content": "package bunny\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n\t\"github.com/nrdcg/bunny-go\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"bunny: some credentials information are missing: BUNNY_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tttl      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tttl:    minTTL,\n\t\t\tapiKey: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tttl:      minTTL,\n\t\t\texpected: \"bunny: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"invalid TTL\",\n\t\t\tapiKey:   \"123\",\n\t\t\tttl:      10,\n\t\t\texpected: \"bunny: invalid TTL, TTL (10) must be greater than 60\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.TTL = test.ttl\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc Test_findZone(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tdomain   string\n\t\titems    []*bunny.DNSZone\n\t\texpected *bunny.DNSZone\n\t}{\n\t\t{\n\t\t\tdesc:   \"found subdomain\",\n\t\t\tdomain: \"_acme-challenge.foo.bar.example.com\",\n\t\t\titems: []*bunny.DNSZone{\n\t\t\t\t{ID: ptr.Pointer[int64](1), Domain: ptr.Pointer(\"example.com\")},\n\t\t\t\t{ID: ptr.Pointer[int64](2), Domain: ptr.Pointer(\"example.org\")},\n\t\t\t\t{ID: ptr.Pointer[int64](4), Domain: ptr.Pointer(\"bar.example.org\")},\n\t\t\t\t{ID: ptr.Pointer[int64](5), Domain: ptr.Pointer(\"bar.example.com\")},\n\t\t\t\t{ID: ptr.Pointer[int64](6), Domain: ptr.Pointer(\"foo.example.com\")},\n\t\t\t},\n\t\t\texpected: &bunny.DNSZone{\n\t\t\t\tID:     ptr.Pointer[int64](5),\n\t\t\t\tDomain: ptr.Pointer(\"bar.example.com\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"found the longest subdomain\",\n\t\t\tdomain: \"_acme-challenge.foo.bar.example.com\",\n\t\t\titems: []*bunny.DNSZone{\n\t\t\t\t{ID: ptr.Pointer[int64](7), Domain: ptr.Pointer(\"foo.bar.example.com\")},\n\t\t\t\t{ID: ptr.Pointer[int64](1), Domain: ptr.Pointer(\"example.com\")},\n\t\t\t\t{ID: ptr.Pointer[int64](2), Domain: ptr.Pointer(\"example.org\")},\n\t\t\t\t{ID: ptr.Pointer[int64](4), Domain: ptr.Pointer(\"bar.example.org\")},\n\t\t\t\t{ID: ptr.Pointer[int64](5), Domain: ptr.Pointer(\"bar.example.com\")},\n\t\t\t\t{ID: ptr.Pointer[int64](6), Domain: ptr.Pointer(\"foo.example.com\")},\n\t\t\t},\n\t\t\texpected: &bunny.DNSZone{\n\t\t\t\tID:     ptr.Pointer[int64](7),\n\t\t\t\tDomain: ptr.Pointer(\"foo.bar.example.com\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"found apex\",\n\t\t\tdomain: \"_acme-challenge.foo.bar.example.com\",\n\t\t\titems: []*bunny.DNSZone{\n\t\t\t\t{ID: ptr.Pointer[int64](1), Domain: ptr.Pointer(\"example.com\")},\n\t\t\t\t{ID: ptr.Pointer[int64](2), Domain: ptr.Pointer(\"example.org\")},\n\t\t\t\t{ID: ptr.Pointer[int64](4), Domain: ptr.Pointer(\"bar.example.org\")},\n\t\t\t\t{ID: ptr.Pointer[int64](6), Domain: ptr.Pointer(\"foo.example.com\")},\n\t\t\t},\n\t\t\texpected: &bunny.DNSZone{\n\t\t\t\tID:     ptr.Pointer[int64](1),\n\t\t\t\tDomain: ptr.Pointer(\"example.com\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"not found\",\n\t\t\tdomain: \"_acme-challenge.foo.bar.example.com\",\n\t\t\titems: []*bunny.DNSZone{\n\t\t\t\t{ID: ptr.Pointer[int64](2), Domain: ptr.Pointer(\"example.org\")},\n\t\t\t\t{ID: ptr.Pointer[int64](4), Domain: ptr.Pointer(\"bar.example.org\")},\n\t\t\t\t{ID: ptr.Pointer[int64](6), Domain: ptr.Pointer(\"foo.example.com\")},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tzones := &bunny.DNSZones{Items: test.items}\n\n\t\t\tzone := findZone(zones, test.domain)\n\n\t\t\tassert.Equal(t, test.expected, zone)\n\t\t})\n\t}\n}\n\nfunc Test_possibleDomains(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tdomain   string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tdesc:     \"apex\",\n\t\t\tdomain:   \"example.com\",\n\t\t\texpected: []string{\"example.com\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"CCTLD\",\n\t\t\tdomain:   \"example.co.uk\",\n\t\t\texpected: []string{\"example.co.uk\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"long domain\",\n\t\t\tdomain:   \"_acme-challenge.foo.bar.example.com\",\n\t\t\texpected: []string{\"_acme-challenge.foo.bar.example.com\", \"foo.bar.example.com\", \"bar.example.com\", \"example.com\"},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"empty\",\n\t\t\tdomain: \"\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tdomains := possibleDomains(test.domain)\n\n\t\t\tassert.Equal(t, test.expected, domains)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/checkdomain/checkdomain.go",
    "content": "// Package checkdomain implements a DNS provider for solving the DNS-01 challenge using CheckDomain DNS.\npackage checkdomain\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/checkdomain/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"CHECKDOMAIN_\"\n\n\tEnvEndpoint = envNamespace + \"ENDPOINT\"\n\tEnvToken    = envNamespace + \"TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tEndpoint           *url.URL\n\tToken              string\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 7*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for CheckDomain.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"checkdomain: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvToken]\n\n\tendpoint, err := url.Parse(env.GetOrDefaultString(EnvEndpoint, internal.DefaultEndpoint))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"checkdomain: invalid %s: %w\", EnvEndpoint, err)\n\t}\n\n\tconfig.Endpoint = endpoint\n\n\treturn NewDNSProviderConfig(config)\n}\n\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config.Endpoint == nil {\n\t\treturn nil, errors.New(\"checkdomain: invalid endpoint\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"checkdomain: missing token\")\n\t}\n\n\tclient := internal.NewClient(\n\t\tclientdebug.Wrap(\n\t\t\tinternal.OAuthStaticAccessToken(config.HTTPClient, config.Token),\n\t\t),\n\t)\n\n\tif config.Endpoint != nil {\n\t\tclient.BaseURL = config.Endpoint\n\t}\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\tdomainID, err := d.client.GetDomainIDByName(ctx, domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"checkdomain: %w\", err)\n\t}\n\n\terr = d.client.CheckNameservers(ctx, domainID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"checkdomain: %w\", err)\n\t}\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr = d.client.CreateRecord(ctx, domainID, &internal.Record{\n\t\tName:  info.EffectiveFQDN,\n\t\tTTL:   d.config.TTL,\n\t\tType:  \"TXT\",\n\t\tValue: info.Value,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"checkdomain: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record previously created.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\tdomainID, err := d.client.GetDomainIDByName(ctx, domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"checkdomain: %w\", err)\n\t}\n\n\terr = d.client.CheckNameservers(ctx, domainID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"checkdomain: %w\", err)\n\t}\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tdefer d.client.CleanCache(info.EffectiveFQDN)\n\n\terr = d.client.DeleteTXTRecord(ctx, domainID, info.EffectiveFQDN, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"checkdomain: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/checkdomain/checkdomain.toml",
    "content": "Name = \"Checkdomain\"\nDescription = ''''''\nURL = \"https://checkdomain.de/\"\nCode = \"checkdomain\"\nSince = \"v3.3.0\"\n\nExample = '''\nCHECKDOMAIN_TOKEN=yoursecrettoken \\\nlego --dns checkdomain -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    CHECKDOMAIN_TOKEN = \"API token\"\n  [Configuration.Additional]\n    CHECKDOMAIN_ENDPOINT = \"API endpoint URL, defaults to https://api.checkdomain.de\"\n    CHECKDOMAIN_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    CHECKDOMAIN_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 300)\"\n    CHECKDOMAIN_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 7)\"\n    CHECKDOMAIN_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://developer.checkdomain.de/reference/\"\n  Guide = \"https://developer.checkdomain.de/guide/\"\n  Settings = \"https://www.checkdomain.net/en/login/data/api/\"\n"
  },
  {
    "path": "providers/dns/checkdomain/checkdomain_test.go",
    "content": "package checkdomain\n\nimport (\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/providers/dns/checkdomain/internal\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvEndpoint,\n\tEnvToken).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"dummy\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"no token\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"checkdomain: some credentials information are missing: CHECKDOMAIN_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid endpoint\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken:    \"dummy\",\n\t\t\t\tEnvEndpoint: \":\",\n\t\t\t},\n\t\t\texpected: `checkdomain: invalid CHECKDOMAIN_ENDPOINT: parse \":\": missing protocol scheme`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"dummy\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing token\",\n\t\t\ttoken:    \"\",\n\t\t\texpected: \"checkdomain: missing token\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Endpoint, _ = url.Parse(internal.DefaultEndpoint)\n\n\t\t\tif test.token != \"\" {\n\t\t\t\tconfig.Token = test.token\n\t\t\t}\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/checkdomain/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"golang.org/x/oauth2\"\n)\n\nconst (\n\tns1 = \"ns.checkdomain.de\"\n\tns2 = \"ns2.checkdomain.de\"\n)\n\n// DefaultEndpoint the default API endpoint.\nconst DefaultEndpoint = \"https://api.checkdomain.de\"\n\nconst domainNotFound = -1\n\n// max page limit that the checkdomain api allows.\nconst maxLimit = 100\n\n// max integer value.\nconst maxInt = int((^uint(0)) >> 1)\n\n// Client the Autodns API client.\ntype Client struct {\n\tBaseURL    *url.URL\n\thttpClient *http.Client\n\n\tdomainIDMapping map[string]int\n\tdomainIDMu      sync.Mutex\n}\n\n// NewClient creates a new Client.\nfunc NewClient(hc *http.Client) *Client {\n\tbaseURL, _ := url.Parse(DefaultEndpoint)\n\n\tif hc == nil {\n\t\thc = &http.Client{Timeout: 10 * time.Second}\n\t}\n\n\treturn &Client{\n\t\tBaseURL:         baseURL,\n\t\thttpClient:      hc,\n\t\tdomainIDMapping: make(map[string]int),\n\t}\n}\n\nfunc (c *Client) GetDomainIDByName(ctx context.Context, name string) (int, error) {\n\t// Load from cache if exists\n\tc.domainIDMu.Lock()\n\tid, ok := c.domainIDMapping[name]\n\tc.domainIDMu.Unlock()\n\n\tif ok {\n\t\treturn id, nil\n\t}\n\n\t// Find out by querying API\n\tdomains, err := c.listDomains(ctx)\n\tif err != nil {\n\t\treturn domainNotFound, err\n\t}\n\n\t// Linear search over all registered domains\n\tfor _, domain := range domains {\n\t\tif domain.Name == name || strings.HasSuffix(name, \".\"+domain.Name) {\n\t\t\tc.domainIDMu.Lock()\n\t\t\tc.domainIDMapping[name] = domain.ID\n\t\t\tc.domainIDMu.Unlock()\n\n\t\t\treturn domain.ID, nil\n\t\t}\n\t}\n\n\treturn domainNotFound, errors.New(\"domain not found\")\n}\n\nfunc (c *Client) listDomains(ctx context.Context) ([]*Domain, error) {\n\tendpoint := c.BaseURL.JoinPath(\"v1\", \"domains\")\n\n\t// Checkdomain also provides a query param 'query' which allows filtering domains for a string.\n\t// But that functionality is kinda broken,\n\t// so we scan through the whole list of registered domains to later find the one that is of interest to us.\n\tq := endpoint.Query()\n\tq.Set(\"limit\", strconv.Itoa(maxLimit))\n\n\tcurrentPage := 1\n\ttotalPages := maxInt\n\n\tvar domainList []*Domain\n\n\tfor currentPage <= totalPages {\n\t\tq.Set(\"page\", strconv.Itoa(currentPage))\n\t\tendpoint.RawQuery = q.Encode()\n\n\t\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to make request: %w\", err)\n\t\t}\n\n\t\tvar res DomainListingResponse\n\t\tif err := c.do(req, &res); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to send domain listing request: %w\", err)\n\t\t}\n\n\t\t// This is the first response,\n\t\t// so we update totalPages and allocate the slice memory.\n\t\tif totalPages == maxInt {\n\t\t\ttotalPages = res.Pages\n\t\t\tdomainList = make([]*Domain, 0, res.Total)\n\t\t}\n\n\t\tdomainList = append(domainList, res.Embedded.Domains...)\n\t\tcurrentPage++\n\t}\n\n\treturn domainList, nil\n}\n\nfunc (c *Client) getNameserverInfo(ctx context.Context, domainID int) (*NameserverResponse, error) {\n\tendpoint := c.BaseURL.JoinPath(\"v1\", \"domains\", strconv.Itoa(domainID), \"nameservers\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tres := &NameserverResponse{}\n\tif err := c.do(req, res); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn res, nil\n}\n\nfunc (c *Client) CheckNameservers(ctx context.Context, domainID int) error {\n\tinfo, err := c.getNameserverInfo(ctx, domainID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar found1, found2 bool\n\n\tfor _, item := range info.Nameservers {\n\t\tswitch item.Name {\n\t\tcase ns1:\n\t\t\tfound1 = true\n\t\tcase ns2:\n\t\t\tfound2 = true\n\t\t}\n\t}\n\n\tif !found1 || !found2 {\n\t\treturn errors.New(\"not using checkdomain nameservers, can not update records\")\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) CreateRecord(ctx context.Context, domainID int, record *Record) error {\n\tendpoint := c.BaseURL.JoinPath(\"v1\", \"domains\", strconv.Itoa(domainID), \"nameservers\", \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\n// DeleteTXTRecord Checkdomain doesn't seem provide a way to delete records but one can replace all records at once.\n// The current solution is to fetch all records and then use that list minus the record deleted as the new record list.\n// TODO: Simplify this function once Checkdomain do provide the functionality.\nfunc (c *Client) DeleteTXTRecord(ctx context.Context, domainID int, recordName, recordValue string) error {\n\tdomainInfo, err := c.getDomainInfo(ctx, domainID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnsInfo, err := c.getNameserverInfo(ctx, domainID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tallRecords, err := c.listRecords(ctx, domainID, \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trecordName = strings.TrimSuffix(recordName, \".\"+domainInfo.Name+\".\")\n\n\tvar recordsToKeep []*Record\n\n\t// Find and delete matching records\n\tfor _, record := range allRecords {\n\t\tif skipRecord(recordName, recordValue, record, nsInfo) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Checkdomain API can return records without any TTL set (indicated by the value of 0).\n\t\t// The API Call to replace the records would fail if we wouldn't specify a value.\n\t\t// Thus, we use the default TTL queried beforehand\n\t\tif record.TTL == 0 {\n\t\t\trecord.TTL = nsInfo.SOA.TTL\n\t\t}\n\n\t\trecordsToKeep = append(recordsToKeep, record)\n\t}\n\n\treturn c.replaceRecords(ctx, domainID, recordsToKeep)\n}\n\nfunc (c *Client) getDomainInfo(ctx context.Context, domainID int) (*DomainResponse, error) {\n\tendpoint := c.BaseURL.JoinPath(\"v1\", \"domains\", strconv.Itoa(domainID))\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar res DomainResponse\n\n\terr = c.do(req, &res)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &res, nil\n}\n\nfunc (c *Client) listRecords(ctx context.Context, domainID int, recordType string) ([]*Record, error) {\n\tendpoint := c.BaseURL.JoinPath(\"v1\", \"domains\", strconv.Itoa(domainID), \"nameservers\", \"records\")\n\n\tq := endpoint.Query()\n\tq.Set(\"limit\", strconv.Itoa(maxLimit))\n\n\tif recordType != \"\" {\n\t\tq.Set(\"type\", recordType)\n\t}\n\n\tcurrentPage := 1\n\ttotalPages := maxInt\n\n\tvar recordList []*Record\n\n\tfor currentPage <= totalPages {\n\t\tq.Set(\"page\", strconv.Itoa(currentPage))\n\t\tendpoint.RawQuery = q.Encode()\n\n\t\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t\t}\n\n\t\tvar res RecordListingResponse\n\t\tif err := c.do(req, &res); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to send record listing request: %w\", err)\n\t\t}\n\n\t\t// This is the first response, so we update totalPages and allocate the slice memory.\n\t\tif totalPages == maxInt {\n\t\t\ttotalPages = res.Pages\n\t\t\trecordList = make([]*Record, 0, res.Total)\n\t\t}\n\n\t\trecordList = append(recordList, res.Embedded.Records...)\n\t\tcurrentPage++\n\t}\n\n\treturn recordList, nil\n}\n\nfunc (c *Client) replaceRecords(ctx context.Context, domainID int, records []*Record) error {\n\tendpoint := c.BaseURL.JoinPath(\"v1\", \"domains\", strconv.Itoa(domainID), \"nameservers\", \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, records)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) CleanCache(fqdn string) {\n\tc.domainIDMu.Lock()\n\tdelete(c.domainIDMapping, fqdn)\n\tc.domainIDMu.Unlock()\n}\n\nfunc skipRecord(recordName, recordValue string, record *Record, nsInfo *NameserverResponse) bool {\n\t// Skip empty records\n\tif record.Value == \"\" {\n\t\treturn true\n\t}\n\n\t// Skip some special records, otherwise we would get a \"Nameserver update failed\"\n\tif record.Type == \"SOA\" || record.Type == \"NS\" || record.Name == \"@\" || (nsInfo.General.IncludeWWW && record.Name == \"www\") {\n\t\treturn true\n\t}\n\n\tnameMatch := recordName == \"\" || record.Name == recordName\n\tvalueMatch := recordValue == \"\" || record.Value == recordValue\n\n\t// Skip our matching record\n\tif record.Type == \"TXT\" && nameMatch && valueMatch {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {\n\tif client == nil {\n\t\tclient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tclient.Transport = &oauth2.Transport{\n\t\tSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),\n\t\tBase:   client.Transport,\n\t}\n\n\treturn client\n}\n"
  },
  {
    "path": "providers/dns/checkdomain/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(OAuthStaticAccessToken(server.Client(), \"secret\"))\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer secret\"))\n}\n\nfunc TestClient_GetDomainIDByName(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/domains\",\n\t\t\tservermock.JSONEncode(DomainListingResponse{\n\t\t\t\tEmbedded: EmbeddedDomainList{Domains: []*Domain{\n\t\t\t\t\t{ID: 1, Name: \"test.com\"},\n\t\t\t\t\t{ID: 2, Name: \"test.org\"},\n\t\t\t\t}},\n\t\t\t})).\n\t\tBuild(t)\n\n\tid, err := client.GetDomainIDByName(t.Context(), \"test.com\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 1, id)\n}\n\nfunc TestClient_CheckNameservers(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/domains/1/nameservers\",\n\t\t\tservermock.JSONEncode(NameserverResponse{\n\t\t\t\tNameservers: []*Nameserver{\n\t\t\t\t\t{Name: ns1},\n\t\t\t\t\t{Name: ns2},\n\t\t\t\t\t// {Name: \"ns.fake.de\"},\n\t\t\t\t},\n\t\t\t})).\n\t\tBuild(t)\n\n\terr := client.CheckNameservers(t.Context(), 1)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /v1/domains/1/nameservers/records\", nil,\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\trecord := &Record{\n\t\tName:  \"test.com\",\n\t\tTTL:   300,\n\t\tType:  \"TXT\",\n\t\tValue: \"value\",\n\t}\n\n\terr := client.CreateRecord(t.Context(), 1, record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteTXTRecord(t *testing.T) {\n\tdomainName := \"lego.test\"\n\trecordValue := \"test\"\n\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/domains/\",\n\t\t\tservermock.JSONEncode(DomainResponse{\n\t\t\t\tID:   1,\n\t\t\t\tName: domainName,\n\t\t\t})).\n\t\tRoute(\"GET /v1/domains/1/nameservers\",\n\t\t\tservermock.JSONEncode(NameserverResponse{\n\t\t\t\tNameservers: []*Nameserver{{Name: ns1}, {Name: ns2}},\n\t\t\t})).\n\t\tRoute(\"GET /v1/domains/1/nameservers/records\",\n\t\t\tservermock.JSONEncode(RecordListingResponse{\n\t\t\t\tEmbedded: EmbeddedRecordList{\n\t\t\t\t\tRecords: []*Record{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"_acme-challenge\",\n\t\t\t\t\t\t\tValue: recordValue,\n\t\t\t\t\t\t\tType:  \"TXT\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"_acme-challenge\",\n\t\t\t\t\t\t\tValue: recordValue,\n\t\t\t\t\t\t\tType:  \"A\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"foobar\",\n\t\t\t\t\t\t\tValue: recordValue,\n\t\t\t\t\t\t\tType:  \"TXT\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})).\n\t\tRoute(\"PUT /v1/domains/1/nameservers/records\", nil,\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"delete_txt_record-request.json\")).\n\t\tBuild(t)\n\n\tinfo := dns01.GetChallengeInfo(domainName, \"abc\")\n\terr := client.DeleteTXTRecord(t.Context(), 1, info.EffectiveFQDN, recordValue)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/checkdomain/internal/fixtures/create_record-request.json",
    "content": "{\n  \"name\": \"test.com\",\n  \"value\": \"value\",\n  \"ttl\": 300,\n  \"priority\": 0,\n  \"type\": \"TXT\"\n}\n"
  },
  {
    "path": "providers/dns/checkdomain/internal/fixtures/delete_txt_record-request.json",
    "content": "[\n  {\n    \"name\": \"_acme-challenge\",\n    \"value\": \"test\",\n    \"ttl\": 0,\n    \"priority\": 0,\n    \"type\": \"A\"\n  },\n  {\n    \"name\": \"foobar\",\n    \"value\": \"test\",\n    \"ttl\": 0,\n    \"priority\": 0,\n    \"type\": \"TXT\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/checkdomain/internal/types.go",
    "content": "package internal\n\n// Some fields have been omitted from the structs\n// because they are not required for this application.\n\ntype DomainListingResponse struct {\n\tPage     int                `json:\"page\"`\n\tLimit    int                `json:\"limit\"`\n\tPages    int                `json:\"pages\"`\n\tTotal    int                `json:\"total\"`\n\tEmbedded EmbeddedDomainList `json:\"_embedded\"`\n}\n\ntype EmbeddedDomainList struct {\n\tDomains []*Domain `json:\"domains\"`\n}\n\ntype Domain struct {\n\tID   int    `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\ntype DomainResponse struct {\n\tID      int    `json:\"id\"`\n\tName    string `json:\"name\"`\n\tCreated string `json:\"created\"`\n\tPaidUp  string `json:\"payed_up\"`\n\tActive  bool   `json:\"active\"`\n}\n\ntype NameserverResponse struct {\n\tGeneral     NameserverGeneral `json:\"general\"`\n\tNameservers []*Nameserver     `json:\"nameservers\"`\n\tSOA         NameserverSOA     `json:\"soa\"`\n}\n\ntype NameserverGeneral struct {\n\tIPv4       string `json:\"ip_v4\"`\n\tIPv6       string `json:\"ip_v6\"`\n\tIncludeWWW bool   `json:\"include_www\"`\n}\n\ntype NameserverSOA struct {\n\tMail    string `json:\"mail\"`\n\tRefresh int    `json:\"refresh\"`\n\tRetry   int    `json:\"retry\"`\n\tExpiry  int    `json:\"expiry\"`\n\tTTL     int    `json:\"ttl\"`\n}\n\ntype Nameserver struct {\n\tName string `json:\"name\"`\n}\n\ntype RecordListingResponse struct {\n\tPage     int                `json:\"page\"`\n\tLimit    int                `json:\"limit\"`\n\tPages    int                `json:\"pages\"`\n\tTotal    int                `json:\"total\"`\n\tEmbedded EmbeddedRecordList `json:\"_embedded\"`\n}\n\ntype EmbeddedRecordList struct {\n\tRecords []*Record `json:\"records\"`\n}\n\ntype Record struct {\n\tName     string `json:\"name\"`\n\tValue    string `json:\"value\"`\n\tTTL      int    `json:\"ttl\"`\n\tPriority int    `json:\"priority\"`\n\tType     string `json:\"type\"`\n}\n"
  },
  {
    "path": "providers/dns/civo/civo.go",
    "content": "// Package civo implements a DNS provider for solving the DNS-01 challenge using CIVO.\npackage civo\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/civo/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"CIVO_\"\n\n\tEnvAPIToken = envNamespace + \"TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst (\n\tminTTL                    = 600\n\tdefaultPollingInterval    = 30 * time.Second\n\tdefaultPropagationTimeout = 300 * time.Second\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for CIVO.\n// Credentials must be passed in the environment variables: API_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"civo: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvAPIToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for CIVO.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"civo: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"civo: credentials missing\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\tconfig.TTL = minTTL\n\t}\n\n\t// Create a Civo client - DNS is region independent, we can use any region\n\tclient, err := internal.NewClient(\n\t\tclientdebug.Wrap(\n\t\t\tinternal.OAuthStaticAccessToken(config.HTTPClient, config.Token),\n\t\t),\n\t\t\"LON1\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"civo: %w\", err)\n\t}\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"civo: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzone := dns01.UnFqdn(authZone)\n\n\tdomainID, err := d.getDomainIDByName(ctx, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"civo: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"civo: %w\", err)\n\t}\n\n\t_, err = d.client.CreateDNSRecord(ctx, domainID, internal.Record{\n\t\tName:  subDomain,\n\t\tValue: info.Value,\n\t\tType:  \"TXT\",\n\t\tTTL:   d.config.TTL,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"civo: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"civo: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzone := dns01.UnFqdn(authZone)\n\n\tdomainID, err := d.getDomainIDByName(ctx, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"civo: %w\", err)\n\t}\n\n\tdnsRecords, err := d.client.ListDNSRecords(ctx, domainID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"civo: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"civo: %w\", err)\n\t}\n\n\tvar dnsRecord internal.Record\n\n\tfor _, entry := range dnsRecords {\n\t\tif entry.Name == subDomain && entry.Value == info.Value {\n\t\t\tdnsRecord = entry\n\t\t\tbreak\n\t\t}\n\t}\n\n\terr = d.client.DeleteDNSRecord(ctx, dnsRecord)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"civo: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) getDomainIDByName(ctx context.Context, domain string) (string, error) {\n\tdomains, err := d.client.ListDomains(ctx)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"list domains: %w\", err)\n\t}\n\n\tfor _, d := range domains {\n\t\tif d.Name == domain {\n\t\t\treturn d.ID, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"domain %q not found\", domain)\n}\n"
  },
  {
    "path": "providers/dns/civo/civo.toml",
    "content": "Name = \"Civo\"\nDescription = ''''''\nURL = \"https://civo.com\"\nCode = \"civo\"\nSince = \"v4.9.0\"\n\nExample = '''\nCIVO_TOKEN=xxxxxx \\\nlego --dns civo -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    CIVO_TOKEN = \"Authentication token\"\n  [Configuration.Additional]\n    CIVO_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 30)\"\n    CIVO_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 300)\"\n    CIVO_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)\"\n\n[Links]\n    API = \"https://www.civo.com/api/dns\"\n"
  },
  {
    "path": "providers/dns/civo/civo_test.go",
    "content": "package civo\n\nimport (\n\t\"fmt\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIToken).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"00000000000000000000000000000000000000000000000000\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"\",\n\t\t\t},\n\t\t\texpected: fmt.Sprintf(\"civo: some credentials information are missing: %s\", EnvAPIToken),\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\tttl      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"00000000000000000000000000000000000000000000000000\",\n\t\t\tttl:   minTTL,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api key\",\n\t\t\ttoken:    \"\",\n\t\t\tttl:      minTTL,\n\t\t\texpected: \"civo: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.TTL = test.ttl\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.HTTPClient = server.Client()\n\t\t\tconfig.Token = \"secret\"\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(\"Authorization\", \"Bearer secret\").\n\t\t\tWithRegexp(\"User-Agent\", `goacme-lego/[0-9.]+ \\(.+\\)`),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\t// https://www.civo.com/api/dns#list-domain-names\n\t\tRoute(\"GET /dns\",\n\t\t\tservermock.ResponseFromInternal(\"list_domain_names.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"region\", \"LON1\")).\n\t\t// https://www.civo.com/api/dns#create-a-new-dns-record\n\t\tRoute(\"POST /dns/7088fcea-7658-43e6-97fa-273f901978fd/records\",\n\t\t\tservermock.ResponseFromInternal(\"create_dns_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"create_dns_record-request.json\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abd\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\t// https://www.civo.com/api/dns#list-domain-names\n\t\tRoute(\"GET /dns\",\n\t\t\tservermock.ResponseFromInternal(\"list_domain_names.json\"),\n\t\t\tservermock.CheckQueryParameter().\n\t\t\t\tWith(\"region\", \"LON1\")).\n\t\t// https://www.civo.com/api/dns#list-dns-records\n\t\tRoute(\"GET /dns/7088fcea-7658-43e6-97fa-273f901978fd/records\",\n\t\t\tservermock.ResponseFromInternal(\"list_dns_records.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"region\", \"LON1\")).\n\t\t// https://www.civo.com/api/dns#deleting-a-dns-record\n\t\tRoute(\"DELETE /dns/edc5dacf-a2ad-4757-41ee-c12f06259c70/records/76cc107f-fbef-4e2b-b97f-f5d34f4075d3\",\n\t\t\tservermock.ResponseFromInternal(\"delete_dns_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"region\", \"LON1\")).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"abd\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/civo/internal/client.go",
    "content": "/*\nPackage internal Civo API client.\n\nBecause the dependencies on k8s, the official client cannot be used.\n- https://github.com/civo/civogo/blob/v0.2.99/go.mod -> k8s.io/client-go\n- https://github.com/civo/civogo/blob/v0.3.34/go.mod -> k8s.io/api\n- https://github.com/civo/civogo/blob/v0.3.38/go.mod -> k8s.io/api + k8s.io/apimachinery\n- Current version -> https://github.com/civo/civogo/blob/v0.6.1/go.mod\n*/\npackage internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\t\"golang.org/x/oauth2\"\n)\n\nconst defaultBaseURL = \"https://api.civo.com/v2\"\n\n// Client the Civo API client.\ntype Client struct {\n\tregion string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(hc *http.Client, region string) (*Client, error) {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\tif hc == nil {\n\t\thc = &http.Client{Timeout: 10 * time.Second}\n\t}\n\n\treturn &Client{\n\t\tregion:     region,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: hc,\n\t}, nil\n}\n\n// ListDomains a list of all domain names within the account.\n// https://www.civo.com/api/dns#list-domain-names\nfunc (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {\n\tendpoint := c.BaseURL.JoinPath(\"dns\")\n\n\treq, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result []Domain\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// ListDNSRecords a list of all DNS records in the specified domain.\n// https://www.civo.com/api/dns#list-dns-records\nfunc (c *Client) ListDNSRecords(ctx context.Context, domainID string) ([]Record, error) {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", domainID, \"records\")\n\n\treq, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result []Record\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// CreateDNSRecord creates DNS records for a specific domain.\n// https://www.civo.com/api/dns#create-a-new-dns-record\nfunc (c *Client) CreateDNSRecord(ctx context.Context, domainID string, record Record) (*Record, error) {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", domainID, \"records\")\n\n\treq, err := c.newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result Record\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &result, nil\n}\n\n// DeleteDNSRecord remove a DNS record from a domain.\n// https://www.civo.com/api/dns#deleting-a-dns-record\nfunc (c *Client) DeleteDNSRecord(ctx context.Context, record Record) error {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", record.DomainID, \"records\", record.ID)\n\n\treq, err := c.newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\tif method == http.MethodGet || method == http.MethodDelete {\n\t\tquery := endpoint.Query()\n\t\tquery.Set(\"region\", c.region)\n\n\t\tendpoint.RawQuery = query.Encode()\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\tuseragent.SetHeader(req.Header)\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n\n// OAuthStaticAccessToken Authorization header.\n// https://www.civo.com/api#authentication\nfunc OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {\n\tif client == nil {\n\t\tclient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tclient.Transport = &oauth2.Transport{\n\t\tSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),\n\t\tBase:   client.Transport,\n\t}\n\n\treturn client\n}\n"
  },
  {
    "path": "providers/dns/civo/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(OAuthStaticAccessToken(server.Client(), \"secret\"), \"LON1\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(\"Authorization\", \"Bearer secret\").\n\t\t\tWithRegexp(\"User-Agent\", `goacme-lego/[0-9.]+ \\(.+\\)`),\n\t)\n}\n\nfunc TestClient_ListDomains(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns\",\n\t\t\tservermock.ResponseFromFixture(\"list_domain_names.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"region\", \"LON1\")).\n\t\tBuild(t)\n\n\tdomains, err := client.ListDomains(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []Domain{{\n\t\tID:        \"7088fcea-7658-43e6-97fa-273f901978fd\",\n\t\tAccountID: \"e7e8386e-434e-482f-95e0-c406e5d564c2\",\n\t\tName:      \"example.com\",\n\t}}\n\n\tassert.Equal(t, expected, domains)\n}\n\nfunc TestClient_ListDNSRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/7088fcea-7658-43e6-97fa-273f901978fd/records\",\n\t\t\tservermock.ResponseFromFixture(\"list_dns_records.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"region\", \"LON1\")).\n\t\tBuild(t)\n\n\trecords, err := client.ListDNSRecords(t.Context(), \"7088fcea-7658-43e6-97fa-273f901978fd\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tID:       \"76cc107f-fbef-4e2b-b97f-f5d34f4075d3\",\n\t\t\tDomainID: \"edc5dacf-a2ad-4757-41ee-c12f06259c70\",\n\t\t\tName:     \"_acme-challenge\",\n\t\t\tValue:    \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\t\tType:     \"txt\",\n\t\t\tTTL:      600,\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_ListDNSRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/7088fcea-7658-43e6-97fa-273f901978fd/records\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.ListDNSRecords(t.Context(), \"7088fcea-7658-43e6-97fa-273f901978fd\")\n\trequire.EqualError(t, err, \"database_account_not_found: Failed to find the account within the internal database\")\n}\n\nfunc TestClient_ListDNSRecords_error_raw(t *testing.T) {\n\t// the API says:\n\t// > 4xx/5xx status may not be JSON, unless it's obvious that the response should be parsed for a specific reason.\n\t// > So, for example, 404 Not Found pages are a standard page of text\n\t// > but 403 Unauthorized requests may have a reason attribute available in the JSON object.\n\t// https://www.civo.com/api#parameters-and-responses\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/7088fcea-7658-43e6-97fa-273f901978fd/records\",\n\t\t\tservermock.RawStringResponse(http.StatusText(http.StatusNotFound)).\n\t\t\t\tWithStatusCode(http.StatusNotFound)).\n\t\tBuild(t)\n\n\t_, err := client.ListDNSRecords(t.Context(), \"7088fcea-7658-43e6-97fa-273f901978fd\")\n\trequire.EqualError(t, err, \"unexpected status code: [status code: 404] body: Not Found\")\n}\n\nfunc TestClient_CreateDNSRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/7088fcea-7658-43e6-97fa-273f901978fd/records\",\n\t\t\tservermock.ResponseFromFixture(\"create_dns_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_dns_record-request.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:  \"_acme-challenge\",\n\t\tValue: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tType:  \"TXT\",\n\t\tTTL:   600,\n\t}\n\n\tnewRecord, err := client.CreateDNSRecord(t.Context(), \"7088fcea-7658-43e6-97fa-273f901978fd\", record)\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tID:       \"76cc107f-fbef-4e2b-b97f-f5d34f4075d3\",\n\t\tDomainID: \"edc5dacf-a2ad-4757-41ee-c12f06259c70\",\n\t\tName:     \"_acme-challenge\",\n\t\tValue:    \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tType:     \"txt\",\n\t\tTTL:      600,\n\t}\n\n\tassert.Equal(t, expected, newRecord)\n}\n\nfunc TestClient_DeleteDNSRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns/edc5dacf-a2ad-4757-41ee-c12f06259c70/records/76cc107f-fbef-4e2b-b97f-f5d34f4075d3\",\n\t\t\tservermock.ResponseFromFixture(\"delete_dns_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"region\", \"LON1\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tID:       \"76cc107f-fbef-4e2b-b97f-f5d34f4075d3\",\n\t\tDomainID: \"edc5dacf-a2ad-4757-41ee-c12f06259c70\",\n\t\tName:     \"_acme-challenge\",\n\t\tValue:    \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tType:     \"TXT\",\n\t\tTTL:      600,\n\t}\n\n\terr := client.DeleteDNSRecord(t.Context(), record)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/civo/internal/fixtures/create_dns_record-request.json",
    "content": "{\n  \"type\": \"TXT\",\n  \"name\": \"_acme-challenge\",\n  \"value\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n  \"ttl\": 600\n}\n"
  },
  {
    "path": "providers/dns/civo/internal/fixtures/create_dns_record.json",
    "content": "{\n  \"id\": \"76cc107f-fbef-4e2b-b97f-f5d34f4075d3\",\n  \"created_at\": \"2019-04-11T12:47:56.000+01:00\",\n  \"updated_at\": \"2019-04-11T12:47:56.000+01:00\",\n  \"account_id\": null,\n  \"domain_id\": \"edc5dacf-a2ad-4757-41ee-c12f06259c70\",\n  \"name\": \"_acme-challenge\",\n  \"value\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n  \"type\": \"txt\",\n  \"ttl\": 600\n}\n"
  },
  {
    "path": "providers/dns/civo/internal/fixtures/delete_dns_record.json",
    "content": "{\n  \"result\": \"success\"\n}\n"
  },
  {
    "path": "providers/dns/civo/internal/fixtures/error.json",
    "content": "{\n  \"code\": \"database_account_not_found\",\n  \"reason\": \"Failed to find the account within the internal database\"\n}\n"
  },
  {
    "path": "providers/dns/civo/internal/fixtures/list_dns_records.json",
    "content": "[\n  {\n    \"id\": \"76cc107f-fbef-4e2b-b97f-f5d34f4075d3\",\n    \"created_at\": \"2019-04-11T12:47:56.000+01:00\",\n    \"updated_at\": \"2019-04-11T12:47:56.000+01:00\",\n    \"account_id\": null,\n    \"domain_id\": \"edc5dacf-a2ad-4757-41ee-c12f06259c70\",\n    \"name\": \"_acme-challenge\",\n    \"value\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n    \"type\": \"txt\",\n    \"ttl\": 600\n  }\n]\n"
  },
  {
    "path": "providers/dns/civo/internal/fixtures/list_domain_names.json",
    "content": "[\n  {\n    \"id\": \"7088fcea-7658-43e6-97fa-273f901978fd\",\n    \"account_id\": \"e7e8386e-434e-482f-95e0-c406e5d564c2\",\n    \"name\": \"example.com\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/civo/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype APIError struct {\n\tCode   string `json:\"code\"`\n\tReason string `json:\"reason\"`\n}\n\nfunc (a *APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", a.Code, a.Reason)\n}\n\ntype Record struct {\n\tID        string `json:\"id,omitempty\"`\n\tAccountID string `json:\"account_id,omitempty\"`\n\tDomainID  string `json:\"domain_id,omitempty\"`\n\tName      string `json:\"name,omitempty\"`\n\tValue     string `json:\"value,omitempty\"`\n\tType      string `json:\"type,omitempty\"`\n\tTTL       int    `json:\"ttl,omitempty\"`\n}\n\ntype Domain struct {\n\tID        string `json:\"id,omitempty\"`\n\tAccountID string `json:\"account_id,omitempty\"`\n\tName      string `json:\"name,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/clouddns/clouddns.go",
    "content": "// Package clouddns implements a DNS provider for solving the DNS-01 challenge using CloudDNS API.\npackage clouddns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/clouddns/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"CLOUDDNS_\"\n\n\tEnvClientID = envNamespace + \"CLIENT_ID\"\n\tEnvEmail    = envNamespace + \"EMAIL\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the DNSProvider.\ntype Config struct {\n\tClientID string\n\tEmail    string\n\tPassword string\n\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *internal.Client\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for CloudDNS.\n// Credentials must be passed in the environment variables:\n// CLOUDDNS_CLIENT_ID, CLOUDDNS_EMAIL, CLOUDDNS_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvClientID, EnvEmail, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"clouddns: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.ClientID = values[EnvClientID]\n\tconfig.Email = values[EnvEmail]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for CloudDNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"clouddns: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.ClientID == \"\" || config.Email == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"clouddns: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(config.ClientID, config.Email, config.Password, config.TTL)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"clouddns: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = d.client.AddRecord(ctx, authZone, info.EffectiveFQDN, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"clouddns: add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"clouddns: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = d.client.DeleteRecord(ctx, authZone, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"clouddns: delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/clouddns/clouddns.toml",
    "content": "Name = \"CloudDNS\"\nDescription = ''''''\nURL = \"https://vshosting.eu/\"\nCode = \"clouddns\"\nSince = \"v3.6.0\"\n\nExample = '''\nCLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \\\nCLOUDDNS_EMAIL=you@example.com \\\nCLOUDDNS_PASSWORD=b9841238feb177a84330f \\\nlego --dns clouddns -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    CLOUDDNS_CLIENT_ID = \"Client ID\"\n    CLOUDDNS_EMAIL = \"Account email\"\n    CLOUDDNS_PASSWORD = \"Account password\"\n  [Configuration.Additional]\n    CLOUDDNS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 5)\"\n    CLOUDDNS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    CLOUDDNS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    CLOUDDNS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://admin.vshosting.cloud/clouddns/swagger/\"\n  APIAdmin = \"https://admin.vshosting.cloud/api/public/swagger/\"\n  Documentation = \"https://github.com/vshosting/clouddns\"\n"
  },
  {
    "path": "providers/dns/clouddns/clouddns_test.go",
    "content": "package clouddns\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvClientID,\n\tEnvEmail,\n\tEnvPassword).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvClientID: \"client123\",\n\t\t\t\tEnvEmail:    \"test@example.com\",\n\t\t\t\tEnvPassword: \"password123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing clientId\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvClientID: \"\",\n\t\t\t\tEnvEmail:    \"test@example.com\",\n\t\t\t\tEnvPassword: \"password123\",\n\t\t\t},\n\t\t\texpected: \"clouddns: some credentials information are missing: CLOUDDNS_CLIENT_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing email\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvClientID: \"client123\",\n\t\t\t\tEnvEmail:    \"\",\n\t\t\t\tEnvPassword: \"password123\",\n\t\t\t},\n\t\t\texpected: \"clouddns: some credentials information are missing: CLOUDDNS_EMAIL\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvClientID: \"client123\",\n\t\t\t\tEnvEmail:    \"test@example.com\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"clouddns: some credentials information are missing: CLOUDDNS_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tclientID string\n\t\temail    string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tclientID: \"ID\",\n\t\t\temail:    \"test@example.com\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"clouddns: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing client ID\",\n\t\t\tclientID: \"\",\n\t\t\temail:    \"test@example.com\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"clouddns: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing email\",\n\t\t\tclientID: \"ID\",\n\t\t\temail:    \"\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"clouddns: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tclientID: \"ID\",\n\t\t\temail:    \"test@example.com\",\n\t\t\tpassword: \"\",\n\t\t\texpected: \"clouddns: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.ClientID = test.clientID\n\t\t\tconfig.Email = test.email\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/clouddns/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst apiBaseURL = \"https://admin.vshosting.cloud/clouddns\"\n\nconst authorizationHeader = \"Authorization\"\n\n// Client handles all communication with CloudDNS API.\ntype Client struct {\n\tclientID string\n\temail    string\n\tpassword string\n\tttl      int\n\n\tapiBaseURL *url.URL\n\n\tloginURL *url.URL\n\n\tHTTPClient *http.Client\n}\n\n// NewClient returns a Client instance configured to handle CloudDNS API communication.\nfunc NewClient(clientID, email, password string, ttl int) *Client {\n\tbaseURL, _ := url.Parse(apiBaseURL)\n\tloginBaseURL, _ := url.Parse(loginURL)\n\n\treturn &Client{\n\t\tclientID:   clientID,\n\t\temail:      email,\n\t\tpassword:   password,\n\t\tttl:        ttl,\n\t\tapiBaseURL: baseURL,\n\t\tloginURL:   loginBaseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// AddRecord is a high level method to add a new record into CloudDNS zone.\nfunc (c *Client) AddRecord(ctx context.Context, zone, recordName, recordValue string) error {\n\tdomain, err := c.getDomain(ctx, zone)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trecord := Record{DomainID: domain.ID, Name: recordName, Value: recordValue, Type: \"TXT\"}\n\n\terr = c.addTxtRecord(ctx, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.publishRecords(ctx, domain.ID)\n}\n\n// DeleteRecord is a high level method to remove a record from zone.\nfunc (c *Client) DeleteRecord(ctx context.Context, zone, recordName string) error {\n\tdomain, err := c.getDomain(ctx, zone)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trecord, err := c.getRecord(ctx, domain.ID, recordName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.deleteRecord(ctx, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.publishRecords(ctx, domain.ID)\n}\n\nfunc (c *Client) addTxtRecord(ctx context.Context, record Record) error {\n\tendpoint := c.apiBaseURL.JoinPath(\"record-txt\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) deleteRecord(ctx context.Context, record Record) error {\n\tendpoint := c.apiBaseURL.JoinPath(\"record\", record.ID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) getDomain(ctx context.Context, zone string) (Domain, error) {\n\tsearchQuery := SearchQuery{\n\t\tSearch: []Search{\n\t\t\t{Name: \"clientId\", Operator: \"eq\", Value: c.clientID},\n\t\t\t{Name: \"domainName\", Operator: \"eq\", Value: zone},\n\t\t},\n\t}\n\n\tendpoint := c.apiBaseURL.JoinPath(\"domain\", \"search\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, searchQuery)\n\tif err != nil {\n\t\treturn Domain{}, err\n\t}\n\n\tvar result SearchResponse\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn Domain{}, err\n\t}\n\n\tif len(result.Items) == 0 {\n\t\treturn Domain{}, fmt.Errorf(\"domain not found: %s\", zone)\n\t}\n\n\treturn result.Items[0], nil\n}\n\nfunc (c *Client) getRecord(ctx context.Context, domainID, recordName string) (Record, error) {\n\tendpoint := c.apiBaseURL.JoinPath(\"domain\", domainID)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn Record{}, err\n\t}\n\n\tvar result DomainInfo\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn Record{}, err\n\t}\n\n\tfor _, record := range result.LastDomainRecordList {\n\t\tif record.Name == recordName && record.Type == \"TXT\" {\n\t\t\treturn record, nil\n\t\t}\n\t}\n\n\treturn Record{}, fmt.Errorf(\"record not found: domainID %s, name %s\", domainID, recordName)\n}\n\nfunc (c *Client) publishRecords(ctx context.Context, domainID string) error {\n\tendpoint := c.apiBaseURL.JoinPath(\"domain\", domainID, \"publish\")\n\n\tpayload := DomainInfo{SoaTTL: c.ttl}\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tat := getAccessToken(req.Context())\n\tif at != \"\" {\n\t\treq.Header.Set(authorizationHeader, \"Bearer \"+at)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar response APIError\n\n\terr := json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code %d] %w\", resp.StatusCode, response.Error)\n}\n"
  },
  {
    "path": "providers/dns/clouddns/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"clientID\", \"email@example.com\", \"secret\", 300)\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.apiBaseURL, _ = url.Parse(server.URL + \"/api\")\n\t\t\tclient.loginURL, _ = url.Parse(server.URL + \"/login\")\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /api/domain/search\",\n\t\t\tservermock.ResponseFromFixture(\"domain_search.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"domain_search-request.json\")).\n\t\tRoute(\"POST /api/record-txt\", nil,\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"record_txt-request.json\")).\n\t\tRoute(\"PUT /api/domain/A/publish\", nil,\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"publish-request.json\")).\n\t\tRoute(\"POST /login\",\n\t\t\tservermock.ResponseFromFixture(\"login.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"login-request.json\")).\n\t\tBuild(t)\n\n\tctx, err := client.CreateAuthenticatedContext(t.Context())\n\trequire.NoError(t, err)\n\n\terr = client.AddRecord(ctx, \"example.com\", \"_acme-challenge.example.com\", \"txt\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /api/domain/search\",\n\t\t\tservermock.ResponseFromFixture(\"domain_search.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"domain_search-request.json\")).\n\t\tRoute(\"GET /api/domain/A\",\n\t\t\tservermock.ResponseFromFixture(\"domain-request.json\")).\n\t\tRoute(\"DELETE /api/record/R01\", nil).\n\t\tRoute(\"PUT /api/domain/A/publish\", nil,\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"publish-request.json\")).\n\t\tRoute(\"POST /login\",\n\t\t\tservermock.ResponseFromFixture(\"login.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"login-request.json\")).\n\t\tBuild(t)\n\n\tctx, err := client.CreateAuthenticatedContext(t.Context())\n\trequire.NoError(t, err)\n\n\terr = client.DeleteRecord(ctx, \"example.com\", \"_acme-challenge.example.com\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/clouddns/internal/fixtures/domain-request.json",
    "content": "{\n  \"id\": \"Z\",\n  \"domainName\": \"example.com\",\n  \"lastDomainRecordList\": [\n    {\n      \"id\": \"R01\",\n      \"domainId\": \"A\",\n      \"name\": \"_acme-challenge.example.com\",\n      \"value\": \"txt\",\n      \"type\": \"TXT\"\n    }\n  ],\n  \"soaTtl\": 300\n}\n"
  },
  {
    "path": "providers/dns/clouddns/internal/fixtures/domain_search-request.json",
    "content": "{\n  \"search\": [\n    {\n      \"name\": \"clientId\",\n      \"operator\": \"eq\",\n      \"value\": \"clientID\"\n    },\n    {\n      \"name\": \"domainName\",\n      \"operator\": \"eq\",\n      \"value\": \"example.com\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/clouddns/internal/fixtures/domain_search.json",
    "content": "{\n  \"items\": [\n    {\n      \"id\": \"A\",\n      \"domainName\": \"example.com\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/clouddns/internal/fixtures/login-request.json",
    "content": "{\n  \"email\": \"email@example.com\",\n  \"password\": \"secret\"\n}\n"
  },
  {
    "path": "providers/dns/clouddns/internal/fixtures/login.json",
    "content": "{\n  \"auth\": {\n    \"accessToken\": \"at\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/clouddns/internal/fixtures/publish-request.json",
    "content": "{\n  \"soaTtl\": 300\n}\n"
  },
  {
    "path": "providers/dns/clouddns/internal/fixtures/record_txt-request.json",
    "content": "{\n  \"domainId\": \"A\",\n  \"name\": \"_acme-challenge.example.com\",\n  \"value\": \"txt\",\n  \"type\": \"TXT\"\n}\n"
  },
  {
    "path": "providers/dns/clouddns/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\nconst loginURL = \"https://admin.vshosting.cloud/api/public/auth/login\"\n\ntype token string\n\nconst accessTokenKey token = \"accessToken\"\n\nfunc (c *Client) login(ctx context.Context) (*AuthResponse, error) {\n\tauthorization := Authorization{Email: c.email, Password: c.password}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, c.loginURL, authorization)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result AuthResponse\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &result, nil\n}\n\nfunc (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) {\n\ttok, err := c.login(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn context.WithValue(ctx, accessTokenKey, tok.Auth.AccessToken), nil\n}\n\nfunc getAccessToken(ctx context.Context) string {\n\ttok, ok := ctx.Value(accessTokenKey).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn tok\n}\n"
  },
  {
    "path": "providers/dns/clouddns/internal/identity_test.go",
    "content": "package internal\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestClient_CreateAuthenticatedContext(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /login\",\n\t\t\tservermock.ResponseFromFixture(\"login.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"login-request.json\")).\n\t\tRoute(\"DELETE /api/record/xxx\", nil).\n\t\tBuild(t)\n\n\tctx, err := client.CreateAuthenticatedContext(t.Context())\n\trequire.NoError(t, err)\n\n\tat := getAccessToken(ctx)\n\tassert.Equal(t, \"at\", at)\n\n\terr = client.deleteRecord(ctx, Record{ID: \"xxx\"})\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/clouddns/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype APIError struct {\n\tError ErrorContent `json:\"error\"`\n}\n\ntype ErrorContent struct {\n\tCode    int    `json:\"code,omitempty\"`\n\tMessage string `json:\"message,omitempty\"`\n}\n\nfunc (e ErrorContent) Error() string {\n\treturn fmt.Sprintf(\"%d: %s\", e.Code, e.Message)\n}\n\ntype Authorization struct {\n\tEmail    string `json:\"email,omitempty\"`\n\tPassword string `json:\"password,omitempty\"`\n}\n\ntype AuthResponse struct {\n\tAuth Auth `json:\"auth\"`\n}\n\ntype Auth struct {\n\tAccessToken  string `json:\"accessToken,omitempty\"`\n\tRefreshToken string `json:\"refreshToken,omitempty\"`\n}\n\ntype SearchQuery struct {\n\tLimit  int      `json:\"limit,omitempty\"`\n\tOffset int      `json:\"offset,omitempty\"`\n\tSearch []Search `json:\"search,omitempty\"`\n\tSort   []Sort   `json:\"sort,omitempty\"`\n}\n\n// Search used for searches in the CloudDNS API.\ntype Search struct {\n\tName     string `json:\"name,omitempty\"`\n\tOperator string `json:\"operator,omitempty\"`\n\tType     string `json:\"type,omitempty\"`\n\tValue    string `json:\"value,omitempty\"`\n}\n\ntype Sort struct {\n\tAscending bool   `json:\"ascending,omitempty\"`\n\tName      string `json:\"name,omitempty\"`\n}\n\ntype SearchResponse struct {\n\tItems     []Domain `json:\"items,omitempty\"`\n\tLimit     int      `json:\"limit,omitempty\"`\n\tOffset    int      `json:\"offset,omitempty\"`\n\tTotalHits int      `json:\"totalHits,omitempty\"`\n}\n\ntype Domain struct {\n\tID         string `json:\"id,omitempty\"`\n\tDomainName string `json:\"domainName,omitempty\"`\n\tStatus     string `json:\"status,omitempty\"`\n}\n\n// Record represents a DNS record.\ntype Record struct {\n\tID       string `json:\"id,omitempty\"`\n\tDomainID string `json:\"domainId,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tValue    string `json:\"value,omitempty\"`\n\tType     string `json:\"type,omitempty\"`\n}\n\ntype DomainInfo struct {\n\tID                   string   `json:\"id,omitempty\"`\n\tDomainName           string   `json:\"domainName,omitempty\"`\n\tLastDomainRecordList []Record `json:\"lastDomainRecordList,omitempty\"`\n\tSoaTTL               int      `json:\"soaTtl,omitempty\"`\n\tStatus               string   `json:\"status,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/cloudflare/cloudflare.go",
    "content": "// Package cloudflare implements a DNS provider for solving the DNS-01 challenge using cloudflare DNS.\npackage cloudflare\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cloudflare/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"CLOUDFLARE_\"\n\n\tEnvEmail  = envNamespace + \"EMAIL\"\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvDNSAPIToken  = envNamespace + \"DNS_API_TOKEN\"\n\tEnvZoneAPIToken = envNamespace + \"ZONE_API_TOKEN\"\n\n\tEnvBaseURL = envNamespace + \"BASE_URL\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst (\n\taltEnvNamespace = \"CF_\"\n\n\taltEnvEmail = altEnvNamespace + \"API_EMAIL\"\n)\n\nconst (\n\tminTTL = 120\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAuthEmail string\n\tAuthKey   string\n\n\tAuthToken string\n\tZoneToken string\n\n\tBaseURL string\n\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOneWithFallback(EnvTTL, minTTL, strconv.Atoi, altEnvName(EnvTTL)),\n\t\tPropagationTimeout: env.GetOneWithFallback(EnvPropagationTimeout, 2*time.Minute, env.ParseSecond, altEnvName(EnvPropagationTimeout)),\n\t\tPollingInterval:    env.GetOneWithFallback(EnvPollingInterval, dns01.DefaultPollingInterval, env.ParseSecond, altEnvName(EnvPollingInterval)),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOneWithFallback(EnvHTTPTimeout, 30*time.Second, env.ParseSecond, altEnvName(EnvHTTPTimeout)),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *metaClient\n\tconfig *Config\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Cloudflare.\n// Credentials must be passed in as environment variables:\n//\n// Either provide CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY,\n// or a CLOUDFLARE_DNS_API_TOKEN.\n//\n// For a more paranoid setup, provide CLOUDFLARE_DNS_API_TOKEN and CLOUDFLARE_ZONE_API_TOKEN.\n//\n// The email and API key should be avoided, if possible.\n// Instead, set up an API token with both Zone:Read and DNS:Edit permission, and pass the CLOUDFLARE_DNS_API_TOKEN environment variable.\n// You can split the Zone:Read and DNS:Edit permissions across multiple API tokens:\n// in this case pass both CLOUDFLARE_ZONE_API_TOKEN and CLOUDFLARE_DNS_API_TOKEN accordingly.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.GetWithFallback(\n\t\t[]string{EnvEmail, altEnvEmail},\n\t\t[]string{EnvAPIKey, altEnvName(EnvAPIKey)},\n\t)\n\tif err != nil {\n\t\tvar errT error\n\n\t\tvalues, errT = env.GetWithFallback(\n\t\t\t[]string{EnvDNSAPIToken, altEnvName(EnvDNSAPIToken)},\n\t\t\t[]string{EnvZoneAPIToken, altEnvName(EnvZoneAPIToken), EnvDNSAPIToken, altEnvName(EnvDNSAPIToken)},\n\t\t)\n\t\tif errT != nil {\n\t\t\t//nolint:errorlint\n\t\t\treturn nil, fmt.Errorf(\"cloudflare: %v or %v\", err, errT)\n\t\t}\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AuthEmail = values[EnvEmail]\n\tconfig.AuthKey = values[EnvAPIKey]\n\tconfig.AuthToken = values[EnvDNSAPIToken]\n\tconfig.ZoneToken = values[EnvZoneAPIToken]\n\tconfig.BaseURL = env.GetOrFile(EnvBaseURL)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Cloudflare.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"cloudflare: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"cloudflare: invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\tclient, err := newClient(config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cloudflare: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tclient:    client,\n\t\tconfig:    config,\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cloudflare: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzoneID, err := d.client.ZoneIDByName(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cloudflare: failed to find zone %s: %w\", authZone, err)\n\t}\n\n\tdnsRecord := internal.Record{\n\t\tType:    \"TXT\",\n\t\tName:    dns01.UnFqdn(info.EffectiveFQDN),\n\t\tContent: `\"` + info.Value + `\"`,\n\t\tTTL:     d.config.TTL,\n\t}\n\n\tresponse, err := d.client.CreateDNSRecord(ctx, zoneID, dnsRecord)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cloudflare: failed to create TXT record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = response.ID\n\td.recordIDsMu.Unlock()\n\n\tlog.Infof(\"cloudflare: new record for %s, ID %s\", domain, response.ID)\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cloudflare: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzoneID, err := d.client.ZoneIDByName(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cloudflare: failed to find zone %s: %w\", authZone, err)\n\t}\n\n\t// get the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"cloudflare: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\terr = d.client.DeleteDNSRecord(ctx, zoneID, recordID)\n\tif err != nil {\n\t\tlog.Printf(\"cloudflare: failed to delete TXT record: %v\", err)\n\t}\n\n\t// Delete record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc altEnvName(v string) string {\n\treturn strings.ReplaceAll(v, envNamespace, altEnvNamespace)\n}\n"
  },
  {
    "path": "providers/dns/cloudflare/cloudflare.toml",
    "content": "Name = \"Cloudflare\"\nDescription = ''''''\nURL = \"https://www.cloudflare.com/dns/\"\nCode = \"cloudflare\"\nSince = \"v0.3.0\"\n\nExample = '''\nCLOUDFLARE_EMAIL=you@example.com \\\nCLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \\\nlego --dns cloudflare -d '*.example.com' -d example.com run\n\n# or\n\nCLOUDFLARE_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \\\nlego --dns cloudflare -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Description\n\nYou may use `CF_API_EMAIL` and `CF_API_KEY` to authenticate, or `CF_DNS_API_TOKEN`, or `CF_DNS_API_TOKEN` and `CF_ZONE_API_TOKEN`.\n\n### API keys\n\nIf using API keys (`CF_API_EMAIL` and `CF_API_KEY`), the Global API Key needs to be used, not the Origin CA Key.\n\nPlease be aware, that this in principle allows Lego to read and change *everything* related to this account.\n\n### API tokens\n\nWith API tokens (`CF_DNS_API_TOKEN`, and optionally `CF_ZONE_API_TOKEN`),\nvery specific access can be granted to your resources at Cloudflare.\nSee this [Cloudflare announcement](https://blog.cloudflare.com/api-tokens-general-availability/) for details.\n\nThe main resources Lego cares for are the DNS entries for your Zones.\nIt also needs to resolve a domain name to an internal Zone ID in order to manipulate DNS entries.\n\nHence, you should create an API token with the following permissions:\n\n* Zone / Zone / Read\n* Zone / DNS / Edit\n\nYou also need to scope the access to all your domains for this to work.\nThen pass the API token as `CF_DNS_API_TOKEN` to Lego.\n\n**Alternatively,** if you prefer a more strict set of privileges,\nyou can split the access tokens:\n\n* Create one with *Zone / Zone / Read* permissions and scope it to all your zones or just the individual zone you need to edit.\n  This is needed to resolve domain names to Zone IDs and can be shared among multiple Lego installations.\n  Pass this API token as `CF_ZONE_API_TOKEN` to Lego.\n* Create another API token with *Zone / DNS / Edit* permissions and set the scope to the domains you want to manage with a single Lego installation.\n  Pass this token as `CF_DNS_API_TOKEN` to Lego.\n* Repeat the previous step for each host you want to run Lego on.\n* It is possible to use the same api token for both variables if it is given `Zone:Read` and `DNS:Edit` permission for the zone.\n\nThis \"paranoid\" setup is mainly interesting for users who manage many zones/domains with a single Cloudflare account.\nIt follows the principle of least privilege and limits the possible damage, should one of the hosts become compromised.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    CF_API_EMAIL = \"Account email\"\n    CF_API_KEY = \"API key\"\n    CF_DNS_API_TOKEN = \"API token with DNS:Edit permission (since v3.1.0)\"\n    CF_ZONE_API_TOKEN = \"API token with Zone:Read permission (since v3.1.0)\"\n    CLOUDFLARE_EMAIL = \"Alias to CF_API_EMAIL\"\n    CLOUDFLARE_API_KEY = \"Alias to CF_API_KEY\"\n    CLOUDFLARE_DNS_API_TOKEN = \"Alias to CF_DNS_API_TOKEN\"\n    CLOUDFLARE_ZONE_API_TOKEN = \"Alias to CF_ZONE_API_TOKEN\"\n  [Configuration.Additional]\n    CLOUDFLARE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    CLOUDFLARE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    CLOUDFLARE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    CLOUDFLARE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: )\"\n    CLOUDFLARE_BASE_URL = \"API base URL (Default: https://api.cloudflare.com/client/v4)\"\n\n[Links]\n  API = \"https://api.cloudflare.com/\"\n  GoClient = \"https://github.com/cloudflare/cloudflare-go\"\n"
  },
  {
    "path": "providers/dns/cloudflare/cloudflare_test.go",
    "content": "package cloudflare\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvEmail,\n\tEnvAPIKey,\n\tEnvDNSAPIToken,\n\tEnvZoneAPIToken,\n\tEnvBaseURL,\n\taltEnvEmail,\n\taltEnvName(EnvAPIKey),\n\taltEnvName(EnvDNSAPIToken),\n\taltEnvName(EnvZoneAPIToken)).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success email, API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEmail:  \"test@example.com\",\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success API token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvDNSAPIToken: \"012345abcdef\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success separate API tokens\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvDNSAPIToken:  \"012345abcdef\",\n\t\t\t\tEnvZoneAPIToken: \"abcdef012345\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEmail:       \"\",\n\t\t\t\tEnvAPIKey:      \"\",\n\t\t\t\tEnvDNSAPIToken: \"\",\n\t\t\t},\n\t\t\texpected: \"cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY or some credentials information are missing: CLOUDFLARE_DNS_API_TOKEN,CLOUDFLARE_ZONE_API_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing email\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEmail:  \"\",\n\t\t\t\tEnvAPIKey: \"key\",\n\t\t\t},\n\t\t\texpected: \"cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL or some credentials information are missing: CLOUDFLARE_DNS_API_TOKEN,CLOUDFLARE_ZONE_API_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEmail:  \"awesome@possum.com\",\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"cloudflare: some credentials information are missing: CLOUDFLARE_API_KEY or some credentials information are missing: CLOUDFLARE_DNS_API_TOKEN,CLOUDFLARE_ZONE_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderWithToken(t *testing.T) {\n\ttype expected struct {\n\t\tdnsToken   string\n\t\tzoneToken  string\n\t\tsameClient bool\n\t\terror      string\n\t}\n\n\ttestCases := []struct {\n\t\tdesc string\n\n\t\t// test input\n\t\tenvVars map[string]string\n\n\t\t// expectations\n\t\texpected expected\n\t}{\n\t\t{\n\t\t\tdesc: \"same client when zone token is missing\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvDNSAPIToken: \"123\",\n\t\t\t},\n\t\t\texpected: expected{\n\t\t\t\tdnsToken:   \"123\",\n\t\t\t\tzoneToken:  \"123\",\n\t\t\t\tsameClient: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"same client when zone token equals dns token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvDNSAPIToken:  \"123\",\n\t\t\t\tEnvZoneAPIToken: \"123\",\n\t\t\t},\n\t\t\texpected: expected{\n\t\t\t\tdnsToken:   \"123\",\n\t\t\t\tzoneToken:  \"123\",\n\t\t\t\tsameClient: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"failure when only zone api given\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvZoneAPIToken: \"123\",\n\t\t\t},\n\t\t\texpected: expected{\n\t\t\t\terror: \"cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY or some credentials information are missing: CLOUDFLARE_DNS_API_TOKEN\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"different clients when zone and dns token differ\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvDNSAPIToken:  \"123\",\n\t\t\t\tEnvZoneAPIToken: \"abc\",\n\t\t\t},\n\t\t\texpected: expected{\n\t\t\t\tdnsToken:   \"123\",\n\t\t\t\tzoneToken:  \"abc\",\n\t\t\t\tsameClient: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"aliases work as expected\", // CLOUDFLARE_* takes precedence over CF_*\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvDNSAPIToken:              \"123\",\n\t\t\t\taltEnvName(EnvDNSAPIToken):  \"456\",\n\t\t\t\tEnvZoneAPIToken:             \"abc\",\n\t\t\t\taltEnvName(EnvZoneAPIToken): \"def\",\n\t\t\t},\n\t\t\texpected: expected{\n\t\t\t\tdnsToken:   \"123\",\n\t\t\t\tzoneToken:  \"abc\",\n\t\t\t\tsameClient: false,\n\t\t\t},\n\t\t},\n\t}\n\n\tdefer envTest.RestoreEnv()\n\n\tlocalEnvTest := tester.NewEnvTest(\n\t\tEnvDNSAPIToken, altEnvName(EnvDNSAPIToken),\n\t\tEnvZoneAPIToken, altEnvName(EnvZoneAPIToken),\n\t).WithDomain(envDomain)\n\n\tenvTest.ClearEnv()\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer localEnvTest.RestoreEnv()\n\n\t\t\tlocalEnvTest.ClearEnv()\n\t\t\tlocalEnvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected.error != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expected.error)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, p)\n\t\t\tassert.Equal(t, test.expected.dnsToken, p.config.AuthToken)\n\t\t\tassert.Equal(t, test.expected.zoneToken, p.config.ZoneToken)\n\n\t\t\tif test.expected.sameClient {\n\t\t\t\tassert.Equal(t, p.client.clientRead, p.client.clientEdit)\n\t\t\t} else {\n\t\t\t\tassert.NotEqual(t, p.client.clientRead, p.client.clientEdit)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tauthEmail string\n\t\tauthKey   string\n\t\tauthToken string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success with email and api key\",\n\t\t\tauthEmail: \"test@example.com\",\n\t\t\tauthKey:   \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"success with api token\",\n\t\t\tauthToken: \"012345abcdef\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"prefer api token\",\n\t\t\tauthToken: \"012345abcdef\",\n\t\t\tauthEmail: \"test@example.com\",\n\t\t\tauthKey:   \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"cloudflare: invalid credentials: authEmail, authKey or authToken must be set\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing email\",\n\t\t\tauthKey:  \"123\",\n\t\t\texpected: \"cloudflare: invalid credentials: authEmail and authKey must be set together\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing api key\",\n\t\t\tauthEmail: \"test@example.com\",\n\t\t\texpected:  \"cloudflare: invalid credentials: authEmail and authKey must be set together\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AuthEmail = test.authEmail\n\t\t\tconfig.AuthKey = test.authKey\n\t\t\tconfig.AuthToken = test.authToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AuthEmail = \"foo@example.com\"\n\t\t\tconfig.AuthKey = \"secret\"\n\t\t\tconfig.BaseURL = server.URL\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithRegexp(\"User-Agent\", `goacme-lego/[0-9.]+ \\(.+\\)`).\n\t\t\tWith(\"X-Auth-Email\", \"foo@example.com\").\n\t\t\tWith(\"X-Auth-Key\", \"secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\t// https://developers.cloudflare.com/api/resources/zones/methods/list/\n\t\tRoute(\"GET /zones\",\n\t\t\tservermock.ResponseFromInternal(\"zones.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com\").\n\t\t\t\tWith(\"per_page\", \"50\")).\n\t\t// https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/create/\n\t\tRoute(\"POST /zones/023e105f4ecef8ad9ca31a8372d0c353/dns_records\",\n\t\t\tservermock.ResponseFromInternal(\"create_record.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentType(\"application/json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\t// https://developers.cloudflare.com/api/resources/zones/methods/list/\n\t\tRoute(\"GET /zones\",\n\t\t\tservermock.ResponseFromInternal(\"zones.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com\").\n\t\t\t\tWith(\"per_page\", \"50\")).\n\t\t// https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/delete/\n\t\tRoute(\"DELETE /zones/023e105f4ecef8ad9ca31a8372d0c353/dns_records/xxx\",\n\t\t\tservermock.ResponseFromInternal(\"delete_record.json\")).\n\t\tBuild(t)\n\n\ttoken := \"abc\"\n\n\tprovider.recordIDsMu.Lock()\n\tprovider.recordIDs[\"abc\"] = \"xxx\"\n\tprovider.recordIDsMu.Unlock()\n\n\terr := provider.CleanUp(\"example.com\", token, \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/cloudflare/internal/client.go",
    "content": "/*\nPackage internal Cloudflare API client.\n\nThe official client is huge and still growing.\n- https://github.com/cloudflare/cloudflare-go/issues/4171\n*/\npackage internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\nconst defaultBaseURL = \"https://api.cloudflare.com/client/v4\"\n\n// Client the Cloudflare API client.\ntype Client struct {\n\tauthEmail string\n\tauthKey   string\n\tauthToken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(opts ...Option) (*Client, error) {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\tclient := &Client{\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}\n\n\tfor _, opt := range opts {\n\t\terr := opt(client)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif client.authToken != \"\" {\n\t\treturn client, nil\n\t}\n\n\tif client.authEmail == \"\" && client.authKey == \"\" {\n\t\treturn nil, errors.New(\"invalid credentials: authEmail, authKey or authToken must be set\")\n\t}\n\n\tif client.authEmail == \"\" || client.authKey == \"\" {\n\t\treturn nil, errors.New(\"invalid credentials: authEmail and authKey must be set together\")\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn client, nil\n}\n\n// CreateDNSRecord creates a new DNS record for a zone.\n// https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/create/\nfunc (c *Client) CreateDNSRecord(ctx context.Context, zoneID string, record Record) (*Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"zones\", zoneID, \"dns_records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result APIResponse[Record]\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &result.Result, nil\n}\n\n// DeleteDNSRecord deletes DNS record.\n// https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/delete/\nfunc (c *Client) DeleteDNSRecord(ctx context.Context, zoneID, recordID string) error {\n\tendpoint := c.baseURL.JoinPath(\"zones\", zoneID, \"dns_records\", recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\n// ZonesByName returns a list of zones matching the given name.\n// https://developers.cloudflare.com/api/resources/zones/methods/list/\nfunc (c *Client) ZonesByName(ctx context.Context, name string) ([]Zone, error) {\n\tendpoint := c.baseURL.JoinPath(\"zones\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"name\", name)\n\tquery.Set(\"per_page\", \"50\")\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result APIResponse[[]Zone]\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Result, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\t// https://developers.cloudflare.com/fundamentals/api/how-to/make-api-calls/\n\tif c.authToken != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.authToken)\n\t} else {\n\t\treq.Header.Set(\"X-Auth-Email\", c.authEmail)\n\t\treq.Header.Set(\"X-Auth-Key\", c.authKey)\n\t}\n\n\tuseragent.SetHeader(req.Header)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar response APIResponse[any]\n\n\terr := json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code %d] %w\", resp.StatusCode, response.Errors)\n}\n"
  },
  {
    "path": "providers/dns/cloudflare/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\n\t\t\t\tWithAuthKey(\"foo@example.com\", \"secret\"),\n\t\t\t\tWithHTTPClient(server.Client()),\n\t\t\t\tWithBaseURL(server.URL),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithRegexp(\"User-Agent\", `goacme-lego/[0-9.]+ \\(.+\\)`).\n\t\t\tWithAccept(\"application/json\").\n\t\t\tWith(\"X-Auth-Email\", \"foo@example.com\").\n\t\t\tWith(\"X-Auth-Key\", \"secret\"),\n\t)\n}\n\nfunc TestClient_CreateDNSRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zones/023e105f4ecef8ad9ca31a8372d0c353/dns_records\",\n\t\t\tservermock.ResponseFromFixture(\"create_record.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentType(\"application/json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:    \"_acme-challenge.example.com\",\n\t\tTTL:     120,\n\t\tType:    \"TXT\",\n\t\tContent: `\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"`,\n\t}\n\n\tnewRecord, err := client.CreateDNSRecord(t.Context(), \"023e105f4ecef8ad9ca31a8372d0c353\", record)\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tID:      \"023e105f4ecef8ad9ca31a8372d0c353\",\n\t\tName:    \"example.com\",\n\t\tTTL:     3600,\n\t\tType:    \"A\",\n\t\tComment: \"Domain verification record\",\n\t\tContent: \"198.51.100.4\",\n\t}\n\n\tassert.Equal(t, expected, newRecord)\n}\n\nfunc TestClient_CreateDNSRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zones/023e105f4ecef8ad9ca31a8372d0c353/dns_records\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:    \"_acme-challenge.example.com\",\n\t\tTTL:     120,\n\t\tType:    \"TXT\",\n\t\tContent: `\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"`,\n\t}\n\n\t_, err := client.CreateDNSRecord(t.Context(), \"023e105f4ecef8ad9ca31a8372d0c353\", record)\n\trequire.EqualError(t, err, \"[status code 400] 6003: Invalid request headers; 6103: Invalid format for X-Auth-Key header\")\n}\n\nfunc TestClient_DeleteDNSRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /zones/023e105f4ecef8ad9ca31a8372d0c353/dns_records/xxx\",\n\t\t\tservermock.ResponseFromFixture(\"delete_record.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteDNSRecord(context.Background(), \"023e105f4ecef8ad9ca31a8372d0c353\", \"xxx\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteDNSRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /zones/023e105f4ecef8ad9ca31a8372d0c353/dns_records/xxx\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\terr := client.DeleteDNSRecord(context.Background(), \"023e105f4ecef8ad9ca31a8372d0c353\", \"xxx\")\n\trequire.EqualError(t, err, \"[status code 400] 6003: Invalid request headers; 6103: Invalid format for X-Auth-Key header\")\n}\n\nfunc TestClient_ZonesByName(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones\",\n\t\t\tservermock.ResponseFromFixture(\"zones.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com\").\n\t\t\t\tWith(\"per_page\", \"50\")).\n\t\tBuild(t)\n\n\tzones, err := client.ZonesByName(context.Background(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []Zone{\n\t\t{\n\t\t\tID:      \"023e105f4ecef8ad9ca31a8372d0c353\",\n\t\t\tAccount: Account{ID: \"023e105f4ecef8ad9ca31a8372d0c353\", Name: \"Example Account Name\"},\n\t\t\tMeta: Meta{\n\t\t\t\tCdnOnly:                true,\n\t\t\t\tCustomCertificateQuota: 1,\n\t\t\t\tDNSOnly:                true,\n\t\t\t\tFoundationDNS:          true,\n\t\t\t\tPageRuleQuota:          100,\n\t\t\t\tPhishingDetected:       false,\n\t\t\t\tStep:                   2,\n\t\t\t},\n\t\t\tName: \"example.com\",\n\t\t\tOwner: Owner{\n\t\t\t\tID:   \"023e105f4ecef8ad9ca31a8372d0c353\",\n\t\t\t\tName: \"Example Org\",\n\t\t\t\tType: \"organization\",\n\t\t\t},\n\t\t\tPlan: Plan{\n\t\t\t\tID:                \"023e105f4ecef8ad9ca31a8372d0c353\",\n\t\t\t\tCanSubscribe:      false,\n\t\t\t\tCurrency:          \"USD\",\n\t\t\t\tExternallyManaged: false,\n\t\t\t\tFrequency:         \"monthly\",\n\t\t\t\tIsSubscribed:      false,\n\t\t\t\tLegacyDiscount:    false,\n\t\t\t\tLegacyID:          \"free\",\n\t\t\t\tPrice:             10,\n\t\t\t\tName:              \"Example Org\",\n\t\t\t},\n\t\t\tCnameSuffix: \"cdn.cloudflare.com\",\n\t\t\tPaused:      true,\n\t\t\tPermissions: []string{\"#worker:read\"},\n\t\t\tTenant: Tenant{\n\t\t\t\tID:   \"023e105f4ecef8ad9ca31a8372d0c353\",\n\t\t\t\tName: \"Example Account Name\",\n\t\t\t},\n\t\t\tTenantUnit: TenantUnit{\n\t\t\t\tID: \"023e105f4ecef8ad9ca31a8372d0c353\",\n\t\t\t},\n\t\t\tType:              \"full\",\n\t\t\tVanityNameServers: []string{\"ns1.example.com\", \"ns2.example.com\"},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_ZonesByName_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\t_, err := client.ZonesByName(context.Background(), \"example.com\")\n\trequire.EqualError(t, err, \"[status code 400] 6003: Invalid request headers; 6103: Invalid format for X-Auth-Key header\")\n}\n"
  },
  {
    "path": "providers/dns/cloudflare/internal/fixtures/create_record-request.json",
    "content": "{\n  \"type\": \"TXT\",\n  \"name\": \"_acme-challenge.example.com\",\n  \"content\": \"\\\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\\\"\",\n  \"ttl\": 120\n}\n"
  },
  {
    "path": "providers/dns/cloudflare/internal/fixtures/create_record.json",
    "content": "{\n  \"errors\": [\n    {\n      \"code\": 1000,\n      \"message\": \"message\",\n      \"documentation_url\": \"documentation_url\",\n      \"source\": {\n        \"pointer\": \"pointer\"\n      }\n    }\n  ],\n  \"messages\": [\n    {\n      \"code\": 1000,\n      \"message\": \"message\",\n      \"documentation_url\": \"documentation_url\",\n      \"source\": {\n        \"pointer\": \"pointer\"\n      }\n    }\n  ],\n  \"success\": true,\n  \"result\": {\n    \"name\": \"example.com\",\n    \"ttl\": 3600,\n    \"type\": \"A\",\n    \"comment\": \"Domain verification record\",\n    \"content\": \"198.51.100.4\",\n    \"proxied\": true,\n    \"settings\": {\n      \"ipv4_only\": true,\n      \"ipv6_only\": true\n    },\n    \"tags\": [\n      \"owner:dns-team\"\n    ],\n    \"id\": \"023e105f4ecef8ad9ca31a8372d0c353\",\n    \"proxiable\": true\n  }\n}\n"
  },
  {
    "path": "providers/dns/cloudflare/internal/fixtures/delete_record.json",
    "content": "{\n  \"result\": {\n    \"id\": \"023e105f4ecef8ad9ca31a8372d0c353\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/cloudflare/internal/fixtures/error.json",
    "content": "{\n  \"success\": false,\n  \"errors\": [\n    {\n      \"code\": 6003,\n      \"message\": \"Invalid request headers\",\n      \"error_chain\": [\n        {\n          \"code\": 6103,\n          \"message\": \"Invalid format for X-Auth-Key header\"\n        }\n      ]\n    }\n  ],\n  \"messages\": [],\n  \"result\": null\n}\n"
  },
  {
    "path": "providers/dns/cloudflare/internal/fixtures/zones.json",
    "content": "{\n  \"errors\": [\n    {\n      \"code\": 1000,\n      \"message\": \"message\",\n      \"documentation_url\": \"documentation_url\",\n      \"source\": {\n        \"pointer\": \"pointer\"\n      }\n    }\n  ],\n  \"messages\": [\n    {\n      \"code\": 1000,\n      \"message\": \"message\",\n      \"documentation_url\": \"documentation_url\",\n      \"source\": {\n        \"pointer\": \"pointer\"\n      }\n    }\n  ],\n  \"success\": true,\n  \"result\": [\n    {\n      \"id\": \"023e105f4ecef8ad9ca31a8372d0c353\",\n      \"account\": {\n        \"id\": \"023e105f4ecef8ad9ca31a8372d0c353\",\n        \"name\": \"Example Account Name\"\n      },\n      \"meta\": {\n        \"cdn_only\": true,\n        \"custom_certificate_quota\": 1,\n        \"dns_only\": true,\n        \"foundation_dns\": true,\n        \"page_rule_quota\": 100,\n        \"phishing_detected\": false,\n        \"step\": 2\n      },\n      \"name\": \"example.com\",\n      \"owner\": {\n        \"id\": \"023e105f4ecef8ad9ca31a8372d0c353\",\n        \"name\": \"Example Org\",\n        \"type\": \"organization\"\n      },\n      \"plan\": {\n        \"id\": \"023e105f4ecef8ad9ca31a8372d0c353\",\n        \"can_subscribe\": false,\n        \"currency\": \"USD\",\n        \"externally_managed\": false,\n        \"frequency\": \"monthly\",\n        \"is_subscribed\": false,\n        \"legacy_discount\": false,\n        \"legacy_id\": \"free\",\n        \"price\": 10,\n        \"name\": \"Example Org\"\n      },\n      \"cname_suffix\": \"cdn.cloudflare.com\",\n      \"paused\": true,\n      \"permissions\": [\n        \"#worker:read\"\n      ],\n      \"tenant\": {\n        \"id\": \"023e105f4ecef8ad9ca31a8372d0c353\",\n        \"name\": \"Example Account Name\"\n      },\n      \"tenant_unit\": {\n        \"id\": \"023e105f4ecef8ad9ca31a8372d0c353\"\n      },\n      \"type\": \"full\",\n      \"vanity_name_servers\": [\n        \"ns1.example.com\",\n        \"ns2.example.com\"\n      ]\n    }\n  ],\n  \"result_info\": {\n    \"count\": 1,\n    \"page\": 1,\n    \"per_page\": 20,\n    \"total_count\": 1,\n    \"total_pages\": 1\n  }\n}\n"
  },
  {
    "path": "providers/dns/cloudflare/internal/options.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n)\n\ntype Option func(c *Client) error\n\nfunc WithAuthKey(authEmail, authKey string) Option {\n\treturn func(c *Client) error {\n\t\tc.authEmail = authEmail\n\t\tc.authKey = authKey\n\n\t\treturn nil\n\t}\n}\n\nfunc WithAuthToken(authToken string) Option {\n\treturn func(c *Client) error {\n\t\tc.authToken = authToken\n\n\t\treturn nil\n\t}\n}\n\nfunc WithBaseURL(baseURL string) Option {\n\treturn func(c *Client) error {\n\t\tif baseURL == \"\" {\n\t\t\treturn nil\n\t\t}\n\n\t\tbu, err := url.Parse(baseURL)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.baseURL = bu\n\n\t\treturn nil\n\t}\n}\n\nfunc WithHTTPClient(client *http.Client) Option {\n\treturn func(c *Client) error {\n\t\tif client != nil {\n\t\t\tc.HTTPClient = client\n\t\t}\n\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "providers/dns/cloudflare/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype Record struct {\n\tID      string `json:\"id,omitempty\"`\n\tName    string `json:\"name,omitempty\"`\n\tTTL     int    `json:\"ttl,omitempty\"`\n\tType    string `json:\"type,omitempty\"`\n\tComment string `json:\"comment,omitempty\"`\n\tContent string `json:\"content,omitempty\"`\n}\n\ntype APIResponse[T any] struct {\n\tErrors     Errors      `json:\"errors,omitempty\"`\n\tMessages   []Message   `json:\"messages,omitempty\"`\n\tSuccess    bool        `json:\"success,omitempty\"`\n\tResult     T           `json:\"result,omitempty\"`\n\tResultInfo *ResultInfo `json:\"result_info,omitempty\"`\n}\n\ntype Message struct {\n\tCode             int          `json:\"code\"`\n\tMessage          string       `json:\"message\"`\n\tDocumentationURL string       `json:\"documentation_url\"`\n\tSource           *Source      `json:\"source\"`\n\tErrorChain       []ErrorChain `json:\"error_chain\"`\n}\n\ntype Source struct {\n\tPointer string `json:\"pointer\"`\n}\n\ntype ErrorChain struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\ntype Errors []Message\n\nfunc (e Errors) Error() string {\n\tmsg := new(strings.Builder)\n\n\tfor _, item := range e {\n\t\t_, _ = fmt.Fprintf(msg, \"%d: %s\", item.Code, item.Message)\n\n\t\tfor _, link := range item.ErrorChain {\n\t\t\t_, _ = fmt.Fprintf(msg, \"; %d: %s\", link.Code, link.Message)\n\t\t}\n\t}\n\n\treturn msg.String()\n}\n\ntype ResultInfo struct {\n\tCount      int `json:\"count\"`\n\tPage       int `json:\"page\"`\n\tPerPage    int `json:\"per_page\"`\n\tTotalCount int `json:\"total_count\"`\n\tTotalPages int `json:\"total_pages\"`\n}\n\ntype Zone struct {\n\tID                string     `json:\"id\"`\n\tAccount           Account    `json:\"account\"`\n\tMeta              Meta       `json:\"meta\"`\n\tName              string     `json:\"name\"`\n\tOwner             Owner      `json:\"owner\"`\n\tPlan              Plan       `json:\"plan\"`\n\tCnameSuffix       string     `json:\"cname_suffix\"`\n\tPaused            bool       `json:\"paused\"`\n\tPermissions       []string   `json:\"permissions\"`\n\tTenant            Tenant     `json:\"tenant\"`\n\tTenantUnit        TenantUnit `json:\"tenant_unit\"`\n\tType              string     `json:\"type\"`\n\tVanityNameServers []string   `json:\"vanity_name_servers\"`\n}\n\ntype Account struct {\n\tID   string `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\ntype Meta struct {\n\tCdnOnly                bool `json:\"cdn_only\"`\n\tCustomCertificateQuota int  `json:\"custom_certificate_quota\"`\n\tDNSOnly                bool `json:\"dns_only\"`\n\tFoundationDNS          bool `json:\"foundation_dns\"`\n\tPageRuleQuota          int  `json:\"page_rule_quota\"`\n\tPhishingDetected       bool `json:\"phishing_detected\"`\n\tStep                   int  `json:\"step\"`\n}\n\ntype Owner struct {\n\tID   string `json:\"id\"`\n\tName string `json:\"name\"`\n\tType string `json:\"type\"`\n}\n\ntype Plan struct {\n\tID                string `json:\"id\"`\n\tCanSubscribe      bool   `json:\"can_subscribe\"`\n\tCurrency          string `json:\"currency\"`\n\tExternallyManaged bool   `json:\"externally_managed\"`\n\tFrequency         string `json:\"frequency\"`\n\tIsSubscribed      bool   `json:\"is_subscribed\"`\n\tLegacyDiscount    bool   `json:\"legacy_discount\"`\n\tLegacyID          string `json:\"legacy_id\"`\n\tPrice             int    `json:\"price\"`\n\tName              string `json:\"name\"`\n}\n\ntype Tenant struct {\n\tID   string `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\ntype TenantUnit struct {\n\tID string `json:\"id\"`\n}\n"
  },
  {
    "path": "providers/dns/cloudflare/wrapper.go",
    "content": "package cloudflare\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"sync\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cloudflare/internal\"\n)\n\ntype metaClient struct {\n\tclientEdit *internal.Client // needs Zone/DNS/Edit permissions\n\tclientRead *internal.Client // needs Zone/Zone/Read permissions\n\n\tzones   map[string]string // caches calls to ZoneIDByName, see lookupZoneID()\n\tzonesMu *sync.RWMutex\n}\n\nfunc newClient(config *Config) (*metaClient, error) {\n\t// with AuthKey/AuthEmail we can access all available APIs\n\tif config.AuthToken == \"\" {\n\t\tclient, err := internal.NewClient(\n\t\t\tinternal.WithBaseURL(config.BaseURL),\n\t\t\tinternal.WithHTTPClient(config.HTTPClient),\n\t\t\tinternal.WithAuthKey(config.AuthEmail, config.AuthKey))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &metaClient{\n\t\t\tclientEdit: client,\n\t\t\tclientRead: client,\n\t\t\tzones:      make(map[string]string),\n\t\t\tzonesMu:    &sync.RWMutex{},\n\t\t}, nil\n\t}\n\n\tdns, err := internal.NewClient(\n\t\tinternal.WithBaseURL(config.BaseURL),\n\t\tinternal.WithHTTPClient(config.HTTPClient),\n\t\tinternal.WithAuthToken(config.AuthToken))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif config.ZoneToken == \"\" || config.ZoneToken == config.AuthToken {\n\t\treturn &metaClient{\n\t\t\tclientEdit: dns,\n\t\t\tclientRead: dns,\n\t\t\tzones:      make(map[string]string),\n\t\t\tzonesMu:    &sync.RWMutex{},\n\t\t}, nil\n\t}\n\n\tzone, err := internal.NewClient(\n\t\tinternal.WithBaseURL(config.BaseURL),\n\t\tinternal.WithHTTPClient(config.HTTPClient),\n\t\tinternal.WithAuthToken(config.ZoneToken))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &metaClient{\n\t\tclientEdit: dns,\n\t\tclientRead: zone,\n\t\tzones:      make(map[string]string),\n\t\tzonesMu:    &sync.RWMutex{},\n\t}, nil\n}\n\nfunc (m *metaClient) CreateDNSRecord(ctx context.Context, zoneID string, rr internal.Record) (*internal.Record, error) {\n\treturn m.clientEdit.CreateDNSRecord(ctx, zoneID, rr)\n}\n\nfunc (m *metaClient) DeleteDNSRecord(ctx context.Context, zoneID, recordID string) error {\n\treturn m.clientEdit.DeleteDNSRecord(ctx, zoneID, recordID)\n}\n\nfunc (m *metaClient) ZoneIDByName(ctx context.Context, fdqn string) (string, error) {\n\tm.zonesMu.RLock()\n\tid := m.zones[fdqn]\n\tm.zonesMu.RUnlock()\n\n\tif id != \"\" {\n\t\treturn id, nil\n\t}\n\n\tzones, err := m.clientRead.ZonesByName(ctx, dns01.UnFqdn(fdqn))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tid, err = extractZoneID(zones)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tm.zonesMu.Lock()\n\tm.zones[fdqn] = id\n\tm.zonesMu.Unlock()\n\n\treturn id, nil\n}\n\nfunc extractZoneID(res []internal.Zone) (string, error) {\n\tswitch len(res) {\n\tcase 0:\n\t\treturn \"\", errors.New(\"zone could not be found\")\n\tcase 1:\n\t\treturn res[0].ID, nil\n\tdefault:\n\t\treturn \"\", errors.New(\"ambiguous zone name; an account ID might help\")\n\t}\n}\n"
  },
  {
    "path": "providers/dns/cloudns/cloudns.go",
    "content": "// Package cloudns implements a DNS provider for solving the DNS-01 challenge using ClouDNS DNS.\npackage cloudns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cloudns/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"CLOUDNS_\"\n\n\tEnvAuthID       = envNamespace + \"AUTH_ID\"\n\tEnvSubAuthID    = envNamespace + \"SUB_AUTH_ID\"\n\tEnvAuthPassword = envNamespace + \"AUTH_PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAuthID             string\n\tSubAuthID          string\n\tAuthPassword       string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 60),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 180*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for ClouDNS.\n// Credentials must be passed in the environment variables:\n// CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvar subAuthID string\n\n\tauthID := env.GetOrFile(EnvAuthID)\n\tif authID == \"\" {\n\t\tsubAuthID = env.GetOrFile(EnvSubAuthID)\n\t}\n\n\tif authID == \"\" && subAuthID == \"\" {\n\t\treturn nil, fmt.Errorf(\"ClouDNS: some credentials information are missing: %s or %s\", EnvAuthID, EnvSubAuthID)\n\t}\n\n\tvalues, err := env.Get(EnvAuthPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ClouDNS: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AuthID = authID\n\tconfig.SubAuthID = subAuthID\n\tconfig.AuthPassword = values[EnvAuthPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for ClouDNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"ClouDNS: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.AuthID, config.SubAuthID, config.AuthPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ClouDNS: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzone, err := d.client.GetZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ClouDNS: %w\", err)\n\t}\n\n\terr = d.client.AddTxtRecord(ctx, zone.Name, info.EffectiveFQDN, info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ClouDNS: %w\", err)\n\t}\n\n\treturn d.waitNameservers(ctx, domain, zone)\n}\n\n// CleanUp removes the TXT records matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzone, err := d.client.GetZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ClouDNS: %w\", err)\n\t}\n\n\trecords, err := d.client.ListTxtRecords(ctx, zone.Name, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ClouDNS: %w\", err)\n\t}\n\n\tif len(records) == 0 {\n\t\treturn nil\n\t}\n\n\tfor _, record := range records {\n\t\terr = d.client.RemoveTxtRecord(ctx, record.ID, zone.Name)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"ClouDNS: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// waitNameservers At the time of writing 4 servers are found as authoritative, but 8 are reported during the sync.\n// If this is not done, the secondary verification done by Let's Encrypt server will fail quire a bit.\nfunc (d *DNSProvider) waitNameservers(ctx context.Context, domain string, zone *internal.Zone) error {\n\treturn wait.Retry(ctx,\n\t\tfunc() error {\n\t\t\tsyncProgress, err := d.client.GetUpdateStatus(ctx, zone.Name)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"nameserver sync on %s: %w\", domain, err)\n\t\t\t}\n\n\t\t\tlog.Infof(\"[%s] Sync %d/%d complete\", domain, syncProgress.Updated, syncProgress.Total)\n\n\t\t\tif !syncProgress.Complete {\n\t\t\t\treturn fmt.Errorf(\"nameserver sync on %s not complete\", domain)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t\tbackoff.WithBackOff(backoff.NewConstantBackOff(d.config.PollingInterval)),\n\t\tbackoff.WithMaxElapsedTime(d.config.PropagationTimeout),\n\t)\n}\n"
  },
  {
    "path": "providers/dns/cloudns/cloudns.toml",
    "content": "Name = \"ClouDNS\"\nDescription = ''''''\nURL = \"https://www.cloudns.net\"\nCode = \"cloudns\"\nSince = \"v2.3.0\"\n\nExample = '''\nCLOUDNS_AUTH_ID=xxxx \\\nCLOUDNS_AUTH_PASSWORD=yyyy \\\nlego --dns cloudns -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    CLOUDNS_AUTH_ID = \"The API user ID\"\n    CLOUDNS_AUTH_PASSWORD = \"The password for API user ID\"\n  [Configuration.Additional]\n    CLOUDNS_SUB_AUTH_ID = \"The API sub user ID\"\n    CLOUDNS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    CLOUDNS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 180)\"\n    CLOUDNS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    CLOUDNS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.cloudns.net/wiki/article/42/\"\n"
  },
  {
    "path": "providers/dns/cloudns/cloudns_test.go",
    "content": "package cloudns\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAuthID,\n\tEnvSubAuthID,\n\tEnvAuthPassword).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success auth-id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthID:       \"123\",\n\t\t\t\tEnvSubAuthID:    \"\",\n\t\t\t\tEnvAuthPassword: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success sub-auth-id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthID:       \"\",\n\t\t\t\tEnvSubAuthID:    \"123\",\n\t\t\t\tEnvAuthPassword: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthID:       \"\",\n\t\t\t\tEnvSubAuthID:    \"\",\n\t\t\t\tEnvAuthPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"ClouDNS: some credentials information are missing: CLOUDNS_AUTH_ID or CLOUDNS_SUB_AUTH_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing auth-id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthID:       \"\",\n\t\t\t\tEnvSubAuthID:    \"\",\n\t\t\t\tEnvAuthPassword: \"456\",\n\t\t\t},\n\t\t\texpected: \"ClouDNS: some credentials information are missing: CLOUDNS_AUTH_ID or CLOUDNS_SUB_AUTH_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing sub-auth-id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthID:       \"\",\n\t\t\t\tEnvSubAuthID:    \"\",\n\t\t\t\tEnvAuthPassword: \"456\",\n\t\t\t},\n\t\t\texpected: \"ClouDNS: some credentials information are missing: CLOUDNS_AUTH_ID or CLOUDNS_SUB_AUTH_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing auth-password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthID:       \"123\",\n\t\t\t\tEnvSubAuthID:    \"\",\n\t\t\t\tEnvAuthPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"ClouDNS: some credentials information are missing: CLOUDNS_AUTH_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc         string\n\t\tauthID       string\n\t\tsubAuthID    string\n\t\tauthPassword string\n\t\texpected     string\n\t}{\n\t\t{\n\t\t\tdesc:         \"success auth-id\",\n\t\t\tauthID:       \"123\",\n\t\t\tsubAuthID:    \"\",\n\t\t\tauthPassword: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"success sub-auth-id\",\n\t\t\tauthID:       \"\",\n\t\t\tsubAuthID:    \"123\",\n\t\t\tauthPassword: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"ClouDNS: credentials missing: authID or subAuthID\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"missing auth-id\",\n\t\t\tauthID:       \"\",\n\t\t\tsubAuthID:    \"\",\n\t\t\tauthPassword: \"456\",\n\t\t\texpected:     \"ClouDNS: credentials missing: authID or subAuthID\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"missing sub-auth-id\",\n\t\t\tauthID:       \"\",\n\t\t\tsubAuthID:    \"\",\n\t\t\tauthPassword: \"456\",\n\t\t\texpected:     \"ClouDNS: credentials missing: authID or subAuthID\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing auth-password\",\n\t\t\tauthID:   \"123\",\n\t\t\texpected: \"ClouDNS: credentials missing: authPassword\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AuthID = test.authID\n\t\t\tconfig.SubAuthID = test.subAuthID\n\t\t\tconfig.AuthPassword = test.authPassword\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/cloudns/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api.cloudns.net/dns/\"\n\n// Client the ClouDNS client.\ntype Client struct {\n\tauthID       string\n\tsubAuthID    string\n\tauthPassword string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a ClouDNS client.\nfunc NewClient(authID, subAuthID, authPassword string) (*Client, error) {\n\tif authID == \"\" && subAuthID == \"\" {\n\t\treturn nil, errors.New(\"credentials missing: authID or subAuthID\")\n\t}\n\n\tif authPassword == \"\" {\n\t\treturn nil, errors.New(\"credentials missing: authPassword\")\n\t}\n\n\tbaseURL, err := url.Parse(defaultBaseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\tauthID:       authID,\n\t\tsubAuthID:    subAuthID,\n\t\tauthPassword: authPassword,\n\t\tBaseURL:      baseURL,\n\t\tHTTPClient:   &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// GetZone Get domain name information for a FQDN.\nfunc (c *Client) GetZone(ctx context.Context, authFQDN string) (*Zone, error) {\n\tauthZone, err := dns01.FindZoneByFqdn(authFQDN)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tauthZoneName := dns01.UnFqdn(authZone)\n\n\tendpoint := c.BaseURL.JoinPath(\"get-zone-info.json\")\n\n\tq := endpoint.Query()\n\tq.Set(\"domain-name\", authZoneName)\n\tendpoint.RawQuery = q.Encode()\n\n\treq, err := c.newRequest(ctx, http.MethodGet, endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trawMessage, err := c.do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar zone Zone\n\n\tif len(rawMessage) > 0 {\n\t\tif err = json.Unmarshal(rawMessage, &zone); err != nil {\n\t\t\treturn nil, errutils.NewUnmarshalError(req, http.StatusOK, rawMessage, err)\n\t\t}\n\t}\n\n\tif zone.Name == authZoneName {\n\t\treturn &zone, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"zone %s not found for authFQDN %s\", authZoneName, authFQDN)\n}\n\n// FindTxtRecord returns the TXT record a zone ID and a FQDN.\nfunc (c *Client) FindTxtRecord(ctx context.Context, zoneName, fqdn string) (*TXTRecord, error) {\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, zoneName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tendpoint := c.BaseURL.JoinPath(\"records.json\")\n\n\tq := endpoint.Query()\n\tq.Set(\"domain-name\", zoneName)\n\tq.Set(\"host\", subDomain)\n\tq.Set(\"type\", \"TXT\")\n\tendpoint.RawQuery = q.Encode()\n\n\treq, err := c.newRequest(ctx, http.MethodGet, endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trawMessage, err := c.do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// the API returns [] when there is no records.\n\tif string(rawMessage) == \"[]\" {\n\t\treturn nil, nil\n\t}\n\n\tvar records map[string]TXTRecord\n\tif err = json.Unmarshal(rawMessage, &records); err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, http.StatusOK, rawMessage, err)\n\t}\n\n\tfor _, record := range records {\n\t\tif record.Host == subDomain && record.Type == \"TXT\" {\n\t\t\treturn &record, nil\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n\n// ListTxtRecords returns the TXT records a zone ID and a FQDN.\nfunc (c *Client) ListTxtRecords(ctx context.Context, zoneName, fqdn string) ([]TXTRecord, error) {\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, zoneName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tendpoint := c.BaseURL.JoinPath(\"records.json\")\n\n\tq := endpoint.Query()\n\tq.Set(\"domain-name\", zoneName)\n\tq.Set(\"host\", subDomain)\n\tq.Set(\"type\", \"TXT\")\n\tendpoint.RawQuery = q.Encode()\n\n\treq, err := c.newRequest(ctx, http.MethodGet, endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trawMessage, err := c.do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// the API returns [] when there is no records.\n\tif string(rawMessage) == \"[]\" {\n\t\treturn nil, nil\n\t}\n\n\tvar raw map[string]TXTRecord\n\tif err = json.Unmarshal(rawMessage, &raw); err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, http.StatusOK, rawMessage, err)\n\t}\n\n\tvar records []TXTRecord\n\n\tfor _, record := range raw {\n\t\tif record.Host == subDomain && record.Type == \"TXT\" {\n\t\t\trecords = append(records, record)\n\t\t}\n\t}\n\n\treturn records, nil\n}\n\n// AddTxtRecord adds a TXT record.\nfunc (c *Client) AddTxtRecord(ctx context.Context, zoneName, fqdn, value string, ttl int) error {\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, zoneName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tendpoint := c.BaseURL.JoinPath(\"add-record.json\")\n\n\tq := endpoint.Query()\n\tq.Set(\"domain-name\", zoneName)\n\tq.Set(\"host\", subDomain)\n\tq.Set(\"record\", value)\n\tq.Set(\"ttl\", strconv.Itoa(ttlRounder(ttl)))\n\tq.Set(\"record-type\", \"TXT\")\n\tendpoint.RawQuery = q.Encode()\n\n\treq, err := c.newRequest(ctx, http.MethodPost, endpoint)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trawMessage, err := c.do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp := apiResponse{}\n\tif err = json.Unmarshal(rawMessage, &resp); err != nil {\n\t\treturn errutils.NewUnmarshalError(req, http.StatusOK, rawMessage, err)\n\t}\n\n\tif resp.Status != \"Success\" {\n\t\treturn fmt.Errorf(\"failed to add TXT record: %s %s\", resp.Status, resp.StatusDescription)\n\t}\n\n\treturn nil\n}\n\n// RemoveTxtRecord removes a TXT record.\nfunc (c *Client) RemoveTxtRecord(ctx context.Context, recordID int, zoneName string) error {\n\tendpoint := c.BaseURL.JoinPath(\"delete-record.json\")\n\n\tq := endpoint.Query()\n\tq.Set(\"domain-name\", zoneName)\n\tq.Set(\"record-id\", strconv.Itoa(recordID))\n\tendpoint.RawQuery = q.Encode()\n\n\treq, err := c.newRequest(ctx, http.MethodPost, endpoint)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trawMessage, err := c.do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp := apiResponse{}\n\tif err = json.Unmarshal(rawMessage, &resp); err != nil {\n\t\treturn errutils.NewUnmarshalError(req, http.StatusOK, rawMessage, err)\n\t}\n\n\tif resp.Status != \"Success\" {\n\t\treturn fmt.Errorf(\"failed to remove TXT record: %s %s\", resp.Status, resp.StatusDescription)\n\t}\n\n\treturn nil\n}\n\n// GetUpdateStatus gets sync progress of all CloudDNS NS servers.\nfunc (c *Client) GetUpdateStatus(ctx context.Context, zoneName string) (*SyncProgress, error) {\n\tendpoint := c.BaseURL.JoinPath(\"update-status.json\")\n\n\tq := endpoint.Query()\n\tq.Set(\"domain-name\", zoneName)\n\tendpoint.RawQuery = q.Encode()\n\n\treq, err := c.newRequest(ctx, http.MethodGet, endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trawMessage, err := c.do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// the API returns [] when there is no records.\n\tif string(rawMessage) == \"[]\" {\n\t\treturn nil, errors.New(\"no nameservers records returned\")\n\t}\n\n\tvar records []UpdateRecord\n\tif err = json.Unmarshal(rawMessage, &records); err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, http.StatusOK, rawMessage, err)\n\t}\n\n\tupdatedCount := 0\n\n\tfor _, record := range records {\n\t\tif record.Updated {\n\t\t\tupdatedCount++\n\t\t}\n\t}\n\n\treturn &SyncProgress{Complete: updatedCount == len(records), Updated: updatedCount, Total: len(records)}, nil\n}\n\nfunc (c *Client) newRequest(ctx context.Context, method string, endpoint *url.URL) (*http.Request, error) {\n\tq := endpoint.Query()\n\n\tif c.subAuthID != \"\" {\n\t\tq.Set(\"sub-auth-id\", c.subAuthID)\n\t} else {\n\t\tq.Set(\"auth-id\", c.authID)\n\t}\n\n\tq.Set(\"auth-password\", c.authPassword)\n\n\tendpoint.RawQuery = q.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treturn req, nil\n}\n\nfunc (c *Client) do(req *http.Request) (json.RawMessage, error) {\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\treturn raw, nil\n}\n\n// Rounds the given TTL in seconds to the next accepted value.\n// Accepted TTL values are:\n//   - 60 = 1 minute\n//   - 300 = 5 minutes\n//   - 900 = 15 minutes\n//   - 1800 = 30 minutes\n//   - 3600 = 1 hour\n//   - 21600 = 6 hours\n//   - 43200 = 12 hours\n//   - 86400 = 1 day\n//   - 172800 = 2 days\n//   - 259200 = 3 days\n//   - 604800 = 1 week\n//   - 1209600 = 2 weeks\n//   - 2592000 = 1 month\n//\n// See https://www.cloudns.net/wiki/article/58/ for details.\nfunc ttlRounder(ttl int) int {\n\tfor _, validTTL := range []int{60, 300, 900, 1800, 3600, 21600, 43200, 86400, 172800, 259200, 604800, 1209600} {\n\t\tif ttl <= validTTL {\n\t\t\treturn validTTL\n\t\t}\n\t}\n\n\treturn 2592000\n}\n"
  },
  {
    "path": "providers/dns/cloudns/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(subAuthID string) func(server *httptest.Server) (*Client, error) {\n\treturn func(server *httptest.Server) (*Client, error) {\n\t\tclient, err := NewClient(\"myAuthID\", subAuthID, \"myAuthPassword\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\tclient.HTTPClient = server.Client()\n\n\t\treturn client, nil\n\t}\n}\n\nfunc TestNewClient(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc         string\n\t\tauthID       string\n\t\tsubAuthID    string\n\t\tauthPassword string\n\t\texpected     string\n\t}{\n\t\t{\n\t\t\tdesc:         \"all provided\",\n\t\t\tauthID:       \"1000\",\n\t\t\tsubAuthID:    \"1111\",\n\t\t\tauthPassword: \"no-secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"missing authID & subAuthID\",\n\t\t\tauthID:       \"\",\n\t\t\tsubAuthID:    \"\",\n\t\t\tauthPassword: \"no-secret\",\n\t\t\texpected:     \"credentials missing: authID or subAuthID\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"missing authID & subAuthID\",\n\t\t\tauthID:       \"\",\n\t\t\tsubAuthID:    \"present\",\n\t\t\tauthPassword: \"\",\n\t\t\texpected:     \"credentials missing: authPassword\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient, err := NewClient(test.authID, test.subAuthID, test.authPassword)\n\n\t\t\tif test.expected != \"\" {\n\t\t\t\tassert.Nil(t, client)\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t} else {\n\t\t\t\tassert.NotNil(t, client)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_GetZone(t *testing.T) {\n\ttype expected struct {\n\t\tzone     *Zone\n\t\terrorMsg string\n\t}\n\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tauthFQDN    string\n\t\tapiResponse string\n\t\texpected    expected\n\t}{\n\t\t{\n\t\t\tdesc:        \"zone found\",\n\t\t\tauthFQDN:    \"_acme-challenge.foo.com.\",\n\t\t\tapiResponse: `{\"name\": \"foo.com\", \"type\": \"master\", \"zone\": \"zone\", \"status\": \"1\"}`,\n\t\t\texpected: expected{\n\t\t\t\tzone: &Zone{\n\t\t\t\t\tName:   \"foo.com\",\n\t\t\t\t\tType:   \"master\",\n\t\t\t\t\tZone:   \"zone\",\n\t\t\t\t\tStatus: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"zone not found\",\n\t\t\tauthFQDN:    \"_acme-challenge.foo.com.\",\n\t\t\tapiResponse: ``,\n\t\t\texpected: expected{\n\t\t\t\terrorMsg: \"zone foo.com not found for authFQDN _acme-challenge.foo.com.\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"invalid json response\",\n\t\t\tauthFQDN:    \"_acme-challenge.foo.com.\",\n\t\t\tapiResponse: `[{}]`,\n\t\t\texpected: expected{\n\t\t\t\terrorMsg: \"unable to unmarshal response: [status code: 200] body: [{}] error: json: cannot unmarshal array into Go value of type internal.Zone\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := servermock.NewBuilder[*Client](setupClient(\"\")).\n\t\t\t\tRoute(\"GET /get-zone-info.json\",\n\t\t\t\t\tservermock.RawStringResponse(test.apiResponse),\n\t\t\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\t\t\tWith(\"auth-id\", \"myAuthID\").\n\t\t\t\t\t\tWith(\"auth-password\", \"myAuthPassword\").\n\t\t\t\t\t\tWith(\"domain-name\", \"foo.com\"),\n\t\t\t\t).\n\t\t\t\tBuild(t)\n\n\t\t\tzone, err := client.GetZone(t.Context(), test.authFQDN)\n\n\t\t\tif test.expected.errorMsg != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expected.errorMsg)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.expected.zone, zone)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_FindTxtRecord(t *testing.T) {\n\ttype expected struct {\n\t\ttxtRecord *TXTRecord\n\t\terrorMsg  string\n\t}\n\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tauthFQDN    string\n\t\tzoneName    string\n\t\tapiResponse string\n\t\texpected    expected\n\t}{\n\t\t{\n\t\t\tdesc:     \"record found\",\n\t\t\tauthFQDN: \"_acme-challenge.foo.com.\",\n\t\t\tzoneName: \"foo.com\",\n\t\t\tapiResponse: `{\n  \"5769228\": {\n    \"id\": \"5769228\",\n    \"type\": \"TXT\",\n    \"host\": \"_acme-challenge\",\n    \"record\": \"txtTXTtxtTXTtxtTXTtxtTXT\",\n    \"failover\": \"0\",\n    \"ttl\": \"3600\",\n    \"status\": 1\n  },\n  \"181805209\": {\n    \"id\": \"181805209\",\n    \"type\": \"TXT\",\n    \"host\": \"_github-challenge\",\n    \"record\": \"b66b8324b5\",\n    \"failover\": \"0\",\n    \"ttl\": \"300\",\n    \"status\": 1\n  }\n}`,\n\t\t\texpected: expected{\n\t\t\t\ttxtRecord: &TXTRecord{\n\t\t\t\t\tID:       5769228,\n\t\t\t\t\tType:     \"TXT\",\n\t\t\t\t\tHost:     \"_acme-challenge\",\n\t\t\t\t\tRecord:   \"txtTXTtxtTXTtxtTXTtxtTXT\",\n\t\t\t\t\tFailover: 0,\n\t\t\t\t\tTTL:      3600,\n\t\t\t\t\tStatus:   1,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"no record found\",\n\t\t\tauthFQDN: \"_acme-challenge.foo.com.\",\n\t\t\tzoneName: \"foo.com\",\n\t\t\tapiResponse: `{\n  \"5769228\": {\n    \"id\": \"5769228\",\n    \"type\": \"TXT\",\n    \"host\": \"_other-challenge\",\n    \"record\": \"txtTXTtxtTXTtxtTXTtxtTXT\",\n    \"failover\": \"0\",\n    \"ttl\": \"3600\",\n    \"status\": 1\n  },\n  \"181805209\": {\n    \"id\": \"181805209\",\n    \"type\": \"TXT\",\n    \"host\": \"_github-challenge\",\n    \"record\": \"b66b8324b5\",\n    \"failover\": \"0\",\n    \"ttl\": \"300\",\n    \"status\": 1\n  }\n}`,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"zero records\",\n\t\t\tauthFQDN:    \"_acme-challenge.example.com.\",\n\t\t\tzoneName:    \"example.com\",\n\t\t\tapiResponse: `[]`,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"invalid json response\",\n\t\t\tauthFQDN:    \"_acme-challenge.example.com.\",\n\t\t\tzoneName:    \"example.com\",\n\t\t\tapiResponse: `[{}]`,\n\t\t\texpected: expected{\n\t\t\t\terrorMsg: \"unable to unmarshal response: [status code: 200] body: [{}] error: json: cannot unmarshal array into Go value of type map[string]internal.TXTRecord\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := servermock.NewBuilder[*Client](setupClient(\"\")).\n\t\t\t\tRoute(\"GET /records.json\",\n\t\t\t\t\tservermock.RawStringResponse(test.apiResponse),\n\t\t\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\t\t\tWith(\"auth-id\", \"myAuthID\").\n\t\t\t\t\t\tWith(\"auth-password\", \"myAuthPassword\").\n\t\t\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\t\t\tWith(\"host\", \"_acme-challenge\").\n\t\t\t\t\t\tWith(\"domain-name\", test.zoneName),\n\t\t\t\t).\n\t\t\t\tBuild(t)\n\n\t\t\ttxtRecord, err := client.FindTxtRecord(t.Context(), test.zoneName, test.authFQDN)\n\n\t\t\tif test.expected.errorMsg != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expected.errorMsg)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.expected.txtRecord, txtRecord)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_ListTxtRecord(t *testing.T) {\n\ttype expected struct {\n\t\ttxtRecords []TXTRecord\n\t\terrorMsg   string\n\t}\n\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tauthFQDN    string\n\t\tzoneName    string\n\t\tapiResponse string\n\t\texpected    expected\n\t}{\n\t\t{\n\t\t\tdesc:     \"record found\",\n\t\t\tauthFQDN: \"_acme-challenge.foo.com.\",\n\t\t\tzoneName: \"foo.com\",\n\t\t\tapiResponse: `{\n  \"5769228\": {\n    \"id\": \"5769228\",\n    \"type\": \"TXT\",\n    \"host\": \"_acme-challenge\",\n    \"record\": \"txtTXTtxtTXTtxtTXTtxtTXT\",\n    \"failover\": \"0\",\n    \"ttl\": \"3600\",\n    \"status\": 1\n  },\n  \"181805209\": {\n    \"id\": \"181805209\",\n    \"type\": \"TXT\",\n    \"host\": \"_github-challenge\",\n    \"record\": \"b66b8324b5\",\n    \"failover\": \"0\",\n    \"ttl\": \"300\",\n    \"status\": 1\n  }\n}`,\n\t\t\texpected: expected{\n\t\t\t\ttxtRecords: []TXTRecord{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:       5769228,\n\t\t\t\t\t\tType:     \"TXT\",\n\t\t\t\t\t\tHost:     \"_acme-challenge\",\n\t\t\t\t\t\tRecord:   \"txtTXTtxtTXTtxtTXTtxtTXT\",\n\t\t\t\t\t\tFailover: 0,\n\t\t\t\t\t\tTTL:      3600,\n\t\t\t\t\t\tStatus:   1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"no record found\",\n\t\t\tauthFQDN: \"_acme-challenge.foo.com.\",\n\t\t\tzoneName: \"foo.com\",\n\t\t\tapiResponse: `{\n  \"5769228\": {\n    \"id\": \"5769228\",\n    \"type\": \"TXT\",\n    \"host\": \"_other-challenge\",\n    \"record\": \"txtTXTtxtTXTtxtTXTtxtTXT\",\n    \"failover\": \"0\",\n    \"ttl\": \"3600\",\n    \"status\": 1\n  },\n  \"181805209\": {\n    \"id\": \"181805209\",\n    \"type\": \"TXT\",\n    \"host\": \"_github-challenge\",\n    \"record\": \"b66b8324b5\",\n    \"failover\": \"0\",\n    \"ttl\": \"300\",\n    \"status\": 1\n  }\n}`,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"zero records\",\n\t\t\tauthFQDN:    \"_acme-challenge.example.com.\",\n\t\t\tzoneName:    \"example.com\",\n\t\t\tapiResponse: `[]`,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"invalid json response\",\n\t\t\tauthFQDN:    \"_acme-challenge.example.com.\",\n\t\t\tzoneName:    \"example.com\",\n\t\t\tapiResponse: `[{}]`,\n\t\t\texpected: expected{\n\t\t\t\terrorMsg: \"unable to unmarshal response: [status code: 200] body: [{}] error: json: cannot unmarshal array into Go value of type map[string]internal.TXTRecord\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := servermock.NewBuilder[*Client](setupClient(\"\")).\n\t\t\t\tRoute(\"GET /records.json\",\n\t\t\t\t\tservermock.RawStringResponse(test.apiResponse),\n\t\t\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\t\t\tWith(\"auth-id\", \"myAuthID\").\n\t\t\t\t\t\tWith(\"auth-password\", \"myAuthPassword\").\n\t\t\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\t\t\tWith(\"host\", \"_acme-challenge\").\n\t\t\t\t\t\tWith(\"domain-name\", test.zoneName),\n\t\t\t\t).\n\t\t\t\tBuild(t)\n\n\t\t\ttxtRecords, err := client.ListTxtRecords(t.Context(), test.zoneName, test.authFQDN)\n\n\t\t\tif test.expected.errorMsg != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expected.errorMsg)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.expected.txtRecords, txtRecords)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_AddTxtRecord(t *testing.T) {\n\ttype expected struct {\n\t\tquery    url.Values\n\t\terrorMsg string\n\t}\n\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tauthID      string\n\t\tsubAuthID   string\n\t\tzoneName    string\n\t\tauthFQDN    string\n\t\tvalue       string\n\t\tttl         int\n\t\tapiResponse string\n\t\texpected    expected\n\t}{\n\t\t{\n\t\t\tdesc:        \"sub-zone\",\n\t\t\tauthID:      \"myAuthID\",\n\t\t\tzoneName:    \"example.com\",\n\t\t\tauthFQDN:    \"_acme-challenge.foo.example.com.\",\n\t\t\tvalue:       \"txtTXTtxtTXTtxtTXTtxtTXT\",\n\t\t\tttl:         60,\n\t\t\tapiResponse: `{\"status\":\"Success\",\"statusDescription\":\"The record was added successfully.\"}`,\n\t\t\texpected: expected{\n\t\t\t\tquery: url.Values{\n\t\t\t\t\t\"auth-id\":       {\"myAuthID\"},\n\t\t\t\t\t\"auth-password\": {\"myAuthPassword\"},\n\t\t\t\t\t\"domain-name\":   {\"example.com\"},\n\t\t\t\t\t\"host\":          {\"_acme-challenge.foo\"},\n\t\t\t\t\t\"record\":        {\"txtTXTtxtTXTtxtTXTtxtTXT\"},\n\t\t\t\t\t\"record-type\":   {\"TXT\"},\n\t\t\t\t\t\"ttl\":           {\"60\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"main zone (authID)\",\n\t\t\tauthID:      \"myAuthID\",\n\t\t\tzoneName:    \"example.com\",\n\t\t\tauthFQDN:    \"_acme-challenge.example.com.\",\n\t\t\tvalue:       \"TXTtxtTXTtxtTXTtxtTXTtxt\",\n\t\t\tttl:         60,\n\t\t\tapiResponse: `{\"status\":\"Success\",\"statusDescription\":\"The record was added successfully.\"}`,\n\t\t\texpected: expected{\n\t\t\t\tquery: url.Values{\n\t\t\t\t\t\"auth-id\":       {\"myAuthID\"},\n\t\t\t\t\t\"auth-password\": {\"myAuthPassword\"},\n\t\t\t\t\t\"domain-name\":   {\"example.com\"},\n\t\t\t\t\t\"host\":          {\"_acme-challenge\"},\n\t\t\t\t\t\"record\":        {\"TXTtxtTXTtxtTXTtxtTXTtxt\"},\n\t\t\t\t\t\"record-type\":   {\"TXT\"},\n\t\t\t\t\t\"ttl\":           {\"60\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"main zone (subAuthID)\",\n\t\t\tsubAuthID:   \"mySubAuthID\",\n\t\t\tzoneName:    \"example.com\",\n\t\t\tauthFQDN:    \"_acme-challenge.example.com.\",\n\t\t\tvalue:       \"TXTtxtTXTtxtTXTtxtTXTtxt\",\n\t\t\tttl:         60,\n\t\t\tapiResponse: `{\"status\":\"Success\",\"statusDescription\":\"The record was added successfully.\"}`,\n\t\t\texpected: expected{\n\t\t\t\tquery: url.Values{\n\t\t\t\t\t\"auth-password\": {\"myAuthPassword\"},\n\t\t\t\t\t\"domain-name\":   {\"example.com\"},\n\t\t\t\t\t\"host\":          {\"_acme-challenge\"},\n\t\t\t\t\t\"record\":        {\"TXTtxtTXTtxtTXTtxtTXTtxt\"},\n\t\t\t\t\t\"record-type\":   {\"TXT\"},\n\t\t\t\t\t\"sub-auth-id\":   {\"mySubAuthID\"},\n\t\t\t\t\t\"ttl\":           {\"60\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"invalid status\",\n\t\t\tauthID:      \"myAuthID\",\n\t\t\tzoneName:    \"example.com\",\n\t\t\tauthFQDN:    \"_acme-challenge.example.com.\",\n\t\t\tvalue:       \"TXTtxtTXTtxtTXTtxtTXTtxt\",\n\t\t\tttl:         120,\n\t\t\tapiResponse: `{\"status\":\"Failed\",\"statusDescription\":\"Invalid TTL. Choose from the list of the values we support.\"}`,\n\t\t\texpected: expected{\n\t\t\t\tquery: url.Values{\n\t\t\t\t\t\"auth-id\":       {\"myAuthID\"},\n\t\t\t\t\t\"auth-password\": {\"myAuthPassword\"},\n\t\t\t\t\t\"domain-name\":   {\"example.com\"},\n\t\t\t\t\t\"host\":          {\"_acme-challenge\"},\n\t\t\t\t\t\"record\":        {\"TXTtxtTXTtxtTXTtxtTXTtxt\"},\n\t\t\t\t\t\"record-type\":   {\"TXT\"},\n\t\t\t\t\t\"ttl\":           {\"300\"},\n\t\t\t\t},\n\t\t\t\terrorMsg: \"failed to add TXT record: Failed Invalid TTL. Choose from the list of the values we support.\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"invalid json response\",\n\t\t\tauthID:      \"myAuthID\",\n\t\t\tzoneName:    \"example.com\",\n\t\t\tauthFQDN:    \"_acme-challenge.example.com.\",\n\t\t\tvalue:       \"TXTtxtTXTtxtTXTtxtTXTtxt\",\n\t\t\tttl:         120,\n\t\t\tapiResponse: `[{}]`,\n\t\t\texpected: expected{\n\t\t\t\tquery: url.Values{\n\t\t\t\t\t\"auth-id\":       {\"myAuthID\"},\n\t\t\t\t\t\"auth-password\": {\"myAuthPassword\"},\n\t\t\t\t\t\"domain-name\":   {\"example.com\"},\n\t\t\t\t\t\"host\":          {\"_acme-challenge\"},\n\t\t\t\t\t\"record\":        {\"TXTtxtTXTtxtTXTtxtTXTtxt\"},\n\t\t\t\t\t\"record-type\":   {\"TXT\"},\n\t\t\t\t\t\"ttl\":           {\"300\"},\n\t\t\t\t},\n\t\t\t\terrorMsg: \"unable to unmarshal response: [status code: 200] body: [{}] error: json: cannot unmarshal array into Go value of type internal.apiResponse\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := servermock.NewBuilder[*Client](setupClient(test.subAuthID)).\n\t\t\t\tRoute(\"POST /add-record.json\",\n\t\t\t\t\tservermock.RawStringResponse(test.apiResponse),\n\t\t\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\t\t\tWithValues(test.expected.query),\n\t\t\t\t).\n\t\t\t\tBuild(t)\n\n\t\t\terr := client.AddTxtRecord(t.Context(), test.zoneName, test.authFQDN, test.value, test.ttl)\n\n\t\t\tif test.expected.errorMsg != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expected.errorMsg)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_RemoveTxtRecord(t *testing.T) {\n\ttype expected struct {\n\t\tquery    url.Values\n\t\terrorMsg string\n\t}\n\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tid          int\n\t\tzoneName    string\n\t\tapiResponse string\n\t\texpected    expected\n\t}{\n\t\t{\n\t\t\tdesc:        \"record found\",\n\t\t\tid:          5769228,\n\t\t\tzoneName:    \"foo.com\",\n\t\t\tapiResponse: `{ \"status\": \"Success\", \"statusDescription\": \"The record was deleted successfully.\" }`,\n\t\t\texpected: expected{\n\t\t\t\tquery: url.Values{\n\t\t\t\t\t\"auth-id\":       {\"myAuthID\"},\n\t\t\t\t\t\"auth-password\": {\"myAuthPassword\"},\n\t\t\t\t\t\"domain-name\":   {\"foo.com\"},\n\t\t\t\t\t\"record-id\":     {\"5769228\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"record not found\",\n\t\t\tid:          5769000,\n\t\t\tzoneName:    \"foo.com\",\n\t\t\tapiResponse: `{ \"status\": \"Failed\", \"statusDescription\": \"Invalid record-id param.\" }`,\n\t\t\texpected: expected{\n\t\t\t\tquery: url.Values{\n\t\t\t\t\t\"auth-id\":       {\"myAuthID\"},\n\t\t\t\t\t\"auth-password\": {\"myAuthPassword\"},\n\t\t\t\t\t\"domain-name\":   {\"foo.com\"},\n\t\t\t\t\t\"record-id\":     {\"5769000\"},\n\t\t\t\t},\n\t\t\t\terrorMsg: \"failed to remove TXT record: Failed Invalid record-id param.\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"invalid json response\",\n\t\t\tid:          44,\n\t\t\tzoneName:    \"foo-plus.com\",\n\t\t\tapiResponse: `[{}]`,\n\t\t\texpected: expected{\n\t\t\t\tquery: url.Values{\n\t\t\t\t\t\"auth-id\":       {\"myAuthID\"},\n\t\t\t\t\t\"auth-password\": {\"myAuthPassword\"},\n\t\t\t\t\t\"domain-name\":   {\"foo-plus.com\"},\n\t\t\t\t\t\"record-id\":     {\"44\"},\n\t\t\t\t},\n\t\t\t\terrorMsg: \"unable to unmarshal response: [status code: 200] body: [{}] error: json: cannot unmarshal array into Go value of type internal.apiResponse\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := servermock.NewBuilder[*Client](setupClient(\"\")).\n\t\t\t\tRoute(\"POST /delete-record.json\",\n\t\t\t\t\tservermock.RawStringResponse(test.apiResponse),\n\t\t\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\t\t\tWithValues(test.expected.query),\n\t\t\t\t).\n\t\t\t\tBuild(t)\n\n\t\t\terr := client.RemoveTxtRecord(t.Context(), test.id, test.zoneName)\n\n\t\t\tif test.expected.errorMsg != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expected.errorMsg)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_GetUpdateStatus(t *testing.T) {\n\ttype expected struct {\n\t\tprogress *SyncProgress\n\t\terrorMsg string\n\t}\n\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tauthFQDN    string\n\t\tzoneName    string\n\t\tapiResponse string\n\t\texpected    expected\n\t}{\n\t\t{\n\t\t\tdesc:     \"50% sync\",\n\t\t\tauthFQDN: \"_acme-challenge.foo.com.\",\n\t\t\tzoneName: \"foo.com\",\n\t\t\tapiResponse: `[\n{\"server\": \"ns101.foo.com.\", \"ip4\": \"10.11.12.13\", \"ip6\": \"2a00:2a00:2a00:9::5\", \"updated\": true },\n{\"server\": \"ns102.foo.com.\", \"ip4\": \"10.14.16.17\", \"ip6\": \"2100:2100:2100:3::1\", \"updated\": false }\n]`,\n\t\t\texpected: expected{progress: &SyncProgress{Updated: 1, Total: 2}},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"100% sync\",\n\t\t\tauthFQDN: \"_acme-challenge.foo.com.\",\n\t\t\tzoneName: \"foo.com\",\n\t\t\tapiResponse: `[\n{\"server\": \"ns101.foo.com.\", \"ip4\": \"10.11.12.13\", \"ip6\": \"2a00:2a00:2a00:9::5\", \"updated\": true },\n{\"server\": \"ns102.foo.com.\", \"ip4\": \"10.14.16.17\", \"ip6\": \"2100:2100:2100:3::1\", \"updated\": true }\n]`,\n\t\t\texpected: expected{progress: &SyncProgress{Complete: true, Updated: 2, Total: 2}},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"record not found\",\n\t\t\tauthFQDN:    \"_acme-challenge.foo.com.\",\n\t\t\tzoneName:    \"test-zone\",\n\t\t\tapiResponse: `[]`,\n\t\t\texpected:    expected{errorMsg: \"no nameservers records returned\"},\n\t\t},\n\t\t{\n\t\t\tdesc:        \"invalid json response\",\n\t\t\tauthFQDN:    \"_acme-challenge.foo.com.\",\n\t\t\tzoneName:    \"test-zone\",\n\t\t\tapiResponse: `[x]`,\n\t\t\texpected:    expected{errorMsg: \"unable to unmarshal response: [status code: 200] body: [x] error: invalid character 'x' looking for beginning of value\"},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := servermock.NewBuilder[*Client](setupClient(\"\")).\n\t\t\t\tRoute(\"GET /update-status.json\",\n\t\t\t\t\tservermock.RawStringResponse(test.apiResponse),\n\t\t\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\t\t\tWith(\"auth-id\", \"myAuthID\").\n\t\t\t\t\t\tWith(\"auth-password\", \"myAuthPassword\").\n\t\t\t\t\t\tWith(\"domain-name\", test.zoneName),\n\t\t\t\t).\n\t\t\t\tBuild(t)\n\n\t\t\tsyncProgress, err := client.GetUpdateStatus(t.Context(), test.zoneName)\n\n\t\t\tif test.expected.errorMsg != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expected.errorMsg)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tassert.Equal(t, test.expected.progress, syncProgress)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/cloudns/internal/types.go",
    "content": "package internal\n\ntype apiResponse struct {\n\tStatus            string `json:\"status\"`\n\tStatusDescription string `json:\"statusDescription\"`\n}\n\n// Zone is a zone.\ntype Zone struct {\n\tName   string\n\tType   string\n\tZone   string\n\tStatus string // is an integer, but cast as string\n}\n\n// TXTRecord is a TXT record.\ntype TXTRecord struct {\n\tID       int    `json:\"id,string\"`\n\tType     string `json:\"type\"`\n\tHost     string `json:\"host\"`\n\tRecord   string `json:\"record\"`\n\tFailover int    `json:\"failover,string\"`\n\tTTL      int    `json:\"ttl,string\"`\n\tStatus   int    `json:\"status\"`\n}\n\n// UpdateRecord is a Server Sync Record.\ntype UpdateRecord struct {\n\tServer  string `json:\"server\"`\n\tIP4     string `json:\"ip4\"`\n\tIP6     string `json:\"ip6\"`\n\tUpdated bool   `json:\"updated\"`\n}\n\ntype SyncProgress struct {\n\tComplete bool\n\tUpdated  int\n\tTotal    int\n}\n"
  },
  {
    "path": "providers/dns/cloudru/cloudru.go",
    "content": "// Package cloudru implements a DNS provider for solving the DNS-01 challenge using cloud.ru DNS.\npackage cloudru\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cloudru/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"CLOUDRU_\"\n\n\tEnvServiceInstanceID = envNamespace + \"SERVICE_INSTANCE_ID\"\n\tEnvKeyID             = envNamespace + \"KEY_ID\"\n\tEnvSecret            = envNamespace + \"SECRET\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tServiceInstanceID string\n\tKeyID             string\n\tSecret            string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tHTTPClient         *http.Client\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecords   map[string]*internal.Record\n\trecordsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for cloud.ru.\n// Credentials must be passed in the environment variables:\n// CLOUDRU_SERVICE_INSTANCE_ID, CLOUDRU_KEY_ID, and CLOUDRU_SECRET.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvServiceInstanceID, EnvKeyID, EnvSecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cloudru: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.ServiceInstanceID = values[EnvServiceInstanceID]\n\tconfig.KeyID = values[EnvKeyID]\n\tconfig.Secret = values[EnvSecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for cloud.ru.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"cloudru: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.ServiceInstanceID == \"\" || config.KeyID == \"\" || config.Secret == \"\" {\n\t\treturn nil, errors.New(\"cloudru: some credentials information are missing\")\n\t}\n\n\tclient := internal.NewClient(config.KeyID, config.Secret)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:  config,\n\t\tclient:  client,\n\t\trecords: make(map[string]*internal.Record),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cloudru: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cloudru: %w\", err)\n\t}\n\n\tzone, err := d.getZoneInformationByName(ctx, d.config.ServiceInstanceID, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cloudru: could not find zone information (ServiceInstanceID: %s, zone: %s): %w\", d.config.ServiceInstanceID, authZone, err)\n\t}\n\n\trecord := internal.Record{\n\t\tName:   info.EffectiveFQDN,\n\t\tType:   \"TXT\",\n\t\tValues: []string{info.Value},\n\t\tTTL:    strconv.Itoa(d.config.TTL),\n\t}\n\n\tnewRecord, err := d.client.CreateRecord(ctx, zone.ID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cloudru: could not create record: %w\", err)\n\t}\n\n\td.recordsMu.Lock()\n\td.records[token] = newRecord\n\td.recordsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes a given record that was generated by Present.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordsMu.Lock()\n\trecord, ok := d.records[token]\n\td.recordsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"cloudru: unknown recordID for %q\", info.EffectiveFQDN)\n\t}\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cloudru: %w\", err)\n\t}\n\n\terr = d.client.DeleteRecord(ctx, record.ZoneID, record.Name, \"TXT\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cloudru: %w\", err)\n\t}\n\n\td.recordsMu.Lock()\n\tdelete(d.records, token)\n\td.recordsMu.Unlock()\n\n\treturn nil\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) getZoneInformationByName(ctx context.Context, parentID, name string) (internal.Zone, error) {\n\tzs, err := d.client.GetZones(ctx, parentID)\n\tif err != nil {\n\t\treturn internal.Zone{}, err\n\t}\n\n\tfor _, element := range zs {\n\t\tif element.Name == name {\n\t\t\treturn element, nil\n\t\t}\n\t}\n\n\treturn internal.Zone{}, errors.New(\"could not find Zone record\")\n}\n"
  },
  {
    "path": "providers/dns/cloudru/cloudru.toml",
    "content": "Name = \"Cloud.ru\"\nDescription = ''''''\nURL = \"https://cloud.ru\"\nCode = \"cloudru\"\nSince = \"v4.14.0\"\n\nExample = '''\nCLOUDRU_SERVICE_INSTANCE_ID=ppp \\\nCLOUDRU_KEY_ID=xxx \\\nCLOUDRU_SECRET=yyy \\\nlego --dns cloudru -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    CLOUDRU_SERVICE_INSTANCE_ID = \"Service Instance ID (parentId)\"\n    CLOUDRU_KEY_ID = \"Key ID (login)\"\n    CLOUDRU_SECRET = \"Key Secret\"\n  [Configuration.Additional]\n    CLOUDRU_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 5)\"\n    CLOUDRU_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 300)\"\n    CLOUDRU_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    CLOUDRU_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n    CLOUDRU_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 120)\"\n\n[Links]\n  API = \"https://cloud.ru/ru/docs/clouddns/ug/topics/api-ref.html\"\n"
  },
  {
    "path": "providers/dns/cloudru/cloudru_test.go",
    "content": "package cloudru\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvServiceInstanceID,\n\tEnvKeyID,\n\tEnvSecret).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServiceInstanceID: \"123\",\n\t\t\t\tEnvKeyID:             \"user\",\n\t\t\t\tEnvSecret:            \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"cloudru: some credentials information are missing: CLOUDRU_SERVICE_INSTANCE_ID,CLOUDRU_KEY_ID,CLOUDRU_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing service instance ID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServiceInstanceID: \"\",\n\t\t\t\tEnvKeyID:             \"user\",\n\t\t\t\tEnvSecret:            \"secret\",\n\t\t\t},\n\t\t\texpected: \"cloudru: some credentials information are missing: CLOUDRU_SERVICE_INSTANCE_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing key ID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServiceInstanceID: \"123\",\n\t\t\t\tEnvKeyID:             \"\",\n\t\t\t\tEnvSecret:            \"secret\",\n\t\t\t},\n\t\t\texpected: \"cloudru: some credentials information are missing: CLOUDRU_KEY_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServiceInstanceID: \"123\",\n\t\t\t\tEnvKeyID:             \"user\",\n\t\t\t\tEnvSecret:            \"\",\n\t\t\t},\n\t\t\texpected: \"cloudru: some credentials information are missing: CLOUDRU_SECRET\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc              string\n\t\tserviceInstanceID string\n\t\tkeyID             string\n\t\tsecret            string\n\t\texpected          string\n\t}{\n\t\t{\n\t\t\tdesc:              \"success\",\n\t\t\tserviceInstanceID: \"123\",\n\t\t\tkeyID:             \"user\",\n\t\t\tsecret:            \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"cloudru: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:              \"missing service instance ID\",\n\t\t\tserviceInstanceID: \"\",\n\t\t\tkeyID:             \"user\",\n\t\t\tsecret:            \"secret\",\n\t\t\texpected:          \"cloudru: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:              \"missing key ID\",\n\t\t\tserviceInstanceID: \"123\",\n\t\t\tkeyID:             \"\",\n\t\t\tsecret:            \"secret\",\n\t\t\texpected:          \"cloudru: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:              \"missing secret\",\n\t\t\tserviceInstanceID: \"123\",\n\t\t\tkeyID:             \"user\",\n\t\t\tsecret:            \"\",\n\t\t\texpected:          \"cloudru: some credentials information are missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.ServiceInstanceID = test.serviceInstanceID\n\t\t\tconfig.KeyID = test.keyID\n\t\t\tconfig.Secret = test.secret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/cloudru/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// Default API endpoints.\nconst (\n\tAPIBaseURL  = \"https://console.cloud.ru/api/clouddns/v1\"\n\tAuthBaseURL = \"https://auth.iam.cloud.ru/auth/system/openid/token\"\n)\n\n// Client the Cloud.ru API client.\ntype Client struct {\n\tkeyID  string\n\tsecret string\n\n\tAPIEndpoint  *url.URL\n\tAuthEndpoint *url.URL\n\tHTTPClient   *http.Client\n\n\ttoken   *Token\n\tmuToken sync.Mutex\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(login, secret string) *Client {\n\tapiEndpoint, _ := url.Parse(APIBaseURL)\n\tauthEndpoint, _ := url.Parse(AuthBaseURL)\n\n\treturn &Client{\n\t\tkeyID:        login,\n\t\tsecret:       secret,\n\t\tAPIEndpoint:  apiEndpoint,\n\t\tAuthEndpoint: authEndpoint,\n\t\tHTTPClient:   &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\nfunc (c *Client) GetZones(ctx context.Context, parentID string) ([]Zone, error) {\n\tendpoint := c.APIEndpoint.JoinPath(\"zones\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"parentId\", parentID)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar zones APIResponse[Zone]\n\n\terr = c.do(req, &zones)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn zones.Items, nil\n}\n\nfunc (c *Client) GetRecords(ctx context.Context, zoneID string) ([]Record, error) {\n\tendpoint := c.APIEndpoint.JoinPath(\"zones\", zoneID, \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar records APIResponse[Record]\n\n\terr = c.do(req, &records)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn records.Items, nil\n}\n\nfunc (c *Client) CreateRecord(ctx context.Context, zoneID string, record Record) (*Record, error) {\n\tendpoint := c.APIEndpoint.JoinPath(\"zones\", zoneID, \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result Record\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &result, nil\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, zoneID, name, recordType string) error {\n\tendpoint := c.APIEndpoint.JoinPath(\"zones\", zoneID, \"records\", name, recordType)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\ttok := getToken(req.Context())\n\tif tok != nil {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+tok.AccessToken)\n\t} else {\n\t\treturn errors.New(\"not logged in\")\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/cloudru/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.APIEndpoint, _ = url.Parse(server.URL)\n\t\t\tclient.token = &Token{\n\t\t\t\tAccessToken: \"secret\",\n\t\t\t\tExpiresIn:   60,\n\t\t\t\tTokenType:   \"Bearer\",\n\t\t\t\tDeadline:    time.Now().Add(1 * time.Minute),\n\t\t\t}\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer xxx\"))\n}\n\nfunc TestClient_GetZones(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones\",\n\t\t\tservermock.ResponseFromFixture(\"zones.json\")).\n\t\tBuild(t)\n\n\tctx := mockContext(t)\n\n\tzones, err := client.GetZones(ctx, \"xxx\")\n\trequire.NoError(t, err)\n\n\texpected := []Zone{\n\t\t{\n\t\t\tID:        \"59556fcd-95ff-451f-b49b-9732f21f944a\",\n\t\t\tParentID:  \"2d7b6194-2b83-4f71-86fd-a1e727e347b2\",\n\t\t\tName:      \"example.com\",\n\t\t\tValid:     true,\n\t\t\tDelegated: true,\n\t\t\tCreatedAt: time.Date(2023, 7, 23, 8, 12, 41, 0, time.UTC),\n\t\t\tUpdatedAt: time.Date(2023, 7, 24, 5, 50, 28, 0, time.UTC),\n\t\t},\n\t}\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones/zzz/records\",\n\t\t\tservermock.ResponseFromFixture(\"records.json\")).\n\t\tBuild(t)\n\n\tctx := mockContext(t)\n\n\trecords, err := client.GetRecords(ctx, \"zzz\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tZoneID: \"59556fcd-95ff-451f-b49b-9732f21f944a\",\n\t\t\tName:   \"example.com.\",\n\t\t\tType:   \"SOA\",\n\t\t\tValues: []string{\n\t\t\t\t\"cdns-ns01.sbercloud.ru. mail.sbercloud.ru 1 120 3600 604800 3600\",\n\t\t\t},\n\t\t\tTTL:     \"3600\",\n\t\t\tEnables: true,\n\t\t},\n\t\t{\n\t\t\tZoneID: \"59556fcd-95ff-451f-b49b-9732f21f944a\",\n\t\t\tName:   \"example.com.\",\n\t\t\tType:   \"NS\",\n\t\t\tValues: []string{\n\t\t\t\t\"cdns-ns01.sbercloud.ru.\",\n\t\t\t\t\"cdns-ns02.sbercloud.ru.\",\n\t\t\t},\n\t\t\tTTL:     \"3600\",\n\t\t\tEnables: true,\n\t\t},\n\t\t{\n\t\t\tZoneID: \"59556fcd-95ff-451f-b49b-9732f21f944a\",\n\t\t\tName:   \"www.example.com.\",\n\t\t\tType:   \"A\",\n\t\t\tValues: []string{\n\t\t\t\t\"8.8.8.8\",\n\t\t\t},\n\t\t\tTTL:     \"3600\",\n\t\t\tEnables: true,\n\t\t},\n\t}\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zones/zzz/records\",\n\t\t\tservermock.ResponseFromFixture(\"record.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"name\":\"www.example.com.\",\"type\":\"TXT\",\"values\":[\"text\"],\"ttl\":\"3600\"}`)).\n\t\tBuild(t)\n\n\tctx := mockContext(t)\n\n\trecordReq := Record{\n\t\tName:   \"www.example.com.\",\n\t\tType:   \"TXT\",\n\t\tValues: []string{\"text\"},\n\t\tTTL:    \"3600\",\n\t}\n\n\trecord, err := client.CreateRecord(ctx, \"zzz\", recordReq)\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tZoneID: \"59556fcd-95ff-451f-b49b-9732f21f944a\",\n\t\tName:   \"www.example.com.\",\n\t\tType:   \"TXT\",\n\t\tValues: []string{\n\t\t\t\"txt\",\n\t\t},\n\t\tTTL:     \"3600\",\n\t\tEnables: true,\n\t}\n\tassert.Equal(t, expected, record)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /zones/zzz/records/example.com/TXT\",\n\t\t\tservermock.ResponseFromFixture(\"record.json\")).\n\t\tBuild(t)\n\n\tctx := mockContext(t)\n\n\terr := client.DeleteRecord(ctx, \"zzz\", \"example.com\", \"TXT\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/cloudru/internal/fixtures/auth-error.json",
    "content": "{\n  \"error\": \"invalid_client\",\n  \"error_description\": \"client not found\"\n}\n"
  },
  {
    "path": "providers/dns/cloudru/internal/fixtures/auth.json",
    "content": "{\n  \"access_token\": \"eyJhbGciOiJSUzI1NiIsImtpZCI6ImEyMjM3ZDhhLWQ0ZDQtNDA5Yi04ZTMxLWM3NGJhYTZhM2NjYiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaWFtIl0sImF1dGhfdGltZSI6MTY5MDA1Mzg1MiwiYXpwIjoiOWE0M2I0OTM1ZDRhMDc5NmRkYjE0Mjk0NjUxYjk2NzciLCJlbWFpbCI6ImxlZ29AOTlmN2I5NzItZmZlYS00OTkyLTgyN2EtY2M4MDYzOTg1MmNhLmlhbS5zYmVyY2xvdWQucnUiLCJleHAiOjE2OTAwNTc0NTIsImlhdCI6MTY5MDA1Mzg1MiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmlhbS5zYmVyY2xvdWQucnUvYXV0aC9zeXN0ZW0iLCJqdGkiOiJlYzk0ZWJhNC03NzU2LTRjNjQtYmNmMC0zMzYxODIwNWM5ODkiLCJuYmYiOjE2OTAwNTM4NTIsIm5vbmNlIjoiIiwicmVhbG1fYWNjZXNzIjpudWxsLCJyZXNvdXJjZV9hY2Nlc3MiOnt9LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIHJvbGVzIiwic3ViIjoiZmVhMzRmYTUtZmE5ZS00OTdkLTk5MDAtNjdhYWJjOWNkZWJjIiwic3ViX2lkIjoiZmVhMzRmYTUtZmE5ZS00OTdkLTk5MDAtNjdhYWJjOWNkZWJjIiwic3ViX3R5cGUiOiJzZXJ2aWNlX2FjY291bnQiLCJ0eXAiOiJCZWFyZXIifQ.hhPr-Xr_NbyRwrqGoqeepthWfpfmD47RjzHUwo2lVPkeMiL8AMWzDPRxs-8gns9eTSHZCoAH0RjyrBnTaOrztInM72h8_rIIFr0MMPIIqrUkp2id_alya9eoiSWg_69PzNZ2CKWJDylL8o4Vi9_cSBYp-6H1xNcOAvO4a9xkNCoGGiogjHWNFq64qnS_P6fYY-pl9leuprCeq1GAKPODevHwzmc4gkEZIj_15SUh_ofJRJICgyLmkELQ8a0wDGYmZcdNKiGQDpd7rHaGrOvO1k8IJHfgs5aCMyuHXybTg6AMlodpYs8MBdk6K_VFY-cxSRB8ocq_Q7Hgt9qaRADg2Q\",\n  \"id_token\": \"eyJhbGciOiJSUzI1NiIsImtpZCI6ImEyMjM3ZDhhLWQ0ZDQtNDA5Yi04ZTMxLWM3NGJhYTZhM2NjYiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaWFtIl0sImF1dGhfdGltZSI6MTY5MDA1Mzg1MiwiYXpwIjoiOWE0M2I0OTM1ZDRhMDc5NmRkYjE0Mjk0NjUxYjk2NzciLCJlbWFpbCI6ImxlZ29AOTlmN2I5NzItZmZlYS00OTkyLTgyN2EtY2M4MDYzOTg1MmNhLmlhbS5zYmVyY2xvdWQucnUiLCJleHAiOjE2OTAwNTc0NTIsImlhdCI6MTY5MDA1Mzg1MiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmlhbS5zYmVyY2xvdWQucnUvYXV0aC9zeXN0ZW0iLCJqdGkiOiIxNDRmMDRlNS1jYjZkLTQ2NTktODJhMi0yMmE5MDQwNGZlZjAiLCJuYmYiOjE2OTAwNTM4NTIsIm5vbmNlIjoiIiwicmVhbG1fYWNjZXNzIjpudWxsLCJyZXNvdXJjZV9hY2Nlc3MiOnt9LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIHJvbGVzIiwic3ViIjoiZmVhMzRmYTUtZmE5ZS00OTdkLTk5MDAtNjdhYWJjOWNkZWJjIiwic3ViX2lkIjoiZmVhMzRmYTUtZmE5ZS00OTdkLTk5MDAtNjdhYWJjOWNkZWJjIiwic3ViX3R5cGUiOiJzZXJ2aWNlX2FjY291bnQiLCJ0eXAiOiJJRCJ9.oW9w9X2EBozdY7JTnL6WBPE114BM52ZOaWLkXamJvUOks_F4fRxw5lJIN-LkTwMZ9jE3PsBV2_SueCL5Ry2ISiEXaZeoQ_FPnSkz-CMFDP6Ph2erOvEWQInTIPA6h-ToIhYMZR8lc_kPOmar2mTT8b043FZ6zFDf28PJCCo8snCgA_tIO7R0fNJYT7Hr-UR7LSrE-Sjz7lsgttyDEPH1P4yPm4ZzRLYLcR240p1iGKG9yxtl8IL6uxseS4pUddimaH6jFPhMFLH44PV4O_-uYs74erjoPiroCHiaWQIdDR5GZDoPCbYXQa0knh9hnK1pX6fO-krHeT3RtfuFf5609A\",\n  \"expires_in\": 3600,\n  \"not-before-policy\": 0,\n  \"scope\": \"openid profile email roles\",\n  \"token_type\": \"Bearer\"\n}\n"
  },
  {
    "path": "providers/dns/cloudru/internal/fixtures/record.json",
    "content": "{\n  \"zone_id\": \"59556fcd-95ff-451f-b49b-9732f21f944a\",\n  \"name\": \"www.example.com.\",\n  \"type\": \"TXT\",\n  \"values\": [\n    \"txt\"\n  ],\n  \"ttl\": \"3600\",\n  \"enables\": true,\n  \"readonly\": false\n}\n"
  },
  {
    "path": "providers/dns/cloudru/internal/fixtures/records.json",
    "content": "{\n  \"items\": [\n    {\n      \"zone_id\": \"59556fcd-95ff-451f-b49b-9732f21f944a\",\n      \"name\": \"example.com.\",\n      \"type\": \"SOA\",\n      \"values\": [\n        \"cdns-ns01.sbercloud.ru. mail.sbercloud.ru 1 120 3600 604800 3600\"\n      ],\n      \"ttl\": \"3600\",\n      \"enables\": true,\n      \"readonly\": true\n    },\n    {\n      \"zone_id\": \"59556fcd-95ff-451f-b49b-9732f21f944a\",\n      \"name\": \"example.com.\",\n      \"type\": \"NS\",\n      \"values\": [\n        \"cdns-ns01.sbercloud.ru.\",\n        \"cdns-ns02.sbercloud.ru.\"\n      ],\n      \"ttl\": \"3600\",\n      \"enables\": true,\n      \"readonly\": true\n    },\n    {\n      \"zone_id\": \"59556fcd-95ff-451f-b49b-9732f21f944a\",\n      \"name\": \"www.example.com.\",\n      \"type\": \"A\",\n      \"values\": [\n        \"8.8.8.8\"\n      ],\n      \"ttl\": \"3600\",\n      \"enables\": true,\n      \"readonly\": false\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/cloudru/internal/fixtures/zones.json",
    "content": "{\n  \"items\": [\n    {\n      \"id\": \"59556fcd-95ff-451f-b49b-9732f21f944a\",\n      \"parent_id\": \"2d7b6194-2b83-4f71-86fd-a1e727e347b2\",\n      \"name\": \"example.com\",\n      \"valid\": true,\n      \"validation_text\": \"sbc-verification: 5c86c962-7ee2-4983-b39b-1d9461959d8b\",\n      \"delegated\": true,\n      \"created_at\": \"2023-07-23T08:12:41.000000Z\",\n      \"updated_at\": \"2023-07-24T05:50:28.000000Z\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/cloudru/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\ntype token string\n\nconst tokenKey token = \"token\"\n\n// obtainToken Logs into cloud.ru and acquires a bearer token for use in future API calls.\n// https://cloud.ru/ru/docs/clouddns/ug/topics/api-ref_authentication.html\nfunc (c *Client) obtainToken(ctx context.Context) (*Token, error) {\n\tdata := make(url.Values)\n\tdata.Set(\"grant_type\", \"access_key\")\n\tdata.Set(\"client_id\", c.keyID)\n\tdata.Set(\"client_secret\", c.secret)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.AuthEndpoint.String(), strings.NewReader(data.Encode()))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, parseError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\ttok := Token{}\n\n\terr = json.Unmarshal(raw, &tok)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tif !strings.EqualFold(tok.TokenType, \"Bearer\") {\n\t\treturn nil, fmt.Errorf(\"received unexpected token type: %s\", tok.TokenType)\n\t}\n\n\ttok.Deadline = time.Now().Add(time.Duration(tok.ExpiresIn) * time.Second)\n\n\treturn &tok, nil\n}\n\nfunc (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) {\n\tc.muToken.Lock()\n\tdefer c.muToken.Unlock()\n\n\tif c.token != nil && time.Now().Before(c.token.Deadline) {\n\t\t// Already authenticated, stop now\n\t\treturn context.WithValue(ctx, tokenKey, c.token), nil\n\t}\n\n\ttok, err := c.obtainToken(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn context.WithValue(ctx, tokenKey, tok), nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\tif resp.StatusCode < 400 || resp.StatusCode > 499 {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, _ := io.ReadAll(resp.Body)\n\n\terrResp := &authResponseError{}\n\n\terr := json.Unmarshal(raw, errResp)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"%d: %w\", resp.StatusCode, errResp)\n}\n\nfunc getToken(ctx context.Context) *Token {\n\ttok, ok := ctx.Value(tokenKey).(*Token)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\treturn tok\n}\n"
  },
  {
    "path": "providers/dns/cloudru/internal/identity_test.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockContext(t *testing.T) context.Context {\n\tt.Helper()\n\n\treturn context.WithValue(t.Context(), tokenKey, &Token{AccessToken: \"xxx\"})\n}\n\nfunc setupIdentityClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"user\", \"secret\")\n\tclient.HTTPClient = server.Client()\n\tclient.AuthEndpoint, _ = url.Parse(server.URL)\n\n\treturn client, nil\n}\n\nfunc TestClient_obtainToken(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupIdentityClient,\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded(),\n\t).\n\t\tRoute(\"POST /\", servermock.JSONEncode(Token{\n\t\t\tAccessToken: \"xxx\",\n\t\t\tTokenID:     \"yyy\",\n\t\t\tExpiresIn:   666,\n\t\t\tTokenType:   \"Bearer\",\n\t\t\tScope:       \"openid profile email roles\",\n\t\t}),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"client_id\", \"user\").\n\t\t\t\tWith(\"client_secret\", \"secret\").\n\t\t\t\tWith(\"grant_type\", \"access_key\"),\n\t\t).\n\t\tBuild(t)\n\n\tassert.Nil(t, client.token)\n\n\ttok, err := client.obtainToken(t.Context())\n\trequire.NoError(t, err)\n\n\tassert.NotNil(t, tok)\n\tassert.NotZero(t, tok.Deadline)\n\tassert.Equal(t, \"xxx\", tok.AccessToken)\n}\n\nfunc TestClient_CreateAuthenticatedContext(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupIdentityClient,\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded(),\n\t).\n\t\tRoute(\"POST /\", servermock.JSONEncode(Token{\n\t\t\tAccessToken: \"xxx\",\n\t\t\tTokenID:     \"yyy\",\n\t\t\tExpiresIn:   666,\n\t\t\tTokenType:   \"Bearer\",\n\t\t\tScope:       \"openid profile email roles\",\n\t\t}),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"client_id\", \"user\").\n\t\t\t\tWith(\"client_secret\", \"secret\").\n\t\t\t\tWith(\"grant_type\", \"access_key\"),\n\t\t).\n\t\tBuild(t)\n\n\tassert.Nil(t, client.token)\n\n\tctx, err := client.CreateAuthenticatedContext(t.Context())\n\trequire.NoError(t, err)\n\n\ttok := getToken(ctx)\n\n\tassert.NotNil(t, tok)\n\tassert.NotZero(t, tok.Deadline)\n\tassert.Equal(t, \"xxx\", tok.AccessToken)\n}\n"
  },
  {
    "path": "providers/dns/cloudru/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\ntype Token struct {\n\t// The bearer token for use in API requests\n\tAccessToken string `json:\"access_token\"`\n\tTokenID     string `json:\"id_token\"`\n\tTokenType   string `json:\"token_type\"`\n\t// Number in seconds before the expiration\n\tExpiresIn       int    `json:\"expires_in\"`\n\tNotBeforePolicy int    `json:\"not-before-policy\"`\n\tScope           string `json:\"scope\"`\n\n\tDeadline time.Time `json:\"-\"`\n}\n\ntype authResponseError struct {\n\tErrorMsg         string `json:\"error\"`\n\tErrorDescription string `json:\"error_description\"`\n}\n\nfunc (a authResponseError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", a.ErrorMsg, a.ErrorDescription)\n}\n\ntype APIResponse[T any] struct {\n\tItems []T `json:\"items\"`\n}\n\ntype Zone struct {\n\tID             string    `json:\"id,omitempty\"`\n\tParentID       string    `json:\"parent_id,omitempty\"`\n\tName           string    `json:\"name,omitempty\"`\n\tValid          bool      `json:\"valid,omitempty\"`\n\tValidationText string    `json:\"validationText,omitempty\"`\n\tDelegated      bool      `json:\"delegated,omitempty\"`\n\tLastCheck      time.Time `json:\"lastCheck,omitzero\"`\n\tCreatedAt      time.Time `json:\"created_at,omitzero\"`\n\tUpdatedAt      time.Time `json:\"updated_at,omitzero\"`\n}\n\ntype Record struct {\n\tZoneID  string   `json:\"zone_id,omitempty\"`\n\tName    string   `json:\"name,omitempty\"`\n\tType    string   `json:\"type,omitempty\"`\n\tValues  []string `json:\"values,omitempty\"`\n\tTTL     string   `json:\"ttl,omitempty\"`\n\tEnables bool     `json:\"enables,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/cloudxns/cloudxns.go",
    "content": "// Package cloudxns implements a DNS provider for solving the DNS-01 challenge using CloudXNS DNS.\npackage cloudxns\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"CLOUDXNS_\"\n\n\tEnvAPIKey    = envNamespace + \"API_KEY\"\n\tEnvSecretKey = envNamespace + \"SECRET_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tSecretKey          string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct{}\n\n// NewDNSProvider returns a DNSProvider instance configured for CloudXNS.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\treturn NewDNSProviderConfig(&Config{})\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for CloudXNS.\nfunc NewDNSProviderConfig(_ *Config) (*DNSProvider, error) {\n\treturn nil, errors.New(\"cloudxns: provider has shut down\")\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(_, _, _ string) error {\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(_, _, _ string) error {\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn dns01.DefaultPropagationTimeout, dns01.DefaultPollingInterval\n}\n"
  },
  {
    "path": "providers/dns/cloudxns/cloudxns.toml",
    "content": "Name = \"CloudXNS (Deprecated)\"\nDescription = '''\nThe CloudXNS DNS provider has shut down.\n'''\nURL = \"https://github.com/go-acme/lego/issues/2323\"\nCode = \"cloudxns\"\nSince = \"v0.5.0\"\n\nExample = '''\nCLOUDXNS_API_KEY=xxxx \\\nCLOUDXNS_SECRET_KEY=yyyy \\\nlego --dns cloudxns -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    CLOUDXNS_API_KEY = \"The API key\"\n    CLOUDXNS_SECRET_KEY = \"The API secret key\"\n  [Configuration.Additional]\n    CLOUDXNS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: )\"\n    CLOUDXNS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: )\"\n    CLOUDXNS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: )\"\n    CLOUDXNS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: )\"\n"
  },
  {
    "path": "providers/dns/com35/com35.go",
    "content": "// Package com35 implements a DNS provider for solving the DNS-01 challenge using 35.com/三五互联.\npackage com35\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/westcn\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"COM35_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultBaseURL = \"https://api.35.cn/api/v2\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config = westcn.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 60),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for 35.com/三五互联.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"35com: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for 35.com/三五互联.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"35com: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := westcn.NewDNSProviderConfig(config, defaultBaseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"35com: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"35com: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"35com: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n"
  },
  {
    "path": "providers/dns/com35/com35.toml",
    "content": "Name = \"35.com/三五互联\"\nDescription = ''''''\nURL = \"https://www.35.cn/\"\nCode = \"com35\"\nSince = \"v4.31.0\"\n\nExample = '''\nCOM35_USERNAME=\"xxx\" \\\nCOM35_PASSWORD=\"yyy\" \\\nlego --dns com35 -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    COM35_USERNAME = \"Username\"\n    COM35_PASSWORD = \"API password\"\n  [Configuration.Additional]\n    COM35_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    COM35_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    COM35_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    COM35_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api.35.cn/CustomerCenter/doc/domain_v2.html\"\n"
  },
  {
    "path": "providers/dns/com35/com35_test.go",
    "content": "package com35\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"35com: some credentials information are missing: COM35_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"35com: some credentials information are missing: COM35_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"35com: some credentials information are missing: COM35_USERNAME,COM35_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"35com: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tusername: \"user\",\n\t\t\texpected: \"35com: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"35com: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/conoha/conoha.go",
    "content": "// Package conoha implements a DNS provider for solving the DNS-01 challenge using ConoHa DNS.\npackage conoha\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/conoha/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"CONOHA_\"\n\n\tEnvRegion      = envNamespace + \"REGION\"\n\tEnvTenantID    = envNamespace + \"TENANT_ID\"\n\tEnvAPIUsername = envNamespace + \"API_USERNAME\"\n\tEnvAPIPassword = envNamespace + \"API_PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tRegion             string\n\tTenantID           string\n\tUsername           string\n\tPassword           string\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tRegion:             env.GetOrDefaultString(EnvRegion, \"tyo1\"),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 60),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for ConoHa DNS.\n// Credentials must be passed in the environment variables:\n// CONOHA_TENANT_ID, CONOHA_API_USERNAME, CONOHA_API_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvTenantID, EnvAPIUsername, EnvAPIPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"conoha: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.TenantID = values[EnvTenantID]\n\tconfig.Username = values[EnvAPIUsername]\n\tconfig.Password = values[EnvAPIPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for ConoHa DNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"conoha: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.TenantID == \"\" || config.Username == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"conoha: some credentials information are missing\")\n\t}\n\n\tidentifier, err := internal.NewIdentifier(config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"conoha: failed to create identity client: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tidentifier.HTTPClient = config.HTTPClient\n\t}\n\n\tidentifier.HTTPClient = clientdebug.Wrap(identifier.HTTPClient)\n\n\tauth := internal.Auth{\n\t\tTenantID: config.TenantID,\n\t\tPasswordCredentials: internal.PasswordCredentials{\n\t\t\tUsername: config.Username,\n\t\t\tPassword: config.Password,\n\t\t},\n\t}\n\n\ttokens, err := identifier.GetToken(context.TODO(), auth)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"conoha: failed to log in: %w\", err)\n\t}\n\n\tclient, err := internal.NewClient(config.Region, tokens.Access.Token.ID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"conoha: failed to create client: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"conoha: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tid, err := d.client.GetDomainID(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"conoha: failed to get domain ID: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName: info.EffectiveFQDN,\n\t\tType: \"TXT\",\n\t\tData: info.Value,\n\t\tTTL:  d.config.TTL,\n\t}\n\n\terr = d.client.CreateRecord(ctx, id, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"conoha: failed to create record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp clears ConoHa DNS TXT record.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"conoha: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tdomID, err := d.client.GetDomainID(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"conoha: failed to get domain ID: %w\", err)\n\t}\n\n\trecID, err := d.client.GetRecordID(ctx, domID, info.EffectiveFQDN, \"TXT\", info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"conoha: failed to get record ID: %w\", err)\n\t}\n\n\terr = d.client.DeleteRecord(ctx, domID, recID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"conoha: failed to delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/conoha/conoha.toml",
    "content": "Name = \"ConoHa v2\"\nDescription = ''''''\nURL = \"https://www.conoha.jp/\"\nCode = \"conoha\"\nSince = \"v1.2.0\"\n\nExample = '''\nCONOHA_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \\\nCONOHA_API_USERNAME=xxxx \\\nCONOHA_API_PASSWORD=yyyy \\\nlego --dns conoha -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    CONOHA_TENANT_ID = \"Tenant ID\"\n    CONOHA_API_USERNAME = \"The API username\"\n    CONOHA_API_PASSWORD = \"The API password\"\n  [Configuration.Additional]\n    CONOHA_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    CONOHA_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    CONOHA_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    CONOHA_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n    CONOHA_REGION = \"The region (Default: tyo1)\"\n\n[Links]\n  API = \"https://doc.conoha.jp/reference/api-vps2/api-dns-vps2\"\n"
  },
  {
    "path": "providers/dns/conoha/conoha_test.go",
    "content": "package conoha\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvTenantID,\n\tEnvAPIUsername,\n\tEnvAPIPassword).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"complete credentials, but login failed\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTenantID:    \"tenant_id\",\n\t\t\t\tEnvAPIUsername: \"api_username\",\n\t\t\t\tEnvAPIPassword: \"api_password\",\n\t\t\t},\n\t\t\texpected: `conoha: failed to log in: unexpected status code: [status code: 401] body: {\"unauthorized\":{\"message\":\"Invalid user: api_username\",\"code\":401}}`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTenantID:    \"\",\n\t\t\t\tEnvAPIUsername: \"\",\n\t\t\t\tEnvAPIPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"conoha: some credentials information are missing: CONOHA_TENANT_ID,CONOHA_API_USERNAME,CONOHA_API_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing tenant id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTenantID:    \"\",\n\t\t\t\tEnvAPIUsername: \"api_username\",\n\t\t\t\tEnvAPIPassword: \"api_password\",\n\t\t\t},\n\t\t\texpected: \"conoha: some credentials information are missing: CONOHA_TENANT_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTenantID:    \"tenant_id\",\n\t\t\t\tEnvAPIUsername: \"\",\n\t\t\t\tEnvAPIPassword: \"api_password\",\n\t\t\t},\n\t\t\texpected: \"conoha: some credentials information are missing: CONOHA_API_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTenantID:    \"tenant_id\",\n\t\t\t\tEnvAPIUsername: \"api_username\",\n\t\t\t\tEnvAPIPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"conoha: some credentials information are missing: CONOHA_API_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\texpected string\n\t\ttenant   string\n\t\tusername string\n\t\tpassword string\n\t}{\n\t\t{\n\t\t\tdesc:     \"complete credentials, but login failed\",\n\t\t\texpected: `conoha: failed to log in: unexpected status code: [status code: 401] body: {\"unauthorized\":{\"message\":\"Invalid user: api_username\",\"code\":401}}`,\n\t\t\ttenant:   \"tenant_id\",\n\t\t\tusername: \"api_username\",\n\t\t\tpassword: \"api_password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"conoha: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing tenant id\",\n\t\t\texpected: \"conoha: some credentials information are missing\",\n\t\t\tusername: \"api_username\",\n\t\t\tpassword: \"api_password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api username\",\n\t\t\texpected: \"conoha: some credentials information are missing\",\n\t\t\ttenant:   \"tenant_id\",\n\t\t\tpassword: \"api_password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api password\",\n\t\t\texpected: \"conoha: some credentials information are missing\",\n\t\t\ttenant:   \"tenant_id\",\n\t\t\tusername: \"api_username\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.TenantID = test.tenant\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/conoha/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst dnsServiceBaseURL = \"https://dns-service.%s.conoha.io\"\n\n// Client is a ConoHa API client.\ntype Client struct {\n\ttoken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient returns a client instance logged into the ConoHa service.\nfunc NewClient(region, token string) (*Client, error) {\n\tbaseURL, err := url.Parse(fmt.Sprintf(dnsServiceBaseURL, region))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\ttoken:      token,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}, nil\n}\n\n// GetDomainID returns an ID of specified domain.\nfunc (c *Client) GetDomainID(ctx context.Context, domainName string) (string, error) {\n\tdomainList, err := c.getDomains(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, domain := range domainList.Domains {\n\t\tif domain.Name == domainName {\n\t\t\treturn domain.ID, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no such domain: %s\", domainName)\n}\n\n// 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\nfunc (c *Client) getDomains(ctx context.Context) (*DomainListResponse, error) {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"domains\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdomainList := &DomainListResponse{}\n\n\terr = c.do(req, domainList)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn domainList, nil\n}\n\n// GetRecordID returns an ID of specified record.\nfunc (c *Client) GetRecordID(ctx context.Context, domainID, recordName, recordType, data string) (string, error) {\n\trecordList, err := c.getRecords(ctx, domainID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, record := range recordList.Records {\n\t\tif record.Name == recordName && record.Type == recordType && record.Data == data {\n\t\t\treturn record.ID, nil\n\t\t}\n\t}\n\n\treturn \"\", errors.New(\"no such record\")\n}\n\n// 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\nfunc (c *Client) getRecords(ctx context.Context, domainID string) (*RecordListResponse, error) {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"domains\", domainID, \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trecordList := &RecordListResponse{}\n\n\terr = c.do(req, recordList)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn recordList, nil\n}\n\n// CreateRecord adds new record.\nfunc (c *Client) CreateRecord(ctx context.Context, domainID string, record Record) error {\n\t_, err := c.createRecord(ctx, domainID, record)\n\treturn err\n}\n\n// 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\nfunc (c *Client) createRecord(ctx context.Context, domainID string, record Record) (*Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"domains\", domainID, \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnewRecord := &Record{}\n\n\terr = c.do(req, newRecord)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn newRecord, nil\n}\n\n// DeleteRecord removes specified record.\n// 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\nfunc (c *Client) DeleteRecord(ctx context.Context, domainID, recordID string) error {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"domains\", domainID, \"records\", recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tif c.token != \"\" {\n\t\treq.Header.Set(\"X-Auth-Token\", c.token)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/conoha/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"tyo1\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(\"X-Auth-Token\", \"secret\"))\n}\n\nfunc TestClient_GetDomainID(t *testing.T) {\n\ttype expected struct {\n\t\tdomainID string\n\t\terror    bool\n\t}\n\n\ttestCases := []struct {\n\t\tdesc       string\n\t\tdomainName string\n\t\tresponse   string\n\t\texpected   expected\n\t}{\n\t\t{\n\t\t\tdesc:       \"success\",\n\t\t\tdomainName: \"domain1.com.\",\n\t\t\tresponse:   \"domains_GET.json\",\n\t\t\texpected:   expected{domainID: \"09494b72-b65b-4297-9efb-187f65a0553e\"},\n\t\t},\n\t\t{\n\t\t\tdesc:       \"non existing domain\",\n\t\t\tdomainName: \"domain1.com.\",\n\t\t\tresponse:   \"empty.json\",\n\t\t\texpected:   expected{error: true},\n\t\t},\n\t\t{\n\t\t\tdesc:       \"marshaling error\",\n\t\t\tdomainName: \"domain1.com.\",\n\t\t\tresponse:   \"empty.json\",\n\t\t\texpected:   expected{error: true},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(\"GET /v1/domains\", servermock.ResponseFromFixture(test.response)).\n\t\t\t\tBuild(t)\n\n\t\t\tdomainID, err := client.GetDomainID(t.Context(), test.domainName)\n\n\t\t\tif test.expected.error {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.expected.domainID, domainID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc    string\n\t\thandler http.HandlerFunc\n\t\tassert  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\thandler: func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\traw, err := io.ReadAll(req.Body)\n\t\t\t\tif err != nil {\n\t\t\t\t\thttp.Error(rw, err.Error(), http.StatusBadRequest)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tdefer func() { _ = req.Body.Close() }()\n\n\t\t\t\tif string(bytes.TrimSpace(raw)) != `{\"name\":\"lego.com.\",\"type\":\"TXT\",\"data\":\"txtTXTtxt\",\"ttl\":300}` {\n\t\t\t\t\thttp.Error(rw, fmt.Sprintf(\"invalid request body: %s\", string(raw)), http.StatusBadRequest)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tfile, err := os.Open(filepath.Join(\"fixtures\", \"domains-records_POST.json\"))\n\t\t\t\tif err != nil {\n\t\t\t\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tdefer func() { _ = file.Close() }()\n\n\t\t\t\t_, _ = io.Copy(rw, file)\n\t\t\t},\n\t\t\tassert: require.NoError,\n\t\t},\n\t\t{\n\t\t\tdesc: \"bad request\",\n\t\t\thandler: func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\thttp.Error(rw, \"OOPS\", http.StatusBadRequest)\n\t\t\t},\n\t\t\tassert: require.Error,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(\"POST /v1/domains/lego/records\", test.handler).\n\t\t\t\tBuild(t)\n\n\t\t\tdomainID := \"lego\"\n\n\t\t\trecord := Record{\n\t\t\t\tName: \"lego.com.\",\n\t\t\t\tType: \"TXT\",\n\t\t\t\tData: \"txtTXTtxt\",\n\t\t\t\tTTL:  300,\n\t\t\t}\n\n\t\t\terr := client.CreateRecord(t.Context(), domainID, record)\n\t\t\ttest.assert(t, err)\n\t\t})\n\t}\n}\n\nfunc TestClient_GetRecordID(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/domains/89acac79-38e7-497d-807c-a011e1310438/records\",\n\t\t\tservermock.ResponseFromFixture(\"domains-records_GET.json\")).\n\t\tBuild(t)\n\n\trecordID, err := client.GetRecordID(t.Context(), \"89acac79-38e7-497d-807c-a011e1310438\", \"www.example.com.\", \"A\", \"15.185.172.153\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"2e32e609-3a4f-45ba-bdef-e50eacd345ad\", recordID)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v1/domains/89acac79-38e7-497d-807c-a011e1310438/records/2e32e609-3a4f-45ba-bdef-e50eacd345ad\",\n\t\t\tservermock.ResponseFromFixture(\"domains-records_GET.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"89acac79-38e7-497d-807c-a011e1310438\", \"2e32e609-3a4f-45ba-bdef-e50eacd345ad\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/conoha/internal/fixtures/domains-records_GET.json",
    "content": "{\n  \"records\": [\n    {\n      \"id\": \"2e32e609-3a4f-45ba-bdef-e50eacd345ad\",\n      \"name\": \"www.example.com.\",\n      \"type\": \"A\",\n      \"ttl\": 3600,\n      \"created_at\": \"2012-11-02T19:56:26.000000\",\n      \"updated_at\": \"2012-11-04T13:22:36.000000\",\n      \"data\": \"15.185.172.153\",\n      \"domain_id\": \"89acac79-38e7-497d-807c-a011e1310438\",\n      \"version\": 1,\n      \"gslb_region\": \"JP\",\n      \"gslb_weight\": 250,\n      \"gslb_check\": 12300\n    },\n    {\n      \"id\": \"8e9ecf3e-fb92-4a3a-a8ae-7596f167bea3\",\n      \"name\": \"host1.example.com.\",\n      \"type\": \"A\",\n      \"ttl\": 3600,\n      \"created_at\": \"2012-11-04T13:57:50.000000\",\n      \"updated_at\": null,\n      \"data\": \"15.185.172.154\",\n      \"domain_id\": \"89acac79-38e7-497d-807c-a011e1310438\",\n      \"version\": 1,\n      \"gslb_region\": \"US\",\n      \"gslb_weight\": 220,\n      \"gslb_check\": 12200\n    },\n    {\n      \"id\": \"4ad19089-3e62-40f8-9482-17cc8ccb92cb\",\n      \"name\": \"web.example.com.\",\n      \"type\": \"CNAME\",\n      \"ttl\": 3600,\n      \"created_at\": \"2012-11-04T13:58:16.393735\",\n      \"updated_at\": null,\n      \"data\": \"www.example.com.\",\n      \"domain_id\": \"89acac79-38e7-497d-807c-a011e1310438\",\n      \"version\": 1\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/conoha/internal/fixtures/domains-records_POST.json",
    "content": "{\n  \"id\": \"2e32e609-3a4f-45ba-bdef-e50eacd345ad\",\n  \"name\": \"www.example.com.\",\n  \"type\": \"A\",\n  \"created_at\": \"2012-11-02T19:56:26.366792\",\n  \"updated_at\": null,\n  \"domain_id\": \"89acac79-38e7-497d-807c-a011e1310438\",\n  \"ttl\": null,\n  \"data\": \"192.0.2.3\",\n  \"gslb_check\": 1,\n  \"gslb_region\": \"JP\",\n  \"gslb_weight\": 250\n}\n"
  },
  {
    "path": "providers/dns/conoha/internal/fixtures/domains_GET.json",
    "content": "{\n  \"domains\":[\n    {\n      \"id\": \"09494b72-b65b-4297-9efb-187f65a0553e\",\n      \"name\": \"domain1.com.\",\n      \"ttl\": 3600,\n      \"serial\": 1351800668,\n      \"email\": \"nsadmin@example.org\",\n      \"gslb\": 0,\n      \"created_at\": \"2012-11-01T20:11:08.000000\",\n      \"updated_at\": null,\n      \"description\": \"memo\"\n    },\n    {\n      \"id\": \"cf661142-e577-40b5-b3eb-75795cdc0cd7\",\n      \"name\": \"domain2.com.\",\n      \"ttl\": 7200,\n      \"serial\": 1351800670,\n      \"email\": \"nsadmin2@example.org\",\n      \"gslb\": 1,\n      \"created_at\": \"2012-11-01T20:11:08.000000\",\n      \"updated_at\": \"2012-12-01T20:11:08.000000\",\n      \"description\": \"memomemo\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/conoha/internal/fixtures/empty.json",
    "content": "{}\n"
  },
  {
    "path": "providers/dns/conoha/internal/fixtures/tokens_POST.json",
    "content": "{\n  \"access\": {\n    \"token\": {\n      \"issued_at\": \"2015-05-19T07:08:21.927295\",\n      \"expires\": \"2015-05-20T07:08:21Z\",\n      \"id\": \"sample00d88246078f2bexample788f7\",\n      \"tenant\": {\n        \"name\": \"example00000000\",\n        \"enabled\": true,\n        \"tyo1_image_size\": \"550GB\"\n      },\n      \"endpoints_links\": [],\n      \"type\": \"mailhosting\",\n      \"name\": \"Mail Hosting Service\"\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/conoha/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst identityBaseURL = \"https://identity.%s.conoha.io\"\n\ntype Identifier struct {\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewIdentifier creates a new Identifier.\nfunc NewIdentifier(region string) (*Identifier, error) {\n\tbaseURL, err := url.Parse(fmt.Sprintf(identityBaseURL, region))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Identifier{\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}, nil\n}\n\n// GetToken gets valid token information.\n// 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\nfunc (c *Identifier) GetToken(ctx context.Context, auth Auth) (*IdentityResponse, error) {\n\tendpoint := c.baseURL.JoinPath(\"v2.0\", \"tokens\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, &IdentityRequest{Auth: auth})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tidentity := &IdentityResponse{}\n\n\terr = c.do(req, identity)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn identity, nil\n}\n\nfunc (c *Identifier) do(req *http.Request, result any) error {\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/conoha/internal/identity_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupIdentifier(server *httptest.Server) (*Identifier, error) {\n\tidentifier, err := NewIdentifier(\"tyo1\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tidentifier.HTTPClient = server.Client()\n\tidentifier.baseURL, _ = url.Parse(server.URL)\n\n\treturn identifier, nil\n}\n\nfunc TestNewClient(t *testing.T) {\n\tidentifier := servermock.NewBuilder[*Identifier](setupIdentifier,\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t).\n\t\tRoute(\"POST /v2.0/tokens\", servermock.ResponseFromFixture(\"tokens_POST.json\")).\n\t\tBuild(t)\n\n\tauth := Auth{\n\t\tTenantID: \"487727e3921d44e3bfe7ebb337bf085e\",\n\t\tPasswordCredentials: PasswordCredentials{\n\t\t\tUsername: \"ConoHa\",\n\t\t\tPassword: \"paSSword123456#$%\",\n\t\t},\n\t}\n\n\ttoken, err := identifier.GetToken(t.Context(), auth)\n\trequire.NoError(t, err)\n\n\texpected := &IdentityResponse{Access: Access{Token: Token{ID: \"sample00d88246078f2bexample788f7\"}}}\n\n\tassert.Equal(t, expected, token)\n}\n"
  },
  {
    "path": "providers/dns/conoha/internal/types.go",
    "content": "package internal\n\n// IdentityRequest is an authentication request body.\ntype IdentityRequest struct {\n\tAuth Auth `json:\"auth\"`\n}\n\n// Auth is an authentication information.\ntype Auth struct {\n\tTenantID            string              `json:\"tenantId\"`\n\tPasswordCredentials PasswordCredentials `json:\"passwordCredentials\"`\n}\n\n// PasswordCredentials is API-user's credentials.\ntype PasswordCredentials struct {\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n}\n\n// IdentityResponse is an authentication response body.\ntype IdentityResponse struct {\n\tAccess Access `json:\"access\"`\n}\n\n// Access is an identity information.\ntype Access struct {\n\tToken Token `json:\"token\"`\n}\n\n// Token is an api access token.\ntype Token struct {\n\tID string `json:\"id\"`\n}\n\n// DomainListResponse is a response of a domain listing request.\ntype DomainListResponse struct {\n\tDomains []Domain `json:\"domains\"`\n}\n\n// Domain is a hosted domain entry.\ntype Domain struct {\n\tID   string `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\n// RecordListResponse is a response of record listing request.\ntype RecordListResponse struct {\n\tRecords []Record `json:\"records\"`\n}\n\n// Record is a record entry.\ntype Record struct {\n\tID   string `json:\"id,omitempty\"`\n\tName string `json:\"name\"`\n\tType string `json:\"type\"`\n\tData string `json:\"data\"`\n\tTTL  int    `json:\"ttl\"`\n}\n"
  },
  {
    "path": "providers/dns/conohav3/conohav3.go",
    "content": "// Package conohav3 implements a DNS provider for solving the DNS-01 challenge using ConoHa VPS Ver 3.0 DNS.\npackage conohav3\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/conohav3/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"CONOHAV3_\"\n\n\tEnvRegion      = envNamespace + \"REGION\"\n\tEnvTenantID    = envNamespace + \"TENANT_ID\"\n\tEnvAPIUserID   = envNamespace + \"API_USER_ID\"\n\tEnvAPIPassword = envNamespace + \"API_PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tRegion             string\n\tTenantID           string\n\tUserID             string\n\tPassword           string\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tRegion:             env.GetOrDefaultString(EnvRegion, \"c3j1\"),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 60),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for ConoHa DNS.\n// Credentials must be passed in the environment variables:\n// CONOHAV3_TENANT_ID, CONOHAV3_API_USER_ID, CONOHAV3_API_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvTenantID, EnvAPIUserID, EnvAPIPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"conohav3: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.TenantID = values[EnvTenantID]\n\tconfig.UserID = values[EnvAPIUserID]\n\tconfig.Password = values[EnvAPIPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for ConoHa DNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"conohav3: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.TenantID == \"\" || config.UserID == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"conohav3: some credentials information are missing\")\n\t}\n\n\tidentifier, err := internal.NewIdentifier(config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"conohav3: failed to create identity client: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tidentifier.HTTPClient = config.HTTPClient\n\t}\n\n\tidentifier.HTTPClient = clientdebug.Wrap(identifier.HTTPClient)\n\n\tauth := internal.Auth{\n\t\tIdentity: internal.Identity{\n\t\t\tMethods: []string{\"password\"},\n\t\t\tPassword: internal.Password{\n\t\t\t\tUser: internal.User{\n\t\t\t\t\tID:       config.UserID,\n\t\t\t\t\tPassword: config.Password,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tScope: internal.Scope{\n\t\t\tProject: internal.Project{\n\t\t\t\tID: config.TenantID,\n\t\t\t},\n\t\t},\n\t}\n\n\ttoken, err := identifier.GetToken(context.Background(), auth)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"conohav3: failed to log in: %w\", err)\n\t}\n\n\tclient, err := internal.NewClient(config.Region, token)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"conohav3: failed to create client: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"conohav3: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tid, err := d.client.GetDomainID(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"conohav3: failed to get domain ID: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName: info.EffectiveFQDN,\n\t\tType: \"TXT\",\n\t\tData: info.Value,\n\t\tTTL:  d.config.TTL,\n\t}\n\n\terr = d.client.CreateRecord(ctx, id, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"conohav3: failed to create record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp clears ConoHa DNS TXT record.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"conohav3: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tdomID, err := d.client.GetDomainID(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"conohav3: failed to get domain ID: %w\", err)\n\t}\n\n\trecID, err := d.client.GetRecordID(ctx, domID, info.EffectiveFQDN, \"TXT\", info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"conohav3: failed to get record ID: %w\", err)\n\t}\n\n\terr = d.client.DeleteRecord(ctx, domID, recID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"conohav3: failed to delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/conohav3/conohav3.toml",
    "content": "Name = \"ConoHa v3\"\nDescription = ''''''\nURL = \"https://www.conoha.jp/\"\nCode = \"conohav3\"\nSince = \"v4.24.0\"\n\nExample = '''\nCONOHAV3_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \\\nCONOHAV3_API_USER_ID=xxxx \\\nCONOHAV3_API_PASSWORD=yyyy \\\nlego --dns conohav3 -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    CONOHAV3_TENANT_ID = \"Tenant ID\"\n    CONOHAV3_API_USER_ID = \"The API user ID\"\n    CONOHAV3_API_PASSWORD = \"The API password\"\n  [Configuration.Additional]\n    CONOHAV3_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    CONOHAV3_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    CONOHAV3_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    CONOHAV3_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n    CONOHAV3_REGION = \"The region (Default: c3j1)\"\n\n[Links]\n  API = \"https://doc.conoha.jp/reference/api-vps3/api-dns-vps3/\"\n"
  },
  {
    "path": "providers/dns/conohav3/conohav3_test.go",
    "content": "package conohav3\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvTenantID,\n\tEnvAPIUserID,\n\tEnvAPIPassword).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"complete credentials, but login failed\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTenantID:    \"tenant_id\",\n\t\t\t\tEnvAPIUserID:   \"api_user_id\",\n\t\t\t\tEnvAPIPassword: \"api_password\",\n\t\t\t},\n\t\t\texpected: `conohav3: failed to log in: unexpected status code: [status code: 400] body: {\"code\": 400, \"error\": \"user does not exist\"}`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTenantID:    \"\",\n\t\t\t\tEnvAPIUserID:   \"\",\n\t\t\t\tEnvAPIPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"conohav3: some credentials information are missing: CONOHAV3_TENANT_ID,CONOHAV3_API_USER_ID,CONOHAV3_API_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing tenant id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTenantID:    \"\",\n\t\t\t\tEnvAPIUserID:   \"api_user_id\",\n\t\t\t\tEnvAPIPassword: \"api_password\",\n\t\t\t},\n\t\t\texpected: \"conohav3: some credentials information are missing: CONOHAV3_TENANT_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api user id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTenantID:    \"tenant_id\",\n\t\t\t\tEnvAPIUserID:   \"\",\n\t\t\t\tEnvAPIPassword: \"api_password\",\n\t\t\t},\n\t\t\texpected: \"conohav3: some credentials information are missing: CONOHAV3_API_USER_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTenantID:    \"tenant_id\",\n\t\t\t\tEnvAPIUserID:   \"api_user_id\",\n\t\t\t\tEnvAPIPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"conohav3: some credentials information are missing: CONOHAV3_API_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\texpected string\n\t\ttenant   string\n\t\tuserid   string\n\t\tpassword string\n\t}{\n\t\t{\n\t\t\tdesc:     \"complete credentials, but login failed\",\n\t\t\texpected: `conohav3: failed to log in: unexpected status code: [status code: 400] body: {\"code\": 400, \"error\": \"user does not exist\"}`,\n\t\t\ttenant:   \"tenant_id\",\n\t\t\tuserid:   \"api_user_id\",\n\t\t\tpassword: \"api_password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"conohav3: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing tenant id\",\n\t\t\texpected: \"conohav3: some credentials information are missing\",\n\t\t\tuserid:   \"api_user_id\",\n\t\t\tpassword: \"api_password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api user id\",\n\t\t\texpected: \"conohav3: some credentials information are missing\",\n\t\t\ttenant:   \"tenant_id\",\n\t\t\tpassword: \"api_password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api password\",\n\t\t\texpected: \"conohav3: some credentials information are missing\",\n\t\t\ttenant:   \"tenant_id\",\n\t\t\tuserid:   \"api_user_id\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.TenantID = test.tenant\n\t\t\tconfig.UserID = test.userid\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/conohav3/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst dnsServiceBaseURL = \"https://dns-service.%s.conoha.io\"\n\n// Client is a ConoHa API client.\ntype Client struct {\n\ttoken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient returns a client instance logged into the ConoHa service.\nfunc NewClient(region, token string) (*Client, error) {\n\tbaseURL, err := url.Parse(fmt.Sprintf(dnsServiceBaseURL, region))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\ttoken:      token,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}, nil\n}\n\n// GetDomainID returns an ID of specified domain.\nfunc (c *Client) GetDomainID(ctx context.Context, domainName string) (string, error) {\n\tdomainList, err := c.getDomains(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, domain := range domainList.Domains {\n\t\tif domain.Name == domainName {\n\t\t\treturn domain.UUID, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no such domain: %s\", domainName)\n}\n\n// 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\nfunc (c *Client) getDomains(ctx context.Context) (*DomainListResponse, error) {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"domains\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdomainList := &DomainListResponse{}\n\n\terr = c.do(req, domainList)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn domainList, nil\n}\n\n// GetRecordID returns an ID of specified record.\nfunc (c *Client) GetRecordID(ctx context.Context, domainID, recordName, recordType, data string) (string, error) {\n\trecordList, err := c.getRecords(ctx, domainID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, record := range recordList.Records {\n\t\tif record.Name == recordName && record.Type == recordType && record.Data == data {\n\t\t\treturn record.UUID, nil\n\t\t}\n\t}\n\n\treturn \"\", errors.New(\"no such record\")\n}\n\n// 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\nfunc (c *Client) getRecords(ctx context.Context, domainID string) (*RecordListResponse, error) {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"domains\", domainID, \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trecordList := &RecordListResponse{}\n\n\terr = c.do(req, recordList)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn recordList, nil\n}\n\n// CreateRecord adds new record.\nfunc (c *Client) CreateRecord(ctx context.Context, domainID string, record Record) error {\n\t_, err := c.createRecord(ctx, domainID, record)\n\treturn err\n}\n\n// 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\nfunc (c *Client) createRecord(ctx context.Context, domainID string, record Record) (*Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"domains\", domainID, \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnewRecord := &Record{}\n\n\terr = c.do(req, newRecord)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn newRecord, nil\n}\n\n// DeleteRecord removes specified record.\n// 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\nfunc (c *Client) DeleteRecord(ctx context.Context, domainID, recordID string) error {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"domains\", domainID, \"records\", recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tif c.token != \"\" {\n\t\treq.Header.Set(\"X-Auth-Token\", c.token)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/conohav3/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"c3j1\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(\"X-Auth-Token\", \"secret\"))\n}\n\nfunc TestClient_GetDomainID(t *testing.T) {\n\ttype expected struct {\n\t\tdomainID string\n\t\terror    bool\n\t}\n\n\ttestCases := []struct {\n\t\tdesc       string\n\t\tdomainName string\n\t\tresponse   string\n\t\texpected   expected\n\t}{\n\t\t{\n\t\t\tdesc:       \"success\",\n\t\t\tdomainName: \"domain1.com.\",\n\t\t\tresponse:   \"domains_GET.json\",\n\t\t\texpected:   expected{domainID: \"09494b72-b65b-4297-9efb-187f65a0553e\"},\n\t\t},\n\t\t{\n\t\t\tdesc:       \"non existing domain\",\n\t\t\tdomainName: \"domain1.com.\",\n\t\t\tresponse:   \"empty.json\",\n\t\t\texpected:   expected{error: true},\n\t\t},\n\t\t{\n\t\t\tdesc:       \"marshaling error\",\n\t\t\tdomainName: \"domain1.com.\",\n\t\t\tresponse:   \"empty.json\",\n\t\t\texpected:   expected{error: true},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(\"GET /v1/domains\", servermock.ResponseFromFixture(test.response)).\n\t\t\t\tBuild(t)\n\n\t\t\tdomainID, err := client.GetDomainID(t.Context(), test.domainName)\n\n\t\t\tif test.expected.error {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.expected.domainID, domainID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc    string\n\t\thandler http.HandlerFunc\n\t\tassert  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\thandler: func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\traw, err := io.ReadAll(req.Body)\n\t\t\t\tif err != nil {\n\t\t\t\t\thttp.Error(rw, err.Error(), http.StatusBadRequest)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tdefer func() { _ = req.Body.Close() }()\n\n\t\t\t\tif string(bytes.TrimSpace(raw)) != `{\"name\":\"lego.com.\",\"type\":\"TXT\",\"data\":\"txtTXTtxt\",\"ttl\":300}` {\n\t\t\t\t\thttp.Error(rw, fmt.Sprintf(\"invalid request body: %s\", string(raw)), http.StatusBadRequest)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tfile, err := os.Open(filepath.Join(\"fixtures\", \"domains-records_POST.json\"))\n\t\t\t\tif err != nil {\n\t\t\t\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tdefer func() { _ = file.Close() }()\n\n\t\t\t\t_, _ = io.Copy(rw, file)\n\t\t\t},\n\t\t\tassert: require.NoError,\n\t\t},\n\t\t{\n\t\t\tdesc: \"bad request\",\n\t\t\thandler: func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\thttp.Error(rw, \"OOPS\", http.StatusBadRequest)\n\t\t\t},\n\t\t\tassert: require.Error,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(\"POST /v1/domains/lego/records\", test.handler).\n\t\t\t\tBuild(t)\n\n\t\t\tdomainID := \"lego\"\n\n\t\t\trecord := Record{\n\t\t\t\tName: \"lego.com.\",\n\t\t\t\tType: \"TXT\",\n\t\t\t\tData: \"txtTXTtxt\",\n\t\t\t\tTTL:  300,\n\t\t\t}\n\n\t\t\terr := client.CreateRecord(t.Context(), domainID, record)\n\t\t\ttest.assert(t, err)\n\t\t})\n\t}\n}\n\nfunc TestClient_GetRecordID(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/domains/89acac79-38e7-497d-807c-a011e1310438/records\",\n\t\t\tservermock.ResponseFromFixture(\"domains-records_GET.json\")).\n\t\tBuild(t)\n\n\trecordID, err := client.GetRecordID(t.Context(), \"89acac79-38e7-497d-807c-a011e1310438\", \"www.example.com.\", \"A\", \"15.185.172.153\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"2e32e609-3a4f-45ba-bdef-e50eacd345ad\", recordID)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v1/domains/89acac79-38e7-497d-807c-a011e1310438/records/2e32e609-3a4f-45ba-bdef-e50eacd345ad\",\n\t\t\tservermock.ResponseFromFixture(\"domains-records_GET.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"89acac79-38e7-497d-807c-a011e1310438\", \"2e32e609-3a4f-45ba-bdef-e50eacd345ad\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/conohav3/internal/fixtures/domains-records_GET.json",
    "content": "{\n  \"records\": [\n    {\n      \"uuid\": \"2e32e609-3a4f-45ba-bdef-e50eacd345ad\",\n      \"name\": \"www.example.com.\",\n      \"type\": \"A\",\n      \"ttl\": 3600,\n      \"created_at\": \"2012-11-02T19:56:26.000000\",\n      \"updated_at\": \"2012-11-04T13:22:36.000000\",\n      \"data\": \"15.185.172.153\",\n      \"domain_id\": \"89acac79-38e7-497d-807c-a011e1310438\",\n      \"version\": 1,\n      \"gslb_region\": \"JP\",\n      \"gslb_weight\": 250,\n      \"gslb_check\": 12300\n    },\n    {\n      \"uuid\": \"8e9ecf3e-fb92-4a3a-a8ae-7596f167bea3\",\n      \"name\": \"host1.example.com.\",\n      \"type\": \"A\",\n      \"ttl\": 3600,\n      \"created_at\": \"2012-11-04T13:57:50.000000\",\n      \"updated_at\": null,\n      \"data\": \"15.185.172.154\",\n      \"domain_id\": \"89acac79-38e7-497d-807c-a011e1310438\",\n      \"version\": 1,\n      \"gslb_region\": \"US\",\n      \"gslb_weight\": 220,\n      \"gslb_check\": 12200\n    },\n    {\n      \"uuid\": \"4ad19089-3e62-40f8-9482-17cc8ccb92cb\",\n      \"name\": \"web.example.com.\",\n      \"type\": \"CNAME\",\n      \"ttl\": 3600,\n      \"created_at\": \"2012-11-04T13:58:16.393735\",\n      \"updated_at\": null,\n      \"data\": \"www.example.com.\",\n      \"domain_id\": \"89acac79-38e7-497d-807c-a011e1310438\",\n      \"version\": 1\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/conohav3/internal/fixtures/domains-records_POST.json",
    "content": "{\n  \"uuid\": \"2e32e609-3a4f-45ba-bdef-e50eacd345ad\",\n  \"name\": \"www.example.com.\",\n  \"type\": \"A\",\n  \"created_at\": \"2012-11-02T19:56:26.366792\",\n  \"updated_at\": null,\n  \"domain_id\": \"89acac79-38e7-497d-807c-a011e1310438\",\n  \"ttl\": null,\n  \"data\": \"192.0.2.3\",\n  \"gslb_check\": 1,\n  \"gslb_region\": \"JP\",\n  \"gslb_weight\": 250\n}\n"
  },
  {
    "path": "providers/dns/conohav3/internal/fixtures/domains_GET.json",
    "content": "{\n    \"domains\": [\n        {\n            \"uuid\": \"09494b72-b65b-4297-9efb-187f65a0553e\",\n            \"name\": \"domain1.com.\",\n            \"project_id\": \"cf661142-e577-40b5-b3eb-75795cdc0cd7\",\n            \"serial\": 1701909248,\n            \"ttl\": 3600,\n            \"email\": \"nsadmin1@example.org\",\n            \"created_at\": \"2023-12-07T00:34:08Z\",\n            \"updated_at\": \"2023-12-07T00:34:08Z\"\n        },\n        {\n            \"uuid\": \"cf661142-e577-40b5-b3eb-75795cdc0cd7\",\n            \"name\": \"domain2.com.\",\n            \"project_id\": \"cf661144-e578-39b6-b4eb-75794cdc1cd8\",\n            \"serial\": 1351800670,\n            \"ttl\": 7200,\n            \"email\": \"nsadmin2@example.org\",\n            \"created_at\": \"2012-11-01T20:11:08Z\",\n            \"updated_at\": \"2012-12-01T20:11:08Z\"\n        }\n    ],\n    \"total_count\": 1\n}\n"
  },
  {
    "path": "providers/dns/conohav3/internal/fixtures/empty.json",
    "content": "{}\n"
  },
  {
    "path": "providers/dns/conohav3/internal/identity.go",
    "content": "// internal/identity.go\n\npackage internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst identityBaseURL = \"https://identity.%s.conoha.io\"\n\ntype Identifier struct {\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewIdentifier creates a new Identifier.\nfunc NewIdentifier(region string) (*Identifier, error) {\n\tbaseURL, err := url.Parse(fmt.Sprintf(identityBaseURL, region))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Identifier{\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}, nil\n}\n\n// GetToken returns the x-subject-token from Identity API.\n// 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\nfunc (c *Identifier) GetToken(ctx context.Context, auth Auth) (string, error) {\n\tendpoint := c.baseURL.JoinPath(\"v3\", \"auth\", \"tokens\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, &IdentityRequest{Auth: auth})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn c.do(req)\n}\n\n// do sends the request and returns the token from x-subject-token header.\nfunc (c *Identifier) do(req *http.Request) (string, error) {\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusCreated {\n\t\treturn \"\", errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\ttoken := resp.Header.Get(\"x-subject-token\")\n\tif token == \"\" {\n\t\treturn \"\", errors.New(\"x-subject-token header is missing in response\")\n\t}\n\n\t_, _ = io.Copy(io.Discard, resp.Body)\n\n\treturn token, nil\n}\n"
  },
  {
    "path": "providers/dns/conohav3/internal/identity_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupIdentifier(server *httptest.Server) (*Identifier, error) {\n\tidentifier, err := NewIdentifier(\"c3j1\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tidentifier.HTTPClient = server.Client()\n\tidentifier.baseURL, _ = url.Parse(server.URL)\n\n\treturn identifier, nil\n}\n\nfunc TestGetToken_HeaderToken(t *testing.T) {\n\tidentifier := servermock.NewBuilder[*Identifier](setupIdentifier,\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t).\n\t\tRoute(\"POST /v3/auth/tokens\",\n\t\t\tservermock.ResponseFromFixture(\"empty.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated).\n\t\t\t\tWithHeader(\"x-subject-token\", \"sample-header-token-123\")).\n\t\tBuild(t)\n\n\tauth := Auth{\n\t\tIdentity: Identity{\n\t\t\tMethods: []string{\"password\"},\n\t\t\tPassword: Password{\n\t\t\t\tUser: User{\n\t\t\t\t\tID:       \"dummy-id\",\n\t\t\t\t\tPassword: \"dummy-password\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tScope: Scope{\n\t\t\tProject: Project{\n\t\t\t\tID: \"dummy-project-id\",\n\t\t\t},\n\t\t},\n\t}\n\n\ttoken, err := identifier.GetToken(t.Context(), auth)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"sample-header-token-123\", token)\n}\n"
  },
  {
    "path": "providers/dns/conohav3/internal/types.go",
    "content": "package internal\n\n// IdentityRequest is the top-level payload sent to the Identity v3.\ntype IdentityRequest struct {\n\tAuth Auth `json:\"auth\"`\n}\n\n// Auth authentication credentials (Identity) and scope (Scope).\ntype Auth struct {\n\tIdentity Identity `json:\"identity\"`\n\tScope    Scope    `json:\"scope\"`\n}\n\n// Identity describes how the client will authenticate.\n// In ConoHa v3.0, only support the \"password\" method.\ntype Identity struct {\n\tMethods  []string `json:\"methods\"`\n\tPassword Password `json:\"password\"`\n}\n\n// Password nests the concrete user credentials used by the password auth method.\ntype Password struct {\n\tUser User `json:\"user\"`\n}\n\n// User holds the API User ID and password that will be verified by the Identity service.\ntype User struct {\n\tID       string `json:\"id\"`\n\tPassword string `json:\"password\"`\n}\n\n// Scope specifies which tenant the issued token should be scoped to.\ntype Scope struct {\n\tProject Project `json:\"project\"`\n}\n\n// Project identifies the target tenant by UUID.\ntype Project struct {\n\tID string `json:\"id\"`\n}\n\n// DomainListResponse is returned by `GET /v1/domains` and contains all DNS zones (domains) owned by the project.\ntype DomainListResponse struct {\n\tDomains []Domain `json:\"domains\"`\n}\n\n// Domain represents a single hosted DNS zone.\ntype Domain struct {\n\tUUID string `json:\"uuid\"`\n\tName string `json:\"name\"`\n}\n\n// RecordListResponse is returned by `GET /v1/domains/{domain_uuid}/records` and lists every record in the zone.\ntype RecordListResponse struct {\n\tRecords []Record `json:\"records\"`\n}\n\n// Record represents a DNS record inside a zone.\ntype Record struct {\n\tUUID string `json:\"uuid,omitempty\"`\n\tName string `json:\"name\"`\n\tType string `json:\"type\"`\n\tData string `json:\"data\"`\n\tTTL  int    `json:\"ttl\"`\n}\n"
  },
  {
    "path": "providers/dns/constellix/constellix.go",
    "content": "// Package constellix implements a DNS provider for solving the DNS-01 challenge using Constellix DNS.\npackage constellix\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/constellix/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/hashicorp/go-retryablehttp\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"CONSTELLIX_\"\n\n\tEnvAPIKey    = envNamespace + \"API_KEY\"\n\tEnvSecretKey = envNamespace + \"SECRET_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tSecretKey          string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 60),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Constellix.\n// Credentials must be passed in the environment variables:\n// CONSTELLIX_API_KEY and CONSTELLIX_SECRET_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey, EnvSecretKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"constellix: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.SecretKey = values[EnvSecretKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Constellix.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"constellix: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.SecretKey == \"\" || config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"constellix: incomplete credentials, missing secret key and/or API key\")\n\t}\n\n\ttr, err := internal.NewTokenTransport(config.APIKey, config.SecretKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"constellix: %w\", err)\n\t}\n\n\tretryClient := retryablehttp.NewClient()\n\tretryClient.RetryMax = 5\n\tretryClient.HTTPClient = tr.Wrap(config.HTTPClient)\n\tretryClient.Backoff = backoff\n\n\tclient := internal.NewClient(clientdebug.Wrap(retryClient.StandardClient()))\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"constellix: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tdom, err := d.client.Domains.GetByName(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"constellix: failed to get domain (%s): %w\", authZone, err)\n\t}\n\n\trecordName, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"constellix: %w\", err)\n\t}\n\n\trecords, err := d.client.TxtRecords.Search(ctx, dom.ID, internal.Exact, recordName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"constellix: failed to search TXT records: %w\", err)\n\t}\n\n\tif len(records) > 1 {\n\t\treturn errors.New(\"constellix: failed to get TXT records\")\n\t}\n\n\t// TXT record entry already existing\n\tif len(records) == 1 {\n\t\treturn d.appendRecordValue(ctx, dom, records[0].ID, info.Value)\n\t}\n\n\terr = d.createRecord(ctx, dom, info.EffectiveFQDN, recordName, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"constellix: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"constellix: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tdom, err := d.client.Domains.GetByName(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"constellix: failed to get domain (%s): %w\", authZone, err)\n\t}\n\n\trecordName, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"constellix: %w\", err)\n\t}\n\n\trecords, err := d.client.TxtRecords.Search(ctx, dom.ID, internal.Exact, recordName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"constellix: failed to search TXT records: %w\", err)\n\t}\n\n\tif len(records) > 1 {\n\t\treturn errors.New(\"constellix: failed to get TXT records\")\n\t}\n\n\tif len(records) == 0 {\n\t\treturn nil\n\t}\n\n\trecord, err := d.client.TxtRecords.Get(ctx, dom.ID, records[0].ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"constellix: failed to get TXT records: %w\", err)\n\t}\n\n\tif !containsValue(record, info.Value) {\n\t\treturn nil\n\t}\n\n\t// only 1 record value, the whole record must be deleted.\n\tif len(record.Value) == 1 {\n\t\t_, err = d.client.TxtRecords.Delete(ctx, dom.ID, record.ID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"constellix: failed to delete TXT records: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\terr = d.removeRecordValue(ctx, dom, record, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"constellix: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) createRecord(ctx context.Context, dom internal.Domain, fqdn, recordName, value string) error {\n\trequest := internal.RecordRequest{\n\t\tName: recordName,\n\t\tTTL:  d.config.TTL,\n\t\tRoundRobin: []internal.RecordValue{\n\t\t\t{Value: fmt.Sprintf(`%q`, value)},\n\t\t},\n\t}\n\n\t_, err := d.client.TxtRecords.Create(ctx, dom.ID, request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create TXT record %s: %w\", fqdn, err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) appendRecordValue(ctx context.Context, dom internal.Domain, recordID int64, value string) error {\n\trecord, err := d.client.TxtRecords.Get(ctx, dom.ID, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get TXT records: %w\", err)\n\t}\n\n\tif containsValue(record, value) {\n\t\treturn nil\n\t}\n\n\trequest := internal.RecordRequest{\n\t\tName:       record.Name,\n\t\tTTL:        record.TTL,\n\t\tRoundRobin: append(record.RoundRobin, internal.RecordValue{Value: fmt.Sprintf(`%q`, value)}),\n\t}\n\n\t_, err = d.client.TxtRecords.Update(ctx, dom.ID, record.ID, request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to update TXT records: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) removeRecordValue(ctx context.Context, dom internal.Domain, record *internal.Record, value string) error {\n\trequest := internal.RecordRequest{\n\t\tName: record.Name,\n\t\tTTL:  record.TTL,\n\t}\n\n\tfor _, val := range record.Value {\n\t\tif val.Value != fmt.Sprintf(`%q`, value) {\n\t\t\trequest.RoundRobin = append(request.RoundRobin, val)\n\t\t}\n\t}\n\n\t_, err := d.client.TxtRecords.Update(ctx, dom.ID, record.ID, request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to update TXT records: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc containsValue(record *internal.Record, value string) bool {\n\tif record == nil {\n\t\treturn false\n\t}\n\n\tqValue := fmt.Sprintf(`%q`, value)\n\n\treturn slices.ContainsFunc(record.Value, func(val internal.RecordValue) bool {\n\t\treturn val.Value == qValue\n\t})\n}\n\nfunc backoff(minimum, maximum time.Duration, attemptNum int, resp *http.Response) time.Duration {\n\tif resp != nil {\n\t\t// https://api.dns.constellix.com/v4/docs#section/Using-the-API/Rate-Limiting\n\t\tif resp.StatusCode == http.StatusTooManyRequests {\n\t\t\tif s, ok := resp.Header[\"X-Ratelimit-Reset\"]; ok {\n\t\t\t\tif sleep, err := strconv.ParseInt(s[0], 10, 64); err == nil {\n\t\t\t\t\treturn time.Second * time.Duration(sleep)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn retryablehttp.DefaultBackoff(minimum, maximum, attemptNum, resp)\n}\n"
  },
  {
    "path": "providers/dns/constellix/constellix.toml",
    "content": "Name = \"Constellix\"\nDescription = ''''''\nURL = \"https://constellix.com\"\nCode = \"constellix\"\nSince = \"v3.4.0\"\n\nExample = '''\nCONSTELLIX_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \\\nCONSTELLIX_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \\\nlego --dns constellix -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    CONSTELLIX_API_KEY = \"User API key\"\n    CONSTELLIX_SECRET_KEY = \"User secret key\"\n  [Configuration.Additional]\n    CONSTELLIX_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    CONSTELLIX_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    CONSTELLIX_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    CONSTELLIX_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api-docs.constellix.com\"\n"
  },
  {
    "path": "providers/dns/constellix/constellix_test.go",
    "content": "package constellix\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIKey,\n\tEnvSecretKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"123\",\n\t\t\t\tEnvSecretKey: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"\",\n\t\t\t\tEnvSecretKey: \"\",\n\t\t\t},\n\t\t\texpected: \"constellix: some credentials information are missing: CONSTELLIX_API_KEY,CONSTELLIX_SECRET_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"\",\n\t\t\t\tEnvSecretKey: \"api_password\",\n\t\t\t},\n\t\t\texpected: \"constellix: some credentials information are missing: CONSTELLIX_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"api_username\",\n\t\t\t\tEnvSecretKey: \"\",\n\t\t\t},\n\t\t\texpected: \"constellix: some credentials information are missing: CONSTELLIX_SECRET_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\texpected  string\n\t\tapiKey    string\n\t\tsecretKey string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tapiKey:    \"api_key\",\n\t\t\tsecretKey: \"api_secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"constellix: incomplete credentials, missing secret key and/or API key\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing api key\",\n\t\t\tapiKey:    \"\",\n\t\t\tsecretKey: \"api_secret\",\n\t\t\texpected:  \"constellix: incomplete credentials, missing secret key and/or API key\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing secret key\",\n\t\t\tapiKey:    \"api_key\",\n\t\t\tsecretKey: \"\",\n\t\t\texpected:  \"constellix: incomplete credentials, missing secret key and/or API key\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.SecretKey = test.secretKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/constellix/internal/auth.go",
    "content": "package internal\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n)\n\nconst securityTokenHeader = \"x-cns-security-token\"\n\n// TokenTransport HTTP transport for API authentication.\ntype TokenTransport struct {\n\tapiKey    string\n\tsecretKey string\n\n\t// Transport is the underlying HTTP transport to use when making requests.\n\t// It will default to http.DefaultTransport if nil.\n\tTransport http.RoundTripper\n}\n\n// NewTokenTransport Creates an HTTP transport for API authentication.\nfunc NewTokenTransport(apiKey, secretKey string) (*TokenTransport, error) {\n\tif apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing: API key\")\n\t}\n\n\tif secretKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing: secret key\")\n\t}\n\n\treturn &TokenTransport{apiKey: apiKey, secretKey: secretKey}, nil\n}\n\n// RoundTrip executes a single HTTP transaction.\nfunc (t *TokenTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\tenrichedReq := &http.Request{}\n\t*enrichedReq = *req\n\n\tenrichedReq.Header = make(http.Header, len(req.Header))\n\tfor k, s := range req.Header {\n\t\tenrichedReq.Header[k] = append([]string(nil), s...)\n\t}\n\n\tif t.apiKey != \"\" && t.secretKey != \"\" {\n\t\tsecurityToken := createCnsSecurityToken(t.apiKey, t.secretKey)\n\t\tenrichedReq.Header.Set(securityTokenHeader, securityToken)\n\t}\n\n\treturn t.transport().RoundTrip(enrichedReq)\n}\n\nfunc (t *TokenTransport) transport() http.RoundTripper {\n\tif t.Transport != nil {\n\t\treturn t.Transport\n\t}\n\n\treturn http.DefaultTransport\n}\n\n// Client Creates a new HTTP client.\nfunc (t *TokenTransport) Client() *http.Client {\n\treturn &http.Client{Transport: t}\n}\n\n// Wrap wraps an HTTP client Transport with the TokenTransport.\nfunc (t *TokenTransport) Wrap(client *http.Client) *http.Client {\n\tbackup := client.Transport\n\tt.Transport = backup\n\tclient.Transport = t\n\n\treturn client\n}\n\nfunc createCnsSecurityToken(apiKey, secretKey string) string {\n\ttimestamp := time.Now().Round(time.Millisecond).UnixNano() / int64(time.Millisecond)\n\n\thm := encodedHmac(timestamp, secretKey)\n\trequestDate := strconv.FormatInt(timestamp, 10)\n\n\treturn fmt.Sprintf(\"%s:%s:%s\", apiKey, hm, requestDate)\n}\n\nfunc encodedHmac(message int64, secret string) string {\n\th := hmac.New(sha1.New, []byte(secret))\n\t_, _ = h.Write([]byte(strconv.FormatInt(message, 10)))\n\n\treturn base64.StdEncoding.EncodeToString(h.Sum(nil))\n}\n"
  },
  {
    "path": "providers/dns/constellix/internal/auth_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewTokenTransport_success(t *testing.T) {\n\tapiKey := \"api\"\n\tsecretKey := \"secret\"\n\n\ttransport, err := NewTokenTransport(apiKey, secretKey)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, transport)\n}\n\nfunc TestNewTokenTransport_missing_credentials(t *testing.T) {\n\tapiKey := \"\"\n\tsecretKey := \"\"\n\n\ttransport, err := NewTokenTransport(apiKey, secretKey)\n\trequire.Error(t, err)\n\tassert.Nil(t, transport)\n}\n\nfunc TestTokenTransport_RoundTrip(t *testing.T) {\n\tapiKey := \"api\"\n\tsecretKey := \"secret\"\n\n\ttransport, err := NewTokenTransport(apiKey, secretKey)\n\trequire.NoError(t, err)\n\n\treq := httptest.NewRequest(http.MethodGet, \"http://example.com\", nil)\n\n\tresp, err := transport.RoundTrip(req)\n\trequire.NoError(t, err)\n\n\tassert.Regexp(t, `api:[^:]{28}:\\d{13}`, resp.Request.Header.Get(securityTokenHeader))\n}\n"
  },
  {
    "path": "providers/dns/constellix/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst (\n\tdefaultBaseURL = \"https://api.dns.constellix.com\"\n\tdefaultVersion = \"v1\"\n)\n\n// Client the Constellix client.\ntype Client struct {\n\tBaseURL    string\n\tHTTPClient *http.Client\n\n\tcommon service // Reuse a single struct instead of allocating one for each service on the heap.\n\n\t// Services used for communicating with the API\n\tDomains    *DomainService\n\tTxtRecords *TxtRecordService\n}\n\n// NewClient Creates a Constellix client.\nfunc NewClient(httpClient *http.Client) *Client {\n\tif httpClient == nil {\n\t\thttpClient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tclient := &Client{\n\t\tBaseURL:    defaultBaseURL,\n\t\tHTTPClient: httpClient,\n\t}\n\n\tclient.common.client = client\n\tclient.Domains = (*DomainService)(&client.common)\n\tclient.TxtRecords = (*TxtRecordService)(&client.common)\n\n\treturn client\n}\n\ntype service struct {\n\tclient *Client\n}\n\n// do sends an API request and returns the API response.\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\terr = checkResponse(resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif err = json.Unmarshal(raw, result); err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) createEndpoint(fragment ...string) (string, error) {\n\treturn url.JoinPath(c.BaseURL, fragment...)\n}\n\nfunc checkResponse(resp *http.Response) error {\n\tif resp.StatusCode == http.StatusOK {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err == nil && raw != nil {\n\t\terrAPI := &APIError{StatusCode: resp.StatusCode}\n\n\t\tif json.Unmarshal(raw, errAPI) != nil {\n\t\t\treturn fmt.Errorf(\"API error: status code: %d: %v\", resp.StatusCode, string(raw))\n\t\t}\n\n\t\tswitch resp.StatusCode {\n\t\tcase http.StatusNotFound:\n\t\t\treturn &NotFound{APIError: errAPI}\n\t\tcase http.StatusBadRequest:\n\t\t\treturn &BadRequest{APIError: errAPI}\n\t\tdefault:\n\t\t\treturn errAPI\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"API error, status code: %d\", resp.StatusCode)\n}\n"
  },
  {
    "path": "providers/dns/constellix/internal/domains.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\n// DomainService API access to Domain.\ntype DomainService service\n\n// GetAll domains.\n// https://api-docs.constellix.com/?version=latest#484c3f21-d724-4ee4-a6fa-ab22c8eb9e9b\nfunc (s *DomainService) GetAll(ctx context.Context, params *PaginationParameters) ([]Domain, error) {\n\tendpoint, err := s.client.createEndpoint(defaultVersion, \"domains\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request endpoint: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tif params != nil {\n\t\tv, errQ := querystring.Values(params)\n\t\tif errQ != nil {\n\t\t\treturn nil, errQ\n\t\t}\n\n\t\treq.URL.RawQuery = v.Encode()\n\t}\n\n\tvar domains []Domain\n\n\terr = s.client.do(req, &domains)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn domains, nil\n}\n\n// GetByName Gets domain by name.\nfunc (s *DomainService) GetByName(ctx context.Context, domainName string) (Domain, error) {\n\tdomains, err := s.Search(ctx, Exact, domainName)\n\tif err != nil {\n\t\treturn Domain{}, err\n\t}\n\n\tif len(domains) == 0 {\n\t\treturn Domain{}, fmt.Errorf(\"domain not found: %s\", domainName)\n\t}\n\n\tif len(domains) > 1 {\n\t\treturn Domain{}, fmt.Errorf(\"multiple domains found: %v\", domains)\n\t}\n\n\treturn domains[0], nil\n}\n\n// Search searches for a domain by name.\n// https://api-docs.constellix.com/?version=latest#3d7b2679-2209-49f3-b011-b7d24e512008\nfunc (s *DomainService) Search(ctx context.Context, filter searchFilter, value string) ([]Domain, error) {\n\tendpoint, err := s.client.createEndpoint(defaultVersion, \"domains\", \"search\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request endpoint: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tquery := req.URL.Query()\n\tquery.Set(string(filter), value)\n\treq.URL.RawQuery = query.Encode()\n\n\tvar domains []Domain\n\n\terr = s.client.do(req, &domains)\n\tif err != nil {\n\t\tvar nf *NotFound\n\t\tif !errors.As(err, &nf) {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn domains, nil\n}\n"
  },
  {
    "path": "providers/dns/constellix/internal/domains_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(server.Client())\n\t\t\tclient.BaseURL = server.URL\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t)\n}\n\nfunc TestDomainService_GetAll(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/domains\", servermock.ResponseFromFixture(\"domains-GetAll.json\")).\n\t\tBuild(t)\n\n\tdata, err := client.Domains.GetAll(t.Context(), nil)\n\trequire.NoError(t, err)\n\n\texpected := []Domain{\n\t\t{ID: 273301, Name: \"aaa.example\", TypeID: 1, Version: 9, Status: \"ACTIVE\"},\n\t\t{ID: 273302, Name: \"bbb.example\", TypeID: 1, Version: 9, Status: \"ACTIVE\"},\n\t\t{ID: 273303, Name: \"ccc.example\", TypeID: 1, Version: 9, Status: \"ACTIVE\"},\n\t\t{ID: 273304, Name: \"ddd.example\", TypeID: 1, Version: 9, Status: \"ACTIVE\"},\n\t}\n\n\tassert.Equal(t, expected, data)\n}\n\nfunc TestDomainService_Search(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/domains/search\",\n\t\t\tservermock.ResponseFromFixture(\"domains-Search.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"exact\", \"example.com\")).\n\t\tBuild(t)\n\n\tdata, err := client.Domains.Search(t.Context(), Exact, \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []Domain{\n\t\t{ID: 273302, Name: \"example.com\", TypeID: 1, Version: 9, Status: \"ACTIVE\"},\n\t}\n\n\tassert.Equal(t, expected, data)\n}\n"
  },
  {
    "path": "providers/dns/constellix/internal/fixtures/domains-GetAll.json",
    "content": "[\n  {\n    \"id\": 273301,\n    \"name\": \"aaa.example\",\n    \"soa\": {\n      \"primaryNameserver\": \"ns11.constellix.com.\",\n      \"email\": \"dns.constellix.com.\",\n      \"ttl\": 86400,\n      \"serial\": 2015010110,\n      \"refresh\": 43200,\n      \"retry\": 3600,\n      \"expire\": 1209600,\n      \"negCache\": 180\n    },\n    \"createdTs\": \"2020-02-04T22:42:10Z\",\n    \"modifiedTs\": \"2020-02-05T09:23:17Z\",\n    \"typeId\": 1,\n    \"domainTags\": [],\n    \"folder\": null,\n    \"hasGtdRegions\": false,\n    \"hasGeoIP\": false,\n    \"nameserverGroup\": 1,\n    \"nameservers\": [\n      \"ns11.constellix.com.\",\n      \"ns21.constellix.com.\",\n      \"ns31.constellix.com.\",\n      \"ns41.constellix.net.\",\n      \"ns51.constellix.net.\",\n      \"ns61.constellix.net.\"\n    ],\n    \"note\": \"\",\n    \"version\": 9,\n    \"status\": \"ACTIVE\",\n    \"tags\": null,\n    \"contactIds\": []\n  },\n  {\n    \"id\": 273302,\n    \"name\": \"bbb.example\",\n    \"soa\": {\n      \"primaryNameserver\": \"ns11.constellix.com.\",\n      \"email\": \"dns.constellix.com.\",\n      \"ttl\": 86400,\n      \"serial\": 2015010110,\n      \"refresh\": 43200,\n      \"retry\": 3600,\n      \"expire\": 1209600,\n      \"negCache\": 180\n    },\n    \"createdTs\": \"2020-02-04T22:42:10Z\",\n    \"modifiedTs\": \"2020-02-05T09:23:17Z\",\n    \"typeId\": 1,\n    \"domainTags\": [],\n    \"folder\": null,\n    \"hasGtdRegions\": false,\n    \"hasGeoIP\": false,\n    \"nameserverGroup\": 1,\n    \"nameservers\": [\n      \"ns11.constellix.com.\",\n      \"ns21.constellix.com.\",\n      \"ns31.constellix.com.\",\n      \"ns41.constellix.net.\",\n      \"ns51.constellix.net.\",\n      \"ns61.constellix.net.\"\n    ],\n    \"note\": \"\",\n    \"version\": 9,\n    \"status\": \"ACTIVE\",\n    \"tags\": null,\n    \"contactIds\": []\n  },\n  {\n    \"id\": 273303,\n    \"name\": \"ccc.example\",\n    \"soa\": {\n      \"primaryNameserver\": \"ns11.constellix.com.\",\n      \"email\": \"dns.constellix.com.\",\n      \"ttl\": 86400,\n      \"serial\": 2015010110,\n      \"refresh\": 43200,\n      \"retry\": 3600,\n      \"expire\": 1209600,\n      \"negCache\": 180\n    },\n    \"createdTs\": \"2020-02-04T22:42:10Z\",\n    \"modifiedTs\": \"2020-02-05T09:23:17Z\",\n    \"typeId\": 1,\n    \"domainTags\": [],\n    \"folder\": null,\n    \"hasGtdRegions\": false,\n    \"hasGeoIP\": false,\n    \"nameserverGroup\": 1,\n    \"nameservers\": [\n      \"ns11.constellix.com.\",\n      \"ns21.constellix.com.\",\n      \"ns31.constellix.com.\",\n      \"ns41.constellix.net.\",\n      \"ns51.constellix.net.\",\n      \"ns61.constellix.net.\"\n    ],\n    \"note\": \"\",\n    \"version\": 9,\n    \"status\": \"ACTIVE\",\n    \"tags\": null,\n    \"contactIds\": []\n  },\n  {\n    \"id\": 273304,\n    \"name\": \"ddd.example\",\n    \"soa\": {\n      \"primaryNameserver\": \"ns11.constellix.com.\",\n      \"email\": \"dns.constellix.com.\",\n      \"ttl\": 86400,\n      \"serial\": 2015010110,\n      \"refresh\": 43200,\n      \"retry\": 3600,\n      \"expire\": 1209600,\n      \"negCache\": 180\n    },\n    \"createdTs\": \"2020-02-04T22:42:10Z\",\n    \"modifiedTs\": \"2020-02-05T09:23:17Z\",\n    \"typeId\": 1,\n    \"domainTags\": [],\n    \"folder\": null,\n    \"hasGtdRegions\": false,\n    \"hasGeoIP\": false,\n    \"nameserverGroup\": 1,\n    \"nameservers\": [\n      \"ns11.constellix.com.\",\n      \"ns21.constellix.com.\",\n      \"ns31.constellix.com.\",\n      \"ns41.constellix.net.\",\n      \"ns51.constellix.net.\",\n      \"ns61.constellix.net.\"\n    ],\n    \"note\": \"\",\n    \"version\": 9,\n    \"status\": \"ACTIVE\",\n    \"tags\": null,\n    \"contactIds\": []\n  }\n]\n"
  },
  {
    "path": "providers/dns/constellix/internal/fixtures/domains-Search.json",
    "content": "[\n  {\n    \"id\": 273302,\n    \"name\": \"example.com\",\n    \"soa\": {\n      \"primaryNameserver\": \"ns11.constellix.com.\",\n      \"email\": \"dns.constellix.com.\",\n      \"ttl\": 86400,\n      \"serial\": 2015010110,\n      \"refresh\": 43200,\n      \"retry\": 3600,\n      \"expire\": 1209600,\n      \"negCache\": 180\n    },\n    \"createdTs\": \"2020-02-04T22:42:10Z\",\n    \"modifiedTs\": \"2020-02-05T09:23:17Z\",\n    \"typeId\": 1,\n    \"domainTags\": [],\n    \"folder\": null,\n    \"hasGtdRegions\": false,\n    \"hasGeoIP\": false,\n    \"nameserverGroup\": 1,\n    \"nameservers\": [\n      \"ns11.constellix.com.\",\n      \"ns21.constellix.com.\",\n      \"ns31.constellix.com.\",\n      \"ns41.constellix.net.\",\n      \"ns51.constellix.net.\",\n      \"ns61.constellix.net.\"\n    ],\n    \"note\": \"\",\n    \"version\": 9,\n    \"status\": \"ACTIVE\",\n    \"tags\": null,\n    \"contactIds\": []\n  }\n]\n"
  },
  {
    "path": "providers/dns/constellix/internal/fixtures/records-Create.json",
    "content": "[\n  {\n    \"id\": 3557066,\n    \"type\": \"TXT\",\n    \"recordType\": \"txt\",\n    \"name\": \"test\",\n    \"recordOption\": \"roundRobin\",\n    \"ttl\": 300,\n    \"gtdRegion\": 1,\n    \"parentId\": 273302,\n    \"parent\": \"domain\",\n    \"source\": \"Domain\",\n    \"modifiedTs\": 1580908547865,\n    \"value\": [\n      {\n        \"value\": \"\\\"test\\\"\"\n      }\n    ],\n    \"roundRobin\": [\n      {\n        \"value\": \"\\\"test\\\"\"\n      }\n    ]\n  }\n]"
  },
  {
    "path": "providers/dns/constellix/internal/fixtures/records-Get.json",
    "content": "{\n  \"id\": 3557066,\n  \"type\": \"TXT\",\n  \"recordType\": \"txt\",\n  \"name\": \"test\",\n  \"recordOption\": \"roundRobin\",\n  \"ttl\": 300,\n  \"gtdRegion\": 1,\n  \"parentId\": 273302,\n  \"parent\": \"domain\",\n  \"source\": \"Domain\",\n  \"modifiedTs\": 1580908547863,\n  \"value\": [\n    {\n      \"value\": \"\\\"test\\\"\"\n    }\n  ],\n  \"roundRobin\": [\n    {\n      \"value\": \"\\\"test\\\"\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/constellix/internal/fixtures/records-GetAll.json",
    "content": "[\n  {\n    \"id\": 3557066,\n    \"type\": \"TXT\",\n    \"recordType\": \"txt\",\n    \"name\": \"test\",\n    \"recordOption\": \"roundRobin\",\n    \"ttl\": 300,\n    \"gtdRegion\": 1,\n    \"parentId\": 273302,\n    \"parent\": \"domain\",\n    \"source\": \"Domain\",\n    \"modifiedTs\": 1580908547865,\n    \"value\": [\n      {\n        \"value\": \"\\\"test\\\"\"\n      }\n    ],\n    \"roundRobin\": [\n      {\n        \"value\": \"\\\"test\\\"\"\n      }\n    ]\n  }\n]"
  },
  {
    "path": "providers/dns/constellix/internal/fixtures/records-Search.json",
    "content": "[\n  {\n    \"id\": 3557066,\n    \"name\": \"test\",\n    \"recordType\": \"\",\n    \"type\": \"\"\n  }\n]"
  },
  {
    "path": "providers/dns/constellix/internal/txtrecords.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n)\n\n// TxtRecordService API access to Record.\ntype TxtRecordService service\n\n// Create a TXT record.\n// https://api-docs.constellix.com/?version=latest#22e24d5b-9ec0-49a7-b2b0-5ff0a28e71be\nfunc (s *TxtRecordService) Create(ctx context.Context, domainID int64, record RecordRequest) ([]Record, error) {\n\tendpoint, err := s.client.createEndpoint(defaultVersion, \"domains\", strconv.FormatInt(domainID, 10), \"records\", \"txt\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request endpoint: %w\", err)\n\t}\n\n\tbody, err := json.Marshal(record)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tvar records []Record\n\n\terr = s.client.do(req, &records)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn records, nil\n}\n\n// GetAll TXT records.\n// https://api-docs.constellix.com/?version=latest#e7103c53-2ad8-4bc8-b5b3-4c22c4b571b2\nfunc (s *TxtRecordService) GetAll(ctx context.Context, domainID int64) ([]Record, error) {\n\tendpoint, err := s.client.createEndpoint(defaultVersion, \"domains\", strconv.FormatInt(domainID, 10), \"records\", \"txt\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create endpoint: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tvar records []Record\n\n\terr = s.client.do(req, &records)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn records, nil\n}\n\n// Get a TXT record.\n// https://api-docs.constellix.com/?version=latest#e7103c53-2ad8-4bc8-b5b3-4c22c4b571b2\nfunc (s *TxtRecordService) Get(ctx context.Context, domainID, recordID int64) (*Record, error) {\n\tendpoint, err := s.client.createEndpoint(defaultVersion, \"domains\", strconv.FormatInt(domainID, 10), \"records\", \"txt\", strconv.FormatInt(recordID, 10))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request endpoint: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tvar records Record\n\n\terr = s.client.do(req, &records)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &records, nil\n}\n\n// Update a TXT record.\n// https://api-docs.constellix.com/?version=latest#d4e9ab2e-fac0-45a6-b0e4-cf62a2d2e3da\nfunc (s *TxtRecordService) Update(ctx context.Context, domainID, recordID int64, record RecordRequest) (*SuccessMessage, error) {\n\tendpoint, err := s.client.createEndpoint(defaultVersion, \"domains\", strconv.FormatInt(domainID, 10), \"records\", \"txt\", strconv.FormatInt(recordID, 10))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request endpoint: %w\", err)\n\t}\n\n\tbody, err := json.Marshal(record)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tvar msg SuccessMessage\n\n\terr = s.client.do(req, &msg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &msg, nil\n}\n\n// Delete a TXT record.\n// https://api-docs.constellix.com/?version=latest#135947f7-d6c8-481a-83c7-4d387b0bdf9e\nfunc (s *TxtRecordService) Delete(ctx context.Context, domainID, recordID int64) (*SuccessMessage, error) {\n\tendpoint, err := s.client.createEndpoint(defaultVersion, \"domains\", strconv.FormatInt(domainID, 10), \"records\", \"txt\", strconv.FormatInt(recordID, 10))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request endpoint: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tvar msg *SuccessMessage\n\n\terr = s.client.do(req, &msg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn msg, nil\n}\n\n// Search searches for a TXT record by name.\n// https://api-docs.constellix.com/?version=latest#81003e4f-bd3f-413f-a18d-6d9d18f10201\nfunc (s *TxtRecordService) Search(ctx context.Context, domainID int64, filter searchFilter, value string) ([]Record, error) {\n\tendpoint, err := s.client.createEndpoint(defaultVersion, \"domains\", strconv.FormatInt(domainID, 10), \"records\", \"txt\", \"search\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request endpoint: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tquery := req.URL.Query()\n\tquery.Set(string(filter), value)\n\treq.URL.RawQuery = query.Encode()\n\n\tvar records []Record\n\n\terr = s.client.do(req, &records)\n\tif err != nil {\n\t\tvar nf *NotFound\n\t\tif !errors.As(err, &nf) {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn records, nil\n}\n"
  },
  {
    "path": "providers/dns/constellix/internal/txtrecords_test.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestTxtRecordService_Create(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /v1/domains/12345/records/txt\", servermock.ResponseFromFixture(\"records-Create.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"name\":\"\"}`)).\n\t\tBuild(t)\n\n\trecords, err := client.TxtRecords.Create(t.Context(), 12345, RecordRequest{})\n\trequire.NoError(t, err)\n\n\trecordsJSON, err := json.Marshal(records)\n\trequire.NoError(t, err)\n\n\texpectedContent, err := os.ReadFile(\"./fixtures/records-Create.json\")\n\trequire.NoError(t, err)\n\n\tassert.JSONEq(t, string(expectedContent), string(recordsJSON))\n}\n\nfunc TestTxtRecordService_GetAll(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/domains/12345/records/txt\", servermock.ResponseFromFixture(\"records-GetAll.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.TxtRecords.GetAll(t.Context(), 12345)\n\trequire.NoError(t, err)\n\n\trecordsJSON, err := json.Marshal(records)\n\trequire.NoError(t, err)\n\n\texpectedContent, err := os.ReadFile(\"./fixtures/records-GetAll.json\")\n\trequire.NoError(t, err)\n\n\tassert.JSONEq(t, string(expectedContent), string(recordsJSON))\n}\n\nfunc TestTxtRecordService_Get(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/domains/12345/records/txt/6789\", servermock.ResponseFromFixture(\"records-Get.json\")).\n\t\tBuild(t)\n\n\trecord, err := client.TxtRecords.Get(t.Context(), 12345, 6789)\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tID:           3557066,\n\t\tType:         \"TXT\",\n\t\tRecordType:   \"txt\",\n\t\tName:         \"test\",\n\t\tTTL:          300,\n\t\tRecordOption: \"roundRobin\",\n\t\tGtdRegion:    1,\n\t\tParentID:     273302,\n\t\tParent:       \"domain\",\n\t\tSource:       \"Domain\",\n\t\tModifiedTS:   1580908547863,\n\t\tValue: []RecordValue{{\n\t\t\tValue: `\"test\"`,\n\t\t}},\n\t\tRoundRobin: []RecordValue{{\n\t\t\tValue: `\"test\"`,\n\t\t}},\n\t}\n\tassert.Equal(t, expected, record)\n}\n\nfunc TestTxtRecordService_Update(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /v1/domains/12345/records/txt/6789\",\n\t\t\tservermock.RawStringResponse(`{\"success\":\"Record  updated successfully\"}`)).\n\t\tBuild(t)\n\n\tmsg, err := client.TxtRecords.Update(t.Context(), 12345, 6789, RecordRequest{})\n\trequire.NoError(t, err)\n\n\texpected := &SuccessMessage{Success: \"Record  updated successfully\"}\n\tassert.Equal(t, expected, msg)\n}\n\nfunc TestTxtRecordService_Delete(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v1/domains/12345/records/txt/6789\",\n\t\t\tservermock.RawStringResponse(`{\"success\":\"Record  deleted successfully\"}`)).\n\t\tBuild(t)\n\n\tmsg, err := client.TxtRecords.Delete(t.Context(), 12345, 6789)\n\trequire.NoError(t, err)\n\n\texpected := &SuccessMessage{Success: \"Record  deleted successfully\"}\n\tassert.Equal(t, expected, msg)\n}\n\nfunc TestTxtRecordService_Search(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/domains/12345/records/txt/search\", servermock.ResponseFromFixture(\"records-Search.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.TxtRecords.Search(t.Context(), 12345, Exact, \"test\")\n\trequire.NoError(t, err)\n\n\trecordsJSON, err := json.Marshal(records)\n\trequire.NoError(t, err)\n\n\texpectedContent, err := os.ReadFile(\"./fixtures/records-Search.json\")\n\trequire.NoError(t, err)\n\n\tassert.JSONEq(t, string(expectedContent), string(recordsJSON))\n}\n"
  },
  {
    "path": "providers/dns/constellix/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// Search filters.\nconst (\n\tStartsWith searchFilter = \"startswith\"\n\tExact      searchFilter = \"exact\"\n\tEndsWith   searchFilter = \"endswith\"\n\tContains   searchFilter = \"contains\"\n)\n\ntype searchFilter string\n\n// NotFound Not found error.\ntype NotFound struct {\n\t*APIError\n}\n\nfunc (e *NotFound) Unwrap() error {\n\treturn e.APIError\n}\n\n// BadRequest Bad request error.\ntype BadRequest struct {\n\t*APIError\n}\n\nfunc (e *BadRequest) Unwrap() error {\n\treturn e.APIError\n}\n\n// APIError is the representation of an API error.\ntype APIError struct {\n\tStatusCode int      `json:\"statusCode\"`\n\tErrors     []string `json:\"errors\"`\n}\n\nfunc (a APIError) Error() string {\n\treturn fmt.Sprintf(\"%d: %s\", a.StatusCode, strings.Join(a.Errors, \": \"))\n}\n\n// SuccessMessage is the representation of a success message.\ntype SuccessMessage struct {\n\tSuccess string `json:\"success\"`\n}\n\n// RecordRequest is the representation of a request's record.\ntype RecordRequest struct {\n\tName       string        `json:\"name\"`\n\tTTL        int           `json:\"ttl,omitempty\"`\n\tRoundRobin []RecordValue `json:\"roundRobin,omitempty\"`\n}\n\n// RecordValue is the representation of a record's value.\ntype RecordValue struct {\n\tValue       string `json:\"value,omitempty\"`\n\tDisableFlag bool   `json:\"disableFlag,omitempty\"` // only for the response\n}\n\n// Record is the representation of a record.\ntype Record struct {\n\tID           int64         `json:\"id\"`\n\tType         string        `json:\"type\"`\n\tRecordType   string        `json:\"recordType\"`\n\tName         string        `json:\"name\"`\n\tRecordOption string        `json:\"recordOption,omitempty\"`\n\tNoAnswer     bool          `json:\"noAnswer,omitempty\"`\n\tNote         string        `json:\"note,omitempty\"`\n\tTTL          int           `json:\"ttl,omitempty\"`\n\tGtdRegion    int           `json:\"gtdRegion,omitempty\"`\n\tParentID     int           `json:\"parentId,omitempty\"`\n\tParent       string        `json:\"parent,omitempty\"`\n\tSource       string        `json:\"source,omitempty\"`\n\tModifiedTS   int64         `json:\"modifiedTs,omitempty\"`\n\tValue        []RecordValue `json:\"value,omitempty\"`\n\tRoundRobin   []RecordValue `json:\"roundRobin,omitempty\"`\n}\n\n// Domain is the representation of a domain.\ntype Domain struct {\n\tID      int64  `json:\"id\"`\n\tName    string `json:\"name,omitempty\"`\n\tTypeID  int64  `json:\"typeId,omitempty\"`\n\tVersion int64  `json:\"version,omitempty\"`\n\tStatus  string `json:\"status,omitempty\"`\n}\n\n// PaginationParameters is pagination parameters.\ntype PaginationParameters struct {\n\t// Offset retrieves a subset of records starting with the offset value.\n\tOffset int `url:\"offset\"`\n\t// Max retrieves maximum number of dataset.\n\tMax int `url:\"max\"`\n\t// Sort on the basis of given property name.\n\tSort string `url:\"sort\"`\n\t// Order Sort order. Possible values are asc / desc.\n\tOrder string `url:\"order\"`\n}\n"
  },
  {
    "path": "providers/dns/corenetworks/corenetworks.go",
    "content": "package corenetworks\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/corenetworks/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"CORENETWORKS_\"\n\n\tEnvLogin    = envNamespace + \"LOGIN\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tLogin              string\n\tPassword           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 3600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Core-Networks.\n// Credentials must be passed in the environment variables: CORENETWORKS_LOGIN, CORENETWORKS_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvLogin, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"corenetworks: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Login = values[EnvLogin]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Bluecat DNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"corenetworks: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Login == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"corenetworks: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(config.Login, config.Password)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create authentication token: %w\", err)\n\t}\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"corenetworks: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"corenetworks: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName: subDomain,\n\t\tTTL:  d.config.TTL,\n\t\tType: \"TXT\",\n\t\tData: info.Value,\n\t}\n\n\terr = d.client.AddRecord(ctx, dns01.UnFqdn(zone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"corenetworks: add record: %w\", err)\n\t}\n\n\terr = d.client.CommitRecords(ctx, dns01.UnFqdn(zone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"corenetworks: commit records: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create authentication token: %w\", err)\n\t}\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"corenetworks: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"corenetworks: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName: subDomain,\n\t\tTTL:  d.config.TTL,\n\t\tType: \"TXT\",\n\t\tData: info.Value,\n\t}\n\n\terr = d.client.DeleteRecords(ctx, dns01.UnFqdn(zone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"corenetworks: delete records: %w\", err)\n\t}\n\n\terr = d.client.CommitRecords(ctx, dns01.UnFqdn(zone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"corenetworks: commit records: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/corenetworks/corenetworks.toml",
    "content": "Name = \"Core-Networks\"\nDescription = ''''''\nURL = \"https://www.core-networks.de/\"\nCode = \"corenetworks\"\nSince = \"v4.20.0\"\n\nExample = '''\nCORENETWORKS_LOGIN=\"xxxx\" \\\nCORENETWORKS_PASSWORD=\"yyyy\" \\\nlego --dns corenetworks -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    CORENETWORKS_LOGIN = \"The username of the API account\"\n    CORENETWORKS_PASSWORD = \"The password\"\n  [Configuration.Additional]\n    CORENETWORKS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    CORENETWORKS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    CORENETWORKS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)\"\n    CORENETWORKS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n    CORENETWORKS_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n\n[Links]\n  API = \"https://beta.api.core-networks.de/doc/\"\n"
  },
  {
    "path": "providers/dns/corenetworks/corenetworks_test.go",
    "content": "package corenetworks\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvLogin, EnvPassword).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvLogin:    \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing login\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"corenetworks: some credentials information are missing: CORENETWORKS_LOGIN\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvLogin: \"user\",\n\t\t\t},\n\t\t\texpected: \"corenetworks: some credentials information are missing: CORENETWORKS_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tlogin    string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tlogin:    \"user\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing login\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"corenetworks: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tlogin:    \"user\",\n\t\t\texpected: \"corenetworks: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Login = test.login\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/corenetworks/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://beta.api.core-networks.de\"\n\n// Client a Core-Networks client.\ntype Client struct {\n\tlogin    string\n\tpassword string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(login, password string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tlogin:      login,\n\t\tpassword:   password,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// ListZone gets a list of all DNS zones.\n// https://beta.api.core-networks.de/doc/#functon_dnszones\nfunc (c *Client) ListZone(ctx context.Context) ([]Zone, error) {\n\tendpoint := c.baseURL.JoinPath(\"dnszones\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar zones []Zone\n\n\terr = c.do(req, &zones)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn zones, nil\n}\n\n// GetZoneDetails provides detailed information about a DNS zone.\n// https://beta.api.core-networks.de/doc/#functon_dnszones_details\nfunc (c *Client) GetZoneDetails(ctx context.Context, zone string) (*ZoneDetails, error) {\n\tendpoint := c.baseURL.JoinPath(\"dnszones\", zone)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar details ZoneDetails\n\n\terr = c.do(req, &details)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &details, nil\n}\n\n// ListRecords gets a list of DNS records belonging to the zone.\n// https://beta.api.core-networks.de/doc/#functon_dnszones_records\nfunc (c *Client) ListRecords(ctx context.Context, zone string) ([]Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"dnszones\", zone, \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar records []Record\n\n\terr = c.do(req, &records)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn records, nil\n}\n\n// AddRecord adds a record.\n// https://beta.api.core-networks.de/doc/#functon_dnszones_records_add\nfunc (c *Client) AddRecord(ctx context.Context, zone string, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"dnszones\", zone, \"records\", \"/\")\n\n\tif record.Name == \"\" {\n\t\trecord.Name = \"@\"\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// DeleteRecords deletes all DNS records of a zone that match the DNS record passed.\n// https://beta.api.core-networks.de/doc/#functon_dnszones_records_delete\nfunc (c *Client) DeleteRecords(ctx context.Context, zone string, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"dnszones\", zone, \"records\", \"delete\")\n\n\tif record.Name == \"\" {\n\t\trecord.Name = \"@\"\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// CommitRecords sends a commit to the zone.\n// https://beta.api.core-networks.de/doc/#functon_dnszones_commit\nfunc (c *Client) CommitRecords(ctx context.Context, zone string) error {\n\tendpoint := c.baseURL.JoinPath(\"dnszones\", zone, \"records\", \"commit\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tat := getToken(req.Context())\n\tif at != \"\" {\n\t\treq.Header.Set(authorizationHeader, \"Bearer \"+at)\n\t}\n\n\tresp, errD := c.HTTPClient.Do(req)\n\tif errD != nil {\n\t\treturn errutils.NewHTTPDoError(req, errD)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/corenetworks/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\")\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t)\n}\n\nfunc TestClient_ListZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dnszones/\",\n\t\t\tservermock.ResponseFromFixture(\"ListZone.json\")).\n\t\tBuild(t)\n\n\tctx := t.Context()\n\n\tzones, err := client.ListZone(ctx)\n\trequire.NoError(t, err)\n\n\texpected := []Zone{\n\t\t{Name: \"example.com\", Type: \"master\"},\n\t\t{Name: \"example.net\", Type: \"slave\"},\n\t}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_GetZoneDetails(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dnszones/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"GetZoneDetails.json\")).\n\t\tBuild(t)\n\n\tzone, err := client.GetZoneDetails(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := &ZoneDetails{\n\t\tActive: true,\n\t\tDNSSec: true,\n\t\tName:   \"example.com\",\n\t\tType:   \"master\",\n\t}\n\n\tassert.Equal(t, expected, zone)\n}\n\nfunc TestClient_ListRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dnszones/example.com/records/\",\n\t\t\tservermock.ResponseFromFixture(\"ListRecords.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.ListRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tName: \"@\",\n\t\t\tTTL:  86400,\n\t\t\tType: \"NS\",\n\t\t\tData: \"ns2.core-networks.eu.\",\n\t\t},\n\t\t{\n\t\t\tName: \"@\",\n\t\t\tTTL:  86400,\n\t\t\tType: \"NS\",\n\t\t\tData: \"ns3.core-networks.com.\",\n\t\t},\n\t\t{\n\t\t\tName: \"@\",\n\t\t\tTTL:  86400,\n\t\t\tType: \"NS\",\n\t\t\tData: \"ns1.core-networks.de.\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dnszones/example.com/records/\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\trecord := Record{Name: \"www\", TTL: 3600, Type: \"A\", Data: \"127.0.0.1\"}\n\n\terr := client.AddRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dnszones/example.com/records/delete\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\trecord := Record{Name: \"www\", Type: \"A\", Data: \"127.0.0.1\"}\n\n\terr := client.DeleteRecords(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_CommitRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dnszones/example.com/records/commit\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\terr := client.CommitRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/corenetworks/internal/fixtures/GetZoneDetails.json",
    "content": "{\n  \"active\": true,\n  \"dnssec\": true,\n  \"master\": null,\n  \"name\": \"example.com\",\n  \"tsig\": null,\n  \"type\": \"master\"\n}\n"
  },
  {
    "path": "providers/dns/corenetworks/internal/fixtures/ListRecords.json",
    "content": "[\n  {\n    \"name\": \"@\",\n    \"ttl\": 86400,\n    \"type\": \"NS\",\n    \"data\": \"ns2.core-networks.eu.\"\n  },\n  {\n    \"name\": \"@\",\n    \"ttl\": 86400,\n    \"type\": \"NS\",\n    \"data\": \"ns3.core-networks.com.\"\n  },\n  {\n    \"name\": \"@\",\n    \"ttl\": 86400,\n    \"type\": \"NS\",\n    \"data\": \"ns1.core-networks.de.\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/corenetworks/internal/fixtures/ListZone.json",
    "content": "[\n  {\n    \"name\": \"example.com\",\n    \"type\": \"master\"\n  },\n  {\n    \"name\": \"example.net\",\n    \"type\": \"slave\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/corenetworks/internal/fixtures/auth.json",
    "content": "{\n  \"token\": \"authsecret\",\n  \"expires\": 123\n}\n"
  },
  {
    "path": "providers/dns/corenetworks/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\nconst authorizationHeader = \"Authorization\"\n\ntype token string\n\nconst tokenKey token = \"token\"\n\n// CreateAuthenticationToken gets an authentication token.\n// https://beta.api.core-networks.de/doc/#functon_auth_token\nfunc (c *Client) CreateAuthenticationToken(ctx context.Context) (*Token, error) {\n\tendpoint := c.baseURL.JoinPath(\"auth\", \"token\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, Auth{Login: c.login, Password: c.password})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar token Token\n\n\terr = c.do(req, &token)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &token, nil\n}\n\nfunc (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) {\n\ttok, err := c.CreateAuthenticationToken(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn context.WithValue(ctx, tokenKey, tok.Token), nil\n}\n\nfunc getToken(ctx context.Context) string {\n\ttok, ok := ctx.Value(tokenKey).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn tok\n}\n"
  },
  {
    "path": "providers/dns/corenetworks/internal/identity_test.go",
    "content": "package internal\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestClient_CreateAuthenticationToken(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /auth/token\", servermock.ResponseFromFixture(\"auth.json\")).\n\t\tBuild(t)\n\n\ttoken, err := client.CreateAuthenticationToken(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := &Token{\n\t\tToken:   \"authsecret\",\n\t\tExpires: 123,\n\t}\n\tassert.Equal(t, expected, token)\n}\n"
  },
  {
    "path": "providers/dns/corenetworks/internal/types.go",
    "content": "package internal\n\ntype Auth struct {\n\tLogin    string `json:\"login,omitempty\"`\n\tPassword string `json:\"password,omitempty\"`\n}\n\ntype Token struct {\n\tToken   string `json:\"token,omitempty\"`\n\tExpires int    `json:\"expires,omitempty\"`\n}\n\ntype Zone struct {\n\tName string `json:\"name,omitempty\"`\n\tType string `json:\"type,omitempty\"`\n}\n\ntype ZoneDetails struct {\n\tActive bool     `json:\"active,omitempty\"`\n\tDNSSec bool     `json:\"dnssec,omitempty\"`\n\tMaster string   `json:\"master,omitempty\"`\n\tName   string   `json:\"name,omitempty\"`\n\tTSIG   *TSIGKey `json:\"tsig,omitempty\"`\n\tType   string   `json:\"type,omitempty\"`\n}\n\ntype TSIGKey struct {\n\tAlgo   string `json:\"algo,omitempty\"`\n\tSecret string `json:\"secret,omitempty\"`\n}\n\ntype Record struct {\n\tName string `json:\"name,omitempty\"`\n\tTTL  int    `json:\"ttl,omitempty\"`\n\tType string `json:\"type,omitempty\"`\n\tData string `json:\"data,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/cpanel/cpanel.go",
    "content": "// Package cpanel implements a DNS provider for solving the DNS-01 challenge using CPanel.\npackage cpanel\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/cpanel\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/whm\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"CPANEL_\"\n\n\tEnvMode     = envNamespace + \"MODE\"\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvToken    = envNamespace + \"TOKEN\"\n\tEnvBaseURL  = envNamespace + \"BASE_URL\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\ntype apiClient interface {\n\tFetchZoneInformation(ctx context.Context, domain string) ([]shared.ZoneRecord, error)\n\tAddRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error)\n\tEditRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error)\n\tDeleteRecord(ctx context.Context, serial uint32, domain string, lineIndex int) (*shared.ZoneSerial, error)\n}\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tMode               string\n\tUsername           string\n\tToken              string\n\tBaseURL            string\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tMode:               env.GetOrDefaultString(EnvMode, \"cpanel\"),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient apiClient\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for CPanel.\n// Credentials must be passed in the environment variables:\n// CPANEL_USERNAME, CPANEL_TOKEN, CPANEL_BASE_URL, CPANEL_NAMESERVER.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvToken, EnvBaseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cpanel: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Token = values[EnvToken]\n\tconfig.BaseURL = values[EnvBaseURL]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for CPanel.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"cpanel: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Username == \"\" || config.Token == \"\" {\n\t\treturn nil, errors.New(\"cpanel: some credentials information are missing\")\n\t}\n\n\tif config.BaseURL == \"\" {\n\t\treturn nil, errors.New(\"cpanel: server information are missing\")\n\t}\n\n\tclient, err := createClient(config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cpanel: create client error: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, _, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"arvancloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzone := dns01.UnFqdn(authZone)\n\n\tzoneInfo, err := d.client.FetchZoneInformation(ctx, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cpanel[mode=%s]: fetch zone information: %w\", d.config.Mode, err)\n\t}\n\n\tserial, err := getZoneSerial(authZone, zoneInfo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cpanel[mode=%s]: get zone serial: %w\", d.config.Mode, err)\n\t}\n\n\tvalueB64 := base64.StdEncoding.EncodeToString([]byte(info.Value))\n\n\tvar (\n\t\tfound          bool\n\t\texistingRecord shared.ZoneRecord\n\t)\n\n\tfor _, record := range zoneInfo {\n\t\tif slices.Contains(record.DataB64, valueB64) {\n\t\t\texistingRecord = record\n\t\t\tfound = true\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\trecord := shared.Record{\n\t\tDName:      info.EffectiveFQDN,\n\t\tTTL:        d.config.TTL,\n\t\tRecordType: \"TXT\",\n\t}\n\n\t// New record.\n\tif !found {\n\t\trecord.Data = []string{info.Value}\n\n\t\t_, err = d.client.AddRecord(ctx, serial, zone, record)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"cpanel[mode=%s]: add record: %w\", d.config.Mode, err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Update existing record.\n\trecord.LineIndex = existingRecord.LineIndex\n\n\tfor _, dataB64 := range existingRecord.DataB64 {\n\t\tdata, errD := base64.StdEncoding.DecodeString(dataB64)\n\t\tif errD != nil {\n\t\t\treturn fmt.Errorf(\"cpanel[mode=%s]: decode base64 record value: %w\", d.config.Mode, errD)\n\t\t}\n\n\t\trecord.Data = append(record.Data, string(data))\n\t}\n\n\trecord.Data = append(record.Data, info.Value)\n\n\t_, err = d.client.EditRecord(ctx, serial, zone, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cpanel[mode=%s]: edit record: %w\", d.config.Mode, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"arvancloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzone := dns01.UnFqdn(authZone)\n\n\tzoneInfo, err := d.client.FetchZoneInformation(ctx, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cpanel[mode=%s]: fetch zone information: %w\", d.config.Mode, err)\n\t}\n\n\tserial, err := getZoneSerial(authZone, zoneInfo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cpanel[mode=%s]: get zone serial: %w\", d.config.Mode, err)\n\t}\n\n\tvalueB64 := base64.StdEncoding.EncodeToString([]byte(info.Value))\n\n\tvar (\n\t\tfound          bool\n\t\texistingRecord shared.ZoneRecord\n\t)\n\n\tfor _, record := range zoneInfo {\n\t\tif slices.Contains(record.DataB64, valueB64) {\n\t\t\texistingRecord = record\n\t\t\tfound = true\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\treturn nil\n\t}\n\n\tvar newData []string\n\n\tfor _, dataB64 := range existingRecord.DataB64 {\n\t\tif dataB64 == valueB64 {\n\t\t\tcontinue\n\t\t}\n\n\t\tdata, errD := base64.StdEncoding.DecodeString(dataB64)\n\t\tif errD != nil {\n\t\t\treturn fmt.Errorf(\"cpanel[mode=%s]: decode base64 record value: %w\", d.config.Mode, errD)\n\t\t}\n\n\t\tnewData = append(newData, string(data))\n\t}\n\n\t// Delete record.\n\tif len(newData) == 0 {\n\t\t_, err = d.client.DeleteRecord(ctx, serial, zone, existingRecord.LineIndex)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"cpanel[mode=%s]: delete record: %w\", d.config.Mode, err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Remove one value.\n\trecord := shared.Record{\n\t\tDName:      info.EffectiveFQDN,\n\t\tTTL:        d.config.TTL,\n\t\tRecordType: \"TXT\",\n\t\tData:       newData,\n\t\tLineIndex:  existingRecord.LineIndex,\n\t}\n\n\t_, err = d.client.EditRecord(ctx, serial, zone, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cpanel[mode=%s]: edit record: %w\", d.config.Mode, err)\n\t}\n\n\treturn nil\n}\n\nfunc getZoneSerial(zoneFqdn string, zoneInfo []shared.ZoneRecord) (uint32, error) {\n\tnameB64 := base64.StdEncoding.EncodeToString([]byte(zoneFqdn))\n\n\tfor _, record := range zoneInfo {\n\t\tif record.Type != \"record\" || record.RecordType != \"SOA\" || record.DNameB64 != nameB64 {\n\t\t\tcontinue\n\t\t}\n\n\t\t// https://github.com/go-acme/lego/issues/1060#issuecomment-1925572386\n\t\t// https://github.com/go-acme/lego/issues/1060#issuecomment-1925581832\n\t\tdata, err := base64.StdEncoding.DecodeString(record.DataB64[2])\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"decode serial DNameB64: %w\", err)\n\t\t}\n\n\t\tvar newSerial uint32\n\n\t\t_, err = fmt.Sscan(string(data), &newSerial)\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"decode serial DNameB64, invalid serial value %q: %w\", string(data), err)\n\t\t}\n\n\t\treturn newSerial, nil\n\t}\n\n\treturn 0, errors.New(\"zone serial not found\")\n}\n\nfunc createClient(config *Config) (apiClient, error) {\n\tswitch strings.ToLower(config.Mode) {\n\tcase \"cpanel\":\n\t\tclient, err := cpanel.NewClient(config.BaseURL, config.Username, config.Token)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create cPanel API client: %w\", err)\n\t\t}\n\n\t\tif config.HTTPClient != nil {\n\t\t\tclient.HTTPClient = config.HTTPClient\n\t\t}\n\n\t\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\t\treturn client, nil\n\n\tcase \"whm\":\n\t\tclient, err := whm.NewClient(config.BaseURL, config.Username, config.Token)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create WHM API client: %w\", err)\n\t\t}\n\n\t\tif config.HTTPClient != nil {\n\t\t\tclient.HTTPClient = config.HTTPClient\n\t\t}\n\n\t\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\t\treturn client, nil\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported mode: %q\", config.Mode)\n\t}\n}\n"
  },
  {
    "path": "providers/dns/cpanel/cpanel.toml",
    "content": "Name = \"CPanel/WHM\"\nDescription = ''''''\nURL = \"https://cpanel.net/\"\nCode = \"cpanel\"\nSince = \"v4.16.0\"\n\nExample = '''\n### CPANEL (default)\n\nCPANEL_USERNAME=\"yyyy\" \\\nCPANEL_TOKEN=\"xxxx\" \\\nCPANEL_BASE_URL=\"https://example.com:2083\" \\\nlego --dns cpanel -d '*.example.com' -d example.com run\n\n## WHM\n\nCPANEL_MODE=whm \\\nCPANEL_USERNAME=\"yyyy\" \\\nCPANEL_TOKEN=\"xxxx\" \\\nCPANEL_BASE_URL=\"https://example.com:2087\" \\\nlego --dns cpanel -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    CPANEL_USERNAME = \"username\"\n    CPANEL_TOKEN = \"API token\"\n    CPANEL_BASE_URL = \"API server URL\"\n  [Configuration.Additional]\n    CPANEL_MODE = \"use cpanel API or WHM API (Default: cpanel)\"\n    CPANEL_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    CPANEL_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    CPANEL_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    CPANEL_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API_CPANEL = \"https://api.docs.cpanel.net/cpanel/introduction/\"\n  API_WHM = \"https://api.docs.cpanel.net/whm/introduction/\"\n"
  },
  {
    "path": "providers/dns/cpanel/cpanel_test.go",
    "content": "package cpanel\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvMode,\n\tEnvUsername,\n\tEnvToken,\n\tEnvBaseURL).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc         string\n\t\tenvVars      map[string]string\n\t\texpected     string\n\t\texpectedMode string\n\t}{\n\t\t{\n\t\t\tdesc: \"success cpanel mode (default)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvToken:    \"secret\",\n\t\t\t\tEnvBaseURL:  \"https://example.com\",\n\t\t\t},\n\t\t\texpectedMode: \"cpanel\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"success whm mode\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvMode:     \"whm\",\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvToken:    \"secret\",\n\t\t\t\tEnvBaseURL:  \"https://example.com\",\n\t\t\t},\n\t\t\texpectedMode: \"whm\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing user\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken:   \"secret\",\n\t\t\t\tEnvBaseURL: \"https://example.com\",\n\t\t\t},\n\t\t\texpected: \"cpanel: some credentials information are missing: CPANEL_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvBaseURL:  \"https://example.com\",\n\t\t\t},\n\t\t\texpected: \"cpanel: some credentials information are missing: CPANEL_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing base URL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvToken:    \"secret\",\n\t\t\t\tEnvBaseURL:  \"\",\n\t\t\t},\n\t\t\texpected: \"cpanel: some credentials information are missing: CPANEL_BASE_URL\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\tassert.Equal(t, test.expectedMode, p.config.Mode)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tmode     string\n\t\tusername string\n\t\ttoken    string\n\t\tbaseURL  string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tmode:     \"whm\",\n\t\t\tusername: \"user\",\n\t\t\ttoken:    \"secret\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing mode\",\n\t\t\tusername: \"user\",\n\t\t\ttoken:    \"secret\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\texpected: `cpanel: create client error: unsupported mode: \"\"`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"invalid mode\",\n\t\t\tmode:     \"test\",\n\t\t\tusername: \"user\",\n\t\t\ttoken:    \"secret\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\texpected: `cpanel: create client error: unsupported mode: \"test\"`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tmode:     \"whm\",\n\t\t\tusername: \"\",\n\t\t\ttoken:    \"secret\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\texpected: \"cpanel: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing token\",\n\t\t\tmode:     \"whm\",\n\t\t\tusername: \"user\",\n\t\t\ttoken:    \"\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\texpected: \"cpanel: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing base URL\",\n\t\t\tmode:     \"whm\",\n\t\t\tusername: \"user\",\n\t\t\ttoken:    \"secret\",\n\t\t\tbaseURL:  \"\",\n\t\t\texpected: \"cpanel: server information are missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Mode = test.mode\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Token = test.token\n\t\t\tconfig.BaseURL = test.baseURL\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_getZoneSerial(t *testing.T) {\n\tzones := []shared.ZoneRecord{\n\t\t{\n\t\t\tType:      \"comment\",\n\t\t\tLineIndex: 1,\n\t\t\tTextB64:   \"OyBab25lIGZpbGUgZm9yIGV4YW1wbGUuY29t\",\n\t\t},\n\t\t{\n\t\t\tType:      \"control\",\n\t\t\tLineIndex: 2,\n\t\t\tTextB64:   \"JFRUTCAxNDQwMA==\",\n\t\t},\n\t\t{\n\t\t\tDNameB64:   \"ZXhhbXBsZS5jb20u\",\n\t\t\tLineIndex:  4,\n\t\t\tRecordType: \"NS\",\n\t\t\tType:       \"record\",\n\t\t\tTTL:        86400,\n\t\t\tDataB64:    []string{\"YWxsMS5kbnNyb3VuZHJvYmluLm5ldC4=\"},\n\t\t},\n\t\t{\n\t\t\tDataB64: []string{\n\t\t\t\t\"YWxsMS5kbnNyb3VuZHJvYmluLm5ldC4=\",\n\t\t\t\t\"ZW1haWwuaXB4Y29yZS5jb20u\",\n\t\t\t\t\"MjAyNDAyMDQwOQ==\",\n\t\t\t\t\"MzYwMA==\",\n\t\t\t\t\"MTgwMA==\",\n\t\t\t\t\"MTIwOTYwMA==\",\n\t\t\t\t\"ODY0MDA=\",\n\t\t\t},\n\t\t\tRecordType: \"SOA\",\n\t\t\tType:       \"record\",\n\t\t\tTTL:        86400,\n\t\t\tLineIndex:  3,\n\t\t\tDNameB64:   \"ZXhhbXBsZS5jb20u\",\n\t\t},\n\t\t{\n\t\t\tRecordType: \"A\",\n\t\t\tType:       \"record\",\n\t\t\tTTL:        3600,\n\t\t\tDataB64:    []string{\"MTAuMTAuMTAuMTA=\"},\n\t\t\tLineIndex:  9,\n\t\t\tDNameB64:   \"ZXhhbXBsZS5jb20u\",\n\t\t},\n\t}\n\n\tserial, err := getZoneSerial(\"example.com.\", zones)\n\trequire.NoError(t, err)\n\n\tassert.EqualValues(t, 2024020409, serial)\n}\n\nfunc Test_getZoneSerial_error(t *testing.T) {\n\tzones := []shared.ZoneRecord{\n\t\t{\n\t\t\tType:      \"comment\",\n\t\t\tLineIndex: 1,\n\t\t\tTextB64:   \"OyBab25lIGZpbGUgZm9yIGV4YW1wbGUuY29t\",\n\t\t},\n\t\t{\n\t\t\tType:      \"control\",\n\t\t\tLineIndex: 2,\n\t\t\tTextB64:   \"JFRUTCAxNDQwMA==\",\n\t\t},\n\t\t{\n\t\t\tDNameB64:   \"ZXhhbXBsZS5jb20u\",\n\t\t\tLineIndex:  4,\n\t\t\tRecordType: \"NS\",\n\t\t\tType:       \"record\",\n\t\t\tTTL:        86400,\n\t\t\tDataB64:    []string{\"YWxsMS5kbnNyb3VuZHJvYmluLm5ldC4=\"},\n\t\t},\n\t\t{\n\t\t\tDataB64: []string{\n\t\t\t\t\"YWxsMS5kbnNyb3VuZHJvYmluLm5ldC4=\",\n\t\t\t\t\"ZW1haWwuaXB4Y29yZS5jb20u\",\n\t\t\t\t\"MjAyNDAyMDQwOQ==\",\n\t\t\t\t\"MzYwMA==\",\n\t\t\t\t\"MTgwMA==\",\n\t\t\t\t\"MTIwOTYwMA==\",\n\t\t\t\t\"ODY0MDA=\",\n\t\t\t},\n\t\t\tRecordType: \"SOA\",\n\t\t\tType:       \"record\",\n\t\t\tTTL:        86400,\n\t\t\tLineIndex:  3,\n\t\t\tDNameB64:   \"ZXhhbXBsZS5vcmcu\",\n\t\t},\n\t\t{\n\t\t\tRecordType: \"A\",\n\t\t\tType:       \"record\",\n\t\t\tTTL:        3600,\n\t\t\tDataB64:    []string{\"MTAuMTAuMTAuMTA=\"},\n\t\t\tLineIndex:  9,\n\t\t\tDNameB64:   \"ZXhhbXBsZS5jb20u\",\n\t\t},\n\t}\n\n\tserial, err := getZoneSerial(\"example.com.\", zones)\n\trequire.Error(t, err)\n\n\tassert.EqualValues(t, 0, serial)\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/cpanel/client.go",
    "content": "package cpanel\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst statusFailed = 0\n\ntype Client struct {\n\tusername string\n\ttoken    string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(baseURL, username, token string) (*Client, error) {\n\tapiEndpoint, err := url.Parse(baseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\tusername:   username,\n\t\ttoken:      token,\n\t\tbaseURL:    apiEndpoint.JoinPath(\"execute\"),\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// FetchZoneInformation fetches zone information.\n// https://api.docs.cpanel.net/openapi/cpanel/operation/dns-parse_zone/\nfunc (c *Client) FetchZoneInformation(ctx context.Context, domain string) ([]shared.ZoneRecord, error) {\n\tendpoint := c.baseURL.JoinPath(\"DNS\", \"parse_zone\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"zone\", domain)\n\tendpoint.RawQuery = query.Encode()\n\n\tvar result APIResponse[[]shared.ZoneRecord]\n\n\terr := c.doRequest(ctx, endpoint, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif result.Status == statusFailed {\n\t\treturn nil, toError(result)\n\t}\n\n\treturn result.Data, nil\n}\n\n// AddRecord adds a new record.\n//\n//\tadd='{\"dname\":\"example\", \"ttl\":14400, \"record_type\":\"TXT\", \"data\":[\"string1\", \"string2\"]}'\nfunc (c *Client) AddRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) {\n\tdata, err := json.Marshal(record)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request JSON data: %w\", err)\n\t}\n\n\treturn c.updateZone(ctx, serial, domain, \"add\", string(data))\n}\n\n// EditRecord edits an existing record.\n//\n//\tedit='{\"line_index\": 9, \"dname\":\"example\", \"ttl\":14400, \"record_type\":\"TXT\", \"data\":[\"string1\", \"string2\"]}'\nfunc (c *Client) EditRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) {\n\tdata, err := json.Marshal(record)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request JSON data: %w\", err)\n\t}\n\n\treturn c.updateZone(ctx, serial, domain, \"edit\", string(data))\n}\n\n// DeleteRecord deletes an existing record.\n//\n//\tremove=22\nfunc (c *Client) DeleteRecord(ctx context.Context, serial uint32, domain string, lineIndex int) (*shared.ZoneSerial, error) {\n\treturn c.updateZone(ctx, serial, domain, \"remove\", strconv.Itoa(lineIndex))\n}\n\n// https://api.docs.cpanel.net/openapi/cpanel/operation/dns-mass_edit_zone/\nfunc (c *Client) updateZone(ctx context.Context, serial uint32, domain, action, data string) (*shared.ZoneSerial, error) {\n\tendpoint := c.baseURL.JoinPath(\"DNS\", \"mass_edit_zone\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"serial\", strconv.FormatUint(uint64(serial), 10))\n\tquery.Set(action, data)\n\tquery.Set(\"zone\", domain)\n\tendpoint.RawQuery = query.Encode()\n\n\tvar result APIResponse[shared.ZoneSerial]\n\n\terr := c.doRequest(ctx, endpoint, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif result.Status == statusFailed {\n\t\treturn nil, toError(result)\n\t}\n\n\treturn &result.Data, nil\n}\n\nfunc (c *Client) doRequest(ctx context.Context, endpoint *url.URL, result any) error {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\t// https://api.docs.cpanel.net/cpanel/tokens/#using-an-api-token\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"cpanel %s:%s\", c.username, c.token))\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/cpanel/client_test.go",
    "content": "package cpanel\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(server.URL, \"user\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"cpanel user:secret\"))\n}\n\nfunc TestClient_FetchZoneInformation(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /execute/DNS/parse_zone\",\n\t\t\tservermock.ResponseFromFixture(\"zone-info.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"zone\", \"example.com\")).\n\t\tBuild(t)\n\n\tzoneInfo, err := client.FetchZoneInformation(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []shared.ZoneRecord{{\n\t\tLineIndex:  22,\n\t\tType:       \"record\",\n\t\tDataB64:    []string{\"dGV4YXMuY29tLg==\"},\n\t\tDNameB64:   \"dGV4YXMuY29tLg==\",\n\t\tRecordType: \"MX\",\n\t\tTTL:        14400,\n\t}}\n\n\tassert.Equal(t, expected, zoneInfo)\n}\n\nfunc TestClient_FetchZoneInformation_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /execute/DNS/parse_zone\",\n\t\t\tservermock.ResponseFromFixture(\"zone-info_error.json\")).\n\t\tBuild(t)\n\n\tzoneInfo, err := client.FetchZoneInformation(t.Context(), \"example.com\")\n\trequire.EqualError(t, err, \"error(0): You do not control a DNS zone named example.com.: a, b, c\")\n\n\tassert.Nil(t, zoneInfo)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /execute/DNS/mass_edit_zone\",\n\t\t\tservermock.ResponseFromFixture(\"update-zone.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"zone\", \"example.com\").\n\t\t\t\tWith(\"add\", `{\"dname\":\"example\",\"ttl\":14400,\"record_type\":\"TXT\",\"data\":[\"string1\",\"string2\"]}`).\n\t\t\t\tWith(\"serial\", \"123456\").\n\t\t\t\tWith(\"zone\", \"example.com\")).\n\t\tBuild(t)\n\n\trecord := shared.Record{\n\t\tDName:      \"example\",\n\t\tTTL:        14400,\n\t\tRecordType: \"TXT\",\n\t\tData:       []string{\"string1\", \"string2\"},\n\t}\n\n\tzoneSerial, err := client.AddRecord(t.Context(), 123456, \"example.com\", record)\n\trequire.NoError(t, err)\n\n\texpected := &shared.ZoneSerial{NewSerial: \"2021031903\"}\n\n\tassert.Equal(t, expected, zoneSerial)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /execute/DNS/mass_edit_zone\",\n\t\t\tservermock.ResponseFromFixture(\"update-zone_error.json\")).\n\t\tBuild(t)\n\n\trecord := shared.Record{\n\t\tDName:      \"example\",\n\t\tTTL:        14400,\n\t\tRecordType: \"TXT\",\n\t\tData:       []string{\"string1\", \"string2\"},\n\t}\n\n\tzoneSerial, err := client.AddRecord(t.Context(), 123456, \"example.com\", record)\n\trequire.Error(t, err)\n\n\tassert.Nil(t, zoneSerial)\n}\n\nfunc TestClient_EditRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /execute/DNS/mass_edit_zone\",\n\t\t\tservermock.ResponseFromFixture(\"update-zone.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"edit\", `{\"dname\":\"example\",\"ttl\":14400,\"record_type\":\"TXT\",\"data\":[\"string1\",\"string2\"],\"line_index\":9}`).\n\t\t\t\tWith(\"serial\", \"123456\").\n\t\t\t\tWith(\"zone\", \"example.com\")).\n\t\tBuild(t)\n\n\trecord := shared.Record{\n\t\tLineIndex:  9,\n\t\tDName:      \"example\",\n\t\tTTL:        14400,\n\t\tRecordType: \"TXT\",\n\t\tData:       []string{\"string1\", \"string2\"},\n\t}\n\n\tzoneSerial, err := client.EditRecord(t.Context(), 123456, \"example.com\", record)\n\trequire.NoError(t, err)\n\n\texpected := &shared.ZoneSerial{NewSerial: \"2021031903\"}\n\n\tassert.Equal(t, expected, zoneSerial)\n}\n\nfunc TestClient_EditRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /execute/DNS/mass_edit_zone\",\n\t\t\tservermock.ResponseFromFixture(\"update-zone_error.json\")).\n\t\tBuild(t)\n\n\trecord := shared.Record{\n\t\tLineIndex:  9,\n\t\tDName:      \"example\",\n\t\tTTL:        14400,\n\t\tRecordType: \"TXT\",\n\t\tData:       []string{\"string1\", \"string2\"},\n\t}\n\n\tzoneSerial, err := client.EditRecord(t.Context(), 123456, \"example.com\", record)\n\trequire.Error(t, err)\n\n\tassert.Nil(t, zoneSerial)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /execute/DNS/mass_edit_zone\",\n\t\t\tservermock.ResponseFromFixture(\"update-zone.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"remove\", \"0\").\n\t\t\t\tWith(\"serial\", \"123456\").\n\t\t\t\tWith(\"zone\", \"example.com\")).\n\t\tBuild(t)\n\n\tzoneSerial, err := client.DeleteRecord(t.Context(), 123456, \"example.com\", 0)\n\trequire.NoError(t, err)\n\n\texpected := &shared.ZoneSerial{NewSerial: \"2021031903\"}\n\n\tassert.Equal(t, expected, zoneSerial)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /execute/DNS/mass_edit_zone\",\n\t\t\tservermock.ResponseFromFixture(\"update-zone_error.json\")).\n\t\tBuild(t)\n\n\tzoneSerial, err := client.DeleteRecord(t.Context(), 123456, \"example.com\", 0)\n\trequire.Error(t, err)\n\n\tassert.Nil(t, zoneSerial)\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/cpanel/fixtures/update-zone.json",
    "content": "{\n  \"metadata\": {\n    \"transformed\": 1\n  },\n  \"messages\": null,\n  \"status\": 1,\n  \"warnings\": null,\n  \"errors\": null,\n  \"data\": {\n    \"new_serial\": \"2021031903\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/cpanel/fixtures/update-zone_error.json",
    "content": "{\n  \"warnings\": null,\n  \"messages\": [\n    \"a\",\n    \"b\",\n    \"c\"\n  ],\n  \"data\": null,\n  \"errors\": [\n    \"You do not control a DNS zone named example.com.\"\n  ],\n  \"metadata\": {},\n  \"status\": 0\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/cpanel/fixtures/zone-info.json",
    "content": "{\n  \"metadata\": {\n    \"transformed\": 1\n  },\n  \"messages\": null,\n  \"status\": 1,\n  \"warnings\": null,\n  \"errors\": null,\n  \"data\": [\n    {\n      \"line_index\": 22,\n      \"dname_b64\": \"dGV4YXMuY29tLg==\",\n      \"data_b64\": [\n        \"dGV4YXMuY29tLg==\"\n      ],\n      \"type\": \"record\",\n      \"ttl\": 14400,\n      \"record_type\": \"MX\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/cpanel/fixtures/zone-info_error.json",
    "content": "{\n  \"warnings\": null,\n  \"messages\": [\n    \"a\",\n    \"b\",\n    \"c\"\n  ],\n  \"data\": null,\n  \"errors\": [\n    \"You do not control a DNS zone named example.com.\"\n  ],\n  \"metadata\": {},\n  \"status\": 0\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/cpanel/types.go",
    "content": "package cpanel\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype APIResponse[T any] struct {\n\tMetadata Metadata `json:\"metadata\"`\n\tData     T        `json:\"data,omitempty\"`\n\n\tStatus   int      `json:\"status,omitempty\"`\n\tMessages []string `json:\"messages,omitempty\"`\n\tWarnings []string `json:\"warnings,omitempty\"`\n\tErrors   []string `json:\"errors,omitempty\"`\n}\n\ntype Metadata struct {\n\tTransformed int `json:\"transformed,omitempty\"`\n}\n\nfunc toError[T any](r APIResponse[T]) error {\n\treturn fmt.Errorf(\"error(%d): %s: %s\", r.Status, strings.Join(r.Errors, \", \"), strings.Join(r.Messages, \", \"))\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/shared/types.go",
    "content": "package shared\n\ntype Record struct {\n\tDName      string   `json:\"dname,omitempty\"`\n\tTTL        int      `json:\"ttl,omitempty\"`\n\tRecordType string   `json:\"record_type,omitempty\"`\n\tData       []string `json:\"data,omitempty\"`\n\tLineIndex  int      `json:\"line_index,omitempty\"`\n}\n\ntype ZoneRecord struct {\n\tLineIndex  int      `json:\"line_index,omitempty\"`\n\tType       string   `json:\"type,omitempty\"`\n\tDataB64    []string `json:\"data_b64,omitempty\"`\n\tDNameB64   string   `json:\"dname_b64,omitempty\"`\n\tTextB64    string   `json:\"text_b64,omitempty\"`\n\tRecordType string   `json:\"record_type,omitempty\"`\n\tTTL        int      `json:\"ttl,omitempty\"`\n}\n\ntype ZoneSerial struct {\n\tNewSerial string `json:\"new_serial,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/whm/client.go",
    "content": "package whm\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst statusFailed = 0\n\ntype Client struct {\n\tusername string\n\ttoken    string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(baseURL, username, token string) (*Client, error) {\n\tapiEndpoint, err := url.Parse(baseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\tusername:   username,\n\t\ttoken:      token,\n\t\tbaseURL:    apiEndpoint.JoinPath(\"json-api\"),\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// FetchZoneInformation fetches zone information.\n// https://api.docs.cpanel.net/openapi/whm/operation/parse_dns_zone/\nfunc (c *Client) FetchZoneInformation(ctx context.Context, domain string) ([]shared.ZoneRecord, error) {\n\tendpoint := c.baseURL.JoinPath(\"parse_dns_zone\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"zone\", domain)\n\tendpoint.RawQuery = query.Encode()\n\n\tvar result APIResponse[ZoneData]\n\n\terr := c.doRequest(ctx, endpoint, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif result.Metadata.Result == statusFailed {\n\t\treturn nil, toError(result.Metadata)\n\t}\n\n\treturn result.Data.Payload, nil\n}\n\n// AddRecord adds a new record.\n//\n//\tadd='{\"dname\":\"example\", \"ttl\":14400, \"record_type\":\"TXT\", \"data\":[\"string1\", \"string2\"]}'\nfunc (c *Client) AddRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) {\n\tdata, err := json.Marshal(record)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request JSON data: %w\", err)\n\t}\n\n\treturn c.updateZone(ctx, serial, domain, \"add\", string(data))\n}\n\n// EditRecord edits an existing record.\n//\n//\tedit='{\"line_index\": 9, \"dname\":\"example\", \"ttl\":14400, \"record_type\":\"TXT\", \"data\":[\"string1\", \"string2\"]}'\nfunc (c *Client) EditRecord(ctx context.Context, serial uint32, domain string, record shared.Record) (*shared.ZoneSerial, error) {\n\tdata, err := json.Marshal(record)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request JSON data: %w\", err)\n\t}\n\n\treturn c.updateZone(ctx, serial, domain, \"edit\", string(data))\n}\n\n// DeleteRecord deletes an existing record.\n//\n//\tremove=22\nfunc (c *Client) DeleteRecord(ctx context.Context, serial uint32, domain string, lineIndex int) (*shared.ZoneSerial, error) {\n\treturn c.updateZone(ctx, serial, domain, \"remove\", strconv.Itoa(lineIndex))\n}\n\n// https://api.docs.cpanel.net/openapi/whm/operation/mass_edit_dns_zone/\nfunc (c *Client) updateZone(ctx context.Context, serial uint32, domain, action, data string) (*shared.ZoneSerial, error) {\n\tendpoint := c.baseURL.JoinPath(\"mass_edit_dns_zone\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"serial\", strconv.FormatUint(uint64(serial), 10))\n\tquery.Set(action, data)\n\tquery.Set(\"zone\", domain)\n\tendpoint.RawQuery = query.Encode()\n\n\tvar result APIResponse[shared.ZoneSerial]\n\n\terr := c.doRequest(ctx, endpoint, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif result.Metadata.Result == statusFailed {\n\t\treturn nil, toError(result.Metadata)\n\t}\n\n\treturn &result.Data, nil\n}\n\nfunc (c *Client) doRequest(ctx context.Context, endpoint *url.URL, result any) error {\n\tquery := endpoint.Query()\n\tquery.Set(\"api.version\", \"1\")\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\t// https://api.docs.cpanel.net/whm/tokens/\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"whm %s:%s\", c.username, c.token))\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/whm/client_test.go",
    "content": "package whm\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(server.URL, \"user\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"whm user:secret\"))\n}\n\nfunc TestClient_FetchZoneInformation(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /json-api/parse_dns_zone\",\n\t\t\tservermock.ResponseFromFixture(\"zone-info.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"api.version\", \"1\").\n\t\t\t\tWith(\"zone\", \"example.com\")).\n\t\tBuild(t)\n\n\tzoneInfo, err := client.FetchZoneInformation(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []shared.ZoneRecord{{\n\t\tLineIndex:  22,\n\t\tType:       \"record\",\n\t\tDataB64:    []string{\"dGV4YXMuY29tLg==\"},\n\t\tDNameB64:   \"dGV4YXMuY29tLg==\",\n\t\tRecordType: \"MX\",\n\t\tTTL:        14400,\n\t}}\n\n\tassert.Equal(t, expected, zoneInfo)\n}\n\nfunc TestClient_FetchZoneInformation_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /json-api/parse_dns_zone\",\n\t\t\tservermock.ResponseFromFixture(\"zone-info_error.json\")).\n\t\tBuild(t)\n\n\tzoneInfo, err := client.FetchZoneInformation(t.Context(), \"example.com\")\n\trequire.Error(t, err)\n\n\tassert.Nil(t, zoneInfo)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /json-api/mass_edit_dns_zone\",\n\t\t\tservermock.ResponseFromFixture(\"update-zone.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"add\", `{\"dname\":\"example\",\"ttl\":14400,\"record_type\":\"TXT\",\"data\":[\"string1\",\"string2\"]}`).\n\t\t\t\tWith(\"api.version\", \"1\").\n\t\t\t\tWith(\"serial\", \"123456\").\n\t\t\t\tWith(\"zone\", \"example.com\")).\n\t\tBuild(t)\n\n\trecord := shared.Record{\n\t\tDName:      \"example\",\n\t\tTTL:        14400,\n\t\tRecordType: \"TXT\",\n\t\tData:       []string{\"string1\", \"string2\"},\n\t}\n\n\tzoneSerial, err := client.AddRecord(t.Context(), 123456, \"example.com\", record)\n\trequire.NoError(t, err)\n\n\texpected := &shared.ZoneSerial{NewSerial: \"2021031903\"}\n\n\tassert.Equal(t, expected, zoneSerial)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /json-api/mass_edit_dns_zone\",\n\t\t\tservermock.ResponseFromFixture(\"update-zone_error.json\")).\n\t\tBuild(t)\n\n\trecord := shared.Record{\n\t\tDName:      \"example\",\n\t\tTTL:        14400,\n\t\tRecordType: \"TXT\",\n\t\tData:       []string{\"string1\", \"string2\"},\n\t}\n\n\tzoneSerial, err := client.AddRecord(t.Context(), 123456, \"example.com\", record)\n\trequire.Error(t, err)\n\n\tassert.Nil(t, zoneSerial)\n}\n\nfunc TestClient_EditRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /json-api/mass_edit_dns_zone\",\n\t\t\tservermock.ResponseFromFixture(\"update-zone.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"edit\", `{\"dname\":\"example\",\"ttl\":14400,\"record_type\":\"TXT\",\"data\":[\"string1\",\"string2\"],\"line_index\":9}`).\n\t\t\t\tWith(\"api.version\", \"1\").\n\t\t\t\tWith(\"serial\", \"123456\").\n\t\t\t\tWith(\"zone\", \"example.com\")).\n\t\tBuild(t)\n\n\trecord := shared.Record{\n\t\tLineIndex:  9,\n\t\tDName:      \"example\",\n\t\tTTL:        14400,\n\t\tRecordType: \"TXT\",\n\t\tData:       []string{\"string1\", \"string2\"},\n\t}\n\n\tzoneSerial, err := client.EditRecord(t.Context(), 123456, \"example.com\", record)\n\trequire.NoError(t, err)\n\n\texpected := &shared.ZoneSerial{NewSerial: \"2021031903\"}\n\n\tassert.Equal(t, expected, zoneSerial)\n}\n\nfunc TestClient_EditRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /json-api/mass_edit_dns_zone\",\n\t\t\tservermock.ResponseFromFixture(\"update-zone_error.json\")).\n\t\tBuild(t)\n\n\trecord := shared.Record{\n\t\tLineIndex:  9,\n\t\tDName:      \"example\",\n\t\tTTL:        14400,\n\t\tRecordType: \"TXT\",\n\t\tData:       []string{\"string1\", \"string2\"},\n\t}\n\n\tzoneSerial, err := client.EditRecord(t.Context(), 123456, \"example.com\", record)\n\trequire.Error(t, err)\n\n\tassert.Nil(t, zoneSerial)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /json-api/mass_edit_dns_zone\",\n\t\t\tservermock.ResponseFromFixture(\"update-zone.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"remove\", \"0\").\n\t\t\t\tWith(\"api.version\", \"1\").\n\t\t\t\tWith(\"serial\", \"123456\").\n\t\t\t\tWith(\"zone\", \"example.com\")).\n\t\tBuild(t)\n\n\tzoneSerial, err := client.DeleteRecord(t.Context(), 123456, \"example.com\", 0)\n\trequire.NoError(t, err)\n\n\texpected := &shared.ZoneSerial{NewSerial: \"2021031903\"}\n\n\tassert.Equal(t, expected, zoneSerial)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /json-api/mass_edit_dns_zone\",\n\t\t\tservermock.ResponseFromFixture(\"update-zone_error.json\")).\n\t\tBuild(t)\n\n\tzoneSerial, err := client.DeleteRecord(t.Context(), 123456, \"example.com\", 0)\n\trequire.Error(t, err)\n\n\tassert.Nil(t, zoneSerial)\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/whm/fixtures/update-zone.json",
    "content": "{\n  \"data\": {\n    \"new_serial\": \"2021031903\"\n  },\n  \"metadata\": {\n    \"command\": \"mass_edit_dns_zone\",\n    \"reason\": \"OK\",\n    \"result\": 1,\n    \"version\": 1\n  }\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/whm/fixtures/update-zone_error.json",
    "content": "{\n  \"data\": null,\n  \"metadata\": {\n    \"command\": \"mass_edit_dns_zone\",\n    \"reason\": \"There is a problem\",\n    \"result\": 0,\n    \"version\": 1\n  }\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/whm/fixtures/zone-info.json",
    "content": "{\n  \"data\": {\n    \"payload\": [\n      {\n        \"line_index\": 22,\n        \"type\": \"record\",\n        \"data_b64\": [\n          \"dGV4YXMuY29tLg==\"\n        ],\n        \"dname_b64\": \"dGV4YXMuY29tLg==\",\n        \"record_type\": \"MX\",\n        \"ttl\": 14400\n      }\n    ]\n  },\n  \"metadata\": {\n    \"command\": \"parse_dns_zone\",\n    \"reason\": \"OK\",\n    \"result\": 1,\n    \"version\": 1\n  }\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/whm/fixtures/zone-info_error.json",
    "content": "{\n  \"data\": null,\n  \"metadata\": {\n    \"command\": \"parse_dns_zone\",\n    \"reason\": \"There is a problem\",\n    \"result\": 0,\n    \"version\": 1\n  }\n}\n"
  },
  {
    "path": "providers/dns/cpanel/internal/whm/types.go",
    "content": "package whm\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared\"\n)\n\ntype APIResponse[T any] struct {\n\tMetadata Metadata `json:\"metadata\"`\n\tData     T        `json:\"data,omitempty\"`\n}\n\ntype Metadata struct {\n\tCommand string `json:\"command,omitempty\"`\n\tReason  string `json:\"reason,omitempty\"`\n\tResult  int    `json:\"result,omitempty\"`\n\tVersion int    `json:\"version,omitempty\"`\n}\n\ntype ZoneData struct {\n\tPayload []shared.ZoneRecord `json:\"payload,omitempty\"`\n}\n\nfunc toError(m Metadata) error {\n\treturn fmt.Errorf(\"%s error(%d): %s\", m.Command, m.Result, m.Reason)\n}\n"
  },
  {
    "path": "providers/dns/czechia/czechia.go",
    "content": "// Package czechia implements a DNS provider for solving the DNS-01 challenge using Czechia.\npackage czechia\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/czechia/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"CZECHIA_\"\n\n\tEnvToken = envNamespace + \"TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Czechia.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"czechia: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Czechia.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"czechia: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.Token)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"czechia: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"czechia: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"czechia: %w\", err)\n\t}\n\n\trecord := internal.TXTRecord{\n\t\tHostname:    subDomain,\n\t\tText:        info.Value,\n\t\tTTL:         d.config.TTL,\n\t\tPublishZone: 1,\n\t}\n\n\terr = d.client.AddTXTRecord(ctx, dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"czechia: add TXT record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"czechia: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"czechia: %w\", err)\n\t}\n\n\trecord := internal.TXTRecord{\n\t\tHostname:    subDomain,\n\t\tText:        info.Value,\n\t\tTTL:         d.config.TTL,\n\t\tPublishZone: 1,\n\t}\n\n\terr = d.client.DeleteTXTRecord(ctx, dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"czechia: delete TXT record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/czechia/czechia.toml",
    "content": "Name = \"Czechia\"\nDescription = ''''''\nURL = \"https://www.czechia.com/\"\nCode = \"czechia\"\nSince = \"v4.33.0\"\n\nExample = '''\nCZECHIA_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns czechia -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    CZECHIA_TOKEN = \"Authorization token\"\n  [Configuration.Additional]\n    CZECHIA_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    CZECHIA_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    CZECHIA_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    CZECHIA_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api.czechia.com/swagger/index.html\"\n"
  },
  {
    "path": "providers/dns/czechia/czechia_test.go",
    "content": "package czechia\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"czechia: some credentials information are missing: CZECHIA_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"czechia: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(\"AuthorizationToken\", \"secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /DNS/example.com/TXT\",\n\t\t\tservermock.Noop(),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"add_txt_record-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"DELETE /DNS/example.com/TXT\",\n\t\t\tservermock.Noop(),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"add_txt_record-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/czechia/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\nconst defaultBaseURL = \"https://api.czechia.com/api\"\n\nconst authorizationTokenHeader = \"AuthorizationToken\"\n\n// Client the Czechia API client.\ntype Client struct {\n\ttoken string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(token string) (*Client, error) {\n\tif token == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\ttoken:      token,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) AddTXTRecord(ctx context.Context, domain string, record TXTRecord) error {\n\tendpoint := c.BaseURL.JoinPath(\"DNS\", domain, \"TXT\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) DeleteTXTRecord(ctx context.Context, domain string, record TXTRecord) error {\n\tendpoint := c.BaseURL.JoinPath(\"DNS\", domain, \"TXT\")\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\treq.Header.Set(authorizationTokenHeader, c.token)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\traw, _ := io.ReadAll(resp.Body)\n\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/czechia/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(authorizationTokenHeader, \"secret\"),\n\t)\n}\n\nfunc TestClient_AddTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /DNS/example.com/TXT\",\n\t\t\tservermock.Noop(),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"add_txt_record-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := TXTRecord{\n\t\tHostname:    \"_acme-challenge\",\n\t\tText:        \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:         120,\n\t\tPublishZone: 1,\n\t}\n\n\terr := client.AddTXTRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /DNS/example.com/TXT\",\n\t\t\tservermock.Noop(),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"add_txt_record-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := TXTRecord{\n\t\tHostname:    \"_acme-challenge\",\n\t\tText:        \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:         120,\n\t\tPublishZone: 1,\n\t}\n\n\terr := client.DeleteTXTRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/czechia/internal/fixtures/add_txt_record-request.json",
    "content": "{\n  \"hostName\": \"_acme-challenge\",\n  \"text\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n  \"ttl\": 120,\n  \"publishZone\": 1\n}\n"
  },
  {
    "path": "providers/dns/czechia/internal/fixtures/delete_txt_record-request.json",
    "content": "{\n  \"hostName\": \"_acme-challenge\",\n  \"text\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n  \"ttl\": 120,\n  \"publishZone\": 1\n}\n"
  },
  {
    "path": "providers/dns/czechia/internal/types.go",
    "content": "package internal\n\ntype TXTRecord struct {\n\tHostname    string `json:\"hostName,omitempty\"`\n\tText        string `json:\"text,omitempty\"`\n\tTTL         int    `json:\"ttl,omitempty\"`\n\tPublishZone int    `json:\"publishZone,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/ddnss/ddnss.go",
    "content": "// Package ddnss implements a DNS provider for solving the DNS-01 challenge using DynDNS Service.\npackage ddnss\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ddnss/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DDNSS_\"\n\n\tEnvKey = envNamespace + \"KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for DynDNS Service.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ddnss: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Key = values[EnvKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for DynDNS Service.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"ddnss: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(&internal.Authentication{Key: config.Key})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ddnss: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN), info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ddnss: add TXT record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.client.RemoveTXTRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ddnss: remove TXT record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n"
  },
  {
    "path": "providers/dns/ddnss/ddnss.toml",
    "content": "Name = \"DDnss (DynDNS Service)\"\nDescription = ''''''\nURL = \"https://ddnss.de/\"\nCode = \"ddnss\"\nSince = \"v4.32.0\"\n\nExample = '''\nDDNSS_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns ddnss -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DDNSS_KEY = \"Update key\"\n  [Configuration.Additional]\n    DDNSS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    DDNSS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    DDNSS_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n    DDNSS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    DDNSS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://ddnss.de/info.php\"\n"
  },
  {
    "path": "providers/dns/ddnss/ddnss_test.go",
    "content": "package ddnss\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvKey: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"ddnss: some credentials information are missing: DDNSS_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tKey      string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tKey:  \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"ddnss: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Key = test.Key\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Key = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL = server.URL\n\n\t\t\treturn p, nil\n\t\t},\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /\",\n\t\t\tservermock.ResponseFromInternal(\"success.html\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"host\", \"_acme-challenge.example.com\").\n\t\t\t\tWith(\"key\", \"secret\").\n\t\t\t\tWith(\"txt\", \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\").\n\t\t\t\tWith(\"txtm\", \"1\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /\",\n\t\t\tservermock.ResponseFromInternal(\"success.html\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"host\", \"_acme-challenge.example.com\").\n\t\t\t\tWith(\"key\", \"secret\").\n\t\t\t\tWith(\"txtm\", \"2\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/ddnss/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\t\"golang.org/x/net/html\"\n)\n\nconst defaultBaseURL = \"https://ddnss.de/upd.php\"\n\n// Client the DDns API client.\ntype Client struct {\n\tauth *Authentication\n\n\tBaseURL    string\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(auth *Authentication) (*Client, error) {\n\tif auth == nil {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\terr := auth.validate()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\tauth:       auth,\n\t\tBaseURL:    defaultBaseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) AddTXTRecord(ctx context.Context, host, value string) error {\n\treturn c.update(ctx, map[string]string{\n\t\t\"host\": host,\n\t\t\"txt\":  value,\n\t\t\"txtm\": \"1\",\n\t})\n}\n\nfunc (c *Client) RemoveTXTRecord(ctx context.Context, host string) error {\n\treturn c.update(ctx, map[string]string{\n\t\t\"host\": host,\n\t\t\"txtm\": \"2\",\n\t})\n}\n\nfunc (c *Client) update(ctx context.Context, params map[string]string) error {\n\tendpoint, err := url.Parse(c.BaseURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tquery := endpoint.Query()\n\n\tfor k, v := range params {\n\t\tquery.Set(k, v)\n\t}\n\n\tc.auth.set(query)\n\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tuseragent.SetHeader(req.Header)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\traw, _ := io.ReadAll(resp.Body)\n\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tcontent, err := readPage(raw)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif strings.Contains(content, \"Updated 1 hostname.\") {\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"unexpected response: %s\", content)\n}\n\nfunc readPage(raw []byte) (string, error) {\n\tpage, err := html.Parse(strings.NewReader(string(raw)))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar b strings.Builder\n\textractText(page, &b)\n\n\treturn strings.TrimSpace(b.String()), nil\n}\n\nfunc extractText(n *html.Node, b *strings.Builder) {\n\tif n.Type == html.TextNode {\n\t\ttext := strings.TrimSpace(n.Data)\n\t\tif text != \"\" {\n\t\t\tb.WriteString(text + \" \")\n\t\t}\n\t}\n\n\tfor c := n.FirstChild; c != nil; c = c.NextSibling {\n\t\textractText(c, b)\n\t}\n}\n"
  },
  {
    "path": "providers/dns/ddnss/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(&Authentication{Key: \"secret\"})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL = server.URL\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t)\n}\n\nfunc TestClient_AddTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /\",\n\t\t\tservermock.ResponseFromFixture(\"success.html\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"host\", \"_acme-challenge.example.com\").\n\t\t\t\tWith(\"key\", \"secret\").\n\t\t\t\tWith(\"txt\", \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\").\n\t\t\t\tWith(\"txtm\", \"1\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.AddTXTRecord(t.Context(), \"_acme-challenge.example.com\", \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_RemoveTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /\",\n\t\t\tservermock.ResponseFromFixture(\"success.html\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"host\", \"_acme-challenge.example.com\").\n\t\t\t\tWith(\"key\", \"secret\").\n\t\t\t\tWith(\"txtm\", \"2\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.RemoveTXTRecord(t.Context(), \"_acme-challenge.example.com\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/ddnss/internal/fixtures/error.html",
    "content": "<head>\n    <meta name=\"robots\" content=\"noindex\">\n    <title>DDNSS - Kostenloser DynDNS Service : Re-ProutDNS v5.01v</title>\n</head>\n<body>\n<p><font face=\"Verdana\" size=\"2\"></font></p>\n<p><font face=\"Verdana\" size=\"2\">Error Occurred While Processing Request :</font></p>\n<blockquote>\n    <font face=\"Verdana\" size=\"2\">- badysys : Der System Parameter ist ungültig.</font><br>\n    <font face=\"Verdana\" size=\"2\">- badauth : Die Authorisation ist fehlgeschlagen. Die Parameter username und/oder password sind falsch.</font><br>\n    <font face=\"Verdana\" size=\"2\">- notfqdn : Hostname fehlt oder ist falsch.</font></blockquote>\n</body>\n"
  },
  {
    "path": "providers/dns/ddnss/internal/fixtures/success.html",
    "content": "<head>\n    <meta name=\"robots\" content=\"noindex\">\n    <title>DDNSS - Kostenloser DynDNS Service : Re-ProutDNS v5.01v</title>\n</head>\n<body>\n<p><font face=\"Verdana\" size=\"2\"></font></p>\n<p><font face=\"Verdana\" size=\"2\">Updated 1 hostname.</font></p>\n</body>\n"
  },
  {
    "path": "providers/dns/ddnss/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n)\n\ntype Authentication struct {\n\tUsername string `url:\"user,omitempty\"`\n\tPassword string `url:\"pwd,omitempty\"`\n\tKey      string `url:\"key,omitempty\"`\n}\n\nfunc (a *Authentication) validate() error {\n\tif a.Username == \"\" && a.Password == \"\" && a.Key == \"\" {\n\t\treturn errors.New(\"missing credentials\")\n\t}\n\n\tif a.Username != \"\" && a.Password != \"\" && a.Key != \"\" {\n\t\treturn errors.New(\"only one of username, password or key can be set\")\n\t}\n\n\tif (a.Username != \"\" && a.Password == \"\") || a.Username == \"\" && a.Password != \"\" {\n\t\treturn errors.New(\"username and password must be set together\")\n\t}\n\n\treturn nil\n}\n\nfunc (a *Authentication) set(query url.Values) {\n\tif a.Key != \"\" {\n\t\tquery.Set(\"key\", a.Key)\n\n\t\treturn\n\t}\n\n\tquery.Set(\"user\", a.Username)\n\tquery.Set(\"pwd\", a.Password)\n}\n"
  },
  {
    "path": "providers/dns/derak/derak.go",
    "content": "// Package derak implements a DNS provider for solving the DNS-01 challenge using Derak Cloud.\npackage derak\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/derak/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/miekg/dns\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DERAK_\"\n\n\tEnvAPIKey    = envNamespace + \"API_KEY\"\n\tEnvWebsiteID = envNamespace + \"WEBSITE_ID\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tWebsiteID          string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Derak Cloud.\n// Credentials must be passed in the environment variable: DERAK_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"derak: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.WebsiteID = env.GetOrDefaultString(EnvWebsiteID, \"\")\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Derak Cloud.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"derak: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"derak: missing credentials\")\n\t}\n\n\tclient := internal.NewClient(config.APIKey)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"derak: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecordName, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"derak: %w\", err)\n\t}\n\n\tzoneID, err := d.getZoneID(ctx, info)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"derak: get zone ID: %w\", err)\n\t}\n\n\tr := internal.Record{\n\t\tType:    \"TXT\",\n\t\tHost:    recordName,\n\t\tContent: info.Value,\n\t\tTTL:     d.config.TTL,\n\t}\n\n\trecord, err := d.client.CreateRecord(ctx, zoneID, r)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"derak: create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = record.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneID, err := d.getZoneID(ctx, info)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"derak: get zone ID: %w\", err)\n\t}\n\n\t// gets the record's unique ID\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"derak: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\terr = d.client.DeleteRecord(ctx, zoneID, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"derak: delete record: %w\", err)\n\t}\n\n\t// deletes record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) getZoneID(ctx context.Context, info dns01.ChallengeInfo) (string, error) {\n\tzoneID := d.config.WebsiteID\n\tif zoneID != \"\" {\n\t\treturn zoneID, nil\n\t}\n\n\tzones, err := d.client.GetZones(ctx)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"get zones: %w\", err)\n\t}\n\n\tfor _, zone := range zones {\n\t\tif strings.HasSuffix(info.EffectiveFQDN, dns.Fqdn(zone.HumanReadable)) {\n\t\t\treturn zone.ID, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"zone/website not found %s\", info.EffectiveFQDN)\n}\n"
  },
  {
    "path": "providers/dns/derak/derak.toml",
    "content": "Name = \"Derak Cloud\"\nDescription = ''''''\nURL = \"https://derak.cloud/\"\nCode = \"derak\"\nSince = \"v4.12.0\"\n\nExample = '''\nDERAK_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns derak -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DERAK_API_KEY = \"The API key\"\n  [Configuration.Additional]\n    DERAK_WEBSITE_ID = \"Force the zone/website ID\"\n    DERAK_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 5)\"\n    DERAK_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    DERAK_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    DERAK_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n"
  },
  {
    "path": "providers/dns/derak/derak_test.go",
    "content": "package derak\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey, EnvWebsiteID).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"key\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"derak: some credentials information are missing: DERAK_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"key\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\texpected: \"derak: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/derak/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\nconst defaultBaseURL = \"https://api.derak.cloud/v1.0\"\n\ntype Client struct {\n\tbaseURL      *url.URL\n\tHTTPClient   *http.Client\n\tzoneEndpoint string\n\n\tapiKey string\n}\n\nfunc NewClient(apiKey string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tHTTPClient:   &http.Client{Timeout: 10 * time.Second},\n\t\tbaseURL:      baseURL,\n\t\tzoneEndpoint: \"https://api.derak.cloud/api/v2/service/cdn/zones\",\n\t\tapiKey:       apiKey,\n\t}\n}\n\n// GetRecords gets all records.\n// Note: the response is not influenced by the query parameters, so the documentation seems wrong.\nfunc (c *Client) GetRecords(ctx context.Context, zoneID string, params *GetRecordsParameters) (*GetRecordsResponse, error) {\n\tendpoint := c.baseURL.JoinPath(\"zones\", zoneID, \"dnsrecords\")\n\n\tv, err := querystring.Values(params)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tendpoint.RawQuery = v.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresponse := &GetRecordsResponse{}\n\n\terr = c.do(req, response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn response, nil\n}\n\n// GetRecord gets a record by ID.\nfunc (c *Client) GetRecord(ctx context.Context, zoneID, recordID string) (*Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"zones\", zoneID, \"dnsrecords\", recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresponse := &Record{}\n\n\terr = c.do(req, response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn response, nil\n}\n\n// CreateRecord creates a new record.\nfunc (c *Client) CreateRecord(ctx context.Context, zoneID string, record Record) (*Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"zones\", zoneID, \"dnsrecords\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresponse := &Record{}\n\n\terr = c.do(req, response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn response, nil\n}\n\n// EditRecord edits an existing record.\nfunc (c *Client) EditRecord(ctx context.Context, zoneID, recordID string, record Record) (*Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"zones\", zoneID, \"dnsrecords\", recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodPatch, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresponse := &Record{}\n\n\terr = c.do(req, response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn response, nil\n}\n\n// DeleteRecord deletes an existing record.\nfunc (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID string) error {\n\tendpoint := c.baseURL.JoinPath(\"zones\", zoneID, \"dnsrecords\", recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresponse := &APIResponse[any]{}\n\n\terr = c.do(req, response)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !response.Success {\n\t\treturn fmt.Errorf(\"API error: %d %s\", response.Error, codeText(response.Error))\n\t}\n\n\treturn nil\n}\n\n// GetZones gets zones.\n// Note: it's not a part of the official API, there is no documentation about this.\n// The endpoint comes from UI calls analysis.\nfunc (c *Client) GetZones(ctx context.Context) ([]Zone, error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, c.zoneEndpoint, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresponse := &APIResponse[[]Zone]{}\n\n\terr = c.do(req, response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !response.Success {\n\t\treturn nil, fmt.Errorf(\"API error: %d %s\", response.Error, codeText(response.Error))\n\t}\n\n\treturn response.Result, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.SetBasicAuth(\"api\", c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tswitch req.Method {\n\tcase http.MethodPut:\n\t\tif resp.StatusCode != http.StatusCreated {\n\t\t\treturn parseError(req, resp)\n\t\t}\n\tdefault:\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\treturn parseError(req, resp)\n\t\t}\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar response APIResponse[any]\n\n\terr := json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code %d] %d: %s\", resp.StatusCode, response.Error, codeText(response.Error))\n}\n"
  },
  {
    "path": "providers/dns/derak/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"secret\")\n\tclient.baseURL, _ = url.Parse(server.URL)\n\tclient.zoneEndpoint = server.URL\n\tclient.HTTPClient = server.Client()\n\n\treturn client, nil\n}\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithBasicAuth(\"api\", \"secret\"))\n}\n\nfunc TestGetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords\",\n\t\t\tservermock.ResponseFromFixture(\"records-GET.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"47c0ecf6c91243308c649ad1d2d618dd\", &GetRecordsParameters{DNSType: \"TXT\", Content: `\"test\"'`})\n\trequire.NoError(t, err)\n\n\texcepted := &GetRecordsResponse{Data: []Record{\n\t\t{\n\t\t\tType:    \"A\",\n\t\t\tHost:    \"example.com\",\n\t\t\tContent: \"188.114.97.3\",\n\t\t\tID:      \"812bee17a0b440b0bd5ee099a78b839c\",\n\t\t},\n\t\t{\n\t\t\tType:    \"A\",\n\t\t\tHost:    \"example.com\",\n\t\t\tContent: \"188.114.96.3\",\n\t\t\tID:      \"90e6029da45d4a36bf31056cf85d0cab\",\n\t\t},\n\t\t{\n\t\t\tType:    \"AAAA\",\n\t\t\tHost:    \"example.com\",\n\t\t\tContent: \"2a06:98c1:3121::7\",\n\t\t\tID:      \"0ac0320da0d24b5ca4f1648986a17340\",\n\t\t},\n\t\t{\n\t\t\tType:    \"AAAA\",\n\t\t\tHost:    \"example.com\",\n\t\t\tContent: \"2a06:98c1:3120::7\",\n\t\t\tID:      \"c91599694aea413498a0b3cd0a54a585\",\n\t\t},\n\t\t{\n\t\t\tType:    \"A\",\n\t\t\tHost:    \"www\",\n\t\t\tContent: \"188.114.96.7\",\n\t\t\tID:      \"c21f974992d549499f92e768bc468374\",\n\t\t},\n\t\t{\n\t\t\tType:    \"A\",\n\t\t\tHost:    \"www\",\n\t\t\tContent: \"188.114.97.7\",\n\t\t\tID:      \"90c3c1f05dca426893f10f122d18ad7a\",\n\t\t},\n\t\t{\n\t\t\tType:    \"AAAA\",\n\t\t\tHost:    \"www\",\n\t\t\tContent: \"2a06:98c1:3121::\",\n\t\t\tID:      \"379ab0ac0e434bc9aee5287e497f88a5\",\n\t\t},\n\t\t{\n\t\t\tType:    \"AAAA\",\n\t\t\tHost:    \"www\",\n\t\t\tContent: \"2a06:98c1:3120::\",\n\t\t\tID:      \"a1c4f9e50ba74791a4d70dc96999474c\",\n\t\t},\n\t}, Count: 8}\n\n\tassert.Equal(t, excepted, records)\n}\n\nfunc TestGetRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetRecords(t.Context(), \"47c0ecf6c91243308c649ad1d2d618dd\", &GetRecordsParameters{DNSType: \"TXT\", Content: `\"test\"'`})\n\trequire.Error(t, err)\n}\n\nfunc TestGetRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/812bee17a0b440b0bd5ee099a78b839c\",\n\t\t\tservermock.ResponseFromFixture(\"record-GET.json\")).\n\t\tBuild(t)\n\n\trecord, err := client.GetRecord(t.Context(), \"47c0ecf6c91243308c649ad1d2d618dd\", \"812bee17a0b440b0bd5ee099a78b839c\")\n\trequire.NoError(t, err)\n\n\texcepted := &Record{\n\t\tType:    \"A\",\n\t\tHost:    \"example.com\",\n\t\tContent: \"188.114.97.3\",\n\t\tID:      \"812bee17a0b440b0bd5ee099a78b839c\",\n\t}\n\n\tassert.Equal(t, excepted, record)\n}\n\nfunc TestGetRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetRecord(t.Context(), \"47c0ecf6c91243308c649ad1d2d618dd\", \"812bee17a0b440b0bd5ee099a78b839c\")\n\trequire.Error(t, err)\n}\n\nfunc TestCreateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords\",\n\t\t\tservermock.ResponseFromFixture(\"record-PUT.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated)).\n\t\tBuild(t)\n\n\tr := Record{\n\t\tType:    \"TXT\",\n\t\tHost:    \"test\",\n\t\tContent: \"test\",\n\t\tTTL:     120,\n\t}\n\n\trecord, err := client.CreateRecord(t.Context(), \"47c0ecf6c91243308c649ad1d2d618dd\", r)\n\trequire.NoError(t, err)\n\n\texcepted := &Record{\n\t\tType:    \"A\",\n\t\tHost:    \"example.com\",\n\t\tContent: \"188.114.97.3\",\n\t\tID:      \"812bee17a0b440b0bd5ee099a78b839c\",\n\t}\n\n\tassert.Equal(t, excepted, record)\n}\n\nfunc TestCreateRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\tr := Record{\n\t\tType:    \"TXT\",\n\t\tHost:    \"test\",\n\t\tContent: \"test\",\n\t\tTTL:     120,\n\t}\n\n\t_, err := client.CreateRecord(t.Context(), \"47c0ecf6c91243308c649ad1d2d618dd\", r)\n\trequire.Error(t, err)\n}\n\nfunc TestEditRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PATCH /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/eebc813de2f94d67b09d91e10e2d65c2\",\n\t\t\tservermock.ResponseFromFixture(\"record-PATCH.json\")).\n\t\tBuild(t)\n\n\trecord, err := client.EditRecord(t.Context(), \"47c0ecf6c91243308c649ad1d2d618dd\", \"eebc813de2f94d67b09d91e10e2d65c2\", Record{\n\t\tContent: \"foo\",\n\t})\n\trequire.NoError(t, err)\n\n\texcepted := &Record{\n\t\tType:    \"A\",\n\t\tHost:    \"example.com\",\n\t\tContent: \"188.114.97.3\",\n\t\tID:      \"812bee17a0b440b0bd5ee099a78b839c\",\n\t}\n\n\tassert.Equal(t, excepted, record)\n}\n\nfunc TestEditRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PATCH /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/eebc813de2f94d67b09d91e10e2d65c2\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.EditRecord(t.Context(), \"47c0ecf6c91243308c649ad1d2d618dd\", \"eebc813de2f94d67b09d91e10e2d65c2\", Record{\n\t\tContent: \"foo\",\n\t})\n\trequire.Error(t, err)\n}\n\nfunc TestDeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/653464211b7447a1bee6b8fcb9fb86df\",\n\t\t\tservermock.ResponseFromFixture(\"record-DELETE.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"47c0ecf6c91243308c649ad1d2d618dd\", \"653464211b7447a1bee6b8fcb9fb86df\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/653464211b7447a1bee6b8fcb9fb86df\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"47c0ecf6c91243308c649ad1d2d618dd\", \"653464211b7447a1bee6b8fcb9fb86df\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetZones(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().\n\t\t\tWithBasicAuth(\"api\", \"secret\"),\n\t).\n\t\tRoute(\"GET /\", servermock.ResponseFromFixture(\"service-cdn-zones.json\")).\n\t\tBuild(t)\n\n\tzones, err := client.GetZones(t.Context())\n\trequire.NoError(t, err)\n\n\texcepted := []Zone{{\n\t\tID:               \"47c0ecf6c91243308c649ad1d2d618dd\",\n\t\tTags:             []string{},\n\t\tContextID:        \"47c0ecf6c91243308c649ad1d2d618dd\",\n\t\tContextType:      \"CDN\",\n\t\tHumanReadable:    \"example.com\",\n\t\tSerial:           \"2301449956\",\n\t\tCreationTime:     1679090659902,\n\t\tCreationTimeDate: time.Date(2023, time.March, 17, 22, 4, 19, 902000000, time.UTC),\n\t\tStatus:           \"active\",\n\t\tIsMoved:          true,\n\t\tPaused:           false,\n\t\tServiceType:      \"CDN\",\n\t\tLimbo:            false,\n\t\tTeamName:         \"test\",\n\t\tTeamID:           \"640ef58496738d38fa7246a4\",\n\t\tMyTeam:           true,\n\t\tRoleName:         \"owner\",\n\t\tIsBoard:          true,\n\t\tBoardRole:        []string{\"owner\"},\n\t}}\n\n\tassert.Equal(t, excepted, zones)\n}\n\nfunc TestGetZones_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /\", servermock.ResponseFromFixture(\"error.json\").\n\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetZones(t.Context())\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "providers/dns/derak/internal/fixtures/error.json",
    "content": "{\"success\":false,\"error\":1010}\n"
  },
  {
    "path": "providers/dns/derak/internal/fixtures/record-DELETE.json",
    "content": "{\n  \"success\": true\n}\n"
  },
  {
    "path": "providers/dns/derak/internal/fixtures/record-GET.json",
    "content": "{\n  \"recordId\": \"812bee17a0b440b0bd5ee099a78b839c\",\n  \"type\": \"A\",\n  \"host\": \"example.com\",\n  \"content\": \"188.114.97.3\",\n  \"ttl\": 0,\n  \"cloud\": false,\n  \"advanced\": false,\n  \"customSSLType\": \"\"\n}\n"
  },
  {
    "path": "providers/dns/derak/internal/fixtures/record-PATCH.json",
    "content": "{\n  \"recordId\": \"812bee17a0b440b0bd5ee099a78b839c\",\n  \"type\": \"A\",\n  \"host\": \"example.com\",\n  \"content\": \"188.114.97.3\",\n  \"ttl\": 0,\n  \"cloud\": false,\n  \"advanced\": false,\n  \"customSSLType\": \"\"\n}\n"
  },
  {
    "path": "providers/dns/derak/internal/fixtures/record-PUT.json",
    "content": "{\n  \"recordId\": \"812bee17a0b440b0bd5ee099a78b839c\",\n  \"type\": \"A\",\n  \"host\": \"example.com\",\n  \"content\": \"188.114.97.3\",\n  \"ttl\": 0,\n  \"cloud\": false,\n  \"advanced\": false,\n  \"customSSLType\": \"\"\n}\n"
  },
  {
    "path": "providers/dns/derak/internal/fixtures/records-GET.json",
    "content": "{\n  \"data\": [\n    {\n      \"recordId\": \"812bee17a0b440b0bd5ee099a78b839c\",\n      \"type\": \"A\",\n      \"host\": \"example.com\",\n      \"content\": \"188.114.97.3\",\n      \"ttl\": 0,\n      \"cloud\": false,\n      \"advanced\": false,\n      \"customSSLType\": \"\"\n    },\n    {\n      \"recordId\": \"90e6029da45d4a36bf31056cf85d0cab\",\n      \"type\": \"A\",\n      \"host\": \"example.com\",\n      \"content\": \"188.114.96.3\",\n      \"ttl\": 0,\n      \"cloud\": false,\n      \"advanced\": false,\n      \"customSSLType\": \"\"\n    },\n    {\n      \"recordId\": \"0ac0320da0d24b5ca4f1648986a17340\",\n      \"type\": \"AAAA\",\n      \"host\": \"example.com\",\n      \"content\": \"2a06:98c1:3121::7\",\n      \"ttl\": 0,\n      \"cloud\": false,\n      \"advanced\": false,\n      \"customSSLType\": \"\"\n    },\n    {\n      \"recordId\": \"c91599694aea413498a0b3cd0a54a585\",\n      \"type\": \"AAAA\",\n      \"host\": \"example.com\",\n      \"content\": \"2a06:98c1:3120::7\",\n      \"ttl\": 0,\n      \"cloud\": false,\n      \"advanced\": false,\n      \"customSSLType\": \"\"\n    },\n    {\n      \"recordId\": \"c21f974992d549499f92e768bc468374\",\n      \"type\": \"A\",\n      \"host\": \"www\",\n      \"content\": \"188.114.96.7\",\n      \"ttl\": 0,\n      \"cloud\": false,\n      \"advanced\": false,\n      \"customSSLType\": \"\"\n    },\n    {\n      \"recordId\": \"90c3c1f05dca426893f10f122d18ad7a\",\n      \"type\": \"A\",\n      \"host\": \"www\",\n      \"content\": \"188.114.97.7\",\n      \"ttl\": 0,\n      \"cloud\": false,\n      \"advanced\": false,\n      \"customSSLType\": \"\"\n    },\n    {\n      \"recordId\": \"379ab0ac0e434bc9aee5287e497f88a5\",\n      \"type\": \"AAAA\",\n      \"host\": \"www\",\n      \"content\": \"2a06:98c1:3121::\",\n      \"ttl\": 0,\n      \"cloud\": false,\n      \"advanced\": false,\n      \"customSSLType\": \"\"\n    },\n    {\n      \"recordId\": \"a1c4f9e50ba74791a4d70dc96999474c\",\n      \"type\": \"AAAA\",\n      \"host\": \"www\",\n      \"content\": \"2a06:98c1:3120::\",\n      \"ttl\": 0,\n      \"cloud\": false,\n      \"advanced\": false,\n      \"customSSLType\": \"\"\n    }\n  ],\n  \"count\": 8\n}\n"
  },
  {
    "path": "providers/dns/derak/internal/fixtures/service-cdn-zones.json",
    "content": "{\n  \"success\": true,\n  \"result\": [\n    {\n      \"zoneId\": \"47c0ecf6c91243308c649ad1d2d618dd\",\n      \"tags\": [],\n      \"contextId\": \"47c0ecf6c91243308c649ad1d2d618dd\",\n      \"contextType\": \"CDN\",\n      \"humanReadable\": \"example.com\",\n      \"serial\": \"2301449956\",\n      \"creationTime\": 1679090659902,\n      \"creationTimeDate\": \"2023-03-17T22:04:19.902Z\",\n      \"status\": \"active\",\n      \"is_moved\": true,\n      \"paused\": false,\n      \"cache\": {\n        \"developmentMode\": false\n      },\n      \"securityOptions\": {\n        \"level\": \"off\"\n      },\n      \"ssl\": {\n        \"active\": true\n      },\n      \"dns\": {\n        \"length\": 8\n      },\n      \"serviceType\": \"CDN\",\n      \"limbo\": false,\n      \"teamName\": \"test\",\n      \"teamId\": \"640ef58496738d38fa7246a4\",\n      \"myTeam\": true,\n      \"roleName\": \"owner\",\n      \"isBoard\": true,\n      \"boardRole\": [\n        \"owner\"\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/derak/internal/readme.md",
    "content": "# Notes\n\n## Forum\n\n- https://derak.cloud/faq/programming/%da%86%da%af%d9%88%d9%86%d9%87-%d9%85%db%8c%d8%aa%d9%88%d8%a7%d9%86-%d8%a8%d9%87-api%d9%87%d8%a7-%d8%af%d8%b3%d8%aa%d8%b1%d8%b3%db%8c-%d8%af%d8%a7%d8%b4%d8%aa%d8%9f/\n- https://derak.cloud/faq/programming/%d8%af%d8%b1%db%8c%d8%a7%d9%81%d8%aa-%da%a9%d9%84%db%8c%d8%af-api-api-key/\n\n---\n\n## DNS records (API)\n\n### GET: Get a list of all DNS records\n\nex: `https://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords`\n\n#### Query\n\n| The name of the parameter | Description                      |\n|---------------------------|----------------------------------|\n| dnsType                   | dnsType query                    |\n| content                   | The Host value of the DNS record |\n\n#### Errors\n\n| type error        | Error code |\n|-------------------|------------|\n| ForbiddenError    | 1003       |\n| RateLimitExceeded | 1013       |\n\n\n#### Example\n\n```bash\ncurl -X GET --user \"api:YOUR_API_KEY\" \\\nhttps://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords\n```\n```bash\ncurl -X GET --user \"api:api-MbmnxdpIBvk14nk5LFFdG1CV9PdMDfqi3tZAixBZLXYzM3qc187d7ede2de\" \\\nhttps://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords \\\n-F dnsType=\"TXT\" \n```\n\n\n### PUT: Creating a new DNS record on the desired website\n\nex: `https://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords`\n\n#### parameters\n\n| The name of the parameter | Description                                                                                                                                                                  |\n|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| *type                     | DNS record type [of types Aand AAAAand CNAMEand MXand NSand CAAand TXTand SPFand PTRand SRV]                                                                                 |\n| *host                     | The Host value of the DNS record                                                                                                                                             |\n| *content                  | The Host value of the DNS record                                                                                                                                             |\n| ttl                       | TTL of DNS record [default: 0]                                                                                                                                               |\n| cloud                     | This parameter specifies whether the traffic of this record passes through the cloud or not [Default: false]                                                                 |\n| priority                  | Priority of MX and SRV records [Default: 0]                                                                                                                                  |\n| service                   | SRV record service                                                                                                                                                           |\n| protocol                  | SRV record protocol [default: _tcp]                                                                                                                                          |\n| weight                    | SRV Record Weight [Default: 0]                                                                                                                                               |\n| port                      | Priority of MX and SRV records [Default: 0]                                                                                                                                  |\n| advanced                  | This parameter specifies whether this record has advanced settings or not [default: false]                                                                                   |\n| upstreamPort              | Upstream Port of DNS record [Default: 80]                                                                                                                                    |\n| upstreamProtocol          | Upstream protocol related to DNS records. Note that if you change these settings for another record of the same subdomain, the settings will be overwritten. [Default: http] |\n| customSSLType             | Custom SSL related DNS record. Note that if you change these settings for another record of the same subdomain, the settings will be overwritten.                            |\n\n#### Errors\n\n| type error         | Error code |\n|--------------------|------------|\n| ForbiddenError     | 1003       |\n| RateLimitExceeded  | 1013       |\n| DNSValidationError | 1008       |\n\n#### Example\n\n```bash\ncurl -X PUT --user \"api:YOUR_API_KEY\" \\\nhttps://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords  \\\n-F type=\"A\"  \\\n-F host=\"app\"  \\\n-F content=\"1.2.3.4\"\n```\n\n### GET: Get the information of a single DNS record\n\nex: `https://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/:recordId`\n\n#### Errors\n\n| type error          | Error code |\n|---------------------|------------|\n| ForbiddenError      | 1003       |\n| RateLimitExceeded   | 1013       |\n| RecordNotFoundError | 1021       |\n\n#### Example\n\n```bash\ncurl -X GET --user \"api:YOUR_API_KEY\" \\\nhttps://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/:recordId\n```\n\n### PATCH: Edit the parameters of a DNS record\n\n`https://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/:recordId`\n\n#### parameters\n\n| The name of the parameter | Description                                                                                                                                                                  |\n|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| type                      | DNS record type [of types Aand AAAAand CNAMEand MXand NSand CAAand TXTand SPFand PTRand SRV]                                                                                 |\n| host                      | The Host value of the DNS record                                                                                                                                             |\n| content                   | The Host value of the DNS record                                                                                                                                             |\n| ttl                       | TTL of DNS record [default: 0]                                                                                                                                               |\n| cloud                     | This parameter specifies whether the traffic of this record passes through the cloud or not [Default: false]                                                                 |\n| priority                  | Priority of MX and SRV records [Default: 0]                                                                                                                                  |\n| service                   | SRV record service                                                                                                                                                           |\n| protocol                  | SRV record protocol [default: _tcp]                                                                                                                                          |\n| weight                    | SRV Record Weight [Default: 0]                                                                                                                                               |\n| port                      | Priority of MX and SRV records [Default: 0]                                                                                                                                  |\n| advanced                  | This parameter specifies whether this record has advanced settings or not [default: false]                                                                                   |\n| upstreamPort              | Upstream Port of DNS record [Default: 80]                                                                                                                                    |\n| upstreamProtocol          | Upstream protocol related to DNS records. Note that if you change these settings for another record of the same subdomain, the settings will be overwritten. [Default: http] |\n| customSSLType             | Custom SSL related DNS record. Note that if you change these settings for another record of the same subdomain, the settings will be overwritten.                            |\n\n#### Errors\n\n| type error          | Error code |\n|---------------------|------------|\n| ForbiddenError      | 1003       |\n| RateLimitExceeded   | 1013       |\n| RecordNotFoundError | 1021       |\n| DNSValidationError  | 1008       |\n\n#### Example\n\n```bash\ncurl -X PATCH --user \"api:YOUR_API_KEY\" \\\nhttps://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/:recordId  \\\n-F cloud=\"true\"\n```\n\n### DELETE: Delete a DNS record\n\nex: `https://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/:recordId`\n\n#### Errors\n\n| type error          | Error code |\n|---------------------|------------|\n| ForbiddenError      | 1003       |\n| RateLimitExceeded   | 1013       |\n| RecordNotFoundError | 1021       |\n\n#### Example\n\n```bash\ncurl -X DELETE --user \"api:YOUR_API_KEY\" \\\nhttps://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/:recordId\n```\n\n---\n\n## Cache clearing (API)\n\n### POST: Clearing (Purge Cache) specified parameters, if no parameter is specified, the entire cache is deleted.\n\nex: `https://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/cache/purge`\n\n#### parameters\n\n| The name of the parameter | Description                         |\n|---------------------------|-------------------------------------|\n| hostname                  | The hostname to be deleted          |\n| hostnames                 | An array of hostnames to be cleared |\n| url                       | The URL to be deleted               |\n| urls                      | An array of URLs to be purged       |\n\n#### Errors\n\n| type error        | Error code |\n|-------------------|------------|\n| ForbiddenError    | 1003       |\n| RateLimitExceeded | 1013       |\n\n#### Examples\n\nPurge URLS:\n\n```bash\ncurl -X POST --user \"api:YOUR_API_KEY\" \\\nhttps://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/cache/purge  \\\n-F urls[]=\"https://www.derak.cloud/post/1\"  \\\n-F urls[]=\"https://www.derak.cloud/post/2\"\n```\n\nPurge HOSTNAMES:\n\n```bash\ncurl -X POST --user \"api:YOUR_API_KEY\" \\\nhttps://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/cache/purge  \\\n-F hostnames[]=\"www.derak.cloud\"  \\\n-F hostnames[]=\"app.derak.cloud\"\n```\n\nPurge EVERYTHING:\n\n```bash\ncurl -X POST --user \"api:YOUR_API_KEY\" \\\nhttps://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/cache/purge\n```\n\n---\n\n## API for SSL certificates\n\n### PUT: Enable SSL for a domain\n\nex: `https://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/ssl/`\n\n#### Errors\n\n| type error     | Error code |\n|----------------|------------|\n| ForbiddenError | 1003       |\n\n#### Example\n\n```bash\ncurl -X PUT --user \"api:YOUR_API_KEY\" \\\nhttps://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/ssl/\n```\n\n### DELETE: Disable SSL for a domain\n\nex: `https://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/ssl/`\n\n#### Errors\n\n| type error     | Error code |\n|----------------|------------|\n| ForbiddenError | 1003       |\n\n#### Example\n\n```bash\ncurl -X DELETE --user \"api:YOUR_API_KEY\" \\\nhttps://api.derak.cloud/v1.0/zones/47c0ecf6c91243308c649ad1d2d618dd/ssl/\n```\n"
  },
  {
    "path": "providers/dns/derak/internal/types.go",
    "content": "package internal\n\nimport \"time\"\n\ntype GetRecordsParameters struct {\n\tDNSType string `url:\"dnsType,omitempty\"`\n\tContent string `url:\"content,omitempty\"`\n}\n\ntype GetRecordsResponse struct {\n\tData  []Record `json:\"data\"`\n\tCount int      `json:\"count\"`\n}\n\ntype Record struct {\n\tType    string `json:\"type,omitempty\"`\n\tHost    string `json:\"host,omitempty\"`\n\tContent string `json:\"content,omitempty\"`\n\n\tID string `json:\"recordId,omitempty\"`\n\n\tTTL              int    `json:\"ttl,omitempty\"`\n\tCloud            bool   `json:\"cloud,omitempty\"`\n\tPriority         int    `json:\"priority,omitempty\"`\n\tService          string `json:\"service,omitempty\"`\n\tProtocol         string `json:\"protocol,omitempty\"`\n\tWeight           int    `json:\"weight,omitempty\"`\n\tPort             int    `json:\"port,omitempty\"`\n\tAdvanced         bool   `json:\"advanced,omitempty\"`\n\tUpstreamPort     int    `json:\"upstreamPort,omitempty\"`\n\tUpstreamProtocol string `json:\"upstreamProtocol,omitempty\"`\n\tCustomSSLType    string `json:\"customSSLType,omitempty\"`\n}\n\ntype APIResponse[T any] struct {\n\tSuccess bool `json:\"success\"`\n\tResult  T    `json:\"result\"`\n\tError   int  `json:\"error\"`\n}\n\ntype Zone struct {\n\tID               string    `json:\"zoneId,omitempty\"`\n\tTags             []string  `json:\"tags,omitempty\"`\n\tContextID        string    `json:\"contextId,omitempty\"`\n\tContextType      string    `json:\"contextType,omitempty\"`\n\tHumanReadable    string    `json:\"humanReadable,omitempty\"`\n\tSerial           string    `json:\"serial,omitempty\"`\n\tCreationTime     int64     `json:\"creationTime,omitempty\"`\n\tCreationTimeDate time.Time `json:\"creationTimeDate,omitzero\"`\n\tStatus           string    `json:\"status,omitempty\"`\n\tIsMoved          bool      `json:\"is_moved,omitempty\"`\n\tPaused           bool      `json:\"paused,omitempty\"`\n\tServiceType      string    `json:\"serviceType,omitempty\"`\n\tLimbo            bool      `json:\"limbo,omitempty\"`\n\tTeamName         string    `json:\"teamName,omitempty\"`\n\tTeamID           string    `json:\"teamId,omitempty\"`\n\tMyTeam           bool      `json:\"myTeam,omitempty\"`\n\tRoleName         string    `json:\"roleName,omitempty\"`\n\tIsBoard          bool      `json:\"isBoard,omitempty\"`\n\tBoardRole        []string  `json:\"boardRole,omitempty\"`\n}\n\nfunc codeText(code int) string {\n\tswitch code {\n\tcase 1008:\n\t\treturn \"DNSValidationError\"\n\tcase 1003:\n\t\treturn \"ForbiddenError\"\n\tcase 1013:\n\t\treturn \"RateLimitExceeded\"\n\tcase 1021:\n\t\treturn \"RecordNotFoundError\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"
  },
  {
    "path": "providers/dns/desec/desec.go",
    "content": "// Package desec implements a DNS provider for solving the DNS-01 challenge using deSEC DNS.\npackage desec\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/nrdcg/desec\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DESEC_\"\n\n\tEnvToken = envNamespace + \"TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// https://github.com/desec-io/desec-stack/issues/216\n// https://desec.readthedocs.io/_/downloads/en/latest/pdf/\nconst defaultTTL int = 3600\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken              string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, defaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *desec.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for deSEC.\n// Credentials must be passed in the environment variable: DESEC_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"desec: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for deSEC.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"desec: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"desec: incomplete credentials, missing token\")\n\t}\n\n\topts := desec.NewDefaultClientOptions()\n\tif config.HTTPClient != nil {\n\t\topts.HTTPClient = config.HTTPClient\n\t}\n\n\topts.HTTPClient = clientdebug.Wrap(opts.HTTPClient)\n\n\topts.Logger = log.Default()\n\n\tclient := desec.New(config.Token, opts)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"desec: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecordName, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"desec: %w\", err)\n\t}\n\n\tdomainName := dns01.UnFqdn(authZone)\n\n\tquotedValue := fmt.Sprintf(`%q`, info.Value)\n\n\trrSet, err := d.client.Records.Get(ctx, domainName, recordName, \"TXT\")\n\tif err != nil {\n\t\tvar nf *desec.NotFoundError\n\t\tif !errors.As(err, &nf) {\n\t\t\treturn fmt.Errorf(\"desec: failed to get records: domainName=%s, recordName=%s: %w\", domainName, recordName, err)\n\t\t}\n\n\t\t// Not found case -> create\n\t\t_, err = d.client.Records.Create(ctx, desec.RRSet{\n\t\t\tDomain:  domainName,\n\t\t\tSubName: recordName,\n\t\t\tType:    \"TXT\",\n\t\t\tRecords: []string{quotedValue},\n\t\t\tTTL:     d.config.TTL,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"desec: failed to create records: domainName=%s, recordName=%s: %w\", domainName, recordName, err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// update\n\trecords := append(rrSet.Records, quotedValue)\n\n\t_, err = d.client.Records.Update(ctx, domainName, recordName, \"TXT\", desec.RRSet{Records: records})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"desec: failed to update records: domainName=%s, recordName=%s: %w\", domainName, recordName, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"desec: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecordName, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"desec: %w\", err)\n\t}\n\n\tdomainName := dns01.UnFqdn(authZone)\n\n\trrSet, err := d.client.Records.Get(ctx, domainName, recordName, \"TXT\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"desec: failed to get records: domainName=%s, recordName=%s: %w\", domainName, recordName, err)\n\t}\n\n\trecords := make([]string, 0)\n\n\tfor _, record := range rrSet.Records {\n\t\tif record != fmt.Sprintf(`%q`, info.Value) {\n\t\t\trecords = append(records, record)\n\t\t}\n\t}\n\n\t_, err = d.client.Records.Update(ctx, domainName, recordName, \"TXT\", desec.RRSet{Records: records})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"desec: failed to update records: domainName=%s, recordName=%s: %w\", domainName, recordName, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/desec/desec.toml",
    "content": "Name = \"deSEC.io\"\nDescription = ''''''\nURL = \"https://desec.io\"\nCode = \"desec\"\nSince = \"v3.7.0\"\n\nExample = '''\nDESEC_TOKEN=x-xxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns desec -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DESEC_TOKEN = \"Domain token\"\n  [Configuration.Additional]\n    DESEC_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 4)\"\n    DESEC_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    DESEC_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)\"\n    DESEC_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://desec.readthedocs.io/en/latest/\"\n"
  },
  {
    "path": "providers/dns/desec/desec_test.go",
    "content": "package desec\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"\",\n\t\t\t},\n\t\t\texpected: \"desec: some credentials information are missing: DESEC_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\texpected string\n\t\ttoken    string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"api_key\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"desec: incomplete credentials, missing token\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/designate/designate.go",
    "content": "// Package designate implements a DNS provider for solving the DNS-01 challenge using the Designate DNSaaS for Openstack.\npackage designate\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/gophercloud/gophercloud\"\n\t\"github.com/gophercloud/gophercloud/openstack\"\n\t\"github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets\"\n\t\"github.com/gophercloud/gophercloud/openstack/dns/v2/zones\"\n\t\"github.com/gophercloud/utils/openstack/clientconfig\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DESIGNATE_\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\n\tEnvZoneName = envNamespace + \"ZONE_NAME\"\n\n\tenvNamespaceClient = \"OS_\"\n\n\tEnvAuthURL       = envNamespaceClient + \"AUTH_URL\"\n\tEnvUsername      = envNamespaceClient + \"USERNAME\"\n\tEnvPassword      = envNamespaceClient + \"PASSWORD\"\n\tEnvUserID        = envNamespaceClient + \"USER_ID\"\n\tEnvAppCredID     = envNamespaceClient + \"APPLICATION_CREDENTIAL_ID\"\n\tEnvAppCredName   = envNamespaceClient + \"APPLICATION_CREDENTIAL_NAME\"\n\tEnvAppCredSecret = envNamespaceClient + \"APPLICATION_CREDENTIAL_SECRET\"\n\tEnvTenantName    = envNamespaceClient + \"TENANT_NAME\"\n\tEnvRegionName    = envNamespaceClient + \"REGION_NAME\"\n\tEnvProjectID     = envNamespaceClient + \"PROJECT_ID\"\n\tEnvCloud         = envNamespaceClient + \"CLOUD\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tZoneName           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\topts               gophercloud.AuthOptions\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tZoneName:           env.GetOrFile(EnvZoneName),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 10),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *gophercloud.ServiceClient\n\n\tdnsEntriesMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Designate.\n// Credentials must be passed in the environment variables:\n// OS_AUTH_URL, OS_USERNAME, OS_PASSWORD, OS_REGION_NAME.\n// Or you can specify OS_CLOUD to read the credentials from the according cloud entry.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tconfig := NewDefaultConfig()\n\n\tval, err := env.Get(EnvCloud)\n\tif err == nil {\n\t\topts, erro := clientconfig.AuthOptions(&clientconfig.ClientOpts{\n\t\t\tCloud: val[EnvCloud],\n\t\t})\n\t\tif erro != nil {\n\t\t\treturn nil, fmt.Errorf(\"designate: %w\", erro)\n\t\t}\n\n\t\tconfig.opts = *opts\n\t} else {\n\t\topts, err := openstack.AuthOptionsFromEnv()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"designate: %w\", err)\n\t\t}\n\n\t\tconfig.opts = opts\n\t}\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Designate.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"designate: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := openstack.AuthenticatedClient(config.opts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"designate: failed to authenticate: %w\", err)\n\t}\n\n\tdnsClient, err := openstack.NewDNSV2(provider, gophercloud.EndpointOpts{\n\t\tRegion: os.Getenv(\"OS_REGION_NAME\"),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"designate: failed to get DNS provider: %w\", err)\n\t}\n\n\treturn &DNSProvider{client: dnsClient, config: config}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getZoneName(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"designate: %w\", err)\n\t}\n\n\tzoneID, err := d.getZoneID(zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"designate: couldn't get zone ID in Present: %w\", err)\n\t}\n\n\t// use mutex to prevent race condition between creating the record and verifying it\n\td.dnsEntriesMu.Lock()\n\tdefer d.dnsEntriesMu.Unlock()\n\n\texistingRecord, err := d.getRecord(zoneID, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"designate: %w\", err)\n\t}\n\n\tif existingRecord != nil {\n\t\tif slices.Contains(existingRecord.Records, info.Value) {\n\t\t\tlog.Printf(\"designate: the record already exists: %s\", info.Value)\n\t\t\treturn nil\n\t\t}\n\n\t\treturn d.updateRecord(existingRecord, info.Value)\n\t}\n\n\terr = d.createRecord(zoneID, info.EffectiveFQDN, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"designate: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getZoneName(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"designate: %w\", err)\n\t}\n\n\tzoneID, err := d.getZoneID(zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"designate: couldn't get zone ID in CleanUp: %w\", err)\n\t}\n\n\t// use mutex to prevent race condition between getting the record and deleting it\n\td.dnsEntriesMu.Lock()\n\tdefer d.dnsEntriesMu.Unlock()\n\n\trecord, err := d.getRecord(zoneID, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"designate: couldn't get Record ID in CleanUp: %w\", err)\n\t}\n\n\tif record == nil {\n\t\t// Record is already deleted\n\t\treturn nil\n\t}\n\n\terr = recordsets.Delete(d.client, zoneID, record.ID).ExtractErr()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"designate: error for %s in CleanUp: %w\", info.EffectiveFQDN, err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) createRecord(zoneID, fqdn, value string) error {\n\tcreateOpts := recordsets.CreateOpts{\n\t\tName:        fqdn,\n\t\tType:        \"TXT\",\n\t\tTTL:         d.config.TTL,\n\t\tDescription: \"ACME verification record\",\n\t\tRecords:     []string{value},\n\t}\n\n\tactual, err := recordsets.Create(d.client, zoneID, createOpts).Extract()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error for %s in Present while creating record: %w\", fqdn, err)\n\t}\n\n\tif actual.Name != fqdn || actual.TTL != d.config.TTL {\n\t\treturn errors.New(\"the created record doesn't match what we wanted to create\")\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) updateRecord(record *recordsets.RecordSet, value string) error {\n\tif slices.Contains(record.Records, value) {\n\t\tlog.Printf(\"skip: the record already exists: %s\", value)\n\t\treturn nil\n\t}\n\n\tvalues := append([]string{value}, record.Records...)\n\n\tupdateOpts := recordsets.UpdateOpts{\n\t\tDescription: &record.Description,\n\t\tTTL:         &record.TTL,\n\t\tRecords:     values,\n\t}\n\n\tresult := recordsets.Update(d.client, record.ZoneID, record.ID, updateOpts)\n\n\treturn result.Err\n}\n\nfunc (d *DNSProvider) getZoneID(wanted string) (string, error) {\n\tlistOpts := zones.ListOpts{\n\t\tName: wanted,\n\t}\n\n\tallPages, err := zones.List(d.client, listOpts).AllPages()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tallZones, err := zones.ExtractZones(allPages)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, zone := range allZones {\n\t\tif zone.Name == wanted {\n\t\t\treturn zone.ID, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"zone id not found for %s\", wanted)\n}\n\nfunc (d *DNSProvider) getRecord(zoneID, wanted string) (*recordsets.RecordSet, error) {\n\tlistOpts := recordsets.ListOpts{\n\t\tName: wanted,\n\t\tType: \"TXT\",\n\t}\n\n\tallPages, err := recordsets.ListByZone(d.client, zoneID, listOpts).AllPages()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tallRecords, err := recordsets.ExtractRecordSets(allPages)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, record := range allRecords {\n\t\tif record.Name == wanted && record.Type == \"TXT\" {\n\t\t\treturn &record, nil\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n\nfunc (d *DNSProvider) getZoneName(fqdn string) (string, error) {\n\tif d.config.ZoneName != \"\" {\n\t\treturn d.config.ZoneName, nil\n\t}\n\n\tauthZone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not find zone for %s: %w\", fqdn, err)\n\t}\n\n\tif authZone == \"\" {\n\t\treturn \"\", errors.New(\"empty zone name\")\n\t}\n\n\treturn authZone, nil\n}\n"
  },
  {
    "path": "providers/dns/designate/designate.toml",
    "content": "Name = \"Designate DNSaaS for Openstack\"\nDescription = ''''''\nURL = \"https://docs.openstack.org/designate/latest/\"\nCode = \"designate\"\nSince = \"v2.2.0\"\n\nExample = '''\n# With a `clouds.yaml`\nOS_CLOUD=my_openstack \\\nlego --dns designate -d '*.example.com' -d example.com run\n\n# or\n\nOS_AUTH_URL=https://openstack.example.org \\\nOS_REGION_NAME=RegionOne \\\nOS_PROJECT_ID=23d4522a987d4ab529f722a007c27846\nOS_USERNAME=myuser \\\nOS_PASSWORD=passw0rd \\\nlego --dns designate -d '*.example.com' -d example.com run\n\n# or\n\nOS_AUTH_URL=https://openstack.example.org \\\nOS_REGION_NAME=RegionOne \\\nOS_AUTH_TYPE=v3applicationcredential \\\nOS_APPLICATION_CREDENTIAL_ID=imn74uq0or7dyzz20dwo1ytls4me8dry \\\nOS_APPLICATION_CREDENTIAL_SECRET=68FuSPSdQqkFQYH5X1OoriEIJOwyLtQ8QSqXZOc9XxFK1A9tzZT6He2PfPw0OMja \\\nlego --dns designate -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Description\n\nThere are three main ways of authenticating with Designate:\n\n1. The first one is by using the `OS_CLOUD` environment variable and a `clouds.yaml` file.\n2. The second one is using your username and password, via the `OS_USERNAME`, `OS_PASSWORD` and `OS_PROJECT_NAME` environment variables.\n3. The third one is by using an application credential, via the `OS_APPLICATION_CREDENTIAL_*` and `OS_USER_ID` environment variables.\n\nFor the username/password and application methods, the `OS_AUTH_URL` and `OS_REGION_NAME` environment variables are required.\n\nFor more information, you can read about the different methods of authentication with OpenStack in the Keystone's documentation and the gophercloud documentation:\n\n- [Keystone username/password](https://docs.openstack.org/keystone/latest/user/supported_clients.html)\n- [Keystone application credentials](https://docs.openstack.org/keystone/latest/user/application_credentials.html)\n\nPublic cloud providers with support for Designate:\n\n- [Fuga Cloud](https://fuga.cloud/)\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    OS_AUTH_URL = \"Identity endpoint URL\"\n    OS_USERNAME = \"Username\"\n    OS_PASSWORD = \"Password\"\n    OS_USER_ID = \"User ID\"\n    OS_APPLICATION_CREDENTIAL_ID = \"Application credential ID\"\n    OS_APPLICATION_CREDENTIAL_NAME = \"Application credential name\"\n    OS_APPLICATION_CREDENTIAL_SECRET = \"Application credential secret\"\n    OS_PROJECT_NAME = \"Project name\"\n    OS_REGION_NAME = \"Region name\"\n  [Configuration.Additional]\n    OS_PROJECT_ID = \"Project ID\"\n    OS_TENANT_NAME = \"Tenant name (deprecated see OS_PROJECT_NAME and OS_PROJECT_ID)\"\n    DESIGNATE_ZONE_NAME = \"The zone name to use in the OpenStack Project to manage TXT records.\"\n    DESIGNATE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    DESIGNATE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 600)\"\n    DESIGNATE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://docs.openstack.org/designate/latest/\"\n  GoClient = \"https://pkg.go.dev/github.com/gophercloud/gophercloud/openstack/dns/v2\"\n"
  },
  {
    "path": "providers/dns/designate/designate_test.go",
    "content": "package designate\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/gophercloud/utils/openstack/clientconfig\"\n\t\"github.com/stretchr/testify/require\"\n\t\"gopkg.in/yaml.v2\"\n)\n\nconst (\n\tenvDomain             = envNamespace + \"DOMAIN\"\n\tenvOSClientConfigFile = \"OS_CLIENT_CONFIG_FILE\"\n)\n\nvar envTest = tester.NewEnvTest(\n\tEnvCloud,\n\tEnvAuthURL,\n\tEnvUsername,\n\tEnvPassword,\n\tEnvUserID,\n\tEnvAppCredID,\n\tEnvAppCredName,\n\tEnvAppCredSecret,\n\tEnvTenantName,\n\tEnvRegionName,\n\tEnvProjectID,\n\tenvOSClientConfigFile).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider_fromEnv(t *testing.T) {\n\tserverURL := setupTestProvider(t)\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthURL:    serverURL + \"/v2.0/\",\n\t\t\t\tEnvUsername:   \"B\",\n\t\t\t\tEnvPassword:   \"C\",\n\t\t\t\tEnvRegionName: \"D\",\n\t\t\t\tEnvProjectID:  \"E\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthURL:    \"\",\n\t\t\t\tEnvUsername:   \"\",\n\t\t\t\tEnvPassword:   \"\",\n\t\t\t\tEnvRegionName: \"\",\n\t\t\t},\n\t\t\texpected: \"designate: Missing environment variable [OS_AUTH_URL]\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing auth url\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthURL:    \"\",\n\t\t\t\tEnvUsername:   \"B\",\n\t\t\t\tEnvPassword:   \"C\",\n\t\t\t\tEnvRegionName: \"D\",\n\t\t\t},\n\t\t\texpected: \"designate: Missing environment variable [OS_AUTH_URL]\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthURL:    serverURL + \"/v2.0/\",\n\t\t\t\tEnvUsername:   \"\",\n\t\t\t\tEnvPassword:   \"C\",\n\t\t\t\tEnvRegionName: \"D\",\n\t\t\t},\n\t\t\texpected: \"designate: Missing one of the following environment variables [OS_USERID, OS_USERNAME]\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthURL:    serverURL + \"/v2.0/\",\n\t\t\t\tEnvUsername:   \"B\",\n\t\t\t\tEnvPassword:   \"\",\n\t\t\t\tEnvRegionName: \"D\",\n\t\t\t},\n\t\t\texpected: \"designate: Missing environment variable [OS_PASSWORD]\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing application credential secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthURL:    serverURL + \"/v2.0/\",\n\t\t\t\tEnvRegionName: \"D\",\n\t\t\t\tEnvAppCredID:  \"F\",\n\t\t\t},\n\t\t\texpected: \"designate: Missing environment variable [OS_APPLICATION_CREDENTIAL_SECRET]\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProvider_fromCloud(t *testing.T) {\n\tserverURL := setupTestProvider(t)\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tosCloud  string\n\t\tcloud    clientconfig.Cloud\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:    \"success\",\n\t\t\tosCloud: \"good_cloud\",\n\t\t\tcloud: clientconfig.Cloud{\n\t\t\t\tAuthInfo: &clientconfig.AuthInfo{\n\t\t\t\t\tAuthURL:     serverURL + \"/v2.0/\",\n\t\t\t\t\tUsername:    \"B\",\n\t\t\t\t\tPassword:    \"C\",\n\t\t\t\t\tProjectName: \"E\",\n\t\t\t\t\tProjectID:   \"F\",\n\t\t\t\t},\n\t\t\t\tRegionName: \"D\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:    \"missing auth url\",\n\t\t\tosCloud: \"missing_auth_url\",\n\t\t\tcloud: clientconfig.Cloud{\n\t\t\t\tAuthInfo: &clientconfig.AuthInfo{\n\t\t\t\t\tUsername:    \"B\",\n\t\t\t\t\tPassword:    \"C\",\n\t\t\t\t\tProjectName: \"E\",\n\t\t\t\t\tProjectID:   \"F\",\n\t\t\t\t},\n\t\t\t\tRegionName: \"D\",\n\t\t\t},\n\t\t\texpected: \"designate: Missing input for argument [auth_url]\",\n\t\t},\n\t\t{\n\t\t\tdesc:    \"missing username\",\n\t\t\tosCloud: \"missing_username\",\n\t\t\tcloud: clientconfig.Cloud{\n\t\t\t\tAuthInfo: &clientconfig.AuthInfo{\n\t\t\t\t\tAuthURL:     serverURL + \"/v2.0/\",\n\t\t\t\t\tPassword:    \"C\",\n\t\t\t\t\tProjectName: \"E\",\n\t\t\t\t\tProjectID:   \"F\",\n\t\t\t\t},\n\t\t\t\tRegionName: \"D\",\n\t\t\t},\n\t\t\texpected: \"designate: failed to authenticate: Missing input for argument [Username]\",\n\t\t},\n\t\t{\n\t\t\tdesc:    \"missing password\",\n\t\t\tosCloud: \"missing_auth_url\",\n\t\t\tcloud: clientconfig.Cloud{\n\t\t\t\tAuthInfo: &clientconfig.AuthInfo{\n\t\t\t\t\tAuthURL:     serverURL + \"/v2.0/\",\n\t\t\t\t\tUsername:    \"B\",\n\t\t\t\t\tProjectName: \"E\",\n\t\t\t\t\tProjectID:   \"F\",\n\t\t\t\t},\n\t\t\t\tRegionName: \"D\",\n\t\t\t},\n\t\t\texpected: \"designate: failed to authenticate: Exactly one of PasswordCredentials and TokenCredentials must be provided\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(map[string]string{\n\t\t\t\tEnvCloud:              test.osCloud,\n\t\t\t\tenvOSClientConfigFile: createCloudsYaml(t, test.osCloud, test.cloud),\n\t\t\t})\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\tserverURL := setupTestProvider(t)\n\n\ttestCases := []struct {\n\t\tdesc       string\n\t\ttenantName string\n\t\tpassword   string\n\t\tuserName   string\n\t\tauthURL    string\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tdesc:       \"success\",\n\t\t\ttenantName: \"A\",\n\t\t\tpassword:   \"B\",\n\t\t\tuserName:   \"C\",\n\t\t\tauthURL:    serverURL + \"/v2.0/\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"wrong auth url\",\n\t\t\ttenantName: \"A\",\n\t\t\tpassword:   \"B\",\n\t\t\tuserName:   \"C\",\n\t\t\tauthURL:    serverURL,\n\t\t\texpected:   \"designate: failed to authenticate: No supported version available from endpoint \" + serverURL + \"/\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.opts.TenantName = test.tenantName\n\t\t\tconfig.opts.Password = test.password\n\t\t\tconfig.opts.Username = test.userName\n\t\t\tconfig.opts.IdentityEndpoint = test.authURL\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// createCloudsYaml creates a temporary cloud file for testing purpose.\nfunc createCloudsYaml(t *testing.T, cloudName string, cloud clientconfig.Cloud) string {\n\tt.Helper()\n\n\tfile, err := os.CreateTemp(t.TempDir(), \"lego_test\")\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() { _ = file.Close() })\n\n\tclouds := clientconfig.Clouds{\n\t\tClouds: map[string]clientconfig.Cloud{\n\t\t\tcloudName: cloud,\n\t\t},\n\t}\n\n\terr = yaml.NewEncoder(file).Encode(&clouds)\n\trequire.NoError(t, err)\n\n\treturn file.Name()\n}\n\nfunc setupTestProvider(t *testing.T) string {\n\tt.Helper()\n\n\tmux := http.NewServeMux()\n\tserver := httptest.NewServer(mux)\n\tt.Cleanup(server.Close)\n\n\tmux.HandleFunc(\"/\", func(w http.ResponseWriter, _ *http.Request) {\n\t\t_, _ = w.Write([]byte(`{\n\t\"access\": {\n\t\t\"token\": {\n\t\t\t\"id\": \"a\",\n\t\t\t\"expires\": \"9015-06-05T16:24:57.637Z\"\n\t\t},\n\t\t\"user\": {\n\t\t\t\"name\": \"a\",\n\t\t\t\"roles\": [ ],\n\t\t\t\"role_links\": [ ]\n\t\t},\n\t\t\"serviceCatalog\": [\n\t\t\t{\n\t\t\t\t\"endpoints\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"adminURL\": \"http://23.253.72.207:9696/\",\n\t\t\t\t\t\t\"region\": \"D\",\n\t\t\t\t\t\t\"internalURL\": \"http://23.253.72.207:9696/\",\n\t\t\t\t\t\t\"id\": \"97c526db8d7a4c88bbb8d68db1bdcdb8\",\n\t\t\t\t\t\t\"publicURL\": \"http://23.253.72.207:9696/\"\n\t\t\t\t\t}\n\t\t\t\t],\n\t\t\t\t\"endpoints_links\": [ ],\n\t\t\t\t\"type\": \"dns\",\n\t\t\t\t\"name\": \"designate\"\n\t\t\t}\n\t\t]\n\t}\n}`))\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\n\treturn server.URL\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/digitalocean/digitalocean.go",
    "content": "// Package digitalocean implements a DNS provider for solving the DNS-01 challenge using digitalocean DNS.\npackage digitalocean\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/digitalocean/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DO_\"\n\n\tEnvAuthToken = envNamespace + \"AUTH_TOKEN\"\n\tEnvAPIUrl    = envNamespace + \"API_URL\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL            string\n\tAuthToken          string\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tBaseURL:            env.GetOrDefaultString(EnvAPIUrl, internal.DefaultBaseURL),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 30),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]int\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Digital\n// Ocean. Credentials must be passed in the environment variable:\n// DO_AUTH_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAuthToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"digitalocean: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AuthToken = values[EnvAuthToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Digital Ocean.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"digitalocean: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.AuthToken == \"\" {\n\t\treturn nil, errors.New(\"digitalocean: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(\n\t\tclientdebug.Wrap(\n\t\t\tinternal.OAuthStaticAccessToken(config.HTTPClient, config.AuthToken),\n\t\t),\n\t)\n\n\tif config.BaseURL != \"\" {\n\t\tvar err error\n\n\t\tclient.BaseURL, err = url.Parse(config.BaseURL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"digitalocean: %w\", err)\n\t\t}\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]int),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"digitalocean: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecord := internal.Record{Type: \"TXT\", Name: info.EffectiveFQDN, Data: info.Value, TTL: d.config.TTL}\n\n\trespData, err := d.client.AddTxtRecord(context.Background(), authZone, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"digitalocean: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = respData.DomainRecord.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"digitalocean: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// get the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"digitalocean: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\terr = d.client.RemoveTxtRecord(context.Background(), authZone, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"digitalocean: %w\", err)\n\t}\n\n\t// Delete record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/digitalocean/digitalocean.toml",
    "content": "Name = \"Digital Ocean\"\nDescription = ''''''\nURL = \"https://www.digitalocean.com/docs/networking/dns/\"\nCode = \"digitalocean\"\nSince = \"v0.3.0\"\n\nExample = '''\nDO_AUTH_TOKEN=xxxxxx \\\nlego --dns digitalocean -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DO_AUTH_TOKEN = \"Authentication token\"\n  [Configuration.Additional]\n    DO_API_URL = \"The URL of the API\"\n    DO_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 5)\"\n    DO_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    DO_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 30)\"\n    DO_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://developers.digitalocean.com/documentation/v2/#domain-records\"\n"
  },
  {
    "path": "providers/dns/digitalocean/digitalocean_test.go",
    "content": "package digitalocean\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar envTest = tester.NewEnvTest(EnvAuthToken)\n\nfunc mockProvider() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AuthToken = \"asdf1234\"\n\t\t\tconfig.BaseURL = server.URL\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(\"Authorization\", \"Bearer asdf1234\"))\n}\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthToken: \"\",\n\t\t\t},\n\t\t\texpected: \"digitalocean: some credentials information are missing: DO_AUTH_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tauthToken string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tauthToken: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"digitalocean: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AuthToken = test.authToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockProvider().\n\t\tRoute(\"POST /v2/domains/example.com/records\",\n\t\t\tservermock.RawStringResponse(`{\n\t\t\t\"domain_record\": {\n\t\t\t\t\"id\": 1234567,\n\t\t\t\t\"type\": \"TXT\",\n\t\t\t\t\"name\": \"_acme-challenge\",\n\t\t\t\t\"data\": \"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\",\n\t\t\t\t\"priority\": null,\n\t\t\t\t\"port\": null,\n\t\t\t\t\"weight\": null\n\t\t\t}\n\t\t}`).\n\t\t\t\tWithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBody(`{\"type\":\"TXT\",\"name\":\"_acme-challenge.example.com.\",\"data\":\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\",\"ttl\":30}`)).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"\", \"foobar\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockProvider().\n\t\tRoute(\"DELETE /v2/domains/example.com/records/1234567\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\tprovider.recordIDsMu.Lock()\n\tprovider.recordIDs[\"token\"] = 1234567\n\tprovider.recordIDsMu.Unlock()\n\n\terr := provider.CleanUp(\"example.com\", \"token\", \"\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/digitalocean/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"golang.org/x/oauth2\"\n)\n\n// DefaultBaseURL default API endpoint.\nconst DefaultBaseURL = \"https://api.digitalocean.com\"\n\n// Client the Digital Ocean API client.\ntype Client struct {\n\tBaseURL    *url.URL\n\thttpClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(hc *http.Client) *Client {\n\tbaseURL, _ := url.Parse(DefaultBaseURL)\n\n\tif hc == nil {\n\t\thc = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\treturn &Client{BaseURL: baseURL, httpClient: hc}\n}\n\nfunc (c *Client) AddTxtRecord(ctx context.Context, zone string, record Record) (*TxtRecordResponse, error) {\n\tendpoint := c.BaseURL.JoinPath(\"v2\", \"domains\", dns01.UnFqdn(zone), \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trespData := &TxtRecordResponse{}\n\n\terr = c.do(req, respData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn respData, nil\n}\n\nfunc (c *Client) RemoveTxtRecord(ctx context.Context, zone string, recordID int) error {\n\tendpoint := c.BaseURL.JoinPath(\"v2\", \"domains\", dns01.UnFqdn(zone), \"records\", strconv.Itoa(recordID))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode >= http.StatusBadRequest {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\t// NOTE: Even though the body is empty, DigitalOcean API docs still show setting this Content-Type...\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errInfo APIError\n\n\terr := json.Unmarshal(raw, &errInfo)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code %d] %w\", resp.StatusCode, errInfo)\n}\n\nfunc OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {\n\tif client == nil {\n\t\tclient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tclient.Transport = &oauth2.Transport{\n\t\tSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),\n\t\tBase:   client.Transport,\n\t}\n\n\treturn client\n}\n"
  },
  {
    "path": "providers/dns/digitalocean/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(OAuthStaticAccessToken(server.Client(), \"secret\"))\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer secret\"))\n}\n\nfunc TestClient_AddTxtRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /v2/domains/example.com/records\",\n\t\t\tservermock.ResponseFromFixture(\"domains-records_POST.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBody(`{\"type\":\"TXT\",\"name\":\"_acme-challenge.example.com.\",\"data\":\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\",\"ttl\":30}`)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType: \"TXT\",\n\t\tName: \"_acme-challenge.example.com.\",\n\t\tData: \"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\",\n\t\tTTL:  30,\n\t}\n\n\tnewRecord, err := client.AddTxtRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n\n\texpected := &TxtRecordResponse{DomainRecord: Record{\n\t\tID:   1234567,\n\t\tType: \"TXT\",\n\t\tName: \"_acme-challenge\",\n\t\tData: \"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\",\n\t\tTTL:  0,\n\t}}\n\n\tassert.Equal(t, expected, newRecord)\n}\n\nfunc TestClient_RemoveTxtRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v2/domains/example.com/records/1234567\",\n\t\t\tservermock.ResponseFromFixture(\"domains-records_POST.json\").\n\t\t\t\tWithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\terr := client.RemoveTxtRecord(t.Context(), \"example.com\", 1234567)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/digitalocean/internal/fixtures/domains-records_POST.json",
    "content": "{\n  \"domain_record\": {\n    \"id\": 1234567,\n    \"type\": \"TXT\",\n    \"name\": \"_acme-challenge\",\n    \"data\": \"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\",\n    \"priority\": null,\n    \"port\": null,\n    \"weight\": null\n  }\n}\n"
  },
  {
    "path": "providers/dns/digitalocean/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\n// TxtRecordResponse represents a response from DO's API after making a TXT record.\ntype TxtRecordResponse struct {\n\tDomainRecord Record `json:\"domain_record\"`\n}\n\ntype Record struct {\n\tID   int    `json:\"id,omitempty\"`\n\tType string `json:\"type,omitempty\"`\n\tName string `json:\"name,omitempty\"`\n\tData string `json:\"data,omitempty\"`\n\tTTL  int    `json:\"ttl,omitempty\"`\n}\n\ntype APIError struct {\n\tID      string `json:\"id\"`\n\tMessage string `json:\"message\"`\n}\n\nfunc (a APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", a.ID, a.Message)\n}\n"
  },
  {
    "path": "providers/dns/directadmin/directadmin.go",
    "content": "package directadmin\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/directadmin/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DIRECTADMIN_\"\n\n\tEnvAPIURL   = envNamespace + \"API_URL\"\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\tEnvZoneName = envNamespace + \"ZONE_NAME\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL  string\n\tUsername string\n\tPassword string\n\n\tZoneName string\n\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tZoneName:           env.GetOrFile(EnvZoneName),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 30),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 60*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *internal.Client\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for DirectAdmin.\n// Credentials must be passed in the environment variables:\n// DIRECTADMIN_API_URL, DIRECTADMIN_USERNAME, DIRECTADMIN_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIURL, EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"directadmin: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.BaseURL = values[EnvAPIURL]\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for DirectAdmin.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config.BaseURL == \"\" {\n\t\treturn nil, errors.New(\"directadmin: missing API URL\")\n\t}\n\n\tif config.Username == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"directadmin: some credentials information are missing\")\n\t}\n\n\tclient, err := internal.NewClient(config.BaseURL, config.Username, config.Password)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"directadmin: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := d.getZoneName(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"directadmin: [domain: %q] %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"directadmin: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName:  subDomain,\n\t\tType:  \"TXT\",\n\t\tValue: info.Value,\n\t\tTTL:   d.config.TTL,\n\t}\n\n\terr = d.client.SetRecord(context.Background(), dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"directadmin: set record for zone %s and subdomain %s: %w\", authZone, subDomain, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := d.getZoneName(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"directadmin: [domain: %q] %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"directadmin: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName:  subDomain,\n\t\tType:  \"TXT\",\n\t\tValue: info.Value,\n\t}\n\n\terr = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"directadmin: delete record for zone %s and subdomain %s: %w\", authZone, subDomain, err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) getZoneName(fqdn string) (string, error) {\n\tif d.config.ZoneName != \"\" {\n\t\treturn d.config.ZoneName, nil\n\t}\n\n\tauthZone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not find zone for %s: %w\", fqdn, err)\n\t}\n\n\tif authZone == \"\" {\n\t\treturn \"\", errors.New(\"empty zone name\")\n\t}\n\n\treturn authZone, nil\n}\n"
  },
  {
    "path": "providers/dns/directadmin/directadmin.toml",
    "content": "Name = \"DirectAdmin\"\nDescription = ''''''\nURL = \"https://www.directadmin.com\"\nCode = \"directadmin\"\nSince = \"v4.18.0\"\n\nExample = '''\nDIRECTADMIN_API_URL=\"http://example.com:2222\" \\\nDIRECTADMIN_USERNAME=xxxx \\\nDIRECTADMIN_PASSWORD=yyy \\\nlego --dns directadmin -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DIRECTADMIN_API_URL = \"URL of the API\"\n    DIRECTADMIN_USERNAME = \"API username\"\n    DIRECTADMIN_PASSWORD = \"API password\"\n  [Configuration.Additional]\n    DIRECTADMIN_ZONE_NAME = \"Zone name used to add the TXT record\"\n    DIRECTADMIN_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 5)\"\n    DIRECTADMIN_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    DIRECTADMIN_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 30)\"\n    DIRECTADMIN_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.directadmin.com/api.php\"\n"
  },
  {
    "path": "providers/dns/directadmin/directadmin_test.go",
    "content": "package directadmin\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIURL, EnvUsername, EnvPassword).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIURL:   \"https://example.com:2222\",\n\t\t\t\tEnvUsername: \"test\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"directadmin: some credentials information are missing: DIRECTADMIN_API_URL,DIRECTADMIN_USERNAME,DIRECTADMIN_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API URL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"test\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"directadmin: some credentials information are missing: DIRECTADMIN_API_URL\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIURL:   \"https://example.com:2222\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"directadmin: some credentials information are missing: DIRECTADMIN_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIURL:   \"https://example.com:2222\",\n\t\t\t\tEnvUsername: \"test\",\n\t\t\t},\n\t\t\texpected: \"directadmin: some credentials information are missing: DIRECTADMIN_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tbaseURL  string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\tusername: \"test\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API URL\",\n\t\t\texpected: \"directadmin: missing API URL\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\texpected: \"directadmin: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\tusername: \"test\",\n\t\t\texpected: \"directadmin: some credentials information are missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.BaseURL = test.baseURL\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/directadmin/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\n// Client the Direct Admin API client.\ntype Client struct {\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n\n\tusername string\n\tpassword string\n}\n\n// NewClient creates a new Client.\nfunc NewClient(baseURL, username, password string) (*Client, error) {\n\tapi, err := url.Parse(baseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\tbaseURL:    api,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t\tusername:   username,\n\t\tpassword:   password,\n\t}, nil\n}\n\nfunc (c *Client) SetRecord(ctx context.Context, domain string, record Record) error {\n\tdata, err := querystring.Values(record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdata.Set(\"action\", \"add\")\n\n\treturn c.do(ctx, domain, data)\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, domain string, record Record) error {\n\tdata, err := querystring.Values(record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdata.Set(\"action\", \"delete\")\n\n\treturn c.do(ctx, domain, data)\n}\n\nfunc (c *Client) do(ctx context.Context, domain string, data url.Values) error {\n\tendpoint := c.baseURL.JoinPath(\"CMD_API_DNS_CONTROL\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"domain\", domain)\n\tquery.Set(\"json\", \"yes\")\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(data.Encode()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.SetBasicAuth(c.username, c.password)\n\treq.Header.Add(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn parseError(req, resp)\n\t}\n\n\treturn nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errInfo APIError\n\n\terr := json.Unmarshal(raw, &errInfo)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code %d] %w\", resp.StatusCode, errInfo)\n}\n"
  },
  {
    "path": "providers/dns/directadmin/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, _ := NewClient(server.URL, \"user\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded())\n}\n\nfunc newAPIError(reason string, a ...any) APIError {\n\treturn APIError{\n\t\tMessage: \"Cannot View Dns Record\",\n\t\tResult:  fmt.Sprintf(reason, a...),\n\t}\n}\n\nfunc TestClient_SetRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /CMD_API_DNS_CONTROL\", nil,\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\tWith(\"json\", \"yes\"),\n\t\t\tservermock.CheckForm().UsePostForm().Strict().\n\t\t\t\tWith(\"action\", \"add\").\n\t\t\t\tWith(\"name\", \"foo\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"value\", \"txtTXTtxt\").\n\t\t\t\tWith(\"ttl\", \"123\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:  \"foo\",\n\t\tType:  \"TXT\",\n\t\tValue: \"txtTXTtxt\",\n\t\tTTL:   123,\n\t}\n\n\terr := client.SetRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_SetRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /CMD_API_DNS_CONTROL\",\n\t\t\tservermock.JSONEncode(newAPIError(\"OOPS\")).\n\t\t\t\tWithStatusCode(http.StatusInternalServerError)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:  \"foo\",\n\t\tType:  \"TXT\",\n\t\tValue: \"txtTXTtxt\",\n\t\tTTL:   123,\n\t}\n\n\terr := client.SetRecord(t.Context(), \"example.com\", record)\n\trequire.EqualError(t, err, \"[status code 500] Cannot View Dns Record: OOPS\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /CMD_API_DNS_CONTROL\", nil,\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\tWith(\"json\", \"yes\"),\n\t\t\tservermock.CheckForm().UsePostForm().Strict().\n\t\t\t\tWith(\"action\", \"delete\").\n\t\t\t\tWith(\"name\", \"foo\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"value\", \"txtTXTtxt\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:  \"foo\",\n\t\tType:  \"TXT\",\n\t\tValue: \"txtTXTtxt\",\n\t}\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /CMD_API_DNS_CONTROL\",\n\t\t\tservermock.JSONEncode(newAPIError(\"OOPS\")).\n\t\t\t\tWithStatusCode(http.StatusInternalServerError)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:  \"foo\",\n\t\tType:  \"TXT\",\n\t\tValue: \"txtTXTtxt\",\n\t}\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", record)\n\trequire.EqualError(t, err, \"[status code 500] Cannot View Dns Record: OOPS\")\n}\n"
  },
  {
    "path": "providers/dns/directadmin/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\n// Record represents a DNS record.\ntype Record struct {\n\tName  string `url:\"name,omitempty\"`\n\tType  string `url:\"type,omitempty\"`\n\tValue string `url:\"value,omitempty\"`\n\tTTL   int    `url:\"ttl,omitempty\"`\n}\n\n// APIError represents a API error.\ntype APIError struct {\n\tMessage string `json:\"error,omitempty\"`\n\tResult  string `json:\"result,omitempty\"`\n}\n\nfunc (a APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", a.Message, a.Result)\n}\n"
  },
  {
    "path": "providers/dns/dns_providers_test.go",
    "content": "package dns\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/providers/dns/exec\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar envTest = tester.NewEnvTest(\"EXEC_PATH\")\n\nfunc TestKnownDNSProviderSuccess(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\tenvTest.Apply(map[string]string{\n\t\t\"EXEC_PATH\": \"abc\",\n\t})\n\n\tprovider, err := NewDNSChallengeProviderByName(\"exec\")\n\trequire.NoError(t, err)\n\tassert.NotNil(t, provider)\n\n\tassert.IsType(t, &exec.DNSProvider{}, provider, \"The loaded DNS provider doesn't have the expected type.\")\n}\n\nfunc TestKnownDNSProviderError(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\tenvTest.ClearEnv()\n\n\tprovider, err := NewDNSChallengeProviderByName(\"exec\")\n\trequire.Error(t, err)\n\tassert.Nil(t, provider)\n}\n\nfunc TestUnknownDNSProvider(t *testing.T) {\n\tprovider, err := NewDNSChallengeProviderByName(\"foobar\")\n\trequire.Error(t, err)\n\tassert.Nil(t, provider)\n}\n"
  },
  {
    "path": "providers/dns/dnsexit/dnsexit.go",
    "content": "// Package dnsexit implements a DNS provider for solving the DNS-01 challenge using DNSExit.\npackage dnsexit\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dnsexit/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DNSEXIT_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for DNSExit.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dnsexit: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for DNSExit.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"dnsexit: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.APIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dnsexit: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsexit: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsexit: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tType:    \"TXT\",\n\t\tName:    subDomain,\n\t\tContent: info.Value,\n\t\tTTL:     toMinutes(d.config.TTL),\n\t}\n\n\terr = d.client.AddRecord(context.Background(), dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsexit: add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsexit: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsexit: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tType:    \"TXT\",\n\t\tName:    subDomain,\n\t\tContent: info.Value,\n\t}\n\n\terr = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsexit: add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc toMinutes(seconds int) int {\n\ti := seconds / 60\n\tif seconds%60 > 0 {\n\t\ti++\n\t}\n\n\treturn i\n}\n"
  },
  {
    "path": "providers/dns/dnsexit/dnsexit.toml",
    "content": "Name = \"DNSExit\"\nDescription = ''''''\nURL = \"https://dnsexit.com\"\nCode = \"dnsexit\"\nSince = \"v4.32.0\"\n\nExample = '''\nDNSEXIT_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns dnsexit -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DNSEXIT_API_KEY = \"API key\"\n  [Configuration.Additional]\n    DNSEXIT_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    DNSEXIT_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 300)\"\n    DNSEXIT_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    DNSEXIT_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://dnsexit.com/dns/dns-api/\"\n"
  },
  {
    "path": "providers/dns/dnsexit/dnsexit_test.go",
    "content": "package dnsexit\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"key\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"dnsexit: some credentials information are missing: DNSEXIT_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"key\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"dnsexit: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(\"apikey\", \"secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromInternal(\"success.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"add_record-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromInternal(\"success.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"delete_record-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/dnsexit/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\nconst defaultBaseURL = \"https://api.dnsexit.com/dns/\"\n\n// Client the DNSExit API client.\ntype Client struct {\n\tapiKey string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiKey string) (*Client, error) {\n\tif apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// AddRecord adds a record.\n// https://dnsexit.com/dns/dns-api/#example-add-spf\n// https://dnsexit.com/dns/dns-api/#example-lse\nfunc (c *Client) AddRecord(ctx context.Context, domain string, record Record) error {\n\tpayload := APIRequest{\n\t\tDomain: domain,\n\t\tAdd:    []Record{record},\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, c.BaseURL, payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// DeleteRecord deletes a record.\n// https://dnsexit.com/dns/dns-api/#delete-a-record\nfunc (c *Client) DeleteRecord(ctx context.Context, domain string, record Record) error {\n\tpayload := APIRequest{\n\t\tDomain: domain,\n\t\tDelete: []Record{record},\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, c.BaseURL, payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) do(req *http.Request) error {\n\tuseragent.SetHeader(req.Header)\n\n\treq.Header.Set(\"apikey\", c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode > http.StatusBadRequest {\n\t\treturn parseError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tresult := &APIResponse{}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tif result.Code != 0 {\n\t\treturn result\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIResponse\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/dnsexit/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(\"apikey\", \"secret\"),\n\t)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"success.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"add_record-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:    \"TXT\",\n\t\tName:    \"_acme-challenge\",\n\t\tContent: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:     2,\n\t}\n\n\terr := client.AddRecord(context.Background(), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:      \"TXT\",\n\t\tName:      \"_acme-challenge\",\n\t\tContent:   \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:       480,\n\t\tOverwrite: true,\n\t}\n\n\terr := client.AddRecord(context.Background(), \"example.com\", record)\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"JSON Defined Record Type not Supported (code=6)\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"success.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"delete_record-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:    \"TXT\",\n\t\tName:    \"_acme-challenge\",\n\t\tContent: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t}\n\n\terr := client.DeleteRecord(context.Background(), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:    \"TXT\",\n\t\tName:    \"foo\",\n\t\tContent: \"txtTXTtxt\",\n\t}\n\n\terr := client.DeleteRecord(context.Background(), \"example.com\", record)\n\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"JSON Defined Record Type not Supported (code=6)\")\n}\n"
  },
  {
    "path": "providers/dns/dnsexit/internal/fixtures/add_record-request.json",
    "content": "{\n  \"domain\": \"example.com\",\n  \"add\": [\n    {\n      \"type\": \"TXT\",\n      \"name\": \"_acme-challenge\",\n      \"content\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n      \"ttl\": 2\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/dnsexit/internal/fixtures/delete_record-request.json",
    "content": "{\n  \"domain\": \"example.com\",\n  \"delete\": [\n    {\n      \"type\": \"TXT\",\n      \"name\": \"_acme-challenge\",\n      \"content\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/dnsexit/internal/fixtures/error.json",
    "content": "{\n  \"code\": 6,\n  \"message\": \"JSON Defined Record Type not Supported\"\n}\n"
  },
  {
    "path": "providers/dns/dnsexit/internal/fixtures/success.json",
    "content": "{\n  \"code\": 0,\n  \"details\": [\n    \"UPDATE Record A example.com. TTL(hh:mm) 08:00  IP   1.1.1.10\"\n  ],\n  \"message\": \"Success\"\n}\n"
  },
  {
    "path": "providers/dns/dnsexit/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype Record struct {\n\tType      string `json:\"type,omitempty\"`\n\tName      string `json:\"name,omitempty\"`\n\tContent   string `json:\"content,omitempty\"`\n\tTTL       int    `json:\"ttl,omitempty\"` // NOTE: ttl value is in minutes.\n\tOverwrite bool   `json:\"overwrite,omitempty\"`\n}\n\ntype APIRequest struct {\n\tDomain string   `json:\"domain,omitempty\"`\n\tAdd    []Record `json:\"add,omitempty\"`\n\tDelete []Record `json:\"delete,omitempty\"`\n\tUpdate []Record `json:\"update,omitempty\"`\n}\n\n// https://dnsexit.com/dns/dns-api/#server-reply\n\ntype APIResponse struct {\n\tCode    int      `json:\"code,omitempty\"`\n\tDetails []string `json:\"details,omitempty\"`\n\tMessage string   `json:\"message,omitempty\"`\n}\n\nfunc (a APIResponse) Error() string {\n\tmsg := new(strings.Builder)\n\n\t_, _ = fmt.Fprintf(msg, \"%s (code=%d)\", a.Message, a.Code)\n\n\tfor _, detail := range a.Details {\n\t\t_, _ = fmt.Fprintf(msg, \", %s\", detail)\n\t}\n\n\treturn msg.String()\n}\n"
  },
  {
    "path": "providers/dns/dnshomede/dnshomede.go",
    "content": "// Package dnshomede implements a DNS provider for solving the DNS-01 challenge using dnsHome.de.\npackage dnshomede\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dnshomede/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DNSHOMEDE_\"\n\n\tEnvCredentials = envNamespace + \"CREDENTIALS\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tCredentials        map[string]string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 20*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, 2*time.Minute),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for dnsHome.de.\n// Credentials must be passed in the environment variable: DNSHOMEDE_CREDENTIALS.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tconfig := NewDefaultConfig()\n\n\tvalues, err := env.Get(EnvCredentials)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dnshomede: %w\", err)\n\t}\n\n\tcredentials, err := env.ParsePairs(values[EnvCredentials])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dnshomede: credentials: %w\", err)\n\t}\n\n\tconfig.Credentials = credentials\n\n\treturn NewDNSProviderConfig(config)\n}\n\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"dnshomede: the configuration of the DNS provider is nil\")\n\t}\n\n\tif len(config.Credentials) == 0 {\n\t\treturn nil, errors.New(\"dnshomede: missing credentials\")\n\t}\n\n\tfor domain, password := range config.Credentials {\n\t\tif domain == \"\" {\n\t\t\treturn nil, fmt.Errorf(`dnshomede: missing domain: \"%s:%s\"`, domain, password)\n\t\t}\n\n\t\tif password == \"\" {\n\t\t\treturn nil, fmt.Errorf(`dnshomede: missing password: \"%s:%s\"`, domain, password)\n\t\t}\n\t}\n\n\tclient := internal.NewClient(config.Credentials)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present updates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.client.Add(context.Background(), dns01.UnFqdn(info.EffectiveFQDN), info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnshomede: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp updates the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.client.Remove(context.Background(), dns01.UnFqdn(info.EffectiveFQDN), info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnshomede: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n"
  },
  {
    "path": "providers/dns/dnshomede/dnshomede.toml",
    "content": "Name = \"dnsHome.de\"\nDescription = ''''''\nURL = \"https://www.dnshome.de\"\nCode = \"dnshomede\"\nSince = \"v4.10.0\"\n\nExample = '''\nDNSHOMEDE_CREDENTIALS=example.org:password \\\nlego --dns dnshomede -d '*.example.com' -d example.com run\n\nDNSHOMEDE_CREDENTIALS=my.example.org:password1,demo.example.org:password2 \\\nlego --dns dnshomede -d my.example.org -d demo.example.org\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DNSHOMEDE_CREDENTIALS = \"Comma-separated list of domain:password credential pairs\"\n  [Configuration.Additional]\n    DNSHOMEDE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 1200)\"\n    DNSHOMEDE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 2)\"\n    DNSHOMEDE_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 120)\"\n    DNSHOMEDE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n"
  },
  {
    "path": "providers/dns/dnshomede/dnshomede_test.go",
    "content": "package dnshomede\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvCredentials).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCredentials: \"example.org:123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success multiple domains\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCredentials: \"example.org:123,example.com:456,example.net:789\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCredentials: \",\",\n\t\t\t},\n\t\t\texpected: `dnshomede: credentials: incorrect pair: `,\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCredentials: \"example.org:\",\n\t\t\t},\n\t\t\texpected: `dnshomede: missing password: \"example.org:\"`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing domain\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCredentials: \":123\",\n\t\t\t},\n\t\t\texpected: `dnshomede: missing domain: \":123\"`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid credentials, partial\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCredentials: \"example.org:123,example.net\",\n\t\t\t},\n\t\t\texpected: \"dnshomede: credentials: incorrect pair: example.net\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCredentials: \"\",\n\t\t\t},\n\t\t\texpected: \"dnshomede: some credentials information are missing: DNSHOMEDE_CREDENTIALS\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tcreds    map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\tcreds: map[string]string{\"example.org\": \"123\"},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success multiple domains\",\n\t\t\tcreds: map[string]string{\n\t\t\t\t\"example.org\": \"123\",\n\t\t\t\t\"example.com\": \"456\",\n\t\t\t\t\"example.net\": \"789\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"dnshomede: missing credentials\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing domain\",\n\t\t\tcreds:    map[string]string{\"\": \"123\"},\n\t\t\texpected: `dnshomede: missing domain: \":123\"`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tcreds:    map[string]string{\"example.org\": \"\"},\n\t\t\texpected: `dnshomede: missing password: \"example.org:\"`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Credentials = test.creds\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/dnshomede/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst (\n\tremoveAction = \"rm\"\n\taddAction    = \"add\"\n)\n\nconst successCode = \"successfully\"\n\nconst defaultBaseURL = \"https://www.dnshome.de/dyndns.php\"\n\n// Client the dnsHome.de client.\ntype Client struct {\n\tbaseURL    string\n\tHTTPClient *http.Client\n\n\tcredentials map[string]string\n\tcredMu      sync.Mutex\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(credentials map[string]string) *Client {\n\treturn &Client{\n\t\tHTTPClient:  &http.Client{Timeout: 10 * time.Second},\n\t\tbaseURL:     defaultBaseURL,\n\t\tcredentials: credentials,\n\t}\n}\n\n// Add adds a TXT record.\n// only one TXT record for ACME is allowed, so it will update the \"current\" TXT record.\nfunc (c *Client) Add(ctx context.Context, hostname, value string) error {\n\tdomain := strings.TrimPrefix(hostname, \"_acme-challenge.\")\n\n\treturn c.doAction(ctx, domain, addAction, value)\n}\n\n// Remove removes a TXT record.\n// only one TXT record for ACME is allowed, so it will remove \"all\" the TXT records.\nfunc (c *Client) Remove(ctx context.Context, hostname, value string) error {\n\tdomain := strings.TrimPrefix(hostname, \"_acme-challenge.\")\n\n\treturn c.doAction(ctx, domain, removeAction, value)\n}\n\nfunc (c *Client) doAction(ctx context.Context, domain, action, value string) error {\n\tendpoint, err := c.createEndpoint(domain, action, value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), http.NoBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\toutput := string(raw)\n\n\tif !strings.HasPrefix(output, successCode) {\n\t\treturn errors.New(output)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) createEndpoint(domain, action, value string) (*url.URL, error) {\n\tif len(value) < 12 {\n\t\treturn nil, fmt.Errorf(\"the TXT value must have more than 12 characters: %s\", value)\n\t}\n\n\tendpoint, err := url.Parse(c.baseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc.credMu.Lock()\n\tpassword, ok := c.credentials[domain]\n\tc.credMu.Unlock()\n\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"domain %s not found in credentials, check your credentials map\", domain)\n\t}\n\n\tendpoint.User = url.UserPassword(domain, password)\n\n\tquery := endpoint.Query()\n\tquery.Set(\"acme\", action)\n\tquery.Set(\"txt\", value)\n\tendpoint.RawQuery = query.Encode()\n\n\treturn endpoint, nil\n}\n"
  },
  {
    "path": "providers/dns/dnshomede/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(credentials map[string]string) func(server *httptest.Server) (*Client, error) {\n\treturn func(server *httptest.Server) (*Client, error) {\n\t\tclient := NewClient(credentials)\n\t\tclient.HTTPClient = server.Client()\n\t\tclient.baseURL = server.URL\n\n\t\treturn client, nil\n\t}\n}\n\nfunc TestClient_Add(t *testing.T) {\n\ttxtValue := \"123456789012\"\n\n\tclient := servermock.NewBuilder[*Client](setupClient(map[string]string{\"example.org\": \"secret\"})).\n\t\tRoute(\"POST /\",\n\t\t\tservermock.RawStringResponse(fmt.Sprintf(\"%s %s\", successCode, txtValue)),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"acme\", addAction).With(\"txt\", txtValue)).\n\t\tBuild(t)\n\n\terr := client.Add(t.Context(), \"example.org\", txtValue)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_Add_error(t *testing.T) {\n\ttxtValue := \"123456789012\"\n\n\tclient := servermock.NewBuilder[*Client](setupClient(map[string]string{\"example.com\": \"secret\"})).\n\t\tRoute(\"POST /\",\n\t\t\tservermock.RawStringResponse(fmt.Sprintf(\"%s %s\", successCode, txtValue)),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"acme\", addAction).With(\"txt\", txtValue)).\n\t\tBuild(t)\n\n\terr := client.Add(t.Context(), \"example.org\", txtValue)\n\n\trequire.EqualError(t, err, \"domain example.org not found in credentials, check your credentials map\")\n}\n\nfunc TestClient_Remove(t *testing.T) {\n\ttxtValue := \"ABCDEFGHIJKL\"\n\n\tclient := servermock.NewBuilder[*Client](setupClient(map[string]string{\"example.org\": \"secret\"})).\n\t\tRoute(\"POST /\",\n\t\t\tservermock.RawStringResponse(fmt.Sprintf(\"%s %s\", successCode, txtValue)),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"acme\", removeAction).With(\"txt\", txtValue)).\n\t\tBuild(t)\n\n\terr := client.Remove(t.Context(), \"example.org\", txtValue)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_Remove_error(t *testing.T) {\n\ttxtValue := \"ABCDEFGHIJKL\"\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\thostname string\n\t\tresponse string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"response error - txt\",\n\t\t\thostname: \"example.com\",\n\t\t\tresponse: \"error - no valid acme txt record\",\n\t\t\texpected: \"error - no valid acme txt record\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"response error - acme\",\n\t\t\thostname: \"example.com\",\n\t\t\tresponse: \"nochg 1234:1234:1234:1234:1234:1234:1234:1234\",\n\t\t\texpected: \"nochg 1234:1234:1234:1234:1234:1234:1234:1234\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"credential error\",\n\t\t\thostname: \"example.org\",\n\t\t\tresponse: fmt.Sprintf(\"%s %s\", successCode, txtValue),\n\t\t\texpected: \"domain example.org not found in credentials, check your credentials map\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := servermock.NewBuilder[*Client](setupClient(map[string]string{\"example.com\": \"secret\"})).\n\t\t\t\tRoute(\"POST /\",\n\t\t\t\t\tservermock.RawStringResponse(test.response),\n\t\t\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\t\t\tWith(\"acme\", removeAction).With(\"txt\", txtValue)).\n\t\t\t\tBuild(t)\n\n\t\t\terr := client.Remove(t.Context(), test.hostname, txtValue)\n\t\t\trequire.EqualError(t, err, test.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/dnshomede/internal/readme.md",
    "content": "# dnshome.de API\n\n## Add TXT record\n\n```\nhttps://<subdomain>:<subdomain_password>@www.dnshome.de/dyndns.php?acme=add&txt=<txtvalue>\n```\n\n- `<subdomain>`: the subdomain (ex: `lego.dnshome.de`).\n- `<subdomain_password>`: the subdomain password.\n- `<txtvalue>`: the value of the TXT record (12 characters minimum)\n\nOnly one TXT record can be used for a subdomain.\n\nAlways returns StatusOK (200)\n\nIf the API call works the first word of the response body is `successfully`.\n\nIf an error occurs the response body is `error - <ERRMSG>`.\n\nCan be a POST or a GET.\n\n## Remove TXT record\n\n```\nhttps://<subdomain>:<subdomain_password>@www.dnshome.de/dyndns.php?acme=rm\n```\n\n- `<subdomain>`: the subdomain (ex: `lego.dnshome.de`).\n- `<subdomain_password>`: the subdomain password.\n\nOnly one TXT record can be used for a subdomain.\n\nAlways returns StatusOK (200)\n\nIf the API call works the first word of the response body is `successfully`.\n\nIf an error occurs the response body is `error - <ERRMSG>`.\n\nCan be a POST or a GET.\n"
  },
  {
    "path": "providers/dns/dnsimple/dnsimple.go",
    "content": "// Package dnsimple implements a DNS provider for solving the DNS-01 challenge using dnsimple DNS.\npackage dnsimple\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/dnsimple/dnsimple-go/v4/dnsimple\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\t\"golang.org/x/oauth2\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DNSIMPLE_\"\n\n\tEnvOAuthToken = envNamespace + \"OAUTH_TOKEN\"\n\tEnvBaseURL    = envNamespace + \"BASE_URL\"\n\tEnvDebug      = envNamespace + \"DEBUG\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tDebug              bool\n\tAccessToken        string\n\tBaseURL            string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tDebug:              env.GetOrDefaultBool(EnvDebug, false),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *dnsimple.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for dnsimple.\n// Credentials must be passed in the environment variable: DNSIMPLE_OAUTH_TOKEN.\n//\n// See: https://developer.dnsimple.com/v2/#authentication\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tconfig := NewDefaultConfig()\n\tconfig.AccessToken = env.GetOrFile(EnvOAuthToken)\n\tconfig.BaseURL = env.GetOrFile(EnvBaseURL)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for DNSimple.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"dnsimple: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.AccessToken == \"\" {\n\t\treturn nil, errors.New(\"dnsimple: OAuth token is missing\")\n\t}\n\n\tclient := dnsimple.NewClient(\n\t\tclientdebug.Wrap(\n\t\t\toauth2.NewClient(\n\t\t\t\tcontext.Background(),\n\t\t\t\toauth2.StaticTokenSource(&oauth2.Token{AccessToken: config.AccessToken}),\n\t\t\t),\n\t\t),\n\t)\n\tclient.SetUserAgent(useragent.Get())\n\n\tif config.BaseURL != \"\" {\n\t\tclient.BaseURL = config.BaseURL\n\t}\n\n\tclient.Debug = config.Debug\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneName, err := d.getHostedZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsimple: %w\", err)\n\t}\n\n\taccountID, err := d.getAccountID(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsimple: %w\", err)\n\t}\n\n\trecordAttributes, err := newTxtRecord(zoneName, info.EffectiveFQDN, info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsimple: %w\", err)\n\t}\n\n\t_, err = d.client.Zones.CreateRecord(ctx, accountID, zoneName, recordAttributes)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsimple: API call failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecords, err := d.findTxtRecords(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsimple: %w\", err)\n\t}\n\n\taccountID, err := d.getAccountID(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsimple: %w\", err)\n\t}\n\n\tvar lastErr error\n\n\tfor _, rec := range records {\n\t\t_, err := d.client.Zones.DeleteRecord(ctx, accountID, rec.ZoneID, rec.ID)\n\t\tif err != nil {\n\t\t\tlastErr = fmt.Errorf(\"dnsimple: %w\", err)\n\t\t}\n\t}\n\n\treturn lastErr\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (string, error) {\n\tauthZone, err := dns01.FindZoneByFqdn(domain)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not find zone for FQDN %q: %w\", domain, err)\n\t}\n\n\taccountID, err := d.getAccountID(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thostedZone, err := d.client.Zones.GetZone(ctx, accountID, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"get zone: %w\", err)\n\t}\n\n\tif hostedZone == nil || hostedZone.Data == nil || hostedZone.Data.ID == 0 {\n\t\treturn \"\", fmt.Errorf(\"zone %s not found in DNSimple for domain %s\", authZone, domain)\n\t}\n\n\treturn hostedZone.Data.Name, nil\n}\n\nfunc (d *DNSProvider) findTxtRecords(ctx context.Context, fqdn string) ([]dnsimple.ZoneRecord, error) {\n\tzoneName, err := d.getHostedZone(ctx, fqdn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\taccountID, err := d.getAccountID(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, zoneName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult, err := d.client.Zones.ListRecords(ctx, accountID, zoneName, &dnsimple.ZoneRecordListOptions{Name: &subDomain, Type: dnsimple.String(\"TXT\"), ListOptions: dnsimple.ListOptions{}})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"API call has failed: %w\", err)\n\t}\n\n\treturn result.Data, nil\n}\n\nfunc newTxtRecord(zoneName, fqdn, value string, ttl int) (dnsimple.ZoneRecordAttributes, error) {\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, zoneName)\n\tif err != nil {\n\t\treturn dnsimple.ZoneRecordAttributes{}, err\n\t}\n\n\treturn dnsimple.ZoneRecordAttributes{\n\t\tType:    \"TXT\",\n\t\tName:    &subDomain,\n\t\tContent: value,\n\t\tTTL:     ttl,\n\t}, nil\n}\n\nfunc (d *DNSProvider) getAccountID(ctx context.Context) (string, error) {\n\twhoamiResponse, err := d.client.Identity.Whoami(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif whoamiResponse.Data.Account == nil {\n\t\treturn \"\", errors.New(\"user tokens are not supported, please use an account token\")\n\t}\n\n\treturn strconv.FormatInt(whoamiResponse.Data.Account.ID, 10), nil\n}\n"
  },
  {
    "path": "providers/dns/dnsimple/dnsimple.toml",
    "content": "Name = \"DNSimple\"\nDescription = ''''''\nURL = \"https://dnsimple.com/\"\nCode = \"dnsimple\"\nSince = \"v0.3.0\"\n\nExample = '''\nDNSIMPLE_OAUTH_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \\\nlego --dns dnsimple -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Description\n\n`DNSIMPLE_BASE_URL` is optional and must be set to production (https://api.dnsimple.com).\nif `DNSIMPLE_BASE_URL` is not defined or empty, the production URL is used by default.\n\nWhile you can manage DNS records in the [DNSimple Sandbox environment](https://developer.dnsimple.com/sandbox/),\nDNS records will not resolve, and you will not be able to satisfy the ACME DNS challenge.\n\nTo authenticate you need to provide a valid API token.\nHTTP Basic Authentication is intentionally not supported.\n\n### API tokens\n\nYou can [generate a new API token](https://support.dnsimple.com/articles/api-access-token/) from your account page.\nOnly Account API tokens are supported, if you try to use a User API token you will receive an error message.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DNSIMPLE_OAUTH_TOKEN = \"OAuth token\"\n  [Configuration.Additional]\n    DNSIMPLE_BASE_URL = \"API endpoint URL\"\n    DNSIMPLE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    DNSIMPLE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    DNSIMPLE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n\n[Links]\n  API = \"https://developer.dnsimple.com/v2/\"\n  GoClient = \"https://github.com/dnsimple/dnsimple-go\"\n"
  },
  {
    "path": "providers/dns/dnsimple/dnsimple_test.go",
    "content": "package dnsimple\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst sandboxURL = \"https://api.sandbox.fake.com\"\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvOAuthToken,\n\tEnvBaseURL).\n\tWithDomain(envDomain).\n\tWithLiveTestRequirements(EnvOAuthToken, envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvOAuthToken: \"my_token\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success: base url\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvOAuthToken: \"my_token\",\n\t\t\t\tEnvBaseURL:    \"https://api.dnsimple.test\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing oauth token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvOAuthToken: \"\",\n\t\t\t},\n\t\t\texpected: \"dnsimple: OAuth token is missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\n\t\t\t\tbaseURL := os.Getenv(EnvBaseURL)\n\t\t\t\tif baseURL != \"\" {\n\t\t\t\t\tassert.Equal(t, baseURL, p.client.BaseURL)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc        string\n\t\taccessToken string\n\t\tbaseURL     string\n\t\texpected    string\n\t}{\n\t\t{\n\t\t\tdesc:        \"success\",\n\t\t\taccessToken: \"my_token\",\n\t\t\tbaseURL:     \"\",\n\t\t},\n\t\t{\n\t\t\tdesc:        \"success: base url\",\n\t\t\taccessToken: \"my_token\",\n\t\t\tbaseURL:     \"https://api.dnsimple.test\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing oauth token\",\n\t\t\texpected: \"dnsimple: OAuth token is missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AccessToken = test.accessToken\n\t\t\tconfig.BaseURL = test.baseURL\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\n\t\t\t\tif test.baseURL != \"\" {\n\t\t\t\t\tassert.Equal(t, test.baseURL, p.client.BaseURL)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tif os.Getenv(EnvBaseURL) == \"\" {\n\t\tos.Setenv(EnvBaseURL, sandboxURL)\n\t}\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tif os.Getenv(EnvBaseURL) == \"\" {\n\t\tos.Setenv(EnvBaseURL, sandboxURL)\n\t}\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/dnsmadeeasy/dnsmadeeasy.go",
    "content": "// Package dnsmadeeasy implements a DNS provider for solving the DNS-01 challenge using DNS Made Easy.\npackage dnsmadeeasy\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DNSMADEEASY_\"\n\n\tEnvAPIKey    = envNamespace + \"API_KEY\"\n\tEnvAPISecret = envNamespace + \"API_SECRET\"\n\tEnvSandbox   = envNamespace + \"SANDBOX\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL            string\n\tAPIKey             string\n\tAPISecret          string\n\tSandbox            bool\n\tHTTPClient         *http.Client\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\ttr := &http.Transport{}\n\n\tdefaultTransport, ok := http.DefaultTransport.(*http.Transport)\n\tif ok {\n\t\ttr = defaultTransport.Clone()\n\t}\n\n\ttr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}\n\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout:   env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t\tTransport: tr,\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for DNSMadeEasy DNS.\n// Credentials must be passed in the environment variables:\n// DNSMADEEASY_API_KEY and DNSMADEEASY_API_SECRET.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey, EnvAPISecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dnsmadeeasy: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Sandbox = env.GetOrDefaultBool(EnvSandbox, false)\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.APISecret = values[EnvAPISecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for DNS Made Easy.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"dnsmadeeasy: the configuration of the DNS provider is nil\")\n\t}\n\n\tvar baseURL string\n\tif config.Sandbox {\n\t\tbaseURL = internal.DefaultSandboxBaseURL\n\t} else {\n\t\tif config.BaseURL == \"\" {\n\t\t\tbaseURL = internal.DefaultProdBaseURL\n\t\t} else {\n\t\t\tbaseURL = config.BaseURL\n\t\t}\n\t}\n\n\tclient, err := internal.NewClient(config.APIKey, config.APISecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dnsmadeeasy: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\tclient.BaseURL, err = url.Parse(baseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &DNSProvider{\n\t\tclient: client,\n\t\tconfig: config,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domainName, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domainName, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsmadeeasy: could not find zone for domain %q: %w\", domainName, err)\n\t}\n\n\tctx := context.Background()\n\n\t// fetch the domain details\n\tdomain, err := d.client.GetDomain(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsmadeeasy: unable to get domain for zone %s: %w\", authZone, err)\n\t}\n\n\t// create the TXT record\n\tname := strings.Replace(info.EffectiveFQDN, \".\"+authZone, \"\", 1)\n\trecord := &internal.Record{Type: \"TXT\", Name: name, Value: info.Value, TTL: d.config.TTL}\n\n\terr = d.client.CreateRecord(ctx, domain, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsmadeeasy: unable to create record for %s: %w\", name, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT records matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domainName, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsmadeeasy: could not find zone for domain %q: %w\", domainName, err)\n\t}\n\n\tctx := context.Background()\n\n\t// fetch the domain details\n\tdomain, err := d.client.GetDomain(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsmadeeasy: unable to get domain for zone %s: %w\", authZone, err)\n\t}\n\n\t// find matching records\n\tname := strings.Replace(info.EffectiveFQDN, \".\"+authZone, \"\", 1)\n\n\trecords, err := d.client.GetRecords(ctx, domain, name, \"TXT\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnsmadeeasy: unable to get records for domain %s: %w\", domain.Name, err)\n\t}\n\n\t// delete records\n\tvar lastError error\n\n\tfor _, record := range *records {\n\t\terr = d.client.DeleteRecord(ctx, record)\n\t\tif err != nil {\n\t\t\tlastError = fmt.Errorf(\"dnsmadeeasy: unable to delete record [id=%d, name=%s]: %w\", record.ID, record.Name, err)\n\t\t}\n\t}\n\n\treturn lastError\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/dnsmadeeasy/dnsmadeeasy.toml",
    "content": "Name = \"DNS Made Easy\"\nDescription = ''''''\nURL = \"https://dnsmadeeasy.com/\"\nCode = \"dnsmadeeasy\"\nSince = \"v0.4.0\"\n\nExample = '''\nDNSMADEEASY_API_KEY=xxxxxx \\\nDNSMADEEASY_API_SECRET=yyyyy \\\nlego --dns dnsmadeeasy -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DNSMADEEASY_API_KEY = \"The API key\"\n    DNSMADEEASY_API_SECRET = \"The API Secret key\"\n  [Configuration.Additional]\n    DNSMADEEASY_SANDBOX = \"Activate the sandbox (boolean)\"\n    DNSMADEEASY_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    DNSMADEEASY_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    DNSMADEEASY_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    DNSMADEEASY_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://api-docs.dnsmadeeasy.com/\"\n"
  },
  {
    "path": "providers/dns/dnsmadeeasy/dnsmadeeasy_test.go",
    "content": "package dnsmadeeasy\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIKey,\n\tEnvAPISecret).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\tos.Setenv(EnvSandbox, \"true\")\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"123\",\n\t\t\t\tEnvAPISecret: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"\",\n\t\t\t\tEnvAPISecret: \"\",\n\t\t\t},\n\t\t\texpected: \"dnsmadeeasy: some credentials information are missing: DNSMADEEASY_API_KEY,DNSMADEEASY_API_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"\",\n\t\t\t\tEnvAPISecret: \"456\",\n\t\t\t},\n\t\t\texpected: \"dnsmadeeasy: some credentials information are missing: DNSMADEEASY_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"123\",\n\t\t\t\tEnvAPISecret: \"\",\n\t\t\t},\n\t\t\texpected: \"dnsmadeeasy: some credentials information are missing: DNSMADEEASY_API_SECRET\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\tos.Setenv(EnvSandbox, \"true\")\n\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tapiKey    string\n\t\tapiSecret string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tapiKey:    \"123\",\n\t\t\tapiSecret: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"dnsmadeeasy: credentials missing: API key\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing api key\",\n\t\t\tapiSecret: \"456\",\n\t\t\texpected:  \"dnsmadeeasy: credentials missing: API key\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret key\",\n\t\t\tapiKey:   \"123\",\n\t\t\texpected: \"dnsmadeeasy: credentials missing: API secret\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.APISecret = test.apiSecret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresentAndCleanup(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tos.Setenv(EnvSandbox, \"true\")\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/dnsmadeeasy/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// Default API endpoints.\nconst (\n\tDefaultSandboxBaseURL = \"https://api.sandbox.dnsmadeeasy.com/V2.0\"\n\tDefaultProdBaseURL    = \"https://api.dnsmadeeasy.com/V2.0\"\n)\n\n// Client DNSMadeEasy client.\ntype Client struct {\n\tapiKey    string\n\tapiSecret string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a DNSMadeEasy client.\nfunc NewClient(apiKey, apiSecret string) (*Client, error) {\n\tif apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing: API key\")\n\t}\n\n\tif apiSecret == \"\" {\n\t\treturn nil, errors.New(\"credentials missing: API secret\")\n\t}\n\n\tbaseURL, _ := url.Parse(DefaultProdBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tapiSecret:  apiSecret,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}, nil\n}\n\n// GetDomain gets a domain.\nfunc (c *Client) GetDomain(ctx context.Context, authZone string) (*Domain, error) {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", \"managed\", \"name\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"domainname\", dns01.UnFqdn(authZone))\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdomain := &Domain{}\n\n\terr = c.do(req, domain)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn domain, nil\n}\n\n// GetRecords gets all TXT records.\nfunc (c *Client) GetRecords(ctx context.Context, domain *Domain, recordName, recordType string) (*[]Record, error) {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", \"managed\", strconv.Itoa(domain.ID), \"records\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"recordName\", recordName)\n\tquery.Set(\"type\", recordType)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trecords := &recordsResponse{}\n\n\terr = c.do(req, records)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn records.Records, nil\n}\n\n// CreateRecord creates a TXT records.\nfunc (c *Client) CreateRecord(ctx context.Context, domain *Domain, record *Record) error {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", \"managed\", strconv.Itoa(domain.ID), \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\n// DeleteRecord deletes a TXT records.\nfunc (c *Client) DeleteRecord(ctx context.Context, record Record) error {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", \"managed\", strconv.Itoa(record.SourceID), \"records\", strconv.Itoa(record.ID))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\terr := c.sign(req, time.Now().UTC().Format(time.RFC1123))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif err = json.Unmarshal(raw, result); err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) sign(req *http.Request, timestamp string) error {\n\tsignature, err := computeHMAC(timestamp, c.apiSecret)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Set(\"x-dnsme-apiKey\", c.apiKey)\n\treq.Header.Set(\"x-dnsme-requestDate\", timestamp)\n\treq.Header.Set(\"x-dnsme-hmac\", signature)\n\n\treturn nil\n}\n\nfunc computeHMAC(message, secret string) (string, error) {\n\tkey := []byte(secret)\n\th := hmac.New(sha1.New, key)\n\n\t_, err := h.Write([]byte(message))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn hex.EncodeToString(h.Sum(nil)), nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/dnsmadeeasy/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"key\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(\"x-dnsme-apiKey\", \"key\").\n\t\t\tWithRegexp(\"x-dnsme-requestDate\", `\\w+, \\d+ \\w+ \\d+ \\d+:\\d+:\\d+ UTC`).\n\t\t\tWithRegexp(\"x-dnsme-hmac\", `[a-z0-9]+`),\n\t)\n}\n\nfunc TestClient_GetDomain(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/managed/name\",\n\t\t\tservermock.RawStringResponse(`{\"id\": 1, \"name\": \"foo\"}`),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"domainname\", \"example.com\")).\n\t\tBuild(t)\n\n\tdomain, err := client.GetDomain(t.Context(), \"example.com.\")\n\trequire.NoError(t, err)\n\n\texpected := &Domain{\n\t\tID:   1,\n\t\tName: \"foo\",\n\t}\n\n\tassert.Equal(t, expected, domain)\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/managed/1/records\",\n\t\t\tservermock.ResponseFromFixture(\"get_records.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"recordName\", \"foo\").\n\t\t\t\tWith(\"type\", \"TXT\"),\n\t\t).\n\t\tBuild(t)\n\n\tdomain := &Domain{ID: 1, Name: \"foo\"}\n\n\trecords, err := client.GetRecords(t.Context(), domain, \"foo\", \"TXT\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tID:       1,\n\t\t\tType:     \"TXT\",\n\t\t\tName:     \"foo\",\n\t\t\tValue:    \"aaa\",\n\t\t\tTTL:      60,\n\t\t\tSourceID: 123,\n\t\t},\n\t\t{\n\t\t\tID:       2,\n\t\t\tType:     \"TXT\",\n\t\t\tName:     \"bar\",\n\t\t\tValue:    \"bbb\",\n\t\t\tTTL:      120,\n\t\t\tSourceID: 456,\n\t\t},\n\t}\n\n\tassert.Equal(t, &expected, records)\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/managed/1/records\", nil,\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\tdomain := &Domain{ID: 1, Name: \"foo\"}\n\n\trecord := &Record{\n\t\tID:       1,\n\t\tType:     \"TXT\",\n\t\tName:     \"foo\",\n\t\tValue:    \"aaa\",\n\t\tTTL:      60,\n\t\tSourceID: 123,\n\t}\n\n\terr := client.CreateRecord(t.Context(), domain, record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns/managed/123/records/1\", nil).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tID:       1,\n\t\tType:     \"TXT\",\n\t\tName:     \"foo\",\n\t\tValue:    \"aaa\",\n\t\tTTL:      60,\n\t\tSourceID: 123,\n\t}\n\n\terr := client.DeleteRecord(t.Context(), record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_sign(t *testing.T) {\n\tapiKey := \"key\"\n\n\tclient := Client{apiKey: apiKey, apiSecret: \"secret\"}\n\n\treq, err := http.NewRequest(http.MethodGet, \"\", http.NoBody)\n\trequire.NoError(t, err)\n\n\ttimestamp := time.Date(2015, time.June, 2, 2, 36, 7, 0, time.UTC).Format(time.RFC1123)\n\n\terr = client.sign(req, timestamp)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, apiKey, req.Header.Get(\"x-dnsme-apiKey\"))\n\tassert.Equal(t, timestamp, req.Header.Get(\"x-dnsme-requestDate\"))\n\tassert.Equal(t, \"6b6c8432119c31e1d3776eb4cd3abd92fae4a71c\", req.Header.Get(\"x-dnsme-hmac\"))\n}\n"
  },
  {
    "path": "providers/dns/dnsmadeeasy/internal/fixtures/create_record-request.json",
    "content": "{\n  \"id\": 1,\n  \"type\": \"TXT\",\n  \"name\": \"foo\",\n  \"value\": \"aaa\",\n  \"ttl\": 60,\n  \"sourceId\": 123\n}\n"
  },
  {
    "path": "providers/dns/dnsmadeeasy/internal/fixtures/get_records.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"type\": \"TXT\",\n      \"name\": \"foo\",\n      \"value\": \"aaa\",\n      \"ttl\": 60,\n      \"sourceId\": 123\n    },\n    {\n      \"id\": 2,\n      \"type\": \"TXT\",\n      \"name\": \"bar\",\n      \"value\": \"bbb\",\n      \"ttl\": 120,\n      \"sourceId\": 456\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/dnsmadeeasy/internal/types.go",
    "content": "package internal\n\n// Domain holds the DNSMadeEasy API representation of a Domain.\ntype Domain struct {\n\tID   int    `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\n// Record holds the DNSMadeEasy API representation of a Domain Record.\ntype Record struct {\n\tID       int    `json:\"id\"`\n\tType     string `json:\"type\"`\n\tName     string `json:\"name\"`\n\tValue    string `json:\"value\"`\n\tTTL      int    `json:\"ttl\"`\n\tSourceID int    `json:\"sourceId\"`\n}\n\ntype recordsResponse struct {\n\tRecords *[]Record `json:\"data\"`\n}\n"
  },
  {
    "path": "providers/dns/dnspod/dnspod.go",
    "content": "// Package dnspod implements a DNS provider for solving the DNS-01 challenge using dnspod DNS.\npackage dnspod\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/nrdcg/dnspod-go\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DNSPOD_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tLoginToken         string\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *dnspod.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for dnspod.\n// Credentials must be passed in the environment variables: DNSPOD_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dnspod: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.LoginToken = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for dnspod.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"dnspod: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.LoginToken == \"\" {\n\t\treturn nil, errors.New(\"dnspod: credentials missing\")\n\t}\n\n\tparams := dnspod.CommonParams{LoginToken: config.LoginToken, Format: \"json\"}\n\n\tclient := dnspod.NewClient(params)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneID, zoneName, err := d.getHostedZone(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trecordAttributes, err := d.newTxtRecord(zoneName, info.EffectiveFQDN, info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, _, err = d.client.Records.Create(zoneID, *recordAttributes)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"API call failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneID, zoneName, err := d.getHostedZone(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trecords, err := d.findTxtRecords(info.EffectiveFQDN, zoneID, zoneName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, rec := range records {\n\t\t_, err := d.client.Records.Delete(zoneID, rec.ID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) getHostedZone(domain string) (string, string, error) {\n\tzones, _, err := d.client.Domains.List()\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"API call failed: %w\", err)\n\t}\n\n\tauthZone, err := dns01.FindZoneByFqdn(domain)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tvar hostedZone dnspod.Domain\n\n\tfor _, zone := range zones {\n\t\tif zone.Name == dns01.UnFqdn(authZone) {\n\t\t\thostedZone = zone\n\t\t}\n\t}\n\n\tif hostedZone.ID == \"\" || hostedZone.ID == \"0\" {\n\t\treturn \"\", \"\", fmt.Errorf(\"zone %s not found for domain %s\", authZone, domain)\n\t}\n\n\treturn hostedZone.ID.String(), hostedZone.Name, nil\n}\n\nfunc (d *DNSProvider) newTxtRecord(zone, fqdn, value string, ttl int) (*dnspod.Record, error) {\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, zone)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &dnspod.Record{\n\t\tType:  \"TXT\",\n\t\tName:  subDomain,\n\t\tValue: value,\n\t\tLine:  \"默认\",\n\t\tTTL:   strconv.Itoa(ttl),\n\t}, nil\n}\n\nfunc (d *DNSProvider) findTxtRecords(fqdn, zoneID, zoneName string) ([]dnspod.Record, error) {\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, zoneName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar records []dnspod.Record\n\n\tresult, _, err := d.client.Records.List(zoneID, subDomain)\n\tif err != nil {\n\t\treturn records, fmt.Errorf(\"API call has failed: %w\", err)\n\t}\n\n\tfor _, record := range result {\n\t\tif record.Name == subDomain {\n\t\t\trecords = append(records, record)\n\t\t}\n\t}\n\n\treturn records, nil\n}\n"
  },
  {
    "path": "providers/dns/dnspod/dnspod.toml",
    "content": "Name = \"DNSPod (deprecated)\"\nDescription = '''\nUse the Tencent Cloud provider instead.\n'''\nURL = \"https://www.dnspod.com/\"\nCode = \"dnspod\"\nSince = \"v0.4.0\"\n\nExample = '''\nDNSPOD_API_KEY=xxxxxx \\\nlego --dns dnspod -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DNSPOD_API_KEY = \"The user token\"\n  [Configuration.Additional]\n    DNSPOD_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    DNSPOD_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    DNSPOD_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)\"\n    DNSPOD_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://docs.dnspod.com/api/\"\n  GoClient = \"https://github.com/nrdcg/dnspod-go\"\n"
  },
  {
    "path": "providers/dns/dnspod/dnspod_test.go",
    "content": "package dnspod\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"dnspod: some credentials information are missing: DNSPOD_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc       string\n\t\tloginToken string\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tdesc:       \"success\",\n\t\t\tloginToken: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"dnspod: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.LoginToken = test.loginToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/dode/dode.go",
    "content": "// Package dode implements a DNS provider for solving the DNS-01 challenge using do.de.\npackage dode\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dode/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DODE_\"\n\n\tEnvToken = envNamespace + \"TOKEN\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken              string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a new DNS provider using\n// environment variable DODE_TOKEN for adding and removing the DNS record.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"do.de: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for do.de.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"do.de: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"do.de: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(config.Token)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\treturn d.client.UpdateTxtRecord(context.Background(), info.EffectiveFQDN, info.Value, false)\n}\n\n// CleanUp clears TXT record.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\treturn d.client.UpdateTxtRecord(context.Background(), info.EffectiveFQDN, \"\", true)\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n"
  },
  {
    "path": "providers/dns/dode/dode.toml",
    "content": "Name = \"Domain Offensive (do.de)\"\nDescription = ''''''\nURL = \"https://www.do.de/\"\nCode = \"dode\"\nSince = \"v2.4.0\"\n\nExample = '''\nDODE_TOKEN=xxxxxx \\\nlego --dns dode -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DODE_TOKEN = \"API token\"\n  [Configuration.Additional]\n    DODE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    DODE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    DODE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n    DODE_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n\n[Links]\n  API = \"https://www.do.de/wiki/freie-ssl-tls-zertifikate-ueber-acme/\"\n"
  },
  {
    "path": "providers/dns/dode/dode_test.go",
    "content": "package dode\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"\",\n\t\t\t},\n\t\t\texpected: \"do.de: some credentials information are missing: DODE_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"do.de: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/dode/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://my.do.de/api\"\n\n// Client the do.de API client.\ntype Client struct {\n\ttoken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(token string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\ttoken:      token,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// UpdateTxtRecord Update the domains TXT record\n// To update the TXT record we just need to make one simple get request.\nfunc (c *Client) UpdateTxtRecord(ctx context.Context, fqdn, txt string, clearRecord bool) error {\n\tendpoint := c.baseURL.JoinPath(\"letsencrypt\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"token\", c.token)\n\tquery.Set(\"domain\", dns01.UnFqdn(fqdn))\n\n\t// api call differs per set/delete\n\tif clearRecord {\n\t\tquery.Set(\"action\", \"delete\")\n\t} else {\n\t\tquery.Set(\"value\", txt)\n\t}\n\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar response apiResponse\n\n\terr = json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tbody := string(raw)\n\tif !response.Success {\n\t\treturn fmt.Errorf(\"request to change TXT record for do.de returned the following error result (%s); used url [%s]\", body, endpoint)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/dode/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"secret\")\n\tclient.HTTPClient = server.Client()\n\tclient.baseURL, _ = url.Parse(server.URL)\n\n\treturn client, nil\n}\n\nfunc TestClient_UpdateTxtRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /letsencrypt\", servermock.ResponseFromFixture(\"success.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\tWith(\"token\", \"secret\").\n\t\t\t\tWith(\"value\", \"value\")).\n\t\tBuild(t)\n\n\terr := client.UpdateTxtRecord(t.Context(), \"example.com.\", \"value\", false)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_UpdateTxtRecord_clear(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /letsencrypt\", servermock.ResponseFromFixture(\"success.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"action\", \"delete\").\n\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\tWith(\"token\", \"secret\")).\n\t\tBuild(t)\n\n\terr := client.UpdateTxtRecord(t.Context(), \"example.com.\", \"value\", true)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/dode/internal/fixtures/success.json",
    "content": "{\n  \"Domain\" : \"example.com\",\n  \"Success\": true\n}\n"
  },
  {
    "path": "providers/dns/dode/internal/types.go",
    "content": "package internal\n\ntype apiResponse struct {\n\tDomain  string\n\tSuccess bool\n}\n"
  },
  {
    "path": "providers/dns/domeneshop/domeneshop.go",
    "content": "// Package domeneshop implements a DNS provider for solving the DNS-01 challenge using domeneshop DNS.\npackage domeneshop\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/domeneshop/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DOMENESHOP_\"\n\n\tEnvAPIToken  = envNamespace + \"API_TOKEN\"\n\tEnvAPISecret = envNamespace + \"API_SECRET\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIToken           string\n\tAPISecret          string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 20*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for domeneshop.\n// Credentials must be passed in the environment variables:\n// DOMENESHOP_API_TOKEN, DOMENESHOP_API_SECRET.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIToken, EnvAPISecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"domeneshop: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIToken = values[EnvAPIToken]\n\tconfig.APISecret = values[EnvAPISecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Domeneshop.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"domeneshop: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIToken == \"\" || config.APISecret == \"\" {\n\t\treturn nil, errors.New(\"domeneshop: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(config.APIToken, config.APISecret)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, host, err := d.splitDomain(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"domeneshop: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\tdomainInstance, err := d.client.GetDomainByName(ctx, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"domeneshop: %w\", err)\n\t}\n\n\terr = d.client.CreateTXTRecord(ctx, domainInstance, host, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"domeneshop: failed to create record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, host, err := d.splitDomain(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"domeneshop: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\tdomainInstance, err := d.client.GetDomainByName(ctx, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"domeneshop: %w\", err)\n\t}\n\n\tif err := d.client.DeleteTXTRecord(ctx, domainInstance, host, info.Value); err != nil {\n\t\treturn fmt.Errorf(\"domeneshop: failed to create record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// splitDomain splits the hostname from the authoritative zone, and returns both parts (non-fqdn).\nfunc (d *DNSProvider) splitDomain(fqdn string) (string, string, error) {\n\tzone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, zone)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\treturn dns01.UnFqdn(zone), subDomain, nil\n}\n"
  },
  {
    "path": "providers/dns/domeneshop/domeneshop.toml",
    "content": "Name = \"Domeneshop\"\nDescription = ''''''\nURL = \"https://domene.shop\"\nCode = \"domeneshop\"\nAliases = [\"domainnameshop\"]\nSince = \"v4.3.0\"\n\nExample = '''\nDOMENESHOP_API_TOKEN=<token> \\\nDOMENESHOP_API_SECRET=<secret> \\\nlego --dns domeneshop -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n### API credentials\n\nVisit the following page for information on how to create API credentials with Domeneshop:\n\n  https://api.domeneshop.no/docs/#section/Authentication\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DOMENESHOP_API_TOKEN = \"API token\"\n    DOMENESHOP_API_SECRET = \"API secret\"\n  [Configuration.Additional]\n    DOMENESHOP_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 20)\"\n    DOMENESHOP_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 300)\"\n    DOMENESHOP_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api.domeneshop.no/docs\"\n"
  },
  {
    "path": "providers/dns/domeneshop/domeneshop_test.go",
    "content": "package domeneshop\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIToken,\n\tEnvAPISecret).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken:  \"A\",\n\t\t\t\tEnvAPISecret: \"B\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken:  \"\",\n\t\t\t\tEnvAPISecret: \"\",\n\t\t\t},\n\t\t\texpected: \"domeneshop: some credentials information are missing: DOMENESHOP_API_TOKEN,DOMENESHOP_API_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken:  \"\",\n\t\t\t\tEnvAPISecret: \"A\",\n\t\t\t},\n\t\t\texpected: \"domeneshop: some credentials information are missing: DOMENESHOP_API_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken:  \"A\",\n\t\t\t\tEnvAPISecret: \"\",\n\t\t\t},\n\t\t\texpected: \"domeneshop: some credentials information are missing: DOMENESHOP_API_SECRET\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tapiSecret string\n\t\tapiToken  string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tapiToken:  \"A\",\n\t\t\tapiSecret: \"B\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"domeneshop: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing api token\",\n\t\t\tapiToken:  \"\",\n\t\t\tapiSecret: \"B\",\n\t\t\texpected:  \"domeneshop: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing api secret\",\n\t\t\tapiToken:  \"A\",\n\t\t\tapiSecret: \"\",\n\t\t\texpected:  \"domeneshop: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\n\t\t\tconfig.APIToken = test.apiToken\n\t\t\tconfig.APISecret = test.apiSecret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/domeneshop/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL string = \"https://api.domeneshop.no/v0\"\n\n// Client implements a very simple wrapper around the Domeneshop API.\n// For now, it will only deal with adding and removing TXT records, as required by ACME providers.\n// https://api.domeneshop.no/docs/\ntype Client struct {\n\tapiToken  string\n\tapiSecret string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient returns an instance of the Domeneshop API wrapper.\nfunc NewClient(apiToken, apiSecret string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiToken:   apiToken,\n\t\tapiSecret:  apiSecret,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// GetDomainByName fetches the domain list and returns the Domain object for the matching domain.\n// https://api.domeneshop.no/docs/#operation/getDomains\nfunc (c *Client) GetDomainByName(ctx context.Context, domain string) (*Domain, error) {\n\tendpoint := c.baseURL.JoinPath(\"domains\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar domains []Domain\n\n\terr = c.do(req, &domains)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, d := range domains {\n\t\tif !d.Services.DNS {\n\t\t\t// Domains without DNS service cannot have DNS record added.\n\t\t\tcontinue\n\t\t}\n\n\t\tif d.Name == domain {\n\t\t\treturn &d, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"failed to find matching domain name: %s\", domain)\n}\n\n// CreateTXTRecord creates a TXT record with the provided host (subdomain) and data.\n// https://api.domeneshop.no/docs/#tag/dns/paths/~1domains~1{domainId}~1dns/post\nfunc (c *Client) CreateTXTRecord(ctx context.Context, domain *Domain, host, data string) error {\n\tendpoint := c.baseURL.JoinPath(\"domains\", strconv.Itoa(domain.ID), \"dns\")\n\n\trecord := DNSRecord{\n\t\tData: data,\n\t\tHost: host,\n\t\tTTL:  300,\n\t\tType: \"TXT\",\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\n// DeleteTXTRecord deletes the DNS record matching the provided host and data.\n// https://api.domeneshop.no/docs/#tag/dns/paths/~1domains~1{domainId}~1dns~1{recordId}/delete\nfunc (c *Client) DeleteTXTRecord(ctx context.Context, domain *Domain, host, data string) error {\n\trecord, err := c.getDNSRecordByHostData(ctx, *domain, host, data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tendpoint := c.baseURL.JoinPath(\"domains\", strconv.Itoa(domain.ID), \"dns\", strconv.Itoa(record.ID))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\n// getDNSRecordByHostData finds the first matching DNS record with the provided host and data.\n// https://api.domeneshop.no/docs/#operation/getDnsRecords\nfunc (c *Client) getDNSRecordByHostData(ctx context.Context, domain Domain, host, data string) (*DNSRecord, error) {\n\tendpoint := c.baseURL.JoinPath(\"domains\", strconv.Itoa(domain.ID), \"dns\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar records []DNSRecord\n\n\terr = c.do(req, &records)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, r := range records {\n\t\tif r.Host == host && r.Data == data {\n\t\t\treturn &r, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"failed to find record with host %s for domain %s\", host, domain.Name)\n}\n\n// do a request against the API,\n// and makes sure that the required Authorization header is set using `setBasicAuth`.\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.SetBasicAuth(c.apiToken, c.apiSecret)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode >= http.StatusBadRequest {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/domeneshop/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"token\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithBasicAuth(\"token\", \"secret\"),\n\t)\n}\n\nfunc TestClient_CreateTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/1/dns\",\n\t\t\tservermock.ResponseFromFixture(\"create_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\terr := client.CreateTXTRecord(t.Context(), &Domain{ID: 1}, \"example.com\", \"txtTXTtxt\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains/1/dns\",\n\t\t\tservermock.ResponseFromFixture(\"delete_record.json\")).\n\t\tRoute(\"DELETE /domains/1/dns/1\", nil).\n\t\tBuild(t)\n\n\terr := client.DeleteTXTRecord(t.Context(), &Domain{ID: 1}, \"example.com\", \"txtTXTtxt\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_getDNSRecordByHostData(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains/1/dns\",\n\t\t\tservermock.ResponseFromFixture(\"getDnsRecords.json\")).\n\t\tBuild(t)\n\n\trecord, err := client.getDNSRecordByHostData(t.Context(), Domain{ID: 1}, \"example.com\", \"txtTXTtxt\")\n\trequire.NoError(t, err)\n\n\texpected := &DNSRecord{\n\t\tID:   1,\n\t\tType: \"TXT\",\n\t\tHost: \"example.com\",\n\t\tData: \"txtTXTtxt\",\n\t\tTTL:  3600,\n\t}\n\n\tassert.Equal(t, expected, record)\n}\n\nfunc TestClient_GetDomainByName(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains/\",\n\t\t\tservermock.ResponseFromFixture(\"getDomains.json\")).\n\t\tBuild(t)\n\n\tdomain, err := client.GetDomainByName(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := &Domain{\n\t\tName:           \"example.com\",\n\t\tID:             1,\n\t\tExpiryDate:     \"2019-08-24\",\n\t\tNameservers:    []string{\"ns1.hyp.net\", \"ns2.hyp.net\", \"ns3.hyp.net\"},\n\t\tRegisteredDate: \"2019-08-24\",\n\t\tRegistrant:     \"Ola Nordmann\",\n\t\tRenew:          true,\n\t\tServices: Service{\n\t\t\tDNS:       true,\n\t\t\tEmail:     true,\n\t\t\tRegistrar: true,\n\t\t\tWebhotel:  \"none\",\n\t\t},\n\t\tStatus: \"active\",\n\t}\n\n\tassert.Equal(t, expected, domain)\n}\n"
  },
  {
    "path": "providers/dns/domeneshop/internal/fixtures/create_record-request.json",
    "content": "{\n  \"data\": \"txtTXTtxt\",\n  \"host\": \"example.com\",\n  \"id\": 0,\n  \"ttl\": 300,\n  \"type\": \"TXT\"\n}\n"
  },
  {
    "path": "providers/dns/domeneshop/internal/fixtures/create_record.json",
    "content": "{\n  \"id\": 1\n}\n"
  },
  {
    "path": "providers/dns/domeneshop/internal/fixtures/delete_record.json",
    "content": "[\n  {\n    \"id\": 1,\n    \"host\": \"example.com\",\n    \"ttl\": 3600,\n    \"type\": \"TXT\",\n    \"data\": \"txtTXTtxt\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/domeneshop/internal/fixtures/getDnsRecords.json",
    "content": "[\n  {\n    \"id\": 1,\n    \"host\": \"example.com\",\n    \"ttl\": 3600,\n    \"type\": \"TXT\",\n    \"data\": \"txtTXTtxt\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/domeneshop/internal/fixtures/getDomains.json",
    "content": "[\n  {\n    \"id\": 1,\n    \"domain\": \"example.com\",\n    \"expiry_date\": \"2019-08-24\",\n    \"registered_date\": \"2019-08-24\",\n    \"renew\": true,\n    \"registrant\": \"Ola Nordmann\",\n    \"status\": \"active\",\n    \"nameservers\": [\n      \"ns1.hyp.net\",\n      \"ns2.hyp.net\",\n      \"ns3.hyp.net\"\n    ],\n    \"services\": {\n      \"registrar\": true,\n      \"dns\": true,\n      \"email\": true,\n      \"webhotel\": \"none\"\n    }\n  }\n]\n"
  },
  {
    "path": "providers/dns/domeneshop/internal/types.go",
    "content": "package internal\n\n// Domain JSON data structure.\ntype Domain struct {\n\tName           string   `json:\"domain\"`\n\tID             int      `json:\"id\"`\n\tExpiryDate     string   `json:\"expiry_date\"`\n\tNameservers    []string `json:\"nameservers\"`\n\tRegisteredDate string   `json:\"registered_date\"`\n\tRegistrant     string   `json:\"registrant\"`\n\tRenew          bool     `json:\"renew\"`\n\tServices       Service  `json:\"services\"`\n\tStatus         string\n}\n\ntype Service struct {\n\tDNS       bool   `json:\"dns\"`\n\tEmail     bool   `json:\"email\"`\n\tRegistrar bool   `json:\"registrar\"`\n\tWebhotel  string `json:\"webhotel\"`\n}\n\n// DNSRecord JSON data structure.\ntype DNSRecord struct {\n\tData string `json:\"data\"`\n\tHost string `json:\"host\"`\n\tID   int    `json:\"id\"`\n\tTTL  int    `json:\"ttl\"`\n\tType string `json:\"type\"`\n}\n"
  },
  {
    "path": "providers/dns/dreamhost/dreamhost.go",
    "content": "// Package dreamhost implements a DNS provider for solving the DNS-01 challenge using DreamHost.\n// See https://help.dreamhost.com/hc/en-us/articles/217560167-API_overview\n// and https://help.dreamhost.com/hc/en-us/articles/217555707-DNS-API-commands for the API spec.\npackage dreamhost\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dreamhost/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DREAMHOST_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL            string\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tBaseURL:            internal.DefaultBaseURL,\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 60*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 1*time.Minute),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a new DNS provider using\n// environment variable DREAMHOST_API_KEY for adding and removing the DNS record.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dreamhost: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for DreamHost.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"dreamhost: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"dreamhost: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(config.APIKey)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\tif config.BaseURL != \"\" {\n\t\tclient.BaseURL = config.BaseURL\n\t}\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.client.AddRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN), info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dreamhost: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.client.RemoveRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN), info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dreamhost: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/dreamhost/dreamhost.toml",
    "content": "Name = \"DreamHost\"\nDescription = ''''''\nURL = \"https://www.dreamhost.com\"\nCode = \"dreamhost\"\nSince = \"v1.1.0\"\n\nExample = '''\nDREAMHOST_API_KEY=\"YOURAPIKEY\" \\\nlego --dns dreamhost -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DREAMHOST_API_KEY = \"The API key\"\n  [Configuration.Additional]\n    DREAMHOST_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 60)\"\n    DREAMHOST_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 3600)\"\n    DREAMHOST_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://help.dreamhost.com/hc/en-us/articles/217560167-API_overview\"\n"
  },
  {
    "path": "providers/dns/dreamhost/dreamhost_test.go",
    "content": "package dreamhost\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).\n\tWithDomain(envDomain)\n\nconst (\n\tfakeAPIKey         = \"asdf1234\"\n\tfakeChallengeToken = \"foobar\"\n\tfakeKeyAuth        = \"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\"\n)\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {\n\t\tconfig := NewDefaultConfig()\n\t\tconfig.APIKey = fakeAPIKey\n\t\tconfig.BaseURL = server.URL\n\t\tconfig.HTTPClient = server.Client()\n\n\t\treturn NewDNSProviderConfig(config)\n\t})\n}\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"dreamhost: some credentials information are missing: DREAMHOST_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, p)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"dreamhost: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, p)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /\",\n\t\t\tservermock.RawStringResponse(`{\"data\":\"record_added\",\"result\":\"success\"}`),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"cmd\", \"dns-add_record\").\n\t\t\t\tWith(\"comment\", \"Managed+By+lego\").\n\t\t\t\tWith(\"format\", \"json\").\n\t\t\t\tWith(\"record\", \"_acme-challenge.example.com\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"key\", fakeAPIKey).\n\t\t\t\tWith(\"value\", fakeKeyAuth),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"\", fakeChallengeToken)\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_PresentFailed(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /\",\n\t\t\tservermock.RawStringResponse(`{\"data\":\"record_already_exists_remove_first\",\"result\":\"error\"}`)).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"\", fakeChallengeToken)\n\trequire.EqualError(t, err, \"dreamhost: add TXT record failed: record_already_exists_remove_first\")\n}\n\nfunc TestDNSProvider_Cleanup(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /\",\n\t\t\tservermock.RawStringResponse(`{\"data\":\"record_removed\",\"result\":\"success\"}`),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"cmd\", \"dns-remove_record\").\n\t\t\t\tWith(\"comment\", \"Managed+By+lego\").\n\t\t\t\tWith(\"format\", \"json\").\n\t\t\t\tWith(\"record\", \"_acme-challenge.example.com\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"key\", fakeAPIKey).\n\t\t\t\tWith(\"value\", fakeKeyAuth),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"\", fakeChallengeToken)\n\trequire.NoError(t, err)\n}\n\nfunc TestLivePresentAndCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/dreamhost/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// DefaultBaseURL the default API endpoint.\nconst DefaultBaseURL = \"https://api.dreamhost.com\"\n\nconst (\n\tcmdAddRecord    = \"dns-add_record\"\n\tcmdRemoveRecord = \"dns-remove_record\"\n)\n\n// Client the Dreamhost API client.\ntype Client struct {\n\tapiKey string\n\n\tBaseURL    string\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(apiKey string) *Client {\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tBaseURL:    DefaultBaseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// AddRecord adds a TXT record.\nfunc (c *Client) AddRecord(ctx context.Context, domain, value string) error {\n\tquery, err := c.buildEndpoint(cmdAddRecord, domain, value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.updateTxtRecord(ctx, query)\n}\n\n// RemoveRecord removes a TXT record.\nfunc (c *Client) RemoveRecord(ctx context.Context, domain, value string) error {\n\tquery, err := c.buildEndpoint(cmdRemoveRecord, domain, value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.updateTxtRecord(ctx, query)\n}\n\n// action is either cmdAddRecord or cmdRemoveRecord.\nfunc (c *Client) buildEndpoint(action, domain, txt string) (*url.URL, error) {\n\tendpoint, err := url.Parse(c.BaseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := endpoint.Query()\n\tquery.Set(\"key\", c.apiKey)\n\tquery.Set(\"cmd\", action)\n\tquery.Set(\"format\", \"json\")\n\tquery.Set(\"record\", domain)\n\tquery.Set(\"type\", \"TXT\")\n\tquery.Set(\"value\", txt)\n\tquery.Set(\"comment\", url.QueryEscape(\"Managed By lego\"))\n\tendpoint.RawQuery = query.Encode()\n\n\treturn endpoint, nil\n}\n\n// updateTxtRecord will either add or remove a TXT record.\nfunc (c *Client) updateTxtRecord(ctx context.Context, endpoint *url.URL) error {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar response apiResponse\n\n\terr = json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tif response.Result == \"error\" {\n\t\treturn fmt.Errorf(\"add TXT record failed: %s\", response.Data)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/dreamhost/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"secret\")\n\tclient.BaseURL = server.URL\n\tclient.HTTPClient = server.Client()\n\n\treturn client, nil\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /\", servermock.RawStringResponse(`{}`),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"cmd\", \"dns-add_record\").\n\t\t\t\tWith(\"comment\", \"Managed+By+lego\").\n\t\t\t\tWith(\"format\", \"json\").\n\t\t\t\tWith(\"key\", \"secret\").\n\t\t\t\tWith(\"record\", \"example.com\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"value\", \"aaa\")).\n\t\tBuild(t)\n\n\terr := client.AddRecord(t.Context(), \"example.com\", \"aaa\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_RemoveRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /\", servermock.RawStringResponse(`{}`),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"cmd\", \"dns-remove_record\").\n\t\t\t\tWith(\"comment\", \"Managed+By+lego\").\n\t\t\t\tWith(\"format\", \"json\").\n\t\t\t\tWith(\"key\", \"secret\").\n\t\t\t\tWith(\"record\", \"example.com\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"value\", \"aaa\")).\n\t\tBuild(t)\n\n\terr := client.RemoveRecord(t.Context(), \"example.com\", \"aaa\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_buildQuery(t *testing.T) {\n\tconst fakeAPIKey = \"asdf1234\"\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tbaseURL  string\n\t\taction   string\n\t\tdomain   string\n\t\ttxt      string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiKey:   fakeAPIKey,\n\t\t\taction:   cmdAddRecord,\n\t\t\tdomain:   \"domain\",\n\t\t\ttxt:      \"TXTtxtTXT\",\n\t\t\texpected: \"https://api.dreamhost.com?cmd=dns-add_record&comment=Managed%2BBy%2Blego&format=json&key=asdf1234&record=domain&type=TXT&value=TXTtxtTXT\",\n\t\t},\n\t\t{\n\t\t\tdesc:    \"Invalid base URL\",\n\t\t\tapiKey:  fakeAPIKey,\n\t\t\tbaseURL: \":\",\n\t\t\taction:  cmdAddRecord,\n\t\t\tdomain:  \"domain\",\n\t\t\ttxt:     \"TXTtxtTXT\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := NewClient(test.apiKey)\n\t\t\tif test.baseURL != \"\" {\n\t\t\t\tclient.BaseURL = test.baseURL\n\t\t\t}\n\n\t\t\tendpoint, err := client.buildEndpoint(test.action, test.domain, test.txt)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.expected, endpoint.String())\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/dreamhost/internal/types.go",
    "content": "package internal\n\ntype apiResponse struct {\n\tData   string `json:\"data\"`\n\tResult string `json:\"result\"`\n}\n"
  },
  {
    "path": "providers/dns/duckdns/duckdns.go",
    "content": "// Package duckdns implements a DNS provider for solving the DNS-01 challenge using DuckDNS.\n// See http://www.duckdns.org/spec.jsp for more info on updating TXT records.\npackage duckdns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/duckdns/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DUCKDNS_\"\n\n\tEnvToken = envNamespace + \"TOKEN\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken              string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a new DNS provider using\n// environment variable DUCKDNS_TOKEN for adding and removing the DNS record.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"duckdns: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for DuckDNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"duckdns: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"duckdns: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(config.Token)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\treturn d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN), info.Value)\n}\n\n// CleanUp clears DuckDNS TXT record.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\treturn d.client.RemoveTXTRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN))\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n"
  },
  {
    "path": "providers/dns/duckdns/duckdns.toml",
    "content": "Name = \"Duck DNS\"\nDescription = ''''''\nURL = \"https://www.duckdns.org/\"\nCode = \"duckdns\"\nSince = \"v0.5.0\"\n\nExample = '''\nDUCKDNS_TOKEN=xxxxxx \\\nlego --dns duckdns -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DUCKDNS_TOKEN = \"Account token\"\n  [Configuration.Additional]\n    DUCKDNS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    DUCKDNS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    DUCKDNS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n    DUCKDNS_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n\n[Links]\n  API = \"https://www.duckdns.org/spec.jsp\"\n"
  },
  {
    "path": "providers/dns/duckdns/duckdns_test.go",
    "content": "package duckdns\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvToken).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"\",\n\t\t\t},\n\t\t\texpected: \"duckdns: some credentials information are missing: DUCKDNS_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"duckdns: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/duckdns/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/miekg/dns\"\n)\n\nconst defaultBaseURL = \"https://www.duckdns.org/update\"\n\n// Client the DuckDNS API client.\ntype Client struct {\n\ttoken string\n\n\tbaseURL    string\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(token string) *Client {\n\treturn &Client{\n\t\ttoken:      token,\n\t\tbaseURL:    defaultBaseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\nfunc (c *Client) AddTXTRecord(ctx context.Context, domain, value string) error {\n\treturn c.UpdateTxtRecord(ctx, domain, value, false)\n}\n\nfunc (c *Client) RemoveTXTRecord(ctx context.Context, domain string) error {\n\treturn c.UpdateTxtRecord(ctx, domain, \"\", true)\n}\n\n// UpdateTxtRecord Update the domains TXT record\n// To update the TXT record we just need to make one simple get request.\n// In DuckDNS you only have one TXT record shared with the domain and all subdomains.\nfunc (c *Client) UpdateTxtRecord(ctx context.Context, domain, txt string, clearRecord bool) error {\n\tendpoint, _ := url.Parse(c.baseURL)\n\n\tmainDomain := getMainDomain(domain)\n\tif mainDomain == \"\" {\n\t\treturn fmt.Errorf(\"unable to find the main domain for: %s\", domain)\n\t}\n\n\tquery := endpoint.Query()\n\tquery.Set(\"domains\", mainDomain)\n\tquery.Set(\"token\", c.token)\n\tquery.Set(\"clear\", strconv.FormatBool(clearRecord))\n\tquery.Set(\"txt\", txt)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tbody := string(raw)\n\tif body != \"OK\" {\n\t\treturn 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)\n\t}\n\n\treturn nil\n}\n\n// DuckDNS only lets you write to your subdomain.\n// It must be in format subdomain.duckdns.org,\n// not in format subsubdomain.subdomain.duckdns.org.\n// So strip off everything that is not top 3 levels.\nfunc getMainDomain(domain string) string {\n\tdomain = dns01.UnFqdn(domain)\n\n\tsplit := dns.Split(domain)\n\tif strings.HasSuffix(strings.ToLower(domain), \"duckdns.org\") {\n\t\tif len(split) < 3 {\n\t\t\treturn \"\"\n\t\t}\n\n\t\tfirstSubDomainIndex := split[len(split)-3]\n\n\t\treturn domain[firstSubDomainIndex:]\n\t}\n\n\treturn domain[split[len(split)-1]:]\n}\n"
  },
  {
    "path": "providers/dns/duckdns/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"secret\")\n\tclient.baseURL = server.URL\n\tclient.HTTPClient = server.Client()\n\n\treturn client, nil\n}\n\nfunc TestClient_AddTXTRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /\", servermock.RawStringResponse(\"OK\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"clear\", \"false\").\n\t\t\t\tWith(\"domains\", \"com\").\n\t\t\t\tWith(\"token\", \"secret\").\n\t\t\t\tWith(\"txt\", \"value\")).\n\t\tBuild(t)\n\n\terr := client.AddTXTRecord(t.Context(), \"example.com\", \"value\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_RemoveTXTRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /\", servermock.RawStringResponse(\"OK\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"clear\", \"true\").\n\t\t\t\tWith(\"domains\", \"com\").\n\t\t\t\tWith(\"token\", \"secret\").\n\t\t\t\tWith(\"txt\", \"\")).\n\t\tBuild(t)\n\n\terr := client.RemoveTXTRecord(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n}\n\nfunc Test_getMainDomain(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tdomain   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"empty\",\n\t\t\tdomain:   \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing sub domain\",\n\t\t\tdomain:   \"duckdns.org\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"explicit domain: sub domain\",\n\t\t\tdomain:   \"_acme-challenge.sub.duckdns.org\",\n\t\t\texpected: \"sub.duckdns.org\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"explicit domain: subsub domain\",\n\t\t\tdomain:   \"_acme-challenge.my.sub.duckdns.org\",\n\t\t\texpected: \"sub.duckdns.org\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"explicit domain: subsubsub domain\",\n\t\t\tdomain:   \"_acme-challenge.my.sub.sub.duckdns.org\",\n\t\t\texpected: \"sub.duckdns.org\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"only subname: sub domain\",\n\t\t\tdomain:   \"_acme-challenge.sub\",\n\t\t\texpected: \"sub\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"only subname: subsub domain\",\n\t\t\tdomain:   \"_acme-challenge.my.sub\",\n\t\t\texpected: \"sub\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"only subname: subsubsub domain\",\n\t\t\tdomain:   \"_acme-challenge.my.sub.sub\",\n\t\t\texpected: \"sub\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\twDomain := getMainDomain(test.domain)\n\t\t\tassert.Equal(t, test.expected, wDomain)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/dyn/dyn.go",
    "content": "// Package dyn implements a DNS provider for solving the DNS-01 challenge using Dyn Managed DNS.\npackage dyn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dyn/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DYN_\"\n\n\tEnvCustomerName = envNamespace + \"CUSTOMER_NAME\"\n\tEnvUserName     = envNamespace + \"USER_NAME\"\n\tEnvPassword     = envNamespace + \"PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tCustomerName       string\n\tUserName           string\n\tPassword           string\n\tHTTPClient         *http.Client\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Dyn DNS.\n// Credentials must be passed in the environment variables:\n// DYN_CUSTOMER_NAME, DYN_USER_NAME and DYN_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvCustomerName, EnvUserName, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dyn: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.CustomerName = values[EnvCustomerName]\n\tconfig.UserName = values[EnvUserName]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Dyn DNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"dyn: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.CustomerName == \"\" || config.UserName == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"dyn: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(config.CustomerName, config.UserName, config.Password)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dyn: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dyn: %w\", err)\n\t}\n\n\terr = d.client.AddTXTRecord(ctx, authZone, info.EffectiveFQDN, info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dyn: %w\", err)\n\t}\n\n\terr = d.client.Publish(ctx, authZone, \"Added TXT record for ACME dns-01 challenge using lego client\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dyn: %w\", err)\n\t}\n\n\treturn d.client.Logout(ctx)\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dyn: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dyn: %w\", err)\n\t}\n\n\terr = d.client.RemoveTXTRecord(ctx, authZone, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dyn: %w\", err)\n\t}\n\n\terr = d.client.Publish(ctx, authZone, \"Removed TXT record for ACME dns-01 challenge using lego client\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dyn: %w\", err)\n\t}\n\n\treturn d.client.Logout(ctx)\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/dyn/dyn.toml",
    "content": "Name = \"Dyn\"\nDescription = ''''''\nURL = \"https://dyn.com/\"\nCode = \"dyn\"\nSince = \"v0.3.0\"\n\nExample = '''\nDYN_CUSTOMER_NAME=xxxxxx \\\nDYN_USER_NAME=yyyyy \\\nDYN_PASSWORD=zzzz \\\nlego --dns dyn -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DYN_CUSTOMER_NAME = \"Customer name\"\n    DYN_USER_NAME = \"User name\"\n    DYN_PASSWORD = \"Password\"\n  [Configuration.Additional]\n    DYN_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    DYN_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    DYN_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    DYN_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://help.dyn.com/rest/\"\n"
  },
  {
    "path": "providers/dns/dyn/dyn_test.go",
    "content": "package dyn\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvCustomerName,\n\tEnvUserName,\n\tEnvPassword).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCustomerName: \"A\",\n\t\t\t\tEnvUserName:     \"B\",\n\t\t\t\tEnvPassword:     \"C\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCustomerName: \"\",\n\t\t\t\tEnvUserName:     \"\",\n\t\t\t\tEnvPassword:     \"\",\n\t\t\t},\n\t\t\texpected: \"dyn: some credentials information are missing: DYN_CUSTOMER_NAME,DYN_USER_NAME,DYN_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing customer name\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCustomerName: \"\",\n\t\t\t\tEnvUserName:     \"B\",\n\t\t\t\tEnvPassword:     \"C\",\n\t\t\t},\n\t\t\texpected: \"dyn: some credentials information are missing: DYN_CUSTOMER_NAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCustomerName: \"A\",\n\t\t\t\tEnvUserName:     \"\",\n\t\t\t\tEnvPassword:     \"C\",\n\t\t\t},\n\t\t\texpected: \"dyn: some credentials information are missing: DYN_USER_NAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCustomerName: \"A\",\n\t\t\t\tEnvUserName:     \"B\",\n\t\t\t\tEnvPassword:     \"\",\n\t\t\t},\n\t\t\texpected: \"dyn: some credentials information are missing: DYN_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc         string\n\t\tcustomerName string\n\t\tpassword     string\n\t\tuserName     string\n\t\texpected     string\n\t}{\n\t\t{\n\t\t\tdesc:         \"success\",\n\t\t\tcustomerName: \"A\",\n\t\t\tpassword:     \"B\",\n\t\t\tuserName:     \"C\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"dyn: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"missing customer name\",\n\t\t\tcustomerName: \"\",\n\t\t\tpassword:     \"B\",\n\t\t\tuserName:     \"C\",\n\t\t\texpected:     \"dyn: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"missing password\",\n\t\t\tcustomerName: \"A\",\n\t\t\tpassword:     \"\",\n\t\t\tuserName:     \"C\",\n\t\t\texpected:     \"dyn: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"missing username\",\n\t\t\tcustomerName: \"A\",\n\t\t\tpassword:     \"B\",\n\t\t\tuserName:     \"\",\n\t\t\texpected:     \"dyn: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.CustomerName = test.customerName\n\t\t\tconfig.Password = test.password\n\t\t\tconfig.UserName = test.userName\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/dyn/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api.dynect.net/REST\"\n\n// Client the Dyn API client.\ntype Client struct {\n\tcustomerName string\n\tusername     string\n\tpassword     string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(customerName, username, password string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tcustomerName: customerName,\n\t\tusername:     username,\n\t\tpassword:     password,\n\t\tbaseURL:      baseURL,\n\t\tHTTPClient:   &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// Publish updating Zone settings.\n// https://help.dyn.com/update-zone-api/\nfunc (c *Client) Publish(ctx context.Context, zone, notes string) error {\n\tendpoint := c.baseURL.JoinPath(\"Zone\", zone)\n\n\tpayload := &publish{Publish: true, Notes: notes}\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = c.do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// AddTXTRecord creating TXT Records.\n// https://help.dyn.com/create-txt-record-api/\nfunc (c *Client) AddTXTRecord(ctx context.Context, authZone, fqdn, value string, ttl int) error {\n\tendpoint := c.baseURL.JoinPath(\"TXTRecord\", authZone, fqdn)\n\n\tpayload := map[string]any{\n\t\t\"rdata\": map[string]string{\n\t\t\t\"txtdata\": value,\n\t\t},\n\t\t\"ttl\": strconv.Itoa(ttl),\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = c.do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// RemoveTXTRecord deleting one or all existing TXT Records.\n// https://help.dyn.com/delete-txt-records-api/\nfunc (c *Client) RemoveTXTRecord(ctx context.Context, authZone, fqdn string) error {\n\tendpoint := c.baseURL.JoinPath(\"TXTRecord\", authZone, fqdn)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) do(req *http.Request) (*APIResponse, error) {\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode >= http.StatusInternalServerError {\n\t\treturn nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar response APIResponse\n\n\terr = json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tif resp.StatusCode >= http.StatusBadRequest {\n\t\treturn nil, fmt.Errorf(\"%s: %w\", response.Messages, errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw))\n\t}\n\n\tif resp.StatusCode == http.StatusTemporaryRedirect {\n\t\t// TODO add support for HTTP 307 response and long running jobs\n\t\treturn nil, errors.New(\"API request returned HTTP 307. This is currently unsupported\")\n\t}\n\n\tif response.Status == \"failure\" {\n\t\treturn nil, fmt.Errorf(\"%s: %w\", response.Messages, errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw))\n\t}\n\n\treturn &response, nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\ttok := getToken(req.Context())\n\tif tok != \"\" {\n\t\treq.Header.Set(authTokenHeader, tok)\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/dyn/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"bob\", \"user\", \"secret\")\n\tclient.HTTPClient = server.Client()\n\tclient.baseURL, _ = url.Parse(server.URL)\n\n\treturn client, nil\n}\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"bob\", \"user\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders())\n}\n\nfunc TestClient_Publish(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /Zone/example.com\", servermock.ResponseFromFixture(\"publish.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"publish\":true,\"notes\":\"my message\"}`)).\n\t\tBuild(t)\n\n\terr := client.Publish(t.Context(), \"example.com\", \"my message\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /TXTRecord/example.com/example.com.\", servermock.ResponseFromFixture(\"create-txt-record.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"rdata\":{\"txtdata\":\"txt\"},\"ttl\":\"120\"}`)).\n\t\tBuild(t)\n\n\terr := client.AddTXTRecord(t.Context(), \"example.com\", \"example.com.\", \"txt\", 120)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_RemoveTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /TXTRecord/example.com/example.com.\", nil).\n\t\tBuild(t)\n\n\terr := client.RemoveTXTRecord(t.Context(), \"example.com\", \"example.com.\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/dyn/internal/fixtures/create-txt-record.json",
    "content": "{\n  \"fqdn\": \"example.com.\",\n  \"rdata\": {\n    \"txtdata\": \"txt\"\n  },\n  \"record_type\": \"TXT\",\n  \"ttl\": 120,\n  \"zone\": \"example.com\"\n}\n\n"
  },
  {
    "path": "providers/dns/dyn/internal/fixtures/login.json",
    "content": "{\n  \"status\": \"success\",\n  \"data\": {\n    \"token\": \"tok\",\n    \"version\": \"456\"\n  },\n  \"job_id\": 123,\n  \"msgs\": []\n}\n"
  },
  {
    "path": "providers/dns/dyn/internal/fixtures/publish.json",
    "content": "{\n  \"status\": \"success\",\n  \"data\": {},\n  \"job_id\": 123,\n  \"msgs\": []\n}\n"
  },
  {
    "path": "providers/dns/dyn/internal/session.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\ntype token string\n\nconst tokenKey token = \"token\"\n\nconst authTokenHeader = \"Auth-Token\"\n\n// login Starts a new Dyn API Session. Authenticates using customerName, username, password\n// and receives a token to be used in for subsequent requests.\n// https://help.dyn.com/session-log-in/\nfunc (c *Client) login(ctx context.Context) (session, error) {\n\tendpoint := c.baseURL.JoinPath(\"Session\")\n\n\tpayload := &credentials{Customer: c.customerName, User: c.username, Pass: c.password}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload)\n\tif err != nil {\n\t\treturn session{}, err\n\t}\n\n\tdynRes, err := c.do(req)\n\tif err != nil {\n\t\treturn session{}, err\n\t}\n\n\tvar s session\n\n\terr = json.Unmarshal(dynRes.Data, &s)\n\tif err != nil {\n\t\treturn session{}, errutils.NewUnmarshalError(req, http.StatusOK, dynRes.Data, err)\n\t}\n\n\treturn s, nil\n}\n\n// Logout Destroys Dyn Session.\n// https://help.dyn.com/session-log-out/\nfunc (c *Client) Logout(ctx context.Context) error {\n\tendpoint := c.baseURL.JoinPath(\"Session\")\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttok := getToken(ctx)\n\tif tok != \"\" {\n\t\treq.Header.Set(authTokenHeader, tok)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) {\n\ttok, err := c.login(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn context.WithValue(ctx, tokenKey, tok.Token), nil\n}\n\nfunc getToken(ctx context.Context) string {\n\ttok, ok := ctx.Value(tokenKey).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn tok\n}\n"
  },
  {
    "path": "providers/dns/dyn/internal/session_test.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockContext(t *testing.T) context.Context {\n\tt.Helper()\n\n\treturn context.WithValue(t.Context(), tokenKey, \"tok\")\n}\n\nfunc TestClient_login(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /Session\", servermock.ResponseFromFixture(\"login.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"customer_name\":\"bob\",\"user_name\":\"user\",\"password\":\"secret\"}`)).\n\t\tBuild(t)\n\n\tsess, err := client.login(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := session{Token: \"tok\", Version: \"456\"}\n\n\tassert.Equal(t, expected, sess)\n}\n\nfunc TestClient_Logout(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(authTokenHeader, \"tok\"),\n\t).\n\t\tRoute(\"DELETE /Session\", nil).\n\t\tBuild(t)\n\n\terr := client.Logout(mockContext(t))\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_CreateAuthenticatedContext(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /Session\", servermock.ResponseFromFixture(\"login.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"customer_name\":\"bob\",\"user_name\":\"user\",\"password\":\"secret\"}`)).\n\t\tBuild(t)\n\n\tctx, err := client.CreateAuthenticatedContext(t.Context())\n\trequire.NoError(t, err)\n\n\tat := getToken(ctx)\n\tassert.Equal(t, \"tok\", at)\n}\n"
  },
  {
    "path": "providers/dns/dyn/internal/types.go",
    "content": "package internal\n\nimport \"encoding/json\"\n\ntype APIResponse struct {\n\t// One of 'success', 'failure', or 'incomplete'\n\tStatus string `json:\"status\"`\n\n\t// The structure containing the actual results of the request\n\tData json.RawMessage `json:\"data\"`\n\n\t// The ID of the job that was created in response to a request.\n\tJobID int `json:\"job_id\"`\n\n\t// A list of zero or more messages\n\tMessages json.RawMessage `json:\"msgs\"`\n}\n\ntype credentials struct {\n\tCustomer string `json:\"customer_name\"`\n\tUser     string `json:\"user_name\"`\n\tPass     string `json:\"password\"`\n}\n\ntype session struct {\n\tToken   string `json:\"token\"`\n\tVersion string `json:\"version\"`\n}\n\ntype publish struct {\n\tPublish bool   `json:\"publish\"`\n\tNotes   string `json:\"notes\"`\n}\n"
  },
  {
    "path": "providers/dns/dyndnsfree/dyndnsfree.go",
    "content": "// Package dyndnsfree implements a DNS provider for solving the DNS-01 challenge using DynDnsFree.de API.\npackage dyndnsfree\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dyndnsfree/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DYNDNSFREE_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername string\n\tPassword string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for DynDNSFree.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dyndnsfree: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for DynDNSFree.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"dyndnsfree: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.Username, config.Password)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dyndnsfree: new client: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dyndnsforfree: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\terr = d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(authZone), dns01.UnFqdn(info.EffectiveFQDN), info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dyndnsfree: add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\t// Records are deleted automatically.\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/dyndnsfree/dyndnsfree.toml",
    "content": "Name = \"DynDnsFree.de\"\nDescription = ''''''\nURL = \"https://www.dyndnsfree.de\"\nCode = \"dyndnsfree\"\nSince = \"v4.23.0\"\n\nExample = '''\nDYNDNSFREE_USERNAME=\"xxx\" \\\nDYNDNSFREE_PASSWORD=\"yyy\" \\\nlego --dns dyndnsfree -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DYNDNSFREE_USERNAME = \"Username\"\n    DYNDNSFREE_PASSWORD = \"Password\"\n  [Configuration.Additional]\n    DYNDNSFREE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    DYNDNSFREE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    DYNDNSFREE_HTTP_TIMEOUT = \"Request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.dyndnsfree.de/user/hilfe.php?hsm=2\"\n"
  },
  {
    "path": "providers/dns/dyndnsfree/dyndnsfree_test.go",
    "content": "package dyndnsfree\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"dyndnsfree: some credentials information are missing: DYNDNSFREE_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"dyndnsfree: some credentials information are missing: DYNDNSFREE_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"dyndnsfree: some credentials information are missing: DYNDNSFREE_USERNAME,DYNDNSFREE_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tusername: \"\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"dyndnsfree: new client: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"\",\n\t\t\texpected: \"dyndnsfree: new client: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"dyndnsfree: new client: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/dyndnsfree/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://dynup.de/acme.php\"\n\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tbaseURL    string\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(username, password string) (*Client, error) {\n\tif username == \"\" || password == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\treturn &Client{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\tbaseURL:    defaultBaseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) AddTXTRecord(ctx context.Context, zone, hostname, value string) error {\n\tbaseURL, err := url.Parse(c.baseURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tquery := baseURL.Query()\n\tquery.Set(\"username\", c.username)\n\tquery.Set(\"password\", c.password)\n\tquery.Set(\"hostname\", zone)\n\tquery.Set(\"add_hostname\", hostname)\n\tquery.Set(\"txt\", value)\n\tbaseURL.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL.String(), http.NoBody)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif !bytes.Equal(raw, []byte(\"success\")) {\n\t\treturn errors.New(string(raw))\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/dyndnsfree/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient, err := NewClient(\"user\", \"secret\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.baseURL = server.URL\n\tclient.HTTPClient = server.Client()\n\n\treturn client, nil\n}\n\nfunc TestAddTXTRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /\", servermock.RawStringResponse(\"success\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"add_hostname\", \"sub.example.com\").\n\t\t\t\tWith(\"hostname\", \"example.com\").\n\t\t\t\tWith(\"password\", \"secret\").\n\t\t\t\tWith(\"txt\", \"value\").\n\t\t\t\tWith(\"username\", \"user\")).\n\t\tBuild(t)\n\n\terr := client.AddTXTRecord(t.Context(), \"example.com\", \"sub.example.com\", \"value\")\n\trequire.NoError(t, err)\n}\n\nfunc TestAddTXTRecord_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /\", servermock.RawStringResponse(\"error: authentification failed\")).\n\t\tBuild(t)\n\n\terr := client.AddTXTRecord(t.Context(), \"example.com\", \"sub.example.com\", \"value\")\n\trequire.EqualError(t, err, \"error: authentification failed\")\n}\n"
  },
  {
    "path": "providers/dns/dynu/dynu.go",
    "content": "// Package dynu implements a DNS provider for solving the DNS-01 challenge using Dynu DNS.\npackage dynu\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dynu/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"DYNU_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 3*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Dynu.\n// Credentials must be passed in the environment variables.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dynu: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Dynu.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"dynu: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"dynu: incomplete credentials, missing API key\")\n\t}\n\n\ttr, err := internal.NewTokenTransport(config.APIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"dynu: %w\", err)\n\t}\n\n\tclient := internal.NewClient()\n\n\tclient.HTTPClient = clientdebug.Wrap(tr.Wrap(config.HTTPClient))\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\trootDomain, err := d.client.GetRootDomain(ctx, dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dynu: could not find root domain for %s: %w\", domain, err)\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, dns01.UnFqdn(info.EffectiveFQDN), \"TXT\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dynu: failed to get records for %s: %w\", domain, err)\n\t}\n\n\tfor _, record := range records {\n\t\t// the record already exist\n\t\tif record.Hostname == dns01.UnFqdn(info.EffectiveFQDN) && record.TextData == info.Value {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, rootDomain.DomainName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dynu: %w\", err)\n\t}\n\n\trecord := internal.DNSRecord{\n\t\tType:       \"TXT\",\n\t\tDomainName: rootDomain.DomainName,\n\t\tHostname:   dns01.UnFqdn(info.EffectiveFQDN),\n\t\tNodeName:   subDomain,\n\t\tTextData:   info.Value,\n\t\tState:      true,\n\t\tTTL:        d.config.TTL,\n\t}\n\n\terr = d.client.AddNewRecord(ctx, rootDomain.ID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dynu: failed to add record to %s: %w\", domain, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\trootDomain, err := d.client.GetRootDomain(ctx, dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dynu: could not find root domain for %s: %w\", domain, err)\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, dns01.UnFqdn(info.EffectiveFQDN), \"TXT\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dynu: failed to get records for %s: %w\", domain, err)\n\t}\n\n\tfor _, record := range records {\n\t\tif record.Hostname == dns01.UnFqdn(info.EffectiveFQDN) && record.TextData == info.Value {\n\t\t\terr = d.client.DeleteRecord(ctx, rootDomain.ID, record.ID)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"dynu: failed to remove TXT record for %s: %w\", domain, err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/dynu/dynu.toml",
    "content": "Name = \"Dynu\"\nDescription = ''''''\nURL = \"https://www.dynu.com/\"\nCode = \"dynu\"\nSince = \"v3.5.0\"\n\nExample = '''\nDYNU_API_KEY=1234567890abcdefghijklmnopqrstuvwxyz \\\nlego --dns dynu -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    DYNU_API_KEY = \"API key\"\n  [Configuration.Additional]\n    DYNU_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    DYNU_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 180)\"\n    DYNU_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    DYNU_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.dynu.com/en-US/Support/API\"\n"
  },
  {
    "path": "providers/dns/dynu/dynu_test.go",
    "content": "package dynu\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"dynu: some credentials information are missing: DYNU_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\texpected string\n\t\tapiKey   string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"api_key\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api key\",\n\t\t\tapiKey:   \"\",\n\t\t\texpected: \"dynu: incomplete credentials, missing API key\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/dynu/internal/auth.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n)\n\nconst apiKeyHeader = \"Api-Key\"\n\n// TokenTransport HTTP transport for API authentication.\ntype TokenTransport struct {\n\tapiKey string\n\n\t// Transport is the underlying HTTP transport to use when making requests.\n\t// It will default to http.DefaultTransport if nil.\n\tTransport http.RoundTripper\n}\n\n// NewTokenTransport Creates an HTTP transport for API authentication.\nfunc NewTokenTransport(apiKey string) (*TokenTransport, error) {\n\tif apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing: API key\")\n\t}\n\n\treturn &TokenTransport{apiKey: apiKey}, nil\n}\n\n// RoundTrip executes a single HTTP transaction.\nfunc (t *TokenTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\tenrichedReq := &http.Request{}\n\t*enrichedReq = *req\n\n\tenrichedReq.Header = make(http.Header, len(req.Header))\n\tfor k, s := range req.Header {\n\t\tenrichedReq.Header[k] = append([]string(nil), s...)\n\t}\n\n\tif t.apiKey != \"\" {\n\t\tenrichedReq.Header.Set(apiKeyHeader, t.apiKey)\n\t}\n\n\treturn t.transport().RoundTrip(enrichedReq)\n}\n\nfunc (t *TokenTransport) transport() http.RoundTripper {\n\tif t.Transport != nil {\n\t\treturn t.Transport\n\t}\n\n\treturn http.DefaultTransport\n}\n\n// Client Creates a new HTTP client.\nfunc (t *TokenTransport) Client() *http.Client {\n\treturn &http.Client{Transport: t}\n}\n\n// Wrap wraps an HTTP client Transport with the TokenTransport.\nfunc (t *TokenTransport) Wrap(client *http.Client) *http.Client {\n\tbackup := client.Transport\n\tt.Transport = backup\n\tclient.Transport = t\n\n\treturn client\n}\n"
  },
  {
    "path": "providers/dns/dynu/internal/auth_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewTokenTransport_success(t *testing.T) {\n\tapiKey := \"api\"\n\n\ttransport, err := NewTokenTransport(apiKey)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, transport)\n}\n\nfunc TestNewTokenTransport_missing_credentials(t *testing.T) {\n\tapiKey := \"\"\n\n\ttransport, err := NewTokenTransport(apiKey)\n\trequire.Error(t, err)\n\tassert.Nil(t, transport)\n}\n\nfunc TestTokenTransport_RoundTrip(t *testing.T) {\n\tapiKey := \"api\"\n\n\ttransport, err := NewTokenTransport(apiKey)\n\trequire.NoError(t, err)\n\n\treq := httptest.NewRequest(http.MethodGet, \"http://example.com\", nil)\n\n\tresp, err := transport.RoundTrip(req)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"api\", resp.Request.Header.Get(apiKeyHeader))\n}\n"
  },
  {
    "path": "providers/dns/dynu/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api.dynu.com/v2\"\n\ntype Client struct {\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewClient() *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t\tbaseURL:    baseURL,\n\t}\n}\n\n// GetRecords Get DNS records based on a hostname and resource record type.\nfunc (c *Client) GetRecords(ctx context.Context, hostname, recordType string) ([]DNSRecord, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns\", \"record\", hostname)\n\n\tquery := endpoint.Query()\n\tquery.Set(\"recordType\", recordType)\n\tendpoint.RawQuery = query.Encode()\n\n\tapiResp := RecordsResponse{}\n\n\terr := c.doRetry(ctx, http.MethodGet, endpoint.String(), nil, &apiResp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif apiResp.StatusCode/100 != 2 {\n\t\treturn nil, fmt.Errorf(\"API error: %w\", apiResp.APIException)\n\t}\n\n\treturn apiResp.DNSRecords, nil\n}\n\n// AddNewRecord Add a new DNS record for DNS service.\nfunc (c *Client) AddNewRecord(ctx context.Context, domainID int64, record DNSRecord) error {\n\tendpoint := c.baseURL.JoinPath(\"dns\", strconv.FormatInt(domainID, 10), \"record\")\n\n\treqBody, err := json.Marshal(record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t}\n\n\tapiResp := RecordResponse{}\n\n\terr = c.doRetry(ctx, http.MethodPost, endpoint.String(), reqBody, &apiResp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif apiResp.StatusCode/100 != 2 {\n\t\treturn fmt.Errorf(\"API error: %w\", apiResp.APIException)\n\t}\n\n\treturn nil\n}\n\n// DeleteRecord Remove a DNS record from DNS service.\nfunc (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int64) error {\n\tendpoint := c.baseURL.JoinPath(\"dns\", strconv.FormatInt(domainID, 10), \"record\", strconv.FormatInt(recordID, 10))\n\n\tapiResp := APIException{}\n\n\terr := c.doRetry(ctx, http.MethodDelete, endpoint.String(), nil, &apiResp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif apiResp.StatusCode/100 != 2 {\n\t\treturn fmt.Errorf(\"API error: %w\", apiResp)\n\t}\n\n\treturn nil\n}\n\n// GetRootDomain Get the root domain name based on a hostname.\nfunc (c *Client) GetRootDomain(ctx context.Context, hostname string) (*DNSHostname, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns\", \"getroot\", hostname)\n\n\tapiResp := DNSHostname{}\n\n\terr := c.doRetry(ctx, http.MethodGet, endpoint.String(), nil, &apiResp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif apiResp.StatusCode/100 != 2 {\n\t\treturn nil, fmt.Errorf(\"API error: %w\", apiResp.APIException)\n\t}\n\n\treturn &apiResp, nil\n}\n\n// doRetry the API is really unstable, so we need to retry on EOF.\nfunc (c *Client) doRetry(ctx context.Context, method, uri string, body []byte, result any) error {\n\toperation := func() error {\n\t\treturn c.do(ctx, method, uri, body, result)\n\t}\n\n\tnotify := func(err error, duration time.Duration) {\n\t\tlog.Printf(\"client retries because of %v\", err)\n\t}\n\n\tbo := backoff.NewExponentialBackOff()\n\tbo.InitialInterval = 1 * time.Second\n\n\treturn wait.Retry(ctx, operation, backoff.WithBackOff(bo), backoff.WithNotify(notify))\n}\n\nfunc (c *Client) do(ctx context.Context, method, uri string, body []byte, result any) error {\n\tvar reqBody io.Reader\n\tif len(body) > 0 {\n\t\treqBody = bytes.NewReader(body)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, uri, reqBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif errors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\tif err != nil {\n\t\treturn backoff.Permanent(fmt.Errorf(\"client error: %w\", errutils.NewHTTPDoError(req, err)))\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn backoff.Permanent(errutils.NewReadResponseError(req, resp.StatusCode, err))\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn backoff.Permanent(errutils.NewUnmarshalError(req, resp.StatusCode, raw, err))\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/dynu/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient()\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t)\n}\n\nfunc TestGetRootDomain(t *testing.T) {\n\ttype expected struct {\n\t\tdomain *DNSHostname\n\t\terror  string\n\t}\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tpattern  string\n\t\tstatus   int\n\t\tfile     string\n\t\texpected expected\n\t}{\n\t\t{\n\t\t\tdesc:    \"success\",\n\t\t\tpattern: \"GET /dns/getroot/test.lego.freeddns.org\",\n\t\t\tstatus:  http.StatusOK,\n\t\t\tfile:    \"get_root_domain.json\",\n\t\t\texpected: expected{\n\t\t\t\tdomain: &DNSHostname{\n\t\t\t\t\tAPIException: &APIException{\n\t\t\t\t\t\tStatusCode: 200,\n\t\t\t\t\t},\n\t\t\t\t\tID:         9007481,\n\t\t\t\t\tDomainName: \"lego.freeddns.org\",\n\t\t\t\t\tHostname:   \"test.lego.freeddns.org\",\n\t\t\t\t\tNode:       \"test\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:    \"invalid\",\n\t\t\tpattern: \"GET /dns/getroot/test.lego.freeddns.org\",\n\t\t\tstatus:  http.StatusNotImplemented,\n\t\t\tfile:    \"get_root_domain_invalid.json\",\n\t\t\texpected: expected{\n\t\t\t\terror: \"API error: 501: Argument Exception: Invalid.\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(test.pattern, servermock.ResponseFromFixture(test.file).WithStatusCode(test.status)).\n\t\t\t\tBuild(t)\n\n\t\t\tdomain, err := client.GetRootDomain(t.Context(), \"test.lego.freeddns.org\")\n\n\t\t\tif test.expected.error != \"\" {\n\t\t\t\tassert.EqualError(t, err, test.expected.error)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.NotNil(t, domain)\n\t\t\tassert.Equal(t, test.expected.domain, domain)\n\t\t})\n\t}\n}\n\nfunc TestGetRecords(t *testing.T) {\n\ttype expected struct {\n\t\trecords []DNSRecord\n\t\terror   string\n\t}\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tpattern  string\n\t\tstatus   int\n\t\tfile     string\n\t\texpected expected\n\t}{\n\t\t{\n\t\t\tdesc:    \"success\",\n\t\t\tpattern: \"GET /dns/record/_acme-challenge.lego.freeddns.org\",\n\t\t\tstatus:  http.StatusOK,\n\t\t\tfile:    \"get_records.json\",\n\t\t\texpected: expected{\n\t\t\t\trecords: []DNSRecord{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:         6041417,\n\t\t\t\t\t\tType:       \"TXT\",\n\t\t\t\t\t\tDomainID:   9007481,\n\t\t\t\t\t\tDomainName: \"lego.freeddns.org\",\n\t\t\t\t\t\tNodeName:   \"_acme-challenge\",\n\t\t\t\t\t\tHostname:   \"_acme-challenge.lego.freeddns.org\",\n\t\t\t\t\t\tState:      true,\n\t\t\t\t\t\tContent:    `_acme-challenge.lego.freeddns.org. 300 IN TXT \"txt_txt_txt_txt_txt_txt_txt\"`,\n\t\t\t\t\t\tTextData:   \"txt_txt_txt_txt_txt_txt_txt\",\n\t\t\t\t\t\tTTL:        300,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tID:         6041422,\n\t\t\t\t\t\tType:       \"TXT\",\n\t\t\t\t\t\tDomainID:   9007481,\n\t\t\t\t\t\tDomainName: \"lego.freeddns.org\",\n\t\t\t\t\t\tNodeName:   \"_acme-challenge\",\n\t\t\t\t\t\tHostname:   \"_acme-challenge.lego.freeddns.org\",\n\t\t\t\t\t\tState:      true,\n\t\t\t\t\t\tContent:    `_acme-challenge.lego.freeddns.org. 300 IN TXT \"txt_txt_txt_txt_txt_txt_txt_2\"`,\n\t\t\t\t\t\tTextData:   \"txt_txt_txt_txt_txt_txt_txt_2\",\n\t\t\t\t\t\tTTL:        300,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:    \"empty\",\n\t\t\tpattern: \"GET /dns/record/_acme-challenge.lego.freeddns.org\",\n\t\t\tstatus:  http.StatusOK,\n\t\t\tfile:    \"get_records_empty.json\",\n\t\t\texpected: expected{\n\t\t\t\trecords: []DNSRecord{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:    \"invalid\",\n\t\t\tpattern: \"GET /dns/record/_acme-challenge.lego.freeddns.org\",\n\t\t\tstatus:  http.StatusNotImplemented,\n\t\t\tfile:    \"get_records_invalid.json\",\n\t\t\texpected: expected{\n\t\t\t\terror: \"API error: 501: Argument Exception: Invalid.\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(test.pattern, servermock.ResponseFromFixture(test.file).WithStatusCode(test.status),\n\t\t\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\t\t\tWith(\"recordType\", \"TXT\")).\n\t\t\t\tBuild(t)\n\n\t\t\trecords, err := client.GetRecords(t.Context(), \"_acme-challenge.lego.freeddns.org\", \"TXT\")\n\n\t\t\tif test.expected.error != \"\" {\n\t\t\t\tassert.EqualError(t, err, test.expected.error)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.NotNil(t, records)\n\t\t\tassert.Equal(t, test.expected.records, records)\n\t\t})\n\t}\n}\n\nfunc TestAddNewRecord(t *testing.T) {\n\ttype expected struct {\n\t\terror string\n\t}\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tpattern  string\n\t\tstatus   int\n\t\tfile     string\n\t\texpected expected\n\t}{\n\t\t{\n\t\t\tdesc:    \"success\",\n\t\t\tpattern: \"POST /dns/9007481/record\",\n\t\t\tstatus:  http.StatusOK,\n\t\t\tfile:    \"add_new_record.json\",\n\t\t},\n\t\t{\n\t\t\tdesc:    \"invalid\",\n\t\t\tpattern: \"POST /dns/9007481/record\",\n\t\t\tstatus:  http.StatusNotImplemented,\n\t\t\tfile:    \"add_new_record_invalid.json\",\n\t\t\texpected: expected{\n\t\t\t\terror: \"API error: 501: Argument Exception: Invalid.\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(test.pattern, servermock.ResponseFromFixture(test.file).WithStatusCode(test.status),\n\t\t\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"add_new_record-request.json\")).\n\t\t\t\tBuild(t)\n\n\t\t\trecord := DNSRecord{\n\t\t\t\tType:       \"TXT\",\n\t\t\t\tDomainName: \"lego.freeddns.org\",\n\t\t\t\tHostname:   \"_acme-challenge.lego.freeddns.org\",\n\t\t\t\tNodeName:   \"_acme-challenge\",\n\t\t\t\tTextData:   \"txt_txt_txt_txt_txt_txt_txt_2\",\n\t\t\t\tState:      true,\n\t\t\t\tTTL:        300,\n\t\t\t}\n\n\t\t\terr := client.AddNewRecord(t.Context(), 9007481, record)\n\n\t\t\tif test.expected.error != \"\" {\n\t\t\t\tassert.EqualError(t, err, test.expected.error)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestDeleteRecord(t *testing.T) {\n\ttype expected struct {\n\t\terror string\n\t}\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tpattern  string\n\t\tstatus   int\n\t\tfile     string\n\t\texpected expected\n\t}{\n\t\t{\n\t\t\tdesc:    \"success\",\n\t\t\tpattern: \"DELETE /\",\n\t\t\tstatus:  http.StatusOK,\n\t\t\tfile:    \"delete_record.json\",\n\t\t},\n\t\t{\n\t\t\tdesc:    \"invalid\",\n\t\t\tpattern: \"DELETE /\",\n\t\t\tstatus:  http.StatusNotImplemented,\n\t\t\tfile:    \"delete_record_invalid.json\",\n\t\t\texpected: expected{\n\t\t\t\terror: \"API error: 501: Argument Exception: Invalid.\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(test.pattern, servermock.ResponseFromFixture(test.file).WithStatusCode(test.status)).\n\t\t\t\tBuild(t)\n\n\t\t\terr := client.DeleteRecord(t.Context(), 9007481, 6041418)\n\n\t\t\tif test.expected.error != \"\" {\n\t\t\t\tassert.EqualError(t, err, test.expected.error)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/dynu/internal/fixtures/add_new_record-request.json",
    "content": "{\n  \"recordType\": \"TXT\",\n  \"domainName\": \"lego.freeddns.org\",\n  \"nodeName\": \"_acme-challenge\",\n  \"hostname\": \"_acme-challenge.lego.freeddns.org\",\n  \"state\": true,\n  \"textData\": \"txt_txt_txt_txt_txt_txt_txt_2\",\n  \"ttl\": 300\n}\n"
  },
  {
    "path": "providers/dns/dynu/internal/fixtures/add_new_record.json",
    "content": "{\n  \"statusCode\": 200,\n  \"id\": 6041417,\n  \"domainId\": 9007481,\n  \"domainName\": \"lego.freeddns.org\",\n  \"nodeName\": \"_acme-challenge\",\n  \"hostname\": \"_acme-challenge.lego.freeddns.org\",\n  \"recordType\": \"TXT\",\n  \"ttl\": 300,\n  \"state\": true,\n  \"content\": \"_acme-challenge.lego.freeddns.org. 300 IN TXT \\\"txt_txt_txt_txt_txt_txt_txt\\\"\",\n  \"updatedOn\": \"2020-03-10T04:00:36.923\",\n  \"textData\": \"txt_txt_txt_txt_txt_txt_txt\"\n}"
  },
  {
    "path": "providers/dns/dynu/internal/fixtures/add_new_record_invalid.json",
    "content": "{\n  \"statusCode\": 501,\n  \"type\": \"Argument Exception\",\n  \"message\": \"Invalid.\"\n}"
  },
  {
    "path": "providers/dns/dynu/internal/fixtures/delete_record.json",
    "content": "{\n  \"statusCode\": 200\n}"
  },
  {
    "path": "providers/dns/dynu/internal/fixtures/delete_record_invalid.json",
    "content": "{\n  \"statusCode\": 501,\n  \"type\": \"Argument Exception\",\n  \"message\": \"Invalid.\"\n}"
  },
  {
    "path": "providers/dns/dynu/internal/fixtures/get_records.json",
    "content": "{\n  \"statusCode\": 200,\n  \"dnsRecords\": [\n    {\n      \"id\": 6041417,\n      \"domainId\": 9007481,\n      \"domainName\": \"lego.freeddns.org\",\n      \"nodeName\": \"_acme-challenge\",\n      \"hostname\": \"_acme-challenge.lego.freeddns.org\",\n      \"recordType\": \"TXT\",\n      \"ttl\": 300,\n      \"state\": true,\n      \"content\": \"_acme-challenge.lego.freeddns.org. 300 IN TXT \\\"txt_txt_txt_txt_txt_txt_txt\\\"\",\n      \"updatedOn\": \"2020-03-10T04:00:36.923\",\n      \"textData\": \"txt_txt_txt_txt_txt_txt_txt\"\n    },\n    {\n      \"id\": 6041422,\n      \"domainId\": 9007481,\n      \"domainName\": \"lego.freeddns.org\",\n      \"nodeName\": \"_acme-challenge\",\n      \"hostname\": \"_acme-challenge.lego.freeddns.org\",\n      \"recordType\": \"TXT\",\n      \"ttl\": 300,\n      \"state\": true,\n      \"content\": \"_acme-challenge.lego.freeddns.org. 300 IN TXT \\\"txt_txt_txt_txt_txt_txt_txt_2\\\"\",\n      \"updatedOn\": \"2020-03-10T04:03:17.563\",\n      \"textData\": \"txt_txt_txt_txt_txt_txt_txt_2\"\n    }\n  ]\n}"
  },
  {
    "path": "providers/dns/dynu/internal/fixtures/get_records_empty.json",
    "content": "{\n  \"statusCode\": 200,\n  \"dnsRecords\": []\n}"
  },
  {
    "path": "providers/dns/dynu/internal/fixtures/get_records_invalid.json",
    "content": "{\n  \"statusCode\": 501,\n  \"type\": \"Argument Exception\",\n  \"message\": \"Invalid.\"\n}"
  },
  {
    "path": "providers/dns/dynu/internal/fixtures/get_root_domain.json",
    "content": "{\n  \"statusCode\": 200,\n  \"id\": 9007481,\n  \"domainName\": \"lego.freeddns.org\",\n  \"hostname\": \"test.lego.freeddns.org\",\n  \"node\": \"test\"\n}"
  },
  {
    "path": "providers/dns/dynu/internal/fixtures/get_root_domain_invalid.json",
    "content": "{\n  \"statusCode\": 501,\n  \"type\": \"Argument Exception\",\n  \"message\": \"Invalid.\"\n}"
  },
  {
    "path": "providers/dns/dynu/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\n// APIException defines model for apiException.\ntype APIException struct {\n\tMessage    string `json:\"message,omitempty\"`\n\tStatusCode int32  `json:\"statusCode,omitempty\"`\n\tType       string `json:\"type,omitempty\"`\n}\n\nfunc (a APIException) Error() string {\n\treturn fmt.Sprintf(\"%d: %s: %s\", a.StatusCode, a.Type, a.Message)\n}\n\n// APIResponse defines model for apiResponse.\ntype APIResponse struct {\n\tException  *APIException `json:\"exception,omitempty\"`\n\tStatusCode int32         `json:\"statusCode,omitempty\"`\n}\n\n// DNSRecord defines model for dnsRecords.\ntype DNSRecord struct {\n\tID         int64  `json:\"id,omitempty\"`\n\tType       string `json:\"recordType,omitempty\"`\n\tDomainID   int64  `json:\"domainId,omitempty\"`\n\tDomainName string `json:\"domainName,omitempty\"`\n\tNodeName   string `json:\"nodeName,omitempty\"`\n\tHostname   string `json:\"hostname,omitempty\"`\n\tState      bool   `json:\"state,omitempty\"`\n\tContent    string `json:\"content,omitempty\"`\n\tTextData   string `json:\"textData,omitempty\"`\n\tTTL        int    `json:\"ttl,omitempty\"`\n}\n\n// DNSHostname defines model for DNS.hostname.\ntype DNSHostname struct {\n\t*APIException\n\n\tID         int64  `json:\"id,omitempty\"`\n\tDomainName string `json:\"domainName,omitempty\"`\n\tHostname   string `json:\"hostname,omitempty\"`\n\tNode       string `json:\"node,omitempty\"`\n}\n\n// RecordsResponse defines model for recordsResponse.\ntype RecordsResponse struct {\n\t*APIException\n\n\tDNSRecords []DNSRecord `json:\"dnsRecords,omitempty\"`\n}\n\n// RecordResponse defines model for recordResponse.\ntype RecordResponse struct {\n\t*APIException\n\n\tDNSRecord\n}\n"
  },
  {
    "path": "providers/dns/easydns/easydns.go",
    "content": "// Package easydns implements a DNS provider for solving the DNS-01 challenge using EasyDNS API.\npackage easydns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/easydns/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"EASYDNS_\"\n\n\tEnvEndpoint = envNamespace + \"ENDPOINT\"\n\tEnvToken    = envNamespace + \"TOKEN\"\n\tEnvKey      = envNamespace + \"KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tEndpoint           *url.URL\n\tToken              string\n\tKey                string\n\tTTL                int\n\tHTTPClient         *http.Client\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tconfig := NewDefaultConfig()\n\n\tendpoint, err := url.Parse(env.GetOrDefaultString(EnvEndpoint, internal.DefaultBaseURL))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"easydns: %w\", err)\n\t}\n\n\tconfig.Endpoint = endpoint\n\n\tvalues, err := env.Get(EnvToken, EnvKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"easydns: %w\", err)\n\t}\n\n\tconfig.Token = values[EnvToken]\n\tconfig.Key = values[EnvKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for EasyDNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"easydns: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"easydns: the API token is missing\")\n\t}\n\n\tif config.Key == \"\" {\n\t\treturn nil, errors.New(\"easydns: the API key is missing\")\n\t}\n\n\tclient := internal.NewClient(config.Token, config.Key)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\tif config.Endpoint != nil {\n\t\tclient.BaseURL = config.Endpoint\n\t}\n\n\treturn &DNSProvider{config: config, client: client, recordIDs: map[string]string{}}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := d.findZone(ctx, dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"easydns: %w\", err)\n\t}\n\n\tif authZone == \"\" {\n\t\treturn fmt.Errorf(\"easydns: could not find zone for domain %q\", domain)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"easydns: %w\", err)\n\t}\n\n\trecord := internal.ZoneRecord{\n\t\tDomain:   authZone,\n\t\tHost:     subDomain,\n\t\tType:     \"TXT\",\n\t\tRdata:    info.Value,\n\t\tTTL:      strconv.Itoa(d.config.TTL),\n\t\tPriority: \"0\",\n\t}\n\n\trecordID, err := d.client.AddRecord(ctx, dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"easydns: error adding zone record: %w\", err)\n\t}\n\n\tkey := getMapKey(info.EffectiveFQDN, info.Value)\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[key] = recordID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tkey := getMapKey(info.EffectiveFQDN, info.Value)\n\n\td.recordIDsMu.Lock()\n\trecordID, exists := d.recordIDs[key]\n\td.recordIDsMu.Unlock()\n\n\tif !exists {\n\t\treturn nil\n\t}\n\n\tauthZone, err := d.findZone(ctx, dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"easydns: %w\", err)\n\t}\n\n\tif authZone == \"\" {\n\t\treturn fmt.Errorf(\"easydns: could not find zone for domain %q\", domain)\n\t}\n\n\terr = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"easydns: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, key)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n\nfunc getMapKey(fqdn, value string) string {\n\treturn fqdn + \"|\" + value\n}\n\nfunc (d *DNSProvider) findZone(ctx context.Context, domain string) (string, error) {\n\tvar errAll error\n\n\tfor {\n\t\ti := strings.Index(domain, \".\")\n\t\tif i == -1 {\n\t\t\tbreak\n\t\t}\n\n\t\t_, err := d.client.ListZones(ctx, domain)\n\t\tif err == nil {\n\t\t\treturn domain, nil\n\t\t}\n\n\t\terrAll = errors.Join(errAll, err)\n\n\t\tdomain = domain[i+1:]\n\t}\n\n\treturn \"\", errAll\n}\n"
  },
  {
    "path": "providers/dns/easydns/easydns.toml",
    "content": "Name = \"EasyDNS\"\nDescription = ''''''\nURL = \"https://easydns.com/\"\nCode = \"easydns\"\nSince = \"v2.6.0\"\n\nExample = '''\nEASYDNS_TOKEN=xxx \\\nEASYDNS_KEY=yyy \\\nlego --dns easydns -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\nTo test with the sandbox environment set ```EASYDNS_ENDPOINT=https://sandbox.rest.easydns.net```\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    EASYDNS_TOKEN = \"API Token\"\n    EASYDNS_KEY = \"API Key\"\n  [Configuration.Additional]\n    EASYDNS_ENDPOINT = \"The endpoint URL of the API Server\"\n    EASYDNS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    EASYDNS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    EASYDNS_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n    EASYDNS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    EASYDNS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://docs.sandbox.rest.easydns.net\"\n"
  },
  {
    "path": "providers/dns/easydns/easydns_test.go",
    "content": "package easydns\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvEndpoint,\n\tEnvToken,\n\tEnvKey).\n\tWithDomain(envDomain)\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tendpoint, err := url.Parse(server.URL)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = \"TOKEN\"\n\t\t\tconfig.Key = \"SECRET\"\n\t\t\tconfig.Endpoint = endpoint\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWithAuthorization(\"Basic VE9LRU46U0VDUkVU\"),\n\t\tservermock.CheckQueryParameter().Strict().\n\t\t\tWith(\"format\", \"json\"))\n}\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"TOKEN\",\n\t\t\t\tEnvKey:   \"SECRET\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvKey: \"SECRET\",\n\t\t\t},\n\t\t\texpected: \"easydns: some credentials information are missing: EASYDNS_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"TOKEN\",\n\t\t\t},\n\t\t\texpected: \"easydns: some credentials information are missing: EASYDNS_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tconfig   *Config\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tconfig: &Config{\n\t\t\t\tToken: \"TOKEN\",\n\t\t\t\tKey:   \"KEY\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"nil config\",\n\t\t\tconfig:   nil,\n\t\t\texpected: \"easydns: the configuration of the DNS provider is nil\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing token\",\n\t\t\tconfig: &Config{\n\t\t\t\tKey: \"KEY\",\n\t\t\t},\n\t\t\texpected: \"easydns: the API token is missing\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing key\",\n\t\t\tconfig: &Config{\n\t\t\t\tToken: \"TOKEN\",\n\t\t\t},\n\t\t\texpected: \"easydns: the API key is missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tp, err := NewDNSProviderConfig(test.config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /zones/records/all/example.com\",\n\t\t\tservermock.RawStringResponse(`{\n\t\t  \"msg\": \"string\",\n\t\t  \"status\": 200,\n\t\t  \"tm\": 0,\n\t\t  \"data\": [{\n\t\t    \"id\": \"60898922\",\n\t\t    \"domain\": \"example.com\",\n\t\t    \"host\": \"hosta\",\n\t\t    \"ttl\": \"300\",\n\t\t    \"prio\": \"0\",\n\t\t    \"geozone_id\": \"0\",\n\t\t    \"type\": \"A\",\n\t\t    \"rdata\": \"1.2.3.4\",\n\t\t    \"last_mod\": \"2019-08-28 19:09:50\"\n\t\t  }],\n\t\t  \"count\": 0,\n\t\t  \"total\": 0,\n\t\t  \"start\": 0,\n\t\t  \"max\": 0\n\t\t}\n\t\t`),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"format\", \"json\")).\n\t\tRoute(\"PUT /zones/records/add/example.com/TXT\",\n\t\t\tservermock.RawStringResponse(`{\n\t\t\t\t\"msg\": \"OK\",\n\t\t\t\t\"tm\": 1554681934,\n\t\t\t\t\"data\": {\n\t\t\t\t\t\"host\": \"_acme-challenge\",\n\t\t\t\t\t\"geozone_id\": 0,\n\t\t\t\t\t\"ttl\": \"120\",\n\t\t\t\t\t\"prio\": \"0\",\n\t\t\t\t\t\"rdata\": \"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\",\n\t\t\t\t\t\"revoked\": 0,\n\t\t\t\t\t\"id\": \"123456789\",\n\t\t\t\t\t\"new_host\": \"_acme-challenge.example.com\"\n\t\t\t\t},\n\t\t\t\t\"status\": 201\n\t\t\t}`),\n\t\t\tservermock.CheckRequestJSONBody(`{\"domain\":\"example.com\",\"host\":\"_acme-challenge\",\"ttl\":\"120\",\"prio\":\"0\",\"type\":\"TXT\",\"rdata\":\"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\"}\n`)).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"token\", \"keyAuth\")\n\trequire.NoError(t, err)\n\trequire.Contains(t, provider.recordIDs, \"_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\")\n}\n\nfunc TestDNSProvider_Cleanup_WhenRecordIdNotSet_NoOp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /zones/records/all/_acme-challenge.example.com\",\n\t\t\tservermock.RawStringResponse(`{\n\t  \"msg\": \"string\",\n\t  \"status\": 200,\n\t  \"tm\": 0,\n\t  \"data\": [{\n\t    \"id\": \"60898922\",\n\t    \"domain\": \"example.com\",\n\t    \"host\": \"hosta\",\n\t    \"ttl\": \"300\",\n\t    \"prio\": \"0\",\n\t    \"geozone_id\": \"0\",\n\t    \"type\": \"A\",\n\t    \"rdata\": \"1.2.3.4\",\n\t    \"last_mod\": \"2019-08-28 19:09:50\"\n\t  }],\n\t  \"count\": 0,\n\t  \"total\": 0,\n\t  \"start\": 0,\n\t  \"max\": 0\n\t}\n\t`)).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"token\", \"keyAuth\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_Cleanup_WhenRecordIdSet_DeletesTxtRecord(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /zones/records/all/_acme-challenge.example.com\",\n\t\t\tservermock.RawStringResponse(`{\n\t  \"msg\": \"string\",\n\t  \"status\": 200,\n\t  \"tm\": 0,\n\t  \"data\": [{\n\t    \"id\": \"60898922\",\n\t    \"domain\": \"example.com\",\n\t    \"host\": \"hosta\",\n\t    \"ttl\": \"300\",\n\t    \"prio\": \"0\",\n\t    \"geozone_id\": \"0\",\n\t    \"type\": \"A\",\n\t    \"rdata\": \"1.2.3.4\",\n\t    \"last_mod\": \"2019-08-28 19:09:50\"\n\t  }],\n\t  \"count\": 0,\n\t  \"total\": 0,\n\t  \"start\": 0,\n\t  \"max\": 0\n\t}\n\t`)).\n\t\tRoute(\"DELETE /zones/records/_acme-challenge.example.com/123456\",\n\t\t\tservermock.RawStringResponse(`{\n\t\t\t\t\"msg\": \"OK\",\n\t\t\t\t\"data\": {\n\t\t\t\t\t\"domain\": \"example.com\",\n\t\t\t\t\t\"id\": \"123456\"\n\t\t\t\t},\n\t\t\t\t\"status\": 200\n\t\t\t}`)).\n\t\tBuild(t)\n\n\tprovider.recordIDs[\"_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\"] = \"123456\"\n\n\terr := provider.CleanUp(\"example.com\", \"token\", \"keyAuth\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_Cleanup_WhenHttpError_ReturnsError(t *testing.T) {\n\terrorMessage := `{\n\t\t\"error\": {\n\t\t\t\"code\": 406,\n\t\t\t\"message\": \"Provided id is invalid or you do not have permission to access it.\"\n\t\t}\n\t}`\n\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /zones/records/all/example.com\",\n\t\t\tservermock.RawStringResponse(`{\n  \"msg\": \"string\",\n  \"status\": 200,\n  \"tm\": 0,\n  \"data\": [{\n    \"id\": \"60898922\",\n    \"domain\": \"example.com\",\n    \"host\": \"hosta\",\n    \"ttl\": \"300\",\n    \"prio\": \"0\",\n    \"geozone_id\": \"0\",\n    \"type\": \"A\",\n    \"rdata\": \"1.2.3.4\",\n    \"last_mod\": \"2019-08-28 19:09:50\"\n  }],\n  \"count\": 0,\n  \"total\": 0,\n  \"start\": 0,\n  \"max\": 0\n}\n`)).\n\t\tRoute(\"DELETE /zones/records/example.com/123456\",\n\t\t\tservermock.RawStringResponse(errorMessage).\n\t\t\t\tWithStatusCode(http.StatusNotAcceptable)).\n\t\tBuild(t)\n\n\tprovider.recordIDs[\"_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\"] = \"123456\"\n\n\terr := provider.CleanUp(\"example.com\", \"token\", \"keyAuth\")\n\n\texpectedError := fmt.Sprintf(\"easydns: unexpected status code: [status code: 406] body: %v\", errorMessage)\n\trequire.EqualError(t, err, expectedError)\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/easydns/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// DefaultBaseURL the default API endpoint.\nconst DefaultBaseURL = \"https://rest.easydns.net\"\n\n// Client the EasyDNS API client.\ntype Client struct {\n\ttoken string\n\tkey   string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(token, key string) *Client {\n\tbaseURL, _ := url.Parse(DefaultBaseURL)\n\n\treturn &Client{\n\t\ttoken:      token,\n\t\tkey:        key,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\nfunc (c *Client) ListZones(ctx context.Context, domain string) ([]ZoneRecord, error) {\n\tendpoint := c.BaseURL.JoinPath(\"zones\", \"records\", \"all\", domain)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresponse := &apiResponse[[]ZoneRecord]{}\n\n\terr = c.do(req, response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif response.Error != nil {\n\t\treturn nil, response.Error\n\t}\n\n\treturn response.Data, nil\n}\n\nfunc (c *Client) AddRecord(ctx context.Context, domain string, record ZoneRecord) (string, error) {\n\tendpoint := c.BaseURL.JoinPath(\"zones\", \"records\", \"add\", domain, \"TXT\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, record)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresponse := &apiResponse[*ZoneRecord]{}\n\n\terr = c.do(req, response)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif response.Error != nil {\n\t\treturn \"\", response.Error\n\t}\n\n\trecordID := response.Data.ID\n\n\treturn recordID, nil\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, domain, recordID string) error {\n\tendpoint := c.BaseURL.JoinPath(\"zones\", \"records\", domain, recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, nil)\n\n\treturn err\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.SetBasicAuth(c.token, c.key)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\tquery := endpoint.Query()\n\tquery.Set(\"format\", \"json\")\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/easydns/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"tok\", \"k\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithBasicAuth(\"tok\", \"k\"),\n\t)\n}\n\nfunc TestClient_ListZones(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones/records/all/example.com\", servermock.ResponseFromFixture(\"list-zone.json\")).\n\t\tBuild(t)\n\n\tzones, err := client.ListZones(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []ZoneRecord{{\n\t\tID:       \"60898922\",\n\t\tDomain:   \"example.com\",\n\t\tHost:     \"hosta\",\n\t\tTTL:      \"300\",\n\t\tPriority: \"0\",\n\t\tType:     \"A\",\n\t\tRdata:    \"1.2.3.4\",\n\t\tLastMod:  \"2019-08-28 19:09:50\",\n\t}}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_ListZones_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones/records/all/example.com\", servermock.ResponseFromFixture(\"error1.json\")).\n\t\tBuild(t)\n\n\t_, err := client.ListZones(t.Context(), \"example.com\")\n\trequire.EqualError(t, err, \"code 420: Enhance Your Calm. Rate limit exceeded (too many requests) OR you did NOT provide any credentials with your request!\")\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /zones/records/add/example.com/TXT\",\n\t\t\tservermock.ResponseFromFixture(\"add-record.json\").WithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBody(`{\"domain\":\"example.com\",\"host\":\"test631\",\"ttl\":\"300\",\"prio\":\"0\",\"type\":\"TXT\",\"rdata\":\"txt\"}`)).\n\t\tBuild(t)\n\n\trecord := ZoneRecord{\n\t\tDomain:   \"example.com\",\n\t\tHost:     \"test631\",\n\t\tType:     \"TXT\",\n\t\tRdata:    \"txt\",\n\t\tTTL:      \"300\",\n\t\tPriority: \"0\",\n\t}\n\n\trecordID, err := client.AddRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"xxx\", recordID)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /zones/records/add/example.com/TXT\",\n\t\t\tservermock.ResponseFromFixture(\"error1.json\").WithStatusCode(http.StatusCreated)).\n\t\tBuild(t)\n\n\trecord := ZoneRecord{\n\t\tDomain:   \"example.com\",\n\t\tHost:     \"test631\",\n\t\tType:     \"TXT\",\n\t\tRdata:    \"txt\",\n\t\tTTL:      \"300\",\n\t\tPriority: \"0\",\n\t}\n\n\t_, err := client.AddRecord(t.Context(), \"example.com\", record)\n\trequire.EqualError(t, err, \"code 420: Enhance Your Calm. Rate limit exceeded (too many requests) OR you did NOT provide any credentials with your request!\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /zones/records/example.com/xxx\", nil).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", \"xxx\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/easydns/internal/fixtures/add-record.json",
    "content": "{\n  \"msg\": \"message\",\n  \"tm\": 1,\n  \"data\": {\n    \"id\": \"xxx\",\n    \"domain\": \"example.com\",\n    \"host\": \"test631\",\n    \"ttl\": \"300\",\n    \"prio\": \"0\",\n    \"type\": \"TXT\",\n    \"rdata\": \"txt\"\n  },\n  \"status\": 201\n}\n"
  },
  {
    "path": "providers/dns/easydns/internal/fixtures/error.json",
    "content": "{\n  \"msg\": \"Enhance your calm\",\n  \"status\": 403\n}\n"
  },
  {
    "path": "providers/dns/easydns/internal/fixtures/error1.json",
    "content": "{\n  \"error\": {\n    \"code\": 420,\n    \"message\": \"Enhance Your Calm. Rate limit exceeded (too many requests) OR you did NOT provide any credentials with your request!\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/easydns/internal/fixtures/list-zone.json",
    "content": "{\n  \"msg\": \"message\",\n  \"status\": 200,\n  \"tm\": 0,\n  \"data\": [\n    {\n      \"id\": \"60898922\",\n      \"domain\": \"example.com\",\n      \"host\": \"hosta\",\n      \"ttl\": \"300\",\n      \"prio\": \"0\",\n      \"geozone_id\": \"0\",\n      \"type\": \"A\",\n      \"rdata\": \"1.2.3.4\",\n      \"last_mod\": \"2019-08-28 19:09:50\"\n    }\n  ],\n  \"count\": 43,\n  \"total\": 43,\n  \"start\": 0,\n  \"max\": 1000\n}\n"
  },
  {
    "path": "providers/dns/easydns/internal/readme.md",
    "content": "The API doc is mainly wrong on the response schema:\n\nex:\n\n- the doc for `/zones/records/all/{domain}`\n\n```json\n{\n  \"msg\": \"string\",\n  \"status\": 200,\n  \"tm\": 1709190001,\n  \"data\": {\n    \"id\": 60898922,\n    \"domain\": \"example.com\",\n    \"host\": \"hosta\",\n    \"ttl\": 300,\n    \"prio\": 0,\n    \"geozone_id\": 0,\n    \"type\": \"A\",\n    \"rdata\": \"1.2.3.4\",\n    \"last_mod\": \"2019-08-28 19:09:50\"\n  },\n  \"count\": 0,\n  \"total\": 0,\n  \"start\": 0,\n  \"max\": 0\n}\n```\n\n- The reality:\n\n```json\n{\n  \"tm\": 1709190001,\n  \"data\": [\n    {\n      \"id\": \"60898922\",\n      \"domain\": \"example.com\",\n      \"host\": \"hosta\",\n      \"ttl\": \"300\",\n      \"prio\": \"0\",\n      \"geozone_id\": \"0\",\n      \"type\": \"A\",\n      \"rdata\": \"1.2.3.4\",\n      \"last_mod\": \"2019-08-28 19:09:50\"\n    }\n  ],\n  \"count\": 0,\n  \"total\": 0,\n  \"start\": 0,\n  \"max\": 0,\n  \"status\": 200\n}\n```\n\n`data` is an array.\n`id`, `ttl`, `geozone_id` are strings.\n"
  },
  {
    "path": "providers/dns/easydns/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype apiResponse[T any] struct {\n\tMsg    string `json:\"msg\"`\n\tStatus int    `json:\"status\"`\n\tTm     int    `json:\"tm\"`\n\tData   T      `json:\"data\"`\n\tCount  int    `json:\"count\"`\n\tTotal  int    `json:\"total\"`\n\tStart  int    `json:\"start\"`\n\tMax    int    `json:\"max\"`\n\tError  *Error `json:\"error,omitempty\"`\n}\n\ntype ZoneRecord struct {\n\tID       string `json:\"id,omitempty\"`\n\tDomain   string `json:\"domain\"`\n\tHost     string `json:\"host\"`\n\tTTL      string `json:\"ttl\"`\n\tPriority string `json:\"prio\"`\n\tType     string `json:\"type\"`\n\tRdata    string `json:\"rdata\"`\n\tLastMod  string `json:\"last_mod,omitempty\"`\n\tRevoked  int    `json:\"revoked,omitempty\"`\n\tNewHost  string `json:\"new_host,omitempty\"`\n}\n\ntype Error struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\nfunc (e *Error) Error() string {\n\treturn fmt.Sprintf(\"code %d: %s\", e.Code, e.Message)\n}\n"
  },
  {
    "path": "providers/dns/edgecenter/edgecenter.go",
    "content": "// Package edgecenter implements a DNS provider for solving the DNS-01 challenge using EdgeCenter.\npackage edgecenter\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/gcore\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"EDGECENTER_\"\n\n\tEnvPermanentAPIToken = envNamespace + \"PERMANENT_API_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultBaseURL = \"https://api.edgecenter.ru/dns\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config for DNSProvider.\ntype Config = gcore.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, gcore.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, gcore.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider an implementation of challenge.Provider contract.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns an instance of DNSProvider configured for G-Core DNS API.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvPermanentAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"edgecenter: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIToken = values[EnvPermanentAPIToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for G-Core DNS API.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"edgecenter: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := gcore.NewDNSProviderConfig(config, defaultBaseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"edgecenter: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"edgecenter: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"edgecenter: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n"
  },
  {
    "path": "providers/dns/edgecenter/edgecenter.toml",
    "content": "Name = \"EdgeCenter\"\nDescription = ''''''\nURL = \"https://edgecenter.ru/dns\"\nCode = \"edgecenter\"\nSince = \"v4.29.0\"\n\nExample = '''\nEDGECENTER_PERMANENT_API_TOKEN=xxxxx \\\nlego --dns edgecenter -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    EDGECENTER_PERMANENT_API_TOKEN = \"Permanent API token (https://edgecenter.ru/blog/permanent-api-token-explained/)\"\n  [Configuration.Additional]\n    EDGECENTER_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 20)\"\n    EDGECENTER_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 360)\"\n    EDGECENTER_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    EDGECENTER_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://apidocs.edgecenter.ru/dns\"\n"
  },
  {
    "path": "providers/dns/edgecenter/edgecenter_test.go",
    "content": "package edgecenter\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar envTest = tester.NewEnvTest(EnvPermanentAPIToken).WithDomain(envNamespace + \"DOMAIN\")\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPermanentAPIToken: \"A\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPermanentAPIToken: \"\",\n\t\t\t},\n\t\t\texpected: \"edgecenter: some credentials information are missing: EDGECENTER_PERMANENT_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiToken string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiToken: \"A\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"edgecenter: incomplete credentials provided\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = test.apiToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/edgedns/edgedns.go",
    "content": "// Package edgedns replaces fastdns, implementing a DNS provider for solving the DNS-01 challenge using Akamai EdgeDNS.\npackage edgedns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\tedgegriddns \"github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/dns\"\n\t\"github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/edgegrid\"\n\t\"github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/session\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"AKAMAI_\"\n\n\tEnvEdgeRc           = envNamespace + \"EDGERC\"\n\tEnvEdgeRcSection    = envNamespace + \"EDGERC_SECTION\"\n\tEnvAccountSwitchKey = envNamespace + \"ACCOUNT_SWITCH_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\n// Test Environment variables names (unused).\n// TODO(ldez): must be moved into test files.\nconst (\n\tEnvHost         = envNamespace + \"HOST\"\n\tEnvClientToken  = envNamespace + \"CLIENT_TOKEN\"\n\tEnvClientSecret = envNamespace + \"CLIENT_SECRET\"\n\tEnvAccessToken  = envNamespace + \"ACCESS_TOKEN\"\n)\n\nconst (\n\tdefaultPropagationTimeout = 3 * time.Minute\n\tdefaultPollInterval       = 15 * time.Second\n)\n\nconst maxBody = 131072\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\t*edgegrid.Config\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, defaultPollInterval),\n\t\tConfig:             &edgegrid.Config{MaxBody: maxBody},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Akamai EdgeDNS:\n// Akamai's credentials are automatically detected in the following locations and prioritized in the following order:\n//\n// 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`\n// 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`\n// 3. .edgerc file located at `AKAMAI_EDGERC` (defaults to `~/.edgerc`, sections can be specified using `AKAMAI_EDGERC_SECTION`)\n//\n// See also: https://developer.akamai.com/api/getting-started\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tconf, err := edgegrid.New(\n\t\tedgegrid.WithEnv(true),\n\t\tedgegrid.WithFile(env.GetOrDefaultString(EnvEdgeRc, \"~/.edgerc\")),\n\t\tedgegrid.WithSection(env.GetOrDefaultString(EnvEdgeRcSection, \"default\")),\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"edgedns: %w\", err)\n\t}\n\n\tconf.MaxBody = maxBody\n\n\taccountSwitchKey := env.GetOrDefaultString(EnvAccountSwitchKey, \"\")\n\n\tif accountSwitchKey != \"\" {\n\t\tconf.AccountKey = accountSwitchKey\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Config = conf\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for EdgeDNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"edgedns: the configuration of the DNS provider is nil\")\n\t}\n\n\terr := config.Validate()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"edgedns: %w\", err)\n\t}\n\n\treturn &DNSProvider{config: config}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tsess, err := session.New(session.WithSigner(d.config))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"edgedns: %w\", err)\n\t}\n\n\tclient := edgegriddns.Client(sess)\n\n\tzone, err := getZone(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"edgedns: %w\", err)\n\t}\n\n\trecord, err := client.GetRecord(ctx, edgegriddns.GetRecordRequest{\n\t\tZone:       zone,\n\t\tName:       info.EffectiveFQDN,\n\t\tRecordType: \"TXT\",\n\t})\n\tif err != nil && !isNotFound(err) {\n\t\treturn fmt.Errorf(\"edgedns: %w\", err)\n\t}\n\n\tif err == nil && record == nil {\n\t\treturn errors.New(\"edgedns: unknown error\")\n\t}\n\n\tif record != nil {\n\t\tlog.Infof(\"TXT record already exists. Updating target\")\n\n\t\tif containsValue(record.Target, info.Value) {\n\t\t\t// have a record and have entry already\n\t\t\treturn nil\n\t\t}\n\n\t\trecord.Target = append(record.Target, `\"`+info.Value+`\"`)\n\t\trecord.TTL = d.config.TTL\n\n\t\terr = client.UpdateRecord(ctx, edgegriddns.UpdateRecordRequest{\n\t\t\tRecord: &edgegriddns.RecordBody{\n\t\t\t\tName:       record.Name,\n\t\t\t\tRecordType: record.RecordType,\n\t\t\t\tTTL:        record.TTL,\n\t\t\t\tActive:     record.Active,\n\t\t\t\tTarget:     record.Target,\n\t\t\t},\n\t\t\tZone: zone,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"edgedns: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\terr = client.CreateRecord(ctx, edgegriddns.CreateRecordRequest{\n\t\tRecord: &edgegriddns.RecordBody{\n\t\t\tName:       info.EffectiveFQDN,\n\t\t\tRecordType: \"TXT\",\n\t\t\tTTL:        d.config.TTL,\n\t\t\tTarget:     []string{`\"` + info.Value + `\"`},\n\t\t},\n\t\tZone:    zone,\n\t\tRecLock: nil,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"edgedns: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tsess, err := session.New(session.WithSigner(d.config))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"edgedns: %w\", err)\n\t}\n\n\tclient := edgegriddns.Client(sess)\n\n\tzone, err := getZone(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"edgedns: %w\", err)\n\t}\n\n\texistingRec, err := client.GetRecord(ctx, edgegriddns.GetRecordRequest{\n\t\tZone:       zone,\n\t\tName:       info.EffectiveFQDN,\n\t\tRecordType: \"TXT\",\n\t})\n\tif err != nil {\n\t\tif isNotFound(err) {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"edgedns: %w\", err)\n\t}\n\n\tif existingRec == nil {\n\t\treturn errors.New(\"edgedns: unknown failure\")\n\t}\n\n\tif len(existingRec.Target) == 0 {\n\t\treturn errors.New(\"edgedns: TXT record is invalid\")\n\t}\n\n\tif !containsValue(existingRec.Target, info.Value) {\n\t\treturn nil\n\t}\n\n\tnewRData := filterRData(existingRec, info)\n\n\tif len(newRData) > 0 {\n\t\texistingRec.Target = newRData\n\n\t\terr = client.UpdateRecord(ctx, edgegriddns.UpdateRecordRequest{\n\t\t\tRecord: &edgegriddns.RecordBody{\n\t\t\t\tName:       existingRec.Name,\n\t\t\t\tRecordType: existingRec.RecordType,\n\t\t\t\tTTL:        existingRec.TTL,\n\t\t\t\tActive:     existingRec.Active,\n\t\t\t\tTarget:     existingRec.Target,\n\t\t\t},\n\t\t\tZone: zone,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"edgedns: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\terr = client.DeleteRecord(ctx, edgegriddns.DeleteRecordRequest{\n\t\tZone:       zone,\n\t\tName:       existingRec.Name,\n\t\tRecordType: \"TXT\",\n\t\tRecLock:    nil,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"edgedns: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc getZone(domain string) (string, error) {\n\tzone, err := dns01.FindZoneByFqdn(domain)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not find zone for FQDN %q: %w\", domain, err)\n\t}\n\n\treturn dns01.UnFqdn(zone), nil\n}\n\nfunc containsValue(values []string, value string) bool {\n\treturn slices.ContainsFunc(values, func(val string) bool {\n\t\treturn strings.Trim(val, `\"`) == value\n\t})\n}\n\nfunc isNotFound(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\tvar e *edgegriddns.Error\n\n\treturn errors.As(err, &e) && e.StatusCode == http.StatusNotFound\n}\n\nfunc filterRData(existingRec *edgegriddns.GetRecordResponse, info dns01.ChallengeInfo) []string {\n\tvar newRData []string\n\n\tfor _, val := range existingRec.Target {\n\t\tval = strings.Trim(val, `\"`)\n\t\tif val == info.Value {\n\t\t\tcontinue\n\t\t}\n\n\t\tnewRData = append(newRData, val)\n\t}\n\n\treturn newRData\n}\n"
  },
  {
    "path": "providers/dns/edgedns/edgedns.toml",
    "content": "Name = \"Akamai EdgeDNS\"\nDescription = '''\nAkamai edgedns supersedes FastDNS; implementing a DNS provider for solving the DNS-01 challenge using Akamai EdgeDNS\n'''\nURL = \"https://www.akamai.com/us/en/products/security/edge-dns.jsp\"\nCode = \"edgedns\"\nAliases = [\"fastdns\"] # \"fastdns\" is for compatibility with v3, must be dropped in v5\nSince = \"v3.9.0\"\n\nExample = '''\nAKAMAI_CLIENT_SECRET=abcdefghijklmnopqrstuvwxyz1234567890ABCDEFG= \\\nAKAMAI_CLIENT_TOKEN=akab-mnbvcxzlkjhgfdsapoiuytrewq1234567 \\\nAKAMAI_HOST=akab-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.luna.akamaiapis.net \\\nAKAMAI_ACCESS_TOKEN=akab-1234567890qwerty-asdfghjklzxcvtnu \\\nlego --dns edgedns -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\nAkamai's credentials are automatically detected in the following locations and prioritized in the following order:\n\n1. Section-specific environment variables (where `{SECTION}` is specified using `AKAMAI_EDGERC_SECTION`):\n  - `AKAMAI_{SECTION}_HOST`\n  - `AKAMAI_{SECTION}_ACCESS_TOKEN`\n  - `AKAMAI_{SECTION}_CLIENT_TOKEN`\n  - `AKAMAI_{SECTION}_CLIENT_SECRET`\n2. If `AKAMAI_EDGERC_SECTION` is not defined or is set to `default`, environment variables:\n  - `AKAMAI_HOST`\n  - `AKAMAI_ACCESS_TOKEN`\n  - `AKAMAI_CLIENT_TOKEN`\n  - `AKAMAI_CLIENT_SECRET`\n3. `.edgerc` file located at `AKAMAI_EDGERC`\n  - defaults to `~/.edgerc`, sections can be specified using `AKAMAI_EDGERC_SECTION`\n4. Default environment variables:\n  - `AKAMAI_HOST`\n  - `AKAMAI_ACCESS_TOKEN`\n  - `AKAMAI_CLIENT_TOKEN`\n  - `AKAMAI_CLIENT_SECRET`\n\nSee also:\n\n- [Setting up Akamai credentials](https://developer.akamai.com/api/getting-started)\n- [.edgerc Format](https://developer.akamai.com/legacy/introduction/Conf_Client.html#edgercformat)\n- [API Client Authentication](https://developer.akamai.com/legacy/introduction/Client_Auth.html)\n- [Config from Env](https://github.com/akamai/AkamaiOPEN-edgegrid-golang/blob/master/pkg/edgegrid/config.go#L118)\n- [Manage many accounts](https://techdocs.akamai.com/developer/docs/manage-many-accounts-with-one-api-client)\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    AKAMAI_HOST = \"API host, managed by the Akamai EdgeGrid client\"\n    AKAMAI_CLIENT_TOKEN = \"Client token, managed by the Akamai EdgeGrid client\"\n    AKAMAI_CLIENT_SECRET = \"Client secret, managed by the Akamai EdgeGrid client\"\n    AKAMAI_ACCESS_TOKEN = \"Access token, managed by the Akamai EdgeGrid client\"\n    AKAMAI_EDGERC = \"Path to the .edgerc file, managed by the Akamai EdgeGrid client\"\n    AKAMAI_EDGERC_SECTION = \"Configuration section, managed by the Akamai EdgeGrid client\"\n  [Configuration.Additional]\n    AKAMAI_ACCOUNT_SWITCH_KEY = \"Target account ID when the DNS zone and credentials belong to different accounts\"\n    AKAMAI_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 15)\"\n    AKAMAI_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 180)\"\n    AKAMAI_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n\n[Links]\n  API = \"https://developer.akamai.com/api/cloud_security/edge_dns_zone_management/v2.html\"\n  GoClient = \"https://github.com/akamai/AkamaiOPEN-edgegrid-golang\"\n\n\n"
  },
  {
    "path": "providers/dns/edgedns/edgedns_integration_test.go",
    "content": "package edgedns\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\tedgegriddns \"github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/dns\"\n\t\"github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/session\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n\n\t// Present Twice to handle create / update\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveTTL(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\tdomain := envTest.GetDomain()\n\n\terr = provider.Present(domain, \"foo\", \"bar\")\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\te := provider.CleanUp(domain, \"foo\", \"bar\")\n\t\tif e != nil {\n\t\t\tt.Log(e)\n\t\t}\n\t}()\n\n\tfqdn := \"_acme-challenge.\" + domain + \".\"\n\tzone, err := getZone(fqdn)\n\trequire.NoError(t, err)\n\n\tctx := context.Background()\n\n\tsess, err := session.New(session.WithSigner(provider.config))\n\trequire.NoError(t, err)\n\n\tclient := edgegriddns.Client(sess)\n\n\tresourceRecordSets, err := client.GetRecordList(ctx, edgegriddns.GetRecordListRequest{\n\t\tZone:       zone,\n\t\tRecordType: \"TXT\",\n\t})\n\n\trequire.NoError(t, err)\n\n\tfor i, rrset := range resourceRecordSets.RecordSets {\n\t\tif rrset.Name != fqdn {\n\t\t\tcontinue\n\t\t}\n\n\t\tt.Run(fmt.Sprintf(\"testing record set %d\", i), func(t *testing.T) {\n\t\t\tassert.Equal(t, fqdn, rrset.Name)\n\t\t\tassert.Equal(t, \"TXT\", rrset.Type)\n\t\t\tassert.Equal(t, dns01.DefaultTTL, rrset.TTL)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/edgedns/edgedns_test.go",
    "content": "package edgedns\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/edgegrid\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tenvDomain           = envNamespace + \"TEST_DOMAIN\"\n\tenvTestHost         = envNamespace + \"TEST_HOST\"\n\tenvTestClientToken  = envNamespace + \"TEST_CLIENT_TOKEN\"\n\tenvTestClientSecret = envNamespace + \"TEST_CLIENT_SECRET\"\n\tenvTestAccessToken  = envNamespace + \"TEST_ACCESS_TOKEN\"\n)\n\nvar envTest = tester.NewEnvTest(\n\tEnvTTL,\n\tEnvPollingInterval,\n\tEnvPropagationTimeout,\n\tEnvHost,\n\tEnvClientToken,\n\tEnvClientSecret,\n\tEnvAccessToken,\n\tEnvAccountSwitchKey,\n\tEnvEdgeRc,\n\tEnvEdgeRcSection,\n\tenvTestHost,\n\tenvTestClientToken,\n\tenvTestClientSecret,\n\tenvTestAccessToken).\n\tWithDomain(envDomain).\n\tWithLiveTestRequirements(EnvHost, EnvClientToken, EnvClientSecret, EnvAccessToken, envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc           string\n\t\tenvVars        map[string]string\n\t\texpectedConfig *edgegrid.Config\n\t\texpectedErr    string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvHost:         \"akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net\",\n\t\t\t\tEnvClientToken:  \"akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx\",\n\t\t\t\tEnvClientSecret: \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",\n\t\t\t\tEnvAccessToken:  \"akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx\",\n\t\t\t},\n\t\t\texpectedConfig: newEdgeConfig(func(config *edgegrid.Config) {\n\t\t\t\tconfig.Host = \"akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net\"\n\t\t\t\tconfig.ClientToken = \"akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx\"\n\t\t\t\tconfig.ClientSecret = \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n\t\t\t\tconfig.AccessToken = \"akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx\"\n\t\t\t\tconfig.MaxBody = maxBody\n\t\t\t}, edgegrid.WithEnv(true), edgegrid.WithFile(\"/dev/null\")),\n\t\t},\n\t\t{\n\t\t\tdesc: \"with account switch key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvHost:             \"akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net\",\n\t\t\t\tEnvClientToken:      \"akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx\",\n\t\t\t\tEnvClientSecret:     \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",\n\t\t\t\tEnvAccessToken:      \"akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx\",\n\t\t\t\tEnvAccountSwitchKey: \"F-AC-1234\",\n\t\t\t},\n\t\t\texpectedConfig: newEdgeConfig(func(config *edgegrid.Config) {\n\t\t\t\tconfig.Host = \"akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net\"\n\t\t\t\tconfig.ClientToken = \"akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx\"\n\t\t\t\tconfig.ClientSecret = \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n\t\t\t\tconfig.AccessToken = \"akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx\"\n\t\t\t\tconfig.MaxBody = maxBody\n\t\t\t\tconfig.AccountKey = \"F-AC-1234\"\n\t\t\t}, edgegrid.WithEnv(true), edgegrid.WithFile(\"/dev/null\")),\n\t\t},\n\t\t{\n\t\t\tdesc: \"with section\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEdgeRcSection:    \"test\",\n\t\t\t\tenvTestHost:         \"akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net\",\n\t\t\t\tenvTestClientToken:  \"akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx\",\n\t\t\t\tenvTestClientSecret: \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",\n\t\t\t\tenvTestAccessToken:  \"akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx\",\n\t\t\t},\n\t\t\texpectedConfig: newEdgeConfig(func(config *edgegrid.Config) {\n\t\t\t\tconfig.Host = \"akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net\"\n\t\t\t\tconfig.ClientToken = \"akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx\"\n\t\t\t\tconfig.ClientSecret = \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n\t\t\t\tconfig.AccessToken = \"akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx\"\n\t\t\t\tconfig.MaxBody = maxBody\n\t\t\t}, edgegrid.WithEnv(true), edgegrid.WithFile(\"/dev/null\"), edgegrid.WithSection(\"test\")),\n\t\t},\n\t\t{\n\t\t\tdesc:        \"missing credentials\",\n\t\t\texpectedErr: `edgedns: unable to load config from environment or .edgerc file`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing host\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvHost:         \"\",\n\t\t\t\tEnvClientToken:  \"B\",\n\t\t\t\tEnvClientSecret: \"C\",\n\t\t\t\tEnvAccessToken:  \"D\",\n\t\t\t},\n\t\t\texpectedErr: `edgedns: unable to load config from environment or .edgerc file`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing client token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvHost:         \"A\",\n\t\t\t\tEnvClientToken:  \"\",\n\t\t\t\tEnvClientSecret: \"C\",\n\t\t\t\tEnvAccessToken:  \"D\",\n\t\t\t},\n\t\t\texpectedErr: `edgedns: unable to load config from environment or .edgerc file`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing client secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvHost:         \"A\",\n\t\t\t\tEnvClientToken:  \"B\",\n\t\t\t\tEnvClientSecret: \"\",\n\t\t\t\tEnvAccessToken:  \"D\",\n\t\t\t},\n\t\t\texpectedErr: `edgedns: unable to load config from environment or .edgerc file`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvHost:         \"A\",\n\t\t\t\tEnvClientToken:  \"B\",\n\t\t\t\tEnvClientSecret: \"C\",\n\t\t\t\tEnvAccessToken:  \"\",\n\t\t\t},\n\t\t\texpectedErr: `edgedns: unable to load config from environment or .edgerc file`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tif test.envVars == nil {\n\t\t\t\ttest.envVars = map[string]string{}\n\t\t\t}\n\n\t\t\ttest.envVars[EnvEdgeRc] = \"/dev/null\"\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expectedErr != \"\" {\n\t\t\t\trequire.ErrorContains(t, err, test.expectedErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, p)\n\t\t\trequire.NotNil(t, p.config)\n\n\t\t\tif test.expectedConfig != nil {\n\t\t\t\trequire.Equal(t, test.expectedConfig, p.config.Config)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDefaultConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected *Config\n\t}{\n\t\t{\n\t\t\tdesc: \"default configuration\",\n\t\t\texpected: &Config{\n\t\t\t\tTTL:                dns01.DefaultTTL,\n\t\t\t\tPropagationTimeout: 3 * time.Minute,\n\t\t\t\tPollingInterval:    15 * time.Second,\n\t\t\t\tConfig: &edgegrid.Config{\n\t\t\t\t\tMaxBody: maxBody,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"custom values\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTTL:                \"99\",\n\t\t\t\tEnvPropagationTimeout: \"60\",\n\t\t\t\tEnvPollingInterval:    \"60\",\n\t\t\t},\n\t\t\texpected: &Config{\n\t\t\t\tTTL:                99,\n\t\t\t\tPropagationTimeout: 60 * time.Second,\n\t\t\t\tPollingInterval:    60 * time.Second,\n\t\t\t\tConfig: &edgegrid.Config{\n\t\t\t\t\tMaxBody: maxBody,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tconfig := NewDefaultConfig()\n\n\t\t\trequire.Equal(t, test.expected, config)\n\t\t})\n\t}\n}\n\nfunc Test_findZone(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tdomain   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"Extract root record name\",\n\t\t\tdomain:   \"example.com.\",\n\t\t\texpected: \"example.com\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"Extract sub record name\",\n\t\t\tdomain:   \"foo.example.com.\",\n\t\t\texpected: \"example.com\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tzone, err := getZone(test.domain)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, test.expected, zone)\n\t\t})\n\t}\n}\n\nfunc newEdgeConfig(opts ...edgegrid.Option) *edgegrid.Config {\n\tconfig, _ := edgegrid.New(opts...)\n\treturn config\n}\n"
  },
  {
    "path": "providers/dns/edgeone/edgeone.go",
    "content": "// Package edgeone implements a DNS provider for solving the DNS-01 challenge using Tencent EdgeOne.\npackage edgeone\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n\tteo \"github.com/go-acme/tencentedgdeone/v20220901\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n\t\"golang.org/x/net/idna\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"EDGEONE_\"\n\n\tEnvSecretID     = envNamespace + \"SECRET_ID\"\n\tEnvSecretKey    = envNamespace + \"SECRET_KEY\"\n\tEnvRegion       = envNamespace + \"REGION\"\n\tEnvSessionToken = envNamespace + \"SESSION_TOKEN\"\n\tEnvZonesMapping = envNamespace + \"ZONES_MAPPING\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tSecretID     string\n\tSecretKey    string\n\tRegion       string\n\tSessionToken string\n\n\tZonesMapping map[string]string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 60),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 20*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 30*time.Second),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *teo.Client\n\n\trecordIDs   map[string]*string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Tencent EdgeOne.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvSecretID, EnvSecretKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"edgeone: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.SecretID = values[EnvSecretID]\n\tconfig.SecretKey = values[EnvSecretKey]\n\tconfig.Region = env.GetOrDefaultString(EnvRegion, \"\")\n\tconfig.SessionToken = env.GetOrDefaultString(EnvSessionToken, \"\")\n\n\tmapping := env.GetOrDefaultString(EnvZonesMapping, \"\")\n\tif mapping != \"\" {\n\t\tconfig.ZonesMapping, err = env.ParsePairs(mapping)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"edgeone: zones mapping: %w\", err)\n\t\t}\n\t}\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Tencent EdgeOne.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"edgeone: the configuration of the DNS provider is nil\")\n\t}\n\n\tvar credential *common.Credential\n\n\tswitch {\n\tcase config.SecretID != \"\" && config.SecretKey != \"\" && config.SessionToken != \"\":\n\t\tcredential = common.NewTokenCredential(config.SecretID, config.SecretKey, config.SessionToken)\n\tcase config.SecretID != \"\" && config.SecretKey != \"\":\n\t\tcredential = common.NewCredential(config.SecretID, config.SecretKey)\n\tdefault:\n\t\treturn nil, errors.New(\"edgeone: credentials missing\")\n\t}\n\n\tcpf := profile.NewClientProfile()\n\tcpf.HttpProfile.Endpoint = \"teo.intl.tencentcloudapi.com\"\n\tcpf.HttpProfile.ReqTimeout = int(math.Round(config.HTTPTimeout.Seconds()))\n\n\tclient, err := teo.NewClient(credential, config.Region, cpf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"edgeone: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: map[string]*string{},\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzoneID, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"edgeone: failed to get hosted zone: %w\", err)\n\t}\n\n\tpunnyCoded, err := idna.ToASCII(dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"edgeone: fail to convert punycode: %w\", err)\n\t}\n\n\trequest := teo.NewCreateDnsRecordRequest()\n\trequest.Name = ptr.Pointer(punnyCoded)\n\trequest.ZoneId = zoneID\n\trequest.Type = ptr.Pointer(\"TXT\")\n\trequest.Content = ptr.Pointer(info.Value)\n\trequest.TTL = ptr.Pointer(int64(d.config.TTL))\n\n\tnr, err := teo.CreateDnsRecordWithContext(ctx, d.client, request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"edgeone: API call failed: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = nr.Response.RecordId\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzoneID, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"edgeone: failed to get hosted zone: %w\", err)\n\t}\n\n\t// get the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"edgeone: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\trequest := teo.NewDeleteDnsRecordsRequest()\n\trequest.ZoneId = zoneID\n\trequest.RecordIds = []*string{recordID}\n\n\t_, err = teo.DeleteDnsRecordsWithContext(ctx, d.client, request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"edgeone: delete record failed: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/edgeone/edgeone.toml",
    "content": "Name = \"Tencent EdgeOne\"\nDescription = ''''''\nURL = \"https://edgeone.ai\"\nCode = \"edgeone\"\nSince = \"v4.26.0\"\n\nExample = '''\nEDGEONE_SECRET_ID=abcdefghijklmnopqrstuvwx \\\nEDGEONE_SECRET_KEY=your-secret-key \\\nlego --dns edgeone -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    EDGEONE_SECRET_ID = \"Access key ID\"\n    EDGEONE_SECRET_KEY = \"Access Key secret\"\n  [Configuration.Additional]\n    EDGEONE_SESSION_TOKEN = \"Access Key token\"\n    EDGEONE_REGION = \"Region\"\n    EDGEONE_ZONES_MAPPING = \"Mapping between DNS zones and site IDs. (ex: 'example.org:id1,example.com:id2')\"\n    EDGEONE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 30)\"\n    EDGEONE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 1200)\"\n    EDGEONE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    EDGEONE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://edgeone.ai/document/50454#dns-record-apis\"\n  GoClient = \"https://github.com/tencentcloud/tencentcloud-sdk-go\"\n"
  },
  {
    "path": "providers/dns/edgeone/edgeone_test.go",
    "content": "package edgeone\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvSecretID,\n\tEnvSecretKey,\n\tEnvZonesMapping,\n).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretID:  \"123\",\n\t\t\t\tEnvSecretKey: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success with zones mapping\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretID:     \"123\",\n\t\t\t\tEnvSecretKey:    \"456\",\n\t\t\t\tEnvZonesMapping: \"example.org:id1,example.com:id2\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretID:  \"\",\n\t\t\t\tEnvSecretKey: \"\",\n\t\t\t},\n\t\t\texpected: \"edgeone: some credentials information are missing: EDGEONE_SECRET_ID,EDGEONE_SECRET_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretID:  \"\",\n\t\t\t\tEnvSecretKey: \"456\",\n\t\t\t},\n\t\t\texpected: \"edgeone: some credentials information are missing: EDGEONE_SECRET_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretID:  \"123\",\n\t\t\t\tEnvSecretKey: \"\",\n\t\t\t},\n\t\t\texpected: \"edgeone: some credentials information are missing: EDGEONE_SECRET_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid mapping\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretID:     \"123\",\n\t\t\t\tEnvSecretKey:    \"456\",\n\t\t\t\tEnvZonesMapping: \"example.org:id1,example.com\",\n\t\t\t},\n\t\t\texpected: \"edgeone: zones mapping: incorrect pair: example.com\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tsecretID  string\n\t\tsecretKey string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tsecretID:  \"123\",\n\t\t\tsecretKey: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"edgeone: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing secret id\",\n\t\t\tsecretKey: \"456\",\n\t\t\texpected:  \"edgeone: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret key\",\n\t\t\tsecretID: \"123\",\n\t\t\texpected: \"edgeone: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.SecretID = test.secretID\n\t\t\tconfig.SecretKey = test.secretKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/edgeone/wrapper.go",
    "content": "package edgeone\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n\tteo \"github.com/go-acme/tencentedgdeone/v20220901\"\n)\n\nfunc (d *DNSProvider) getHostedZoneID(ctx context.Context, domain string) (*string, error) {\n\tauthZone, err := dns01.FindZoneByFqdn(domain)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tif d.config.ZonesMapping != nil {\n\t\tzoneID, ok := d.config.ZonesMapping[authZone]\n\t\tif ok {\n\t\t\treturn ptr.Pointer(zoneID), nil\n\t\t}\n\t}\n\n\trequest := teo.NewDescribeZonesRequest()\n\n\tvar zones []*teo.Zone\n\n\tfor {\n\t\tresponse, err := teo.DescribeZonesWithContext(ctx, d.client, request)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"API call failed: %w\", err)\n\t\t}\n\n\t\tzones = append(zones, response.Response.Zones...)\n\n\t\tif int64(len(zones)) >= ptr.Deref(response.Response.TotalCount) {\n\t\t\tbreak\n\t\t}\n\n\t\trequest.Offset = ptr.Pointer(int64(len(zones)))\n\t}\n\n\tvar hostedZone *teo.Zone\n\n\tfor _, zone := range zones {\n\t\tunfqdn := dns01.UnFqdn(authZone)\n\t\tif ptr.Deref(zone.ZoneName) == unfqdn {\n\t\t\thostedZone = zone\n\t\t}\n\t}\n\n\tif hostedZone == nil {\n\t\treturn nil, fmt.Errorf(\"zone %s not found for domain %s\", authZone, domain)\n\t}\n\n\treturn hostedZone.ZoneId, nil\n}\n"
  },
  {
    "path": "providers/dns/efficientip/efficientip.go",
    "content": "// Package efficientip implements a DNS provider for solving the DNS-01 challenge using Efficient IP.\npackage efficientip\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/efficientip/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"EFFICIENTIP_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\tEnvHostname = envNamespace + \"HOSTNAME\"\n\tEnvDNSName  = envNamespace + \"DNS_NAME\"\n\tEnvViewName = envNamespace + \"VIEW_NAME\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvInsecureSkipVerify = envNamespace + \"INSECURE_SKIP_VERIFY\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername           string\n\tPassword           string\n\tHostname           string\n\tDNSName            string\n\tViewName           string\n\tInsecureSkipVerify bool\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a new DNS provider\n// using environment variable EFFICIENTIP_API_KEY for adding and removing the DNS record.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword, EnvHostname, EnvDNSName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"efficientip: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\tconfig.Hostname = values[EnvHostname]\n\tconfig.DNSName = values[EnvDNSName]\n\tconfig.ViewName = env.GetOrDefaultString(EnvViewName, \"\")\n\tconfig.InsecureSkipVerify = env.GetOrDefaultBool(EnvInsecureSkipVerify, false)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Efficient IP.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"efficientip: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Username == \"\" {\n\t\treturn nil, errors.New(\"efficientip: missing username\")\n\t}\n\n\tif config.Password == \"\" {\n\t\treturn nil, errors.New(\"efficientip: missing password\")\n\t}\n\n\tif config.Hostname == \"\" {\n\t\treturn nil, errors.New(\"efficientip: missing hostname\")\n\t}\n\n\tif config.DNSName == \"\" {\n\t\treturn nil, errors.New(\"efficientip: missing dnsname\")\n\t}\n\n\tclient := internal.NewClient(config.Hostname, config.Username, config.Password)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tif config.InsecureSkipVerify {\n\t\tclient.HTTPClient.Transport = &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t}\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\nfunc (d *DNSProvider) Present(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tr := internal.ResourceRecord{\n\t\tRRName:      dns01.UnFqdn(info.EffectiveFQDN),\n\t\tRRType:      \"TXT\",\n\t\tValue1:      info.Value,\n\t\tDNSName:     d.config.DNSName,\n\t\tDNSViewName: d.config.ViewName,\n\t}\n\n\t_, err := d.client.AddRecord(ctx, r)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"efficientip: add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tparams := internal.DeleteInputParameters{\n\t\tRRName:      dns01.UnFqdn(info.EffectiveFQDN),\n\t\tRRType:      \"TXT\",\n\t\tRRValue1:    info.Value,\n\t\tDNSName:     d.config.DNSName,\n\t\tDNSViewName: d.config.ViewName,\n\t}\n\n\t_, err := d.client.DeleteRecord(ctx, params)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"efficientip: delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/efficientip/efficientip.toml",
    "content": "Name = \"Efficient IP\"\nDescription = ''''''\nURL = \"https://efficientip.com/\"\nCode = \"efficientip\"\nSince = \"v4.13.0\"\n\nExample = '''\nEFFICIENTIP_USERNAME=\"user\" \\\nEFFICIENTIP_PASSWORD=\"secret\" \\\nEFFICIENTIP_HOSTNAME=\"ipam.example.org\" \\\nEFFICIENTIP_DNS_NAME=\"dns.smart\" \\\nlego --dns efficientip -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    EFFICIENTIP_USERNAME = \"Username\"\n    EFFICIENTIP_PASSWORD = \"Password\"\n    EFFICIENTIP_HOSTNAME = \"Hostname (ex: foo.example.com)\"\n    EFFICIENTIP_DNS_NAME = \"DNS name (ex: dns.smart)\"\n  [Configuration.Additional]\n    EFFICIENTIP_INSECURE_SKIP_VERIFY = \"Whether or not to verify EfficientIP API certificate\"\n    EFFICIENTIP_VIEW_NAME = \"View name (ex: external)\"\n    EFFICIENTIP_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    EFFICIENTIP_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    EFFICIENTIP_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n"
  },
  {
    "path": "providers/dns/efficientip/efficientip_test.go",
    "content": "package efficientip\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvUsername,\n\tEnvPassword,\n\tEnvHostname,\n\tEnvDNSName,\n\tEnvViewName,\n).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t\tEnvHostname: \"example.com\",\n\t\t\t\tEnvDNSName:  \"dns.smart\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t\tEnvHostname: \"example.com\",\n\t\t\t\tEnvDNSName:  \"dns.smart\",\n\t\t\t},\n\t\t\texpected: \"efficientip: some credentials information are missing: EFFICIENTIP_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t\tEnvHostname: \"example.com\",\n\t\t\t\tEnvDNSName:  \"dns.smart\",\n\t\t\t},\n\t\t\texpected: \"efficientip: some credentials information are missing: EFFICIENTIP_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing hostname\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t\tEnvHostname: \"\",\n\t\t\t\tEnvDNSName:  \"dns.smart\",\n\t\t\t},\n\t\t\texpected: \"efficientip: some credentials information are missing: EFFICIENTIP_HOSTNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing DNS name\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t\tEnvHostname: \"example.com\",\n\t\t\t\tEnvDNSName:  \"\",\n\t\t\t},\n\t\t\texpected: \"efficientip: some credentials information are missing: EFFICIENTIP_DNS_NAME\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"efficientip: some credentials information are missing: EFFICIENTIP_USERNAME,EFFICIENTIP_PASSWORD,EFFICIENTIP_HOSTNAME,EFFICIENTIP_DNS_NAME\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\thostname string\n\t\tdnsName  string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t\thostname: \"example.com\",\n\t\t\tdnsName:  \"dns.smart\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tpassword: \"secret\",\n\t\t\thostname: \"example.com\",\n\t\t\tdnsName:  \"dns.smart\",\n\t\t\texpected: \"efficientip: missing username\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tusername: \"user\",\n\t\t\thostname: \"example.com\",\n\t\t\tdnsName:  \"dns.smart\",\n\t\t\texpected: \"efficientip: missing password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing hostname\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t\tdnsName:  \"dns.smart\",\n\t\t\texpected: \"efficientip: missing hostname\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing dnsName\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t\thostname: \"example.com\",\n\t\t\texpected: \"efficientip: missing dnsname\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing all\",\n\t\t\texpected: \"efficientip: missing username\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\t\t\tconfig.Hostname = test.hostname\n\t\t\tconfig.DNSName = test.dnsName\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/efficientip/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\ntype Client struct {\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n\n\tusername string\n\tpassword string\n}\n\nfunc NewClient(hostname, username, password string) *Client {\n\tbaseURL, _ := url.Parse(fmt.Sprintf(\"https://%s/rest/\", hostname))\n\n\treturn &Client{\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t\tbaseURL:    baseURL,\n\t\tusername:   username,\n\t\tpassword:   password,\n\t}\n}\n\nfunc (c *Client) ListRecords(ctx context.Context) ([]ResourceRecord, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns_rr_list\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result []ResourceRecord\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\nfunc (c *Client) GetRecord(ctx context.Context, id string) (*ResourceRecord, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns_rr_info\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"rr_id\", id)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result []ResourceRecord\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(result) == 0 {\n\t\treturn nil, nil\n\t}\n\n\treturn &result[0], nil\n}\n\nfunc (c *Client) AddRecord(ctx context.Context, record ResourceRecord) (*BaseOutput, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns_rr_add\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result []BaseOutput\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(result) == 0 {\n\t\treturn nil, nil\n\t}\n\n\treturn &result[0], nil\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, params DeleteInputParameters) (*BaseOutput, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns_rr_delete\")\n\n\t// (rr_id || (rr_name && (dns_id || dns_name || hostaddr)))\n\n\tv, err := querystring.Values(params)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"query parameters: %w\", err)\n\t}\n\n\tendpoint.RawQuery = v.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result []BaseOutput\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(result) == 0 {\n\t\treturn nil, nil\n\t}\n\n\treturn &result[0], nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.SetBasicAuth(c.username, c.password)\n\treq.Header.Set(\"cache-control\", \"no-cache\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tswitch req.Method {\n\tcase http.MethodPost:\n\t\tif resp.StatusCode != http.StatusCreated {\n\t\t\treturn parseError(req, resp)\n\t\t}\n\tdefault:\n\t\tif resp.StatusCode == http.StatusNoContent {\n\t\t\treturn nil\n\t\t}\n\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\treturn parseError(req, resp)\n\t\t}\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar response APIError\n\n\terr := json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code %d] %w\", resp.StatusCode, response)\n}\n"
  },
  {
    "path": "providers/dns/efficientip/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tsrvURL, _ := url.Parse(server.URL)\n\n\t\t\tclient := NewClient(srvURL.Host, \"user\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithBasicAuth(\"user\", \"secret\"),\n\t)\n}\n\nfunc TestListRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns_rr_list\", servermock.ResponseFromFixture(\"dns_rr_list.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.ListRecords(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []ResourceRecord{\n\t\t{\n\t\t\tErrorCode:         \"0\",\n\t\t\tDelayedCreateTime: \"0\",\n\t\t\tDelayedDeleteTime: \"0\",\n\t\t\tDelayedTime:       \"0\",\n\t\t\tDNSCloud:          \"0\",\n\t\t\tDNSID:             \"3\",\n\t\t\tDNSName:           \"dns.smart\",\n\t\t\tDNSType:           \"vdns\",\n\t\t\tDNSViewID:         \"0\",\n\t\t\tDNSViewName:       \"#\",\n\t\t\tDNSZoneID:         \"9\",\n\t\t\tDNSZoneIsReverse:  \"0\",\n\t\t\tDNSZoneIsRpz:      \"0\",\n\t\t\tDNSZoneName:       \"lego.example.com\",\n\t\t\tDNSZoneNameUTF:    \"lego.example.com\",\n\t\t\tDNSZoneSiteName:   \"#\",\n\t\t\tDNSZoneSortZone:   \"lego.example.com\",\n\t\t\tDNSZoneType:       \"master\",\n\t\t\tRRAllValue:        \"test1\",\n\t\t\tRRAuthGsstsig:     \"0\",\n\t\t\tRRFullName:        \"test.lego.example.com\",\n\t\t\tRRFullNameUTF:     \"test.lego.example.com\",\n\t\t\tRRGlue:            \"test\",\n\t\t\tRRGlueID:          \"21\",\n\t\t\tRRID:              \"239\",\n\t\t\tRRNameID:          \"26\",\n\t\t\tRRType:            \"TXT\",\n\t\t\tRRTypeID:          \"6\",\n\t\t\tRRValueID:         \"274\",\n\t\t\tTTL:               \"3600\",\n\t\t\tValue1:            \"test1\",\n\t\t\tVDNSParentID:      \"0\",\n\t\t\tVDNSParentName:    \"#\",\n\t\t},\n\t\t{\n\t\t\tErrorCode:         \"0\",\n\t\t\tDelayedCreateTime: \"0\",\n\t\t\tDelayedDeleteTime: \"0\",\n\t\t\tDelayedTime:       \"0\",\n\t\t\tDNSCloud:          \"0\",\n\t\t\tDNSID:             \"3\",\n\t\t\tDNSName:           \"dns.smart\",\n\t\t\tDNSType:           \"vdns\",\n\t\t\tDNSViewID:         \"0\",\n\t\t\tDNSViewName:       \"#\",\n\t\t\tDNSZoneID:         \"9\",\n\t\t\tDNSZoneIsReverse:  \"0\",\n\t\t\tDNSZoneIsRpz:      \"0\",\n\t\t\tDNSZoneName:       \"lego.example.com\",\n\t\t\tDNSZoneNameUTF:    \"lego.example.com\",\n\t\t\tDNSZoneSiteName:   \"#\",\n\t\t\tDNSZoneSortZone:   \"lego.example.com\",\n\t\t\tDNSZoneType:       \"master\",\n\t\t\tRRAllValue:        \"test2\",\n\t\t\tRRAuthGsstsig:     \"0\",\n\t\t\tRRFullName:        \"test.lego.example.com\",\n\t\t\tRRFullNameUTF:     \"test.lego.example.com\",\n\t\t\tRRGlue:            \"test\",\n\t\t\tRRGlueID:          \"21\",\n\t\t\tRRID:              \"241\",\n\t\t\tRRNameID:          \"26\",\n\t\t\tRRType:            \"TXT\",\n\t\t\tRRTypeID:          \"6\",\n\t\t\tRRValueID:         \"275\",\n\t\t\tTTL:               \"3600\",\n\t\t\tValue1:            \"test2\",\n\t\t\tVDNSParentID:      \"0\",\n\t\t\tVDNSParentName:    \"#\",\n\t\t},\n\t\t{\n\t\t\tErrorCode:         \"0\",\n\t\t\tDelayedCreateTime: \"0\",\n\t\t\tDelayedDeleteTime: \"0\",\n\t\t\tDelayedTime:       \"0\",\n\t\t\tDNSCloud:          \"0\",\n\t\t\tDNSID:             \"3\",\n\t\t\tDNSName:           \"dns.smart\",\n\t\t\tDNSType:           \"vdns\",\n\t\t\tDNSViewID:         \"0\",\n\t\t\tDNSViewName:       \"#\",\n\t\t\tDNSZoneID:         \"9\",\n\t\t\tDNSZoneIsReverse:  \"0\",\n\t\t\tDNSZoneIsRpz:      \"0\",\n\t\t\tDNSZoneName:       \"lego.example.com\",\n\t\t\tDNSZoneNameUTF:    \"lego.example.com\",\n\t\t\tDNSZoneSiteName:   \"#\",\n\t\t\tDNSZoneSortZone:   \"lego.example.com\",\n\t\t\tDNSZoneType:       \"master\",\n\t\t\tRRAllValue:        \"test1\",\n\t\t\tRRAuthGsstsig:     \"0\",\n\t\t\tRRFullName:        \"lego.example.com\",\n\t\t\tRRFullNameUTF:     \"lego.example.com\",\n\t\t\tRRGlue:            \".\",\n\t\t\tRRGlueID:          \"3\",\n\t\t\tRRID:              \"245\",\n\t\t\tRRNameID:          \"21\",\n\t\t\tRRType:            \"TXT\",\n\t\t\tRRTypeID:          \"6\",\n\t\t\tRRValueID:         \"274\",\n\t\t\tTTL:               \"3600\",\n\t\t\tValue1:            \"test1\",\n\t\t\tVDNSParentID:      \"0\",\n\t\t\tVDNSParentName:    \"#\",\n\t\t},\n\t\t{\n\t\t\tErrorCode:         \"0\",\n\t\t\tDelayedCreateTime: \"0\",\n\t\t\tDelayedDeleteTime: \"0\",\n\t\t\tDelayedTime:       \"0\",\n\t\t\tDNSCloud:          \"0\",\n\t\t\tDNSID:             \"3\",\n\t\t\tDNSName:           \"dns.smart\",\n\t\t\tDNSType:           \"vdns\",\n\t\t\tDNSViewID:         \"0\",\n\t\t\tDNSViewName:       \"#\",\n\t\t\tDNSZoneID:         \"9\",\n\t\t\tDNSZoneIsReverse:  \"0\",\n\t\t\tDNSZoneIsRpz:      \"0\",\n\t\t\tDNSZoneName:       \"lego.example.com\",\n\t\t\tDNSZoneNameUTF:    \"lego.example.com\",\n\t\t\tDNSZoneSiteName:   \"#\",\n\t\t\tDNSZoneSortZone:   \"lego.example.com\",\n\t\t\tDNSZoneType:       \"master\",\n\t\t\tRRAllValue:        \"test2\",\n\t\t\tRRAuthGsstsig:     \"0\",\n\t\t\tRRFullName:        \"lego.example.com\",\n\t\t\tRRFullNameUTF:     \"lego.example.com\",\n\t\t\tRRGlue:            \".\",\n\t\t\tRRGlueID:          \"3\",\n\t\t\tRRID:              \"247\",\n\t\t\tRRNameID:          \"21\",\n\t\t\tRRType:            \"TXT\",\n\t\t\tRRTypeID:          \"6\",\n\t\t\tRRValueID:         \"275\",\n\t\t\tTTL:               \"3600\",\n\t\t\tValue1:            \"test2\",\n\t\t\tVDNSParentID:      \"0\",\n\t\t\tVDNSParentName:    \"#\",\n\t\t},\n\t\t{\n\t\t\tErrorCode:         \"0\",\n\t\t\tDelayedCreateTime: \"0\",\n\t\t\tDelayedDeleteTime: \"0\",\n\t\t\tDelayedTime:       \"0\",\n\t\t\tDNSCloud:          \"0\",\n\t\t\tDNSID:             \"3\",\n\t\t\tDNSName:           \"dns.smart\",\n\t\t\tDNSType:           \"vdns\",\n\t\t\tDNSViewID:         \"0\",\n\t\t\tDNSViewName:       \"#\",\n\t\t\tDNSZoneID:         \"9\",\n\t\t\tDNSZoneIsReverse:  \"0\",\n\t\t\tDNSZoneIsRpz:      \"0\",\n\t\t\tDNSZoneName:       \"lego.example.com\",\n\t\t\tDNSZoneNameUTF:    \"lego.example.com\",\n\t\t\tDNSZoneSiteName:   \"#\",\n\t\t\tDNSZoneSortZone:   \"lego.example.com\",\n\t\t\tDNSZoneType:       \"master\",\n\t\t\tRRAllValue:        \"dns.smart, root@lego.example.com, 2023062719, 1200, 600, 1209600, 3600\",\n\t\t\tRRAuthGsstsig:     \"0\",\n\t\t\tRRFullName:        \"lego.example.com\",\n\t\t\tRRFullNameUTF:     \"lego.example.com\",\n\t\t\tRRGlue:            \".\",\n\t\t\tRRGlueID:          \"3\",\n\t\t\tRRID:              \"201\",\n\t\t\tRRNameID:          \"21\",\n\t\t\tRRType:            \"SOA\",\n\t\t\tRRTypeID:          \"2\",\n\t\t\tRRValueID:         \"282\",\n\t\t\tTTL:               \"3600\",\n\t\t\tValue1:            \"dns.smart\",\n\t\t\tValue2:            \"root@lego.example.com\",\n\t\t\tValue3:            \"2023062719\",\n\t\t\tValue4:            \"1200\",\n\t\t\tValue5:            \"600\",\n\t\t\tValue6:            \"1209600\",\n\t\t\tValue7:            \"3600\",\n\t\t\tVDNSParentID:      \"0\",\n\t\t\tVDNSParentName:    \"#\",\n\t\t},\n\t\t{\n\t\t\tErrorCode:         \"0\",\n\t\t\tDelayedCreateTime: \"0\",\n\t\t\tDelayedDeleteTime: \"0\",\n\t\t\tDelayedTime:       \"0\",\n\t\t\tDNSCloud:          \"0\",\n\t\t\tDNSID:             \"3\",\n\t\t\tDNSName:           \"dns.smart\",\n\t\t\tDNSType:           \"vdns\",\n\t\t\tDNSViewID:         \"0\",\n\t\t\tDNSViewName:       \"#\",\n\t\t\tDNSZoneID:         \"9\",\n\t\t\tDNSZoneIsReverse:  \"0\",\n\t\t\tDNSZoneIsRpz:      \"0\",\n\t\t\tDNSZoneName:       \"lego.example.com\",\n\t\t\tDNSZoneNameUTF:    \"lego.example.com\",\n\t\t\tDNSZoneSiteName:   \"#\",\n\t\t\tDNSZoneSortZone:   \"lego.example.com\",\n\t\t\tDNSZoneType:       \"master\",\n\t\t\tRRAllValue:        \"dns.smart\",\n\t\t\tRRAuthGsstsig:     \"0\",\n\t\t\tRRFullName:        \"lego.example.com\",\n\t\t\tRRFullNameUTF:     \"lego.example.com\",\n\t\t\tRRGlue:            \".\",\n\t\t\tRRGlueID:          \"3\",\n\t\t\tRRID:              \"200\",\n\t\t\tRRNameID:          \"21\",\n\t\t\tRRType:            \"NS\",\n\t\t\tRRTypeID:          \"1\",\n\t\t\tRRValueID:         \"10\",\n\t\t\tTTL:               \"3600\",\n\t\t\tValue1:            \"dns.smart\",\n\t\t\tVDNSParentID:      \"0\",\n\t\t\tVDNSParentName:    \"#\",\n\t\t},\n\t\t{\n\t\t\tErrorCode:         \"0\",\n\t\t\tDelayedCreateTime: \"0\",\n\t\t\tDelayedDeleteTime: \"0\",\n\t\t\tDelayedTime:       \"0\",\n\t\t\tDNSCloud:          \"0\",\n\t\t\tDNSID:             \"3\",\n\t\t\tDNSName:           \"dns.smart\",\n\t\t\tDNSType:           \"vdns\",\n\t\t\tDNSViewID:         \"0\",\n\t\t\tDNSViewName:       \"#\",\n\t\t\tDNSZoneID:         \"9\",\n\t\t\tDNSZoneIsReverse:  \"0\",\n\t\t\tDNSZoneIsRpz:      \"0\",\n\t\t\tDNSZoneName:       \"lego.example.com\",\n\t\t\tDNSZoneNameUTF:    \"lego.example.com\",\n\t\t\tDNSZoneSiteName:   \"#\",\n\t\t\tDNSZoneSortZone:   \"lego.example.com\",\n\t\t\tDNSZoneType:       \"master\",\n\t\t\tRRAllValue:        \"127.0.0.1\",\n\t\t\tRRAuthGsstsig:     \"0\",\n\t\t\tRRFullName:        \"loopback.lego.example.com\",\n\t\t\tRRFullNameUTF:     \"loopback.lego.example.com\",\n\t\t\tRRGlue:            \"loopback\",\n\t\t\tRRGlueID:          \"17\",\n\t\t\tRRID:              \"208\",\n\t\t\tRRNameID:          \"22\",\n\t\t\tRRType:            \"A\",\n\t\t\tRRTypeID:          \"3\",\n\t\t\tRRValueID:         \"237\",\n\t\t\tRRValueIP4Addr:    \"7f000001\",\n\t\t\tRRValueIPAddr:     \"7f000001\",\n\t\t\tTTL:               \"3600\",\n\t\t\tValue1:            \"127.0.0.1\",\n\t\t\tVDNSParentID:      \"0\",\n\t\t\tVDNSParentName:    \"#\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestGetRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns_rr_info\", servermock.ResponseFromFixture(\"dns_rr_info.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"rr_id\", \"239\")).\n\t\tBuild(t)\n\n\trecord, err := client.GetRecord(t.Context(), \"239\")\n\trequire.NoError(t, err)\n\n\texpected := &ResourceRecord{\n\t\tErrorCode:         \"0\",\n\t\tDelayedCreateTime: \"0\",\n\t\tDelayedDeleteTime: \"0\",\n\t\tDelayedTime:       \"0\",\n\t\tDNSCloud:          \"0\",\n\t\tDNSID:             \"3\",\n\t\tDNSName:           \"dns.smart\",\n\t\tDNSType:           \"vdns\",\n\t\tDNSViewID:         \"0\",\n\t\tDNSViewName:       \"#\",\n\t\tDNSZoneID:         \"9\",\n\t\tDNSZoneIsReverse:  \"0\",\n\t\tDNSZoneIsRpz:      \"0\",\n\t\tDNSZoneName:       \"lego.example.com\",\n\t\tDNSZoneNameUTF:    \"lego.example.com\",\n\t\tDNSZoneSiteName:   \"#\",\n\t\tDNSZoneSortZone:   \"lego.example.com\",\n\t\tDNSZoneType:       \"master\",\n\t\tRRAllValue:        \"test1\",\n\t\tRRAuthGsstsig:     \"0\",\n\t\tRRFullName:        \"test.lego.example.com\",\n\t\tRRFullNameUTF:     \"test.lego.example.com\",\n\t\tRRGlue:            \"test\",\n\t\tRRGlueID:          \"21\",\n\t\tRRID:              \"239\",\n\t\tRRNameID:          \"26\",\n\t\tRRType:            \"TXT\",\n\t\tRRTypeID:          \"6\",\n\t\tRRValueID:         \"274\",\n\t\tTTL:               \"3600\",\n\t\tValue1:            \"test1\",\n\t\tVDNSParentID:      \"0\",\n\t\tVDNSParentName:    \"#\",\n\t}\n\n\tassert.Equal(t, expected, record)\n}\n\nfunc TestAddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns_rr_add\",\n\t\t\tservermock.ResponseFromFixture(\"dns_rr_add.json\").WithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBody(`{\"dns_name\":\"dns.smart\",\"dnsview_name\":\"external\",\"rr_name\":\"test.example.com\",\"rr_type\":\"TXT\",\"value1\":\"test\"}`)).\n\t\tBuild(t)\n\n\tr := ResourceRecord{\n\t\tRRName:      \"test.example.com\",\n\t\tRRType:      \"TXT\",\n\t\tValue1:      \"test\",\n\t\tDNSName:     \"dns.smart\",\n\t\tDNSViewName: \"external\",\n\t}\n\n\tresp, err := client.AddRecord(t.Context(), r)\n\trequire.NoError(t, err)\n\n\texpected := &BaseOutput{RetOID: \"239\"}\n\n\tassert.Equal(t, expected, resp)\n}\n\nfunc TestDeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns_rr_delete\", servermock.ResponseFromFixture(\"dns_rr_delete.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"rr_id\", \"251\")).\n\t\tBuild(t)\n\n\tresp, err := client.DeleteRecord(t.Context(), DeleteInputParameters{RRID: \"251\"})\n\trequire.NoError(t, err)\n\n\texpected := &BaseOutput{RetOID: \"251\"}\n\n\tassert.Equal(t, expected, resp)\n}\n\nfunc TestDeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns_rr_delete\",\n\t\t\tservermock.ResponseFromFixture(\"dns_rr_delete-error.json\").WithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\t_, err := client.DeleteRecord(t.Context(), DeleteInputParameters{RRID: \"251\"})\n\trequire.ErrorAs(t, err, &APIError{})\n}\n"
  },
  {
    "path": "providers/dns/efficientip/internal/fixtures/dns_rr_add.json",
    "content": "[\n  {\n    \"ret_oid\": \"239\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/efficientip/internal/fixtures/dns_rr_delete-error.json",
    "content": "{\n  \"errno\": \"20117\",\n  \"errmsg\": \"This RR does not exist\",\n  \"severity\": \"error\",\n  \"category\": \"dns_rr_delete\"\n}\n"
  },
  {
    "path": "providers/dns/efficientip/internal/fixtures/dns_rr_delete.json",
    "content": "[\n  {\n    \"ret_oid\": \"251\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/efficientip/internal/fixtures/dns_rr_info.json",
    "content": "[\n  {\n    \"errno\": \"0\",\n    \"rr_all_value\": \"test1\",\n    \"dnszone_sort_zone\": \"lego.example.com\",\n    \"dnszone_is_rpz\": \"0\",\n    \"dnszone_type\": \"master\",\n    \"rr_full_name\": \"test.lego.example.com\",\n    \"rr_full_name_utf\": \"test.lego.example.com\",\n    \"rr_name_ip_addr\": \"\",\n    \"rr_name_ip4_addr\": \"\",\n    \"rr_value_ip_addr\": \"\",\n    \"rr_value_ip4_addr\": \"\",\n    \"rr_glue\": \"test\",\n    \"rr_type\": \"TXT\",\n    \"ttl\": \"3600\",\n    \"delayed_time\": \"0\",\n    \"rr_class_name\": \"\",\n    \"value1\": \"test1\",\n    \"value2\": \"\",\n    \"value3\": \"\",\n    \"value4\": \"\",\n    \"value5\": \"\",\n    \"value6\": \"\",\n    \"value7\": \"\",\n    \"dnszone_id\": \"9\",\n    \"rr_id\": \"239\",\n    \"dns_id\": \"3\",\n    \"dnszone_name_utf\": \"lego.example.com\",\n    \"dnszone_name\": \"lego.example.com\",\n    \"dns_name\": \"dns.smart\",\n    \"dns_type\": \"vdns\",\n    \"dns_cloud\": \"0\",\n    \"vdns_parent_id\": \"0\",\n    \"dnsview_name\": \"#\",\n    \"dnsview_class_name\": \"\",\n    \"dnsview_id\": \"0\",\n    \"dnszone_site_name\": \"#\",\n    \"dnszone_is_reverse\": \"0\",\n    \"dnszone_masters\": \"\",\n    \"vdns_parent_name\": \"#\",\n    \"dnszone_forwarders\": \"\",\n    \"dns_class_name\": \"\",\n    \"dnszone_class_name\": \"\",\n    \"dns_version\": \"\",\n    \"dns_comment\": \"\",\n    \"delayed_create_time\": \"0\",\n    \"delayed_delete_time\": \"0\",\n    \"multistatus\": \"\",\n    \"rr_auth_gsstsig\": \"0\",\n    \"rr_last_update_time\": \"\",\n    \"rr_last_update_days\": \"\",\n    \"rr_name_id\": \"26\",\n    \"rr_value_id\": \"274\",\n    \"rr_type_id\": \"6\",\n    \"rr_glue_id\": \"21\",\n    \"dnsview_class_parameters\": \"\",\n    \"dnsview_class_parameters_properties\": \"\",\n    \"dnsview_class_parameters_inheritance_source\": \"\",\n    \"rr_class_parameters\": \"\",\n    \"rr_class_parameters_properties\": \"\",\n    \"rr_class_parameters_inheritance_source\": \"\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/efficientip/internal/fixtures/dns_rr_list.json",
    "content": "[\n  {\n    \"errno\": \"0\",\n    \"rr_all_value\": \"test1\",\n    \"dnszone_sort_zone\": \"lego.example.com\",\n    \"dnszone_is_rpz\": \"0\",\n    \"dnszone_type\": \"master\",\n    \"rr_full_name\": \"test.lego.example.com\",\n    \"rr_full_name_utf\": \"test.lego.example.com\",\n    \"rr_name_ip_addr\": \"\",\n    \"rr_name_ip4_addr\": \"\",\n    \"rr_value_ip_addr\": \"\",\n    \"rr_value_ip4_addr\": \"\",\n    \"rr_glue\": \"test\",\n    \"rr_type\": \"TXT\",\n    \"ttl\": \"3600\",\n    \"delayed_time\": \"0\",\n    \"rr_class_name\": \"\",\n    \"value1\": \"test1\",\n    \"value2\": \"\",\n    \"value3\": \"\",\n    \"value4\": \"\",\n    \"value5\": \"\",\n    \"value6\": \"\",\n    \"value7\": \"\",\n    \"dnszone_id\": \"9\",\n    \"rr_id\": \"239\",\n    \"dns_id\": \"3\",\n    \"dnszone_name_utf\": \"lego.example.com\",\n    \"dnszone_name\": \"lego.example.com\",\n    \"dns_name\": \"dns.smart\",\n    \"dns_type\": \"vdns\",\n    \"dns_cloud\": \"0\",\n    \"vdns_parent_id\": \"0\",\n    \"dnsview_name\": \"#\",\n    \"dnsview_class_name\": \"\",\n    \"dnsview_id\": \"0\",\n    \"dnszone_site_name\": \"#\",\n    \"dnszone_is_reverse\": \"0\",\n    \"dnszone_masters\": \"\",\n    \"vdns_parent_name\": \"#\",\n    \"dnszone_forwarders\": \"\",\n    \"dns_class_name\": \"\",\n    \"dnszone_class_name\": \"\",\n    \"dns_version\": \"\",\n    \"dns_comment\": \"\",\n    \"delayed_create_time\": \"0\",\n    \"delayed_delete_time\": \"0\",\n    \"multistatus\": \"\",\n    \"rr_auth_gsstsig\": \"0\",\n    \"rr_last_update_time\": \"\",\n    \"rr_last_update_days\": \"\",\n    \"rr_name_id\": \"26\",\n    \"rr_value_id\": \"274\",\n    \"rr_type_id\": \"6\",\n    \"rr_glue_id\": \"21\",\n    \"dnsview_class_parameters\": \"\",\n    \"dnsview_class_parameters_properties\": \"\",\n    \"dnsview_class_parameters_inheritance_source\": \"\",\n    \"rr_class_parameters\": \"\",\n    \"rr_class_parameters_properties\": \"\",\n    \"rr_class_parameters_inheritance_source\": \"\"\n  },\n  {\n    \"errno\": \"0\",\n    \"rr_all_value\": \"test2\",\n    \"dnszone_sort_zone\": \"lego.example.com\",\n    \"dnszone_is_rpz\": \"0\",\n    \"dnszone_type\": \"master\",\n    \"rr_full_name\": \"test.lego.example.com\",\n    \"rr_full_name_utf\": \"test.lego.example.com\",\n    \"rr_name_ip_addr\": \"\",\n    \"rr_name_ip4_addr\": \"\",\n    \"rr_value_ip_addr\": \"\",\n    \"rr_value_ip4_addr\": \"\",\n    \"rr_glue\": \"test\",\n    \"rr_type\": \"TXT\",\n    \"ttl\": \"3600\",\n    \"delayed_time\": \"0\",\n    \"rr_class_name\": \"\",\n    \"value1\": \"test2\",\n    \"value2\": \"\",\n    \"value3\": \"\",\n    \"value4\": \"\",\n    \"value5\": \"\",\n    \"value6\": \"\",\n    \"value7\": \"\",\n    \"dnszone_id\": \"9\",\n    \"rr_id\": \"241\",\n    \"dns_id\": \"3\",\n    \"dnszone_name_utf\": \"lego.example.com\",\n    \"dnszone_name\": \"lego.example.com\",\n    \"dns_name\": \"dns.smart\",\n    \"dns_type\": \"vdns\",\n    \"dns_cloud\": \"0\",\n    \"vdns_parent_id\": \"0\",\n    \"dnsview_name\": \"#\",\n    \"dnsview_class_name\": \"\",\n    \"dnsview_id\": \"0\",\n    \"dnszone_site_name\": \"#\",\n    \"dnszone_is_reverse\": \"0\",\n    \"dnszone_masters\": \"\",\n    \"vdns_parent_name\": \"#\",\n    \"dnszone_forwarders\": \"\",\n    \"dns_class_name\": \"\",\n    \"dnszone_class_name\": \"\",\n    \"dns_version\": \"\",\n    \"dns_comment\": \"\",\n    \"delayed_create_time\": \"0\",\n    \"delayed_delete_time\": \"0\",\n    \"multistatus\": \"\",\n    \"rr_auth_gsstsig\": \"0\",\n    \"rr_last_update_time\": \"\",\n    \"rr_last_update_days\": \"\",\n    \"rr_name_id\": \"26\",\n    \"rr_value_id\": \"275\",\n    \"rr_type_id\": \"6\",\n    \"rr_glue_id\": \"21\",\n    \"dnsview_class_parameters\": \"\",\n    \"dnsview_class_parameters_properties\": \"\",\n    \"dnsview_class_parameters_inheritance_source\": \"\",\n    \"rr_class_parameters\": \"\",\n    \"rr_class_parameters_properties\": \"\",\n    \"rr_class_parameters_inheritance_source\": \"\"\n  },\n  {\n    \"errno\": \"0\",\n    \"rr_all_value\": \"test1\",\n    \"dnszone_sort_zone\": \"lego.example.com\",\n    \"dnszone_is_rpz\": \"0\",\n    \"dnszone_type\": \"master\",\n    \"rr_full_name\": \"lego.example.com\",\n    \"rr_full_name_utf\": \"lego.example.com\",\n    \"rr_name_ip_addr\": \"\",\n    \"rr_name_ip4_addr\": \"\",\n    \"rr_value_ip_addr\": \"\",\n    \"rr_value_ip4_addr\": \"\",\n    \"rr_glue\": \".\",\n    \"rr_type\": \"TXT\",\n    \"ttl\": \"3600\",\n    \"delayed_time\": \"0\",\n    \"rr_class_name\": \"\",\n    \"value1\": \"test1\",\n    \"value2\": \"\",\n    \"value3\": \"\",\n    \"value4\": \"\",\n    \"value5\": \"\",\n    \"value6\": \"\",\n    \"value7\": \"\",\n    \"dnszone_id\": \"9\",\n    \"rr_id\": \"245\",\n    \"dns_id\": \"3\",\n    \"dnszone_name_utf\": \"lego.example.com\",\n    \"dnszone_name\": \"lego.example.com\",\n    \"dns_name\": \"dns.smart\",\n    \"dns_type\": \"vdns\",\n    \"dns_cloud\": \"0\",\n    \"vdns_parent_id\": \"0\",\n    \"dnsview_name\": \"#\",\n    \"dnsview_class_name\": \"\",\n    \"dnsview_id\": \"0\",\n    \"dnszone_site_name\": \"#\",\n    \"dnszone_is_reverse\": \"0\",\n    \"dnszone_masters\": \"\",\n    \"vdns_parent_name\": \"#\",\n    \"dnszone_forwarders\": \"\",\n    \"dns_class_name\": \"\",\n    \"dnszone_class_name\": \"\",\n    \"dns_version\": \"\",\n    \"dns_comment\": \"\",\n    \"delayed_create_time\": \"0\",\n    \"delayed_delete_time\": \"0\",\n    \"multistatus\": \"\",\n    \"rr_auth_gsstsig\": \"0\",\n    \"rr_last_update_time\": \"\",\n    \"rr_last_update_days\": \"\",\n    \"rr_name_id\": \"21\",\n    \"rr_value_id\": \"274\",\n    \"rr_type_id\": \"6\",\n    \"rr_glue_id\": \"3\",\n    \"dnsview_class_parameters\": \"\",\n    \"dnsview_class_parameters_properties\": \"\",\n    \"dnsview_class_parameters_inheritance_source\": \"\",\n    \"rr_class_parameters\": \"\",\n    \"rr_class_parameters_properties\": \"\",\n    \"rr_class_parameters_inheritance_source\": \"\"\n  },\n  {\n    \"errno\": \"0\",\n    \"rr_all_value\": \"test2\",\n    \"dnszone_sort_zone\": \"lego.example.com\",\n    \"dnszone_is_rpz\": \"0\",\n    \"dnszone_type\": \"master\",\n    \"rr_full_name\": \"lego.example.com\",\n    \"rr_full_name_utf\": \"lego.example.com\",\n    \"rr_name_ip_addr\": \"\",\n    \"rr_name_ip4_addr\": \"\",\n    \"rr_value_ip_addr\": \"\",\n    \"rr_value_ip4_addr\": \"\",\n    \"rr_glue\": \".\",\n    \"rr_type\": \"TXT\",\n    \"ttl\": \"3600\",\n    \"delayed_time\": \"0\",\n    \"rr_class_name\": \"\",\n    \"value1\": \"test2\",\n    \"value2\": \"\",\n    \"value3\": \"\",\n    \"value4\": \"\",\n    \"value5\": \"\",\n    \"value6\": \"\",\n    \"value7\": \"\",\n    \"dnszone_id\": \"9\",\n    \"rr_id\": \"247\",\n    \"dns_id\": \"3\",\n    \"dnszone_name_utf\": \"lego.example.com\",\n    \"dnszone_name\": \"lego.example.com\",\n    \"dns_name\": \"dns.smart\",\n    \"dns_type\": \"vdns\",\n    \"dns_cloud\": \"0\",\n    \"vdns_parent_id\": \"0\",\n    \"dnsview_name\": \"#\",\n    \"dnsview_class_name\": \"\",\n    \"dnsview_id\": \"0\",\n    \"dnszone_site_name\": \"#\",\n    \"dnszone_is_reverse\": \"0\",\n    \"dnszone_masters\": \"\",\n    \"vdns_parent_name\": \"#\",\n    \"dnszone_forwarders\": \"\",\n    \"dns_class_name\": \"\",\n    \"dnszone_class_name\": \"\",\n    \"dns_version\": \"\",\n    \"dns_comment\": \"\",\n    \"delayed_create_time\": \"0\",\n    \"delayed_delete_time\": \"0\",\n    \"multistatus\": \"\",\n    \"rr_auth_gsstsig\": \"0\",\n    \"rr_last_update_time\": \"\",\n    \"rr_last_update_days\": \"\",\n    \"rr_name_id\": \"21\",\n    \"rr_value_id\": \"275\",\n    \"rr_type_id\": \"6\",\n    \"rr_glue_id\": \"3\",\n    \"dnsview_class_parameters\": \"\",\n    \"dnsview_class_parameters_properties\": \"\",\n    \"dnsview_class_parameters_inheritance_source\": \"\",\n    \"rr_class_parameters\": \"\",\n    \"rr_class_parameters_properties\": \"\",\n    \"rr_class_parameters_inheritance_source\": \"\"\n  },\n  {\n    \"errno\": \"0\",\n    \"rr_all_value\": \"dns.smart, root@lego.example.com, 2023062719, 1200, 600, 1209600, 3600\",\n    \"dnszone_sort_zone\": \"lego.example.com\",\n    \"dnszone_is_rpz\": \"0\",\n    \"dnszone_type\": \"master\",\n    \"rr_full_name\": \"lego.example.com\",\n    \"rr_full_name_utf\": \"lego.example.com\",\n    \"rr_name_ip_addr\": \"\",\n    \"rr_name_ip4_addr\": \"\",\n    \"rr_value_ip_addr\": \"\",\n    \"rr_value_ip4_addr\": \"\",\n    \"rr_glue\": \".\",\n    \"rr_type\": \"SOA\",\n    \"ttl\": \"3600\",\n    \"delayed_time\": \"0\",\n    \"rr_class_name\": \"\",\n    \"value1\": \"dns.smart\",\n    \"value2\": \"root@lego.example.com\",\n    \"value3\": \"2023062719\",\n    \"value4\": \"1200\",\n    \"value5\": \"600\",\n    \"value6\": \"1209600\",\n    \"value7\": \"3600\",\n    \"dnszone_id\": \"9\",\n    \"rr_id\": \"201\",\n    \"dns_id\": \"3\",\n    \"dnszone_name_utf\": \"lego.example.com\",\n    \"dnszone_name\": \"lego.example.com\",\n    \"dns_name\": \"dns.smart\",\n    \"dns_type\": \"vdns\",\n    \"dns_cloud\": \"0\",\n    \"vdns_parent_id\": \"0\",\n    \"dnsview_name\": \"#\",\n    \"dnsview_class_name\": \"\",\n    \"dnsview_id\": \"0\",\n    \"dnszone_site_name\": \"#\",\n    \"dnszone_is_reverse\": \"0\",\n    \"dnszone_masters\": \"\",\n    \"vdns_parent_name\": \"#\",\n    \"dnszone_forwarders\": \"\",\n    \"dns_class_name\": \"\",\n    \"dnszone_class_name\": \"\",\n    \"dns_version\": \"\",\n    \"dns_comment\": \"\",\n    \"delayed_create_time\": \"0\",\n    \"delayed_delete_time\": \"0\",\n    \"multistatus\": \"\",\n    \"rr_auth_gsstsig\": \"0\",\n    \"rr_last_update_time\": \"\",\n    \"rr_last_update_days\": \"\",\n    \"rr_name_id\": \"21\",\n    \"rr_value_id\": \"282\",\n    \"rr_type_id\": \"2\",\n    \"rr_glue_id\": \"3\",\n    \"dnsview_class_parameters\": \"\",\n    \"dnsview_class_parameters_properties\": \"\",\n    \"dnsview_class_parameters_inheritance_source\": \"\",\n    \"rr_class_parameters\": \"\",\n    \"rr_class_parameters_properties\": \"\",\n    \"rr_class_parameters_inheritance_source\": \"\"\n  },\n  {\n    \"errno\": \"0\",\n    \"rr_all_value\": \"dns.smart\",\n    \"dnszone_sort_zone\": \"lego.example.com\",\n    \"dnszone_is_rpz\": \"0\",\n    \"dnszone_type\": \"master\",\n    \"rr_full_name\": \"lego.example.com\",\n    \"rr_full_name_utf\": \"lego.example.com\",\n    \"rr_name_ip_addr\": \"\",\n    \"rr_name_ip4_addr\": \"\",\n    \"rr_value_ip_addr\": \"\",\n    \"rr_value_ip4_addr\": \"\",\n    \"rr_glue\": \".\",\n    \"rr_type\": \"NS\",\n    \"ttl\": \"3600\",\n    \"delayed_time\": \"0\",\n    \"rr_class_name\": \"\",\n    \"value1\": \"dns.smart\",\n    \"value2\": \"\",\n    \"value3\": \"\",\n    \"value4\": \"\",\n    \"value5\": \"\",\n    \"value6\": \"\",\n    \"value7\": \"\",\n    \"dnszone_id\": \"9\",\n    \"rr_id\": \"200\",\n    \"dns_id\": \"3\",\n    \"dnszone_name_utf\": \"lego.example.com\",\n    \"dnszone_name\": \"lego.example.com\",\n    \"dns_name\": \"dns.smart\",\n    \"dns_type\": \"vdns\",\n    \"dns_cloud\": \"0\",\n    \"vdns_parent_id\": \"0\",\n    \"dnsview_name\": \"#\",\n    \"dnsview_class_name\": \"\",\n    \"dnsview_id\": \"0\",\n    \"dnszone_site_name\": \"#\",\n    \"dnszone_is_reverse\": \"0\",\n    \"dnszone_masters\": \"\",\n    \"vdns_parent_name\": \"#\",\n    \"dnszone_forwarders\": \"\",\n    \"dns_class_name\": \"\",\n    \"dnszone_class_name\": \"\",\n    \"dns_version\": \"\",\n    \"dns_comment\": \"\",\n    \"delayed_create_time\": \"0\",\n    \"delayed_delete_time\": \"0\",\n    \"multistatus\": \"\",\n    \"rr_auth_gsstsig\": \"0\",\n    \"rr_last_update_time\": \"\",\n    \"rr_last_update_days\": \"\",\n    \"rr_name_id\": \"21\",\n    \"rr_value_id\": \"10\",\n    \"rr_type_id\": \"1\",\n    \"rr_glue_id\": \"3\",\n    \"dnsview_class_parameters\": \"\",\n    \"dnsview_class_parameters_properties\": \"\",\n    \"dnsview_class_parameters_inheritance_source\": \"\",\n    \"rr_class_parameters\": \"\",\n    \"rr_class_parameters_properties\": \"\",\n    \"rr_class_parameters_inheritance_source\": \"\"\n  },\n  {\n    \"errno\": \"0\",\n    \"rr_all_value\": \"127.0.0.1\",\n    \"dnszone_sort_zone\": \"lego.example.com\",\n    \"dnszone_is_rpz\": \"0\",\n    \"dnszone_type\": \"master\",\n    \"rr_full_name\": \"loopback.lego.example.com\",\n    \"rr_full_name_utf\": \"loopback.lego.example.com\",\n    \"rr_name_ip_addr\": \"\",\n    \"rr_name_ip4_addr\": \"\",\n    \"rr_value_ip_addr\": \"7f000001\",\n    \"rr_value_ip4_addr\": \"7f000001\",\n    \"rr_glue\": \"loopback\",\n    \"rr_type\": \"A\",\n    \"ttl\": \"3600\",\n    \"delayed_time\": \"0\",\n    \"rr_class_name\": \"\",\n    \"value1\": \"127.0.0.1\",\n    \"value2\": \"\",\n    \"value3\": \"\",\n    \"value4\": \"\",\n    \"value5\": \"\",\n    \"value6\": \"\",\n    \"value7\": \"\",\n    \"dnszone_id\": \"9\",\n    \"rr_id\": \"208\",\n    \"dns_id\": \"3\",\n    \"dnszone_name_utf\": \"lego.example.com\",\n    \"dnszone_name\": \"lego.example.com\",\n    \"dns_name\": \"dns.smart\",\n    \"dns_type\": \"vdns\",\n    \"dns_cloud\": \"0\",\n    \"vdns_parent_id\": \"0\",\n    \"dnsview_name\": \"#\",\n    \"dnsview_class_name\": \"\",\n    \"dnsview_id\": \"0\",\n    \"dnszone_site_name\": \"#\",\n    \"dnszone_is_reverse\": \"0\",\n    \"dnszone_masters\": \"\",\n    \"vdns_parent_name\": \"#\",\n    \"dnszone_forwarders\": \"\",\n    \"dns_class_name\": \"\",\n    \"dnszone_class_name\": \"\",\n    \"dns_version\": \"\",\n    \"dns_comment\": \"\",\n    \"delayed_create_time\": \"0\",\n    \"delayed_delete_time\": \"0\",\n    \"multistatus\": \"\",\n    \"rr_auth_gsstsig\": \"0\",\n    \"rr_last_update_time\": \"\",\n    \"rr_last_update_days\": \"\",\n    \"rr_name_id\": \"22\",\n    \"rr_value_id\": \"237\",\n    \"rr_type_id\": \"3\",\n    \"rr_glue_id\": \"17\",\n    \"dnsview_class_parameters\": \"\",\n    \"dnsview_class_parameters_properties\": \"\",\n    \"dnsview_class_parameters_inheritance_source\": \"\",\n    \"rr_class_parameters\": \"\",\n    \"rr_class_parameters_properties\": \"\",\n    \"rr_class_parameters_inheritance_source\": \"\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/efficientip/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype ResourceRecord struct {\n\tErrorCode string `json:\"errno,omitempty\"`\n\n\tDelayedCreateTime                       string `json:\"delayed_create_time,omitempty\"`\n\tDelayedDeleteTime                       string `json:\"delayed_delete_time,omitempty\"`\n\tDelayedTime                             string `json:\"delayed_time,omitempty\"`\n\tDNSClassName                            string `json:\"dns_class_name,omitempty\"`\n\tDNSCloud                                string `json:\"dns_cloud,omitempty\"`\n\tDNSComment                              string `json:\"dns_comment,omitempty\"`\n\tDNSID                                   string `json:\"dns_id,omitempty\"`\n\tDNSName                                 string `json:\"dns_name,omitempty\"`\n\tDNSType                                 string `json:\"dns_type,omitempty\"`\n\tDNSVersion                              string `json:\"dns_version,omitempty\"`\n\tDNSViewClassName                        string `json:\"dnsview_class_name,omitempty\"`\n\tDNSViewClassParameters                  string `json:\"dnsview_class_parameters,omitempty\"`\n\tDNSViewClassParametersInheritanceSource string `json:\"dnsview_class_parameters_inheritance_source,omitempty\"`\n\tDNSViewClassParametersProperties        string `json:\"dnsview_class_parameters_properties,omitempty\"`\n\tDNSViewID                               string `json:\"dnsview_id,omitempty\"`\n\tDNSViewName                             string `json:\"dnsview_name,omitempty\"`\n\tDNSZoneClassName                        string `json:\"dnszone_class_name,omitempty\"`\n\tDNSZoneForwarders                       string `json:\"dnszone_forwarders,omitempty\"`\n\tDNSZoneID                               string `json:\"dnszone_id,omitempty\"`\n\tDNSZoneIsReverse                        string `json:\"dnszone_is_reverse,omitempty\"`\n\tDNSZoneIsRpz                            string `json:\"dnszone_is_rpz,omitempty\"`\n\tDNSZoneMasters                          string `json:\"dnszone_masters,omitempty\"`\n\tDNSZoneName                             string `json:\"dnszone_name,omitempty\"`\n\tDNSZoneNameUTF                          string `json:\"dnszone_name_utf,omitempty\"`\n\tDNSZoneSiteName                         string `json:\"dnszone_site_name,omitempty\"`\n\tDNSZoneSortZone                         string `json:\"dnszone_sort_zone,omitempty\"`\n\tDNSZoneType                             string `json:\"dnszone_type,omitempty\"`\n\tMultiStatus                             string `json:\"multistatus,omitempty\"`\n\tRRAllValue                              string `json:\"rr_all_value,omitempty\"`\n\tRRAuthGsstsig                           string `json:\"rr_auth_gsstsig,omitempty\"`\n\tRRClassName                             string `json:\"rr_class_name,omitempty\"`\n\tRRClassParameters                       string `json:\"rr_class_parameters,omitempty\"`\n\tRRClassParametersInheritanceSource      string `json:\"rr_class_parameters_inheritance_source,omitempty\"`\n\tRRClassParametersProperties             string `json:\"rr_class_parameters_properties,omitempty\"`\n\tRRFullName                              string `json:\"rr_full_name,omitempty\"`\n\tRRFullNameUTF                           string `json:\"rr_full_name_utf,omitempty\"`\n\tRRGlue                                  string `json:\"rr_glue,omitempty\"`\n\tRRGlueID                                string `json:\"rr_glue_id,omitempty\"`\n\tRRID                                    string `json:\"rr_id,omitempty\"`\n\tRRLastUpdateDays                        string `json:\"rr_last_update_days,omitempty\"`\n\tRRLastUpdateTime                        string `json:\"rr_last_update_time,omitempty\"`\n\tRRName                                  string `json:\"rr_name,omitempty\"`\n\tRRNameID                                string `json:\"rr_name_id,omitempty\"`\n\tRRNameIP4Addr                           string `json:\"rr_name_ip4_addr,omitempty\"`\n\tRRNameIPAddr                            string `json:\"rr_name_ip_addr,omitempty\"`\n\tRRType                                  string `json:\"rr_type,omitempty\"`\n\tRRTypeID                                string `json:\"rr_type_id,omitempty\"`\n\tRRValueID                               string `json:\"rr_value_id,omitempty\"`\n\tRRValueIP4Addr                          string `json:\"rr_value_ip4_addr,omitempty\"`\n\tRRValueIPAddr                           string `json:\"rr_value_ip_addr,omitempty\"`\n\tTTL                                     string `json:\"ttl,omitempty\"`\n\tValue1                                  string `json:\"value1,omitempty\"`\n\tValue2                                  string `json:\"value2,omitempty\"`\n\tValue3                                  string `json:\"value3,omitempty\"`\n\tValue4                                  string `json:\"value4,omitempty\"`\n\tValue5                                  string `json:\"value5,omitempty\"`\n\tValue6                                  string `json:\"value6,omitempty\"`\n\tValue7                                  string `json:\"value7,omitempty\"`\n\tVDNSParentID                            string `json:\"vdns_parent_id,omitempty\"`\n\tVDNSParentName                          string `json:\"vdns_parent_name,omitempty\"`\n}\n\ntype DeleteInputParameters struct {\n\tRRID        string `url:\"rr_id,omitempty\"`\n\tDNSName     string `url:\"dns_name,omitempty\"`\n\tDNSViewName string `url:\"dnsview_name,omitempty\"`\n\tRRName      string `url:\"rr_name,omitempty\"`\n\tRRType      string `url:\"rr_type,omitempty\"`\n\tRRValue1    string `url:\"rr_value1,omitempty\"`\n}\n\ntype BaseOutput struct {\n\tRetOID string `json:\"ret_oid,omitempty\"`\n}\n\ntype APIError struct {\n\tErrorCode   string `json:\"errno,omitempty\"`\n\tErrMsg      string `json:\"errmsg,omitempty\"`\n\tSeverity    string `json:\"severity,omitempty\"`\n\tCategory    string `json:\"category,omitempty\"`\n\tParameters  string `json:\"parameters,omitempty\"`\n\tParamFormat string `json:\"param_format,omitempty\"`\n\tParamValue  string `json:\"param_value,omitempty\"`\n}\n\nfunc (a APIError) Error() string {\n\tmsg := fmt.Sprintf(\"%s: %s %s %s\", a.Category, a.Severity, a.ErrorCode, a.ErrMsg)\n\n\tif a.Parameters != \"\" {\n\t\tmsg += fmt.Sprintf(\" parameters: %s\", a.Parameters)\n\t}\n\n\tif a.ParamFormat != \"\" {\n\t\tmsg += fmt.Sprintf(\" param_format: %s\", a.ParamFormat)\n\t}\n\n\tif a.ParamValue != \"\" {\n\t\tmsg += fmt.Sprintf(\" param_value: %s\", a.ParamValue)\n\t}\n\n\treturn msg\n}\n"
  },
  {
    "path": "providers/dns/epik/epik.go",
    "content": "// Package epik implements a DNS provider for solving the DNS-01 challenge using Epik.\npackage epik\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/epik/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"EPIK_\"\n\n\tEnvSignature = envNamespace + \"SIGNATURE\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tSignature          string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 3600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Epik.\n// Credentials must be passed in the environment variable: EPIK_SIGNATURE.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvSignature)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"epik: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Signature = values[EnvSignature]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Epik.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"epik: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Signature == \"\" {\n\t\treturn nil, errors.New(\"epik: missing credentials\")\n\t}\n\n\tclient := internal.NewClient(config.Signature)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// find authZone\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"epik: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"epik: %w\", err)\n\t}\n\n\trecord := internal.RecordRequest{\n\t\tHost: subDomain,\n\t\tType: \"TXT\",\n\t\tData: info.Value,\n\t\tTTL:  d.config.TTL,\n\t}\n\n\t_, err = d.client.CreateHostRecord(context.Background(), dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"epik: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// find authZone\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"epik: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tdom := dns01.UnFqdn(authZone)\n\n\tctx := context.Background()\n\n\trecords, err := d.client.GetDNSRecords(ctx, dom)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"epik: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"epik: %w\", err)\n\t}\n\n\tfor _, record := range records {\n\t\tif strings.EqualFold(record.Type, \"TXT\") && record.Data == info.Value && record.Name == subDomain {\n\t\t\t_, err = d.client.RemoveHostRecord(ctx, dom, record.ID)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"epik: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/epik/epik.toml",
    "content": "Name = \"Epik\"\nDescription = ''''''\nURL = \"https://www.epik.com/\"\nCode = \"epik\"\nSince = \"v4.5.0\"\n\nExample = '''\nEPIK_SIGNATURE=xxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns epik -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    EPIK_SIGNATURE = \"Epik API signature (https://registrar.epik.com/account/api-settings/)\"\n  [Configuration.Additional]\n    EPIK_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    EPIK_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    EPIK_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)\"\n    EPIK_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://docs-userapi.epik.com/v2/\"\n"
  },
  {
    "path": "providers/dns/epik/epik_test.go",
    "content": "package epik\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvSignature).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSignature: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"epik: some credentials information are missing: EPIK_SIGNATURE\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tsignature string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tsignature: \"A\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"epik: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Signature = test.signature\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/epik/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\nconst defaultBaseURL = \"https://usersapiv2.epik.com/v2\"\n\n// Client the Epik API client.\ntype Client struct {\n\tsignature string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(signature string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tsignature:  signature,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// GetDNSRecords gets DNS records for a domain.\n// https://docs.userapi.epik.com/v2/#/DNS%20Host%20Records/getDnsRecord\nfunc (c *Client) GetDNSRecords(ctx context.Context, domain string) ([]Record, error) {\n\tendpoint := c.createEndpoint(domain, url.Values{})\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar data GetDNSRecordResponse\n\n\terr = c.do(req, &data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn data.Data.Records, nil\n}\n\n// CreateHostRecord creates a record for a domain.\n// https://docs.userapi.epik.com/v2/#/DNS%20Host%20Records/createHostRecord\nfunc (c *Client) CreateHostRecord(ctx context.Context, domain string, record RecordRequest) (*Data, error) {\n\tendpoint := c.createEndpoint(domain, url.Values{})\n\n\tpayload := CreateHostRecords{Payload: record}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar data Data\n\n\terr = c.do(req, &data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &data, nil\n}\n\n// RemoveHostRecord removes a record for a domain.\n// https://docs.userapi.epik.com/v2/#/DNS%20Host%20Records/removeHostRecord\nfunc (c *Client) RemoveHostRecord(ctx context.Context, domain, recordID string) (*Data, error) {\n\tparams := url.Values{}\n\tparams.Set(\"ID\", recordID)\n\n\tendpoint := c.createEndpoint(domain, params)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar data Data\n\n\terr = c.do(req, &data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &data, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) createEndpoint(domain string, params url.Values) *url.URL {\n\tendpoint := c.baseURL.JoinPath(\"domains\", domain, \"records\")\n\n\tparams.Set(\"SIGNATURE\", c.signature)\n\tendpoint.RawQuery = params.Encode()\n\n\treturn endpoint\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar apiErr APIError\n\n\terr := json.Unmarshal(raw, &apiErr)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &apiErr\n}\n"
  },
  {
    "path": "providers/dns/epik/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t)\n}\n\nfunc TestClient_GetDNSRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains/example.com/records\",\n\t\t\tservermock.ResponseFromFixture(\"getDnsRecord.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"SIGNATURE\", \"secret\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetDNSRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tID:   \"abc123\",\n\t\t\tName: \"www\",\n\t\t\tType: \"CAA\",\n\t\t\tData: \"1 issue letsencrypt.org\",\n\t\t\tAUX:  0,\n\t\t\tTTL:  300,\n\t\t},\n\t\t{\n\t\t\tID:   \"abc123\",\n\t\t\tName: \"www\",\n\t\t\tType: \"A\",\n\t\t\tData: \"192.64.147.249\",\n\t\t\tAUX:  0,\n\t\t\tTTL:  300,\n\t\t},\n\t\t{\n\t\t\tID:   \"abc123\",\n\t\t\tName: \"*\",\n\t\t\tType: \"A\",\n\t\t\tData: \"192.64.147.249\",\n\t\t\tAUX:  0,\n\t\t\tTTL:  300,\n\t\t},\n\t\t{\n\t\t\tID:   \"abc123\",\n\t\t\tType: \"CAA\",\n\t\t\tData: \"0 issue trust-provider.com\",\n\t\t\tAUX:  0,\n\t\t\tTTL:  300,\n\t\t},\n\t\t{\n\t\t\tID:   \"abc123\",\n\t\t\tType: \"CAA\",\n\t\t\tData: \"1 issue letsencrypt.org\",\n\t\t\tAUX:  0,\n\t\t\tTTL:  300,\n\t\t},\n\t\t{\n\t\t\tID:   \"abc123\",\n\t\t\tType: \"A\",\n\t\t\tData: \"192.64.147.249\",\n\t\t\tAUX:  0,\n\t\t\tTTL:  300,\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetDNSRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains/example.com/records\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"SIGNATURE\", \"secret\")).\n\t\tBuild(t)\n\n\t_, err := client.GetDNSRecords(t.Context(), \"example.com\")\n\trequire.Error(t, err)\n}\n\nfunc TestClient_CreateHostRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/example.com/records\",\n\t\t\tservermock.ResponseFromFixture(\"createHostRecord.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"SIGNATURE\", \"secret\")).\n\t\tBuild(t)\n\n\trecord := RecordRequest{\n\t\tHost: \"www2\",\n\t\tType: \"A\",\n\t\tData: \"192.64.147.249\",\n\t\tAux:  0,\n\t\tTTL:  300,\n\t}\n\n\tdata, err := client.CreateHostRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n\n\texpected := &Data{\n\t\tCode:    1000,\n\t\tMessage: \"Command completed successfully.\",\n\t}\n\n\tassert.Equal(t, expected, data)\n}\n\nfunc TestClient_CreateHostRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/example.com/records\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"SIGNATURE\", \"secret\")).\n\t\tBuild(t)\n\n\trecord := RecordRequest{\n\t\tHost: \"www2\",\n\t\tType: \"A\",\n\t\tData: \"192.64.147.249\",\n\t\tAux:  0,\n\t\tTTL:  300,\n\t}\n\n\t_, err := client.CreateHostRecord(t.Context(), \"example.com\", record)\n\trequire.Error(t, err)\n}\n\nfunc TestClient_RemoveHostRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domains/example.com/records\",\n\t\t\tservermock.ResponseFromFixture(\"removeHostRecord.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"ID\", \"abc123\").\n\t\t\t\tWith(\"SIGNATURE\", \"secret\")).\n\t\tBuild(t)\n\n\tdata, err := client.RemoveHostRecord(t.Context(), \"example.com\", \"abc123\")\n\trequire.NoError(t, err)\n\n\texpected := &Data{\n\t\tCode:    1000,\n\t\tMessage: \"Command completed successfully.\",\n\t}\n\n\tassert.Equal(t, expected, data)\n}\n\nfunc TestClient_RemoveHostRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domains/example.com/records\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.RemoveHostRecord(t.Context(), \"example.com\", \"abc123\")\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "providers/dns/epik/internal/fixtures/createHostRecord.json",
    "content": "{\n  \"code\": 1000,\n  \"message\": \"Command completed successfully.\",\n  \"description\": null\n}\n"
  },
  {
    "path": "providers/dns/epik/internal/fixtures/error.json",
    "content": "{\n  \"errors\": [\n    {\n      \"code\": 1,\n      \"message\": \"Unauthorized\",\n      \"description\": \"Unauthorized: Signature was not provided or was invalid\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/epik/internal/fixtures/getDnsRecord.json",
    "content": "{\n  \"data\": {\n    \"name\": \"MYDOMAIN.ORG\",\n    \"code\": 1000,\n    \"records\": [\n      {\n        \"id\": \"abc123\",\n        \"name\": \"www\",\n        \"type\": \"CAA\",\n        \"data\": \"1 issue letsencrypt.org\",\n        \"aux\": 0,\n        \"ttl\": 300\n      },\n      {\n        \"id\": \"abc123\",\n        \"name\": \"www\",\n        \"type\": \"A\",\n        \"data\": \"192.64.147.249\",\n        \"aux\": 0,\n        \"ttl\": 300\n      },\n      {\n        \"id\": \"abc123\",\n        \"name\": \"*\",\n        \"type\": \"A\",\n        \"data\": \"192.64.147.249\",\n        \"aux\": 0,\n        \"ttl\": 300\n      },\n      {\n        \"id\": \"abc123\",\n        \"name\": \"\",\n        \"type\": \"CAA\",\n        \"data\": \"0 issue trust-provider.com\",\n        \"aux\": 0,\n        \"ttl\": 300\n      },\n      {\n        \"id\": \"abc123\",\n        \"name\": \"\",\n        \"type\": \"CAA\",\n        \"data\": \"1 issue letsencrypt.org\",\n        \"aux\": 0,\n        \"ttl\": 300\n      },\n      {\n        \"id\": \"abc123\",\n        \"name\": \"\",\n        \"type\": \"A\",\n        \"data\": \"192.64.147.249\",\n        \"aux\": 0,\n        \"ttl\": 300\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/epik/internal/fixtures/removeHostRecord.json",
    "content": "{\n  \"code\": 1000,\n  \"message\": \"Command completed successfully.\",\n  \"description\": null\n}\n"
  },
  {
    "path": "providers/dns/epik/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype RecordRequest struct {\n\tHost string `json:\"HOST,omitempty\"`\n\tType string `json:\"TYPE,omitempty\"`\n\tData string `json:\"DATA,omitempty\"`\n\tAux  int    `json:\"AUX,omitempty\"`\n\tTTL  int    `json:\"TTL,omitempty\"`\n}\n\ntype SetHostRecords struct {\n\tPayload []RecordRequest `json:\"set_host_records_payload\"`\n}\n\ntype CreateHostRecords struct {\n\tPayload RecordRequest `json:\"create_host_records_payload\"`\n}\n\ntype Data struct {\n\tCode        int    `json:\"code,omitempty\"`\n\tMessage     string `json:\"message,omitempty\"`\n\tDescription string `json:\"description,omitempty\"`\n}\n\ntype APIError struct {\n\tErrors []Data `json:\"errors\"`\n}\n\nfunc (a APIError) Error() string {\n\tvar parts []string\n\tfor _, data := range a.Errors {\n\t\tparts = append(parts, fmt.Sprintf(\"code: %d, message: %s, description: %s\", data.Code, data.Message, data.Description))\n\t}\n\n\treturn strings.Join(parts, \", \")\n}\n\ntype Record struct {\n\tID   string `json:\"id\"`\n\tName string `json:\"name\"`\n\tType string `json:\"type\"`\n\tData string `json:\"data\"`\n\tAUX  int    `json:\"aux\"`\n\tTTL  int    `json:\"ttl\"`\n}\n\ntype GetDNSRecordResponse struct {\n\tData struct {\n\t\tName    string   `json:\"name\"`\n\t\tCode    int      `json:\"code\"`\n\t\tRecords []Record `json:\"records\"`\n\t} `json:\"data\"`\n}\n"
  },
  {
    "path": "providers/dns/eurodns/eurodns.go",
    "content": "// Package eurodns implements a DNS provider for solving the DNS-01 challenge using EuroDNS.\npackage eurodns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/eurodns/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"EURODNS_\"\n\n\tEnvApplicationID = envNamespace + \"APP_ID\"\n\tEnvAPIKey        = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tApplicationID string\n\tAPIKey        string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, internal.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for EuroDNS.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvApplicationID, EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"eurodns: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.ApplicationID = values[EnvApplicationID]\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for EuroDNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"eurodns: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.ApplicationID, config.APIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"eurodns: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"eurodns: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"eurodns: %w\", err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tzone, err := d.client.GetZone(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"eurodns: get zone: %w\", err)\n\t}\n\n\tzone.Records = append(zone.Records, internal.Record{\n\t\tType:  \"TXT\",\n\t\tHost:  subDomain,\n\t\tTTL:   internal.TTLRounder(d.config.TTL),\n\t\tRData: info.Value,\n\t})\n\n\tvalidation, err := d.client.ValidateZone(ctx, authZone, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"eurodns: validate zone: %w\", err)\n\t}\n\n\tif validation.Report != nil && !validation.Report.IsValid {\n\t\treturn fmt.Errorf(\"eurodns: validation report: %w\", validation.Report)\n\t}\n\n\terr = d.client.SaveZone(ctx, authZone, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"eurodns: save zone: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"eurodns: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"eurodns: %w\", err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tzone, err := d.client.GetZone(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"eurodns: get zone: %w\", err)\n\t}\n\n\tvar recordsToKeep []internal.Record\n\n\tfor _, record := range zone.Records {\n\t\tif record.Type == \"TXT\" && record.Host == subDomain && record.RData == info.Value {\n\t\t\tcontinue\n\t\t}\n\n\t\trecordsToKeep = append(recordsToKeep, record)\n\t}\n\n\tzone.Records = recordsToKeep\n\n\tvalidation, err := d.client.ValidateZone(ctx, authZone, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"eurodns: validate zone: %w\", err)\n\t}\n\n\tif validation.Report != nil && !validation.Report.IsValid {\n\t\treturn fmt.Errorf(\"eurodns: validation report: %w\", validation.Report)\n\t}\n\n\terr = d.client.SaveZone(ctx, authZone, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"eurodns: save zone: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/eurodns/eurodns.toml",
    "content": "Name = \"EuroDNS\"\nDescription = ''''''\nURL = \"https://www.eurodns.com/\"\nCode = \"eurodns\"\nSince = \"v4.33.0\"\n\nExample = '''\nEURODNS_APP_ID=\"xxx\" \\\nEURODNS_API_KEY=\"yyy\" \\\nlego --dns eurodns -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    EURODNS_APP_ID = \"Application ID\"\n    EURODNS_API_KEY = \"API key\"\n  [Configuration.Additional]\n    EURODNS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    EURODNS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    EURODNS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)\"\n    EURODNS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://docapi.eurodns.com/\"\n"
  },
  {
    "path": "providers/dns/eurodns/eurodns_test.go",
    "content": "package eurodns\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-acme/lego/v4/providers/dns/eurodns/internal\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvApplicationID, EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvApplicationID: \"abc\",\n\t\t\t\tEnvAPIKey:        \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing application ID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvApplicationID: \"\",\n\t\t\t\tEnvAPIKey:        \"secret\",\n\t\t\t},\n\t\t\texpected: \"eurodns: some credentials information are missing: EURODNS_APP_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvApplicationID: \"\",\n\t\t\t\tEnvAPIKey:        \"secret\",\n\t\t\t},\n\t\t\texpected: \"eurodns: some credentials information are missing: EURODNS_APP_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"eurodns: some credentials information are missing: EURODNS_APP_ID,EURODNS_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tappID    string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tappID:  \"abc\",\n\t\t\tapiKey: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing application ID\",\n\t\t\texpected: \"eurodns: credentials missing\",\n\t\t\tapiKey:   \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API secret\",\n\t\t\texpected: \"eurodns: credentials missing\",\n\t\t\tappID:    \"abc\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"eurodns: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.ApplicationID = test.appID\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = \"secret\"\n\t\t\tconfig.ApplicationID = \"abc\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tprovider, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tprovider.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn provider, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(internal.HeaderAppID, \"abc\").\n\t\t\tWith(internal.HeaderAPIKey, \"secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /example.com\",\n\t\t\tservermock.ResponseFromInternal(\"zone_get.json\"),\n\t\t).\n\t\tRoute(\"POST /example.com/check\",\n\t\t\tservermock.ResponseFromInternal(\"zone_add_validate_ok.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"zone_add.json\"),\n\t\t).\n\t\tRoute(\"PUT /example.com\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"zone_add.json\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /example.com\",\n\t\t\tservermock.ResponseFromInternal(\"zone_add.json\"),\n\t\t).\n\t\tRoute(\"POST /example.com/check\",\n\t\t\tservermock.ResponseFromInternal(\"zone_remove.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"zone_remove.json\"),\n\t\t).\n\t\tRoute(\"PUT /example.com\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"zone_remove.json\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/eurodns/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://rest-api.eurodns.com/dns-zones/\"\n\nconst (\n\tHeaderAppID  = \"X-APP-ID\"\n\tHeaderAPIKey = \"X-API-KEY\"\n)\n\n// Client the EuroDNS API client.\ntype Client struct {\n\tappID  string\n\tapiKey string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(appID, apiKey string) (*Client, error) {\n\tif appID == \"\" || apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tappID:      appID,\n\t\tapiKey:     apiKey,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// GetZone gets a DNS Zone.\n// https://docapi.eurodns.com/#/dnsprovider/getdnszone\nfunc (c *Client) GetZone(ctx context.Context, domain string) (*Zone, error) {\n\tendpoint := c.BaseURL.JoinPath(domain)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &Zone{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// SaveZone saves a DNS Zone.\n// https://docapi.eurodns.com/#/dnsprovider/savednszone\nfunc (c *Client) SaveZone(ctx context.Context, domain string, zone *Zone) error {\n\tendpoint := c.BaseURL.JoinPath(domain)\n\n\tif len(zone.URLForwards) == 0 {\n\t\tzone.URLForwards = make([]URLForward, 0)\n\t}\n\n\tif len(zone.MailForwards) == 0 {\n\t\tzone.MailForwards = make([]MailForward, 0)\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, zone)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\n// ValidateZone validates DNS Zone.\n// https://docapi.eurodns.com/#/dnsprovider/checkdnszone\nfunc (c *Client) ValidateZone(ctx context.Context, domain string, zone *Zone) (*Zone, error) {\n\tendpoint := c.BaseURL.JoinPath(domain, \"check\")\n\n\tif len(zone.URLForwards) == 0 {\n\t\tzone.URLForwards = make([]URLForward, 0)\n\t}\n\n\tif len(zone.MailForwards) == 0 {\n\t\tzone.MailForwards = make([]MailForward, 0)\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, zone)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &Zone{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(HeaderAppID, c.appID)\n\treq.Header.Set(HeaderAPIKey, c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"%d: %w\", resp.StatusCode, &errAPI)\n}\n\nconst DefaultTTL = 600\n\n// TTLRounder rounds the given TTL in seconds to the next accepted value.\n// Accepted TTL values are: 600, 900, 1800,3600, 7200, 14400, 21600, 43200, 86400, 172800, 432000, 604800.\nfunc TTLRounder(ttl int) int {\n\tfor _, validTTL := range []int{DefaultTTL, 900, 1800, 3600, 7200, 14400, 21600, 43200, 86400, 172800, 432000, 604800} {\n\t\tif ttl <= validTTL {\n\t\t\treturn validTTL\n\t\t}\n\t}\n\n\treturn DefaultTTL\n}\n"
  },
  {
    "path": "providers/dns/eurodns/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"abc\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(HeaderAppID, \"abc\").\n\t\t\tWith(HeaderAPIKey, \"secret\"),\n\t)\n}\n\nfunc TestClient_GetZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /example.com\",\n\t\t\tservermock.ResponseFromFixture(\"zone_get.json\"),\n\t\t).\n\t\tBuild(t)\n\n\tzone, err := client.GetZone(context.Background(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := &Zone{\n\t\tName:          \"example.com\",\n\t\tDomainConnect: true,\n\t\tRecords:       slices.Concat([]Record{fakeARecord()}),\n\t\tURLForwards:   []URLForward{fakeURLForward()},\n\t\tMailForwards:  []MailForward{fakeMailForward()},\n\t}\n\n\tassert.Equal(t, expected, zone)\n}\n\nfunc TestClient_GetZone_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /example.com\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized),\n\t\t).\n\t\tBuild(t)\n\n\t_, err := client.GetZone(context.Background(), \"example.com\")\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"401: INVALID_API_KEY: Invalid API Key\")\n}\n\nfunc TestClient_SaveZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /example.com\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"zone_add.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:  \"TXT\",\n\t\tHost:  \"_acme-challenge\",\n\t\tRData: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:   600,\n\t}\n\n\tzone := &Zone{\n\t\tName:          \"example.com\",\n\t\tDomainConnect: true,\n\t\tRecords:       []Record{fakeARecord(), record},\n\t\tURLForwards:   []URLForward{fakeURLForward()},\n\t\tMailForwards:  []MailForward{fakeMailForward()},\n\t}\n\n\terr := client.SaveZone(context.Background(), \"example.com\", zone)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_SaveZone_emptyForwards(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /example.com\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"zone_add_empty_forwards.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:  \"TXT\",\n\t\tHost:  \"_acme-challenge\",\n\t\tRData: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:   600,\n\t}\n\n\tzone := &Zone{\n\t\tName:          \"example.com\",\n\t\tDomainConnect: true,\n\t\tRecords:       slices.Concat([]Record{fakeARecord(), record}),\n\t}\n\n\terr := client.SaveZone(context.Background(), \"example.com\", zone)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_SaveZone_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /example.com\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized),\n\t\t).\n\t\tBuild(t)\n\n\tzone := &Zone{\n\t\tName:          \"example.com\",\n\t\tDomainConnect: true,\n\t\tRecords:       []Record{fakeARecord()},\n\t\tURLForwards:   []URLForward{fakeURLForward()},\n\t\tMailForwards:  []MailForward{fakeMailForward()},\n\t}\n\n\terr := client.SaveZone(context.Background(), \"example.com\", zone)\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"401: INVALID_API_KEY: Invalid API Key\")\n}\n\nfunc TestClient_ValidateZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /example.com/check\",\n\t\t\tservermock.ResponseFromFixture(\"zone_add_validate_ok.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"zone_add.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:  \"TXT\",\n\t\tHost:  \"_acme-challenge\",\n\t\tRData: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:   600,\n\t}\n\n\tzone := &Zone{\n\t\tName:          \"example.com\",\n\t\tDomainConnect: true,\n\t\tRecords:       []Record{fakeARecord(), record},\n\t\tURLForwards:   []URLForward{fakeURLForward()},\n\t\tMailForwards:  []MailForward{fakeMailForward()},\n\t}\n\n\tzone, err := client.ValidateZone(context.Background(), \"example.com\", zone)\n\trequire.NoError(t, err)\n\n\texpected := &Zone{\n\t\tName:          \"example.com\",\n\t\tDomainConnect: true,\n\t\tRecords:       []Record{fakeARecord(), record},\n\t\tURLForwards:   []URLForward{fakeURLForward()},\n\t\tMailForwards:  []MailForward{fakeMailForward()},\n\t\tReport:        &Report{IsValid: true},\n\t}\n\n\tassert.Equal(t, expected, zone)\n}\n\nfunc TestClient_ValidateZone_report(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /example.com/check\",\n\t\t\tservermock.ResponseFromFixture(\"zone_add_validate_ko.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"zone_add.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:  \"TXT\",\n\t\tHost:  \"_acme-challenge\",\n\t\tRData: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:   600,\n\t}\n\n\tzone := &Zone{\n\t\tName:          \"example.com\",\n\t\tDomainConnect: true,\n\t\tRecords:       []Record{fakeARecord(), record},\n\t\tURLForwards:   []URLForward{fakeURLForward()},\n\t\tMailForwards:  []MailForward{fakeMailForward()},\n\t}\n\n\tzone, err := client.ValidateZone(context.Background(), \"example.com\", zone)\n\trequire.NoError(t, err)\n\n\texpected := &Zone{\n\t\tName:          \"example.com\",\n\t\tDomainConnect: true,\n\t\tRecords:       []Record{fakeARecord(), record},\n\t\tURLForwards:   []URLForward{fakeURLForward()},\n\t\tMailForwards:  []MailForward{fakeMailForward()},\n\t\tReport:        fakeReport(),\n\t}\n\n\tassert.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`)\n\n\tassert.Equal(t, expected, zone)\n}\n\nfunc TestClient_ValidateZone_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /example.com/check\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized),\n\t\t).\n\t\tBuild(t)\n\n\tzone := &Zone{\n\t\tName:          \"example.com\",\n\t\tDomainConnect: true,\n\t\tRecords:       []Record{fakeARecord()},\n\t\tURLForwards:   []URLForward{fakeURLForward()},\n\t\tMailForwards:  []MailForward{fakeMailForward()},\n\t}\n\n\t_, err := client.ValidateZone(context.Background(), \"example.com\", zone)\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"401: INVALID_API_KEY: Invalid API Key\")\n}\n\nfunc fakeARecord() Record {\n\treturn Record{\n\t\tID:       1000,\n\t\tType:     \"A\",\n\t\tHost:     \"@\",\n\t\tTTL:      600,\n\t\tRData:    \"string\",\n\t\tUpdated:  ptr.Pointer(true),\n\t\tLocked:   ptr.Pointer(true),\n\t\tIsDynDNS: ptr.Pointer(true),\n\t\tProxy:    \"ON\",\n\t}\n}\n\nfunc fakeURLForward() URLForward {\n\treturn URLForward{\n\t\tID:          2000,\n\t\tForwardType: \"FRAME\",\n\t\tHost:        \"string\",\n\t\tURL:         \"string\",\n\t\tTitle:       \"string\",\n\t\tKeywords:    \"string\",\n\t\tDescription: \"string\",\n\t\tUpdated:     ptr.Pointer(true),\n\t}\n}\n\nfunc fakeMailForward() MailForward {\n\treturn MailForward{\n\t\tID:          3000,\n\t\tSource:      \"string\",\n\t\tDestination: \"string\",\n\t\tUpdated:     ptr.Pointer(true),\n\t}\n}\n\nfunc fakeReport() *Report {\n\treturn &Report{\n\t\tIsValid: false,\n\t\tRecordErrors: []RecordError{{\n\t\t\tMessages: []string{`\"120\" is not a valid TTL`},\n\t\t\tSeverity: \"ERROR\",\n\t\t\tRecord:   fakeARecord(),\n\t\t}},\n\t\tURLForwardErrors: []URLForwardError{{\n\t\t\tMessages:   []string{\"string\"},\n\t\t\tSeverity:   \"ERROR\",\n\t\t\tURLForward: fakeURLForward(),\n\t\t}},\n\t\tMailForwardErrors: []MailForwardError{{\n\t\t\tMessages:    []string{\"string\"},\n\t\t\tMailForward: fakeMailForward(),\n\t\t\tSeverity:    \"ERROR\",\n\t\t}},\n\t\tZoneErrors: []ZoneError{{\n\t\t\tMessage:      \"string\",\n\t\t\tSeverity:     \"ERROR\",\n\t\t\tRecords:      []Record{fakeARecord()},\n\t\t\tURLForwards:  []URLForward{fakeURLForward()},\n\t\t\tMailForwards: []MailForward{fakeMailForward()},\n\t\t}},\n\t}\n}\n"
  },
  {
    "path": "providers/dns/eurodns/internal/fixtures/error.json",
    "content": "{\n  \"errors\": [\n    {\n      \"code\": \"INVALID_API_KEY\",\n      \"title\": \"Invalid API Key\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/eurodns/internal/fixtures/zone_add.json",
    "content": "{\n  \"name\": \"example.com\",\n  \"domainConnect\": true,\n  \"records\": [\n    {\n      \"id\": 1000,\n      \"type\": \"A\",\n      \"host\": \"@\",\n      \"ttl\": 600,\n      \"rdata\": \"string\",\n      \"updated\": true,\n      \"locked\": true,\n      \"isDynDns\": true,\n      \"proxy\": \"ON\"\n    },\n    {\n      \"type\": \"TXT\",\n      \"host\": \"_acme-challenge\",\n      \"ttl\": 600,\n      \"rdata\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n      \"updated\": null,\n      \"locked\": null,\n      \"isDynDns\": null\n    }\n  ],\n  \"urlForwards\": [\n    {\n      \"id\": 2000,\n      \"forwardType\": \"FRAME\",\n      \"host\": \"string\",\n      \"url\": \"string\",\n      \"title\": \"string\",\n      \"keywords\": \"string\",\n      \"description\": \"string\",\n      \"updated\": true\n    }\n  ],\n  \"mailForwards\": [\n    {\n      \"id\": 3000,\n      \"source\": \"string\",\n      \"destination\": \"string\",\n      \"updated\": true\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/eurodns/internal/fixtures/zone_add_empty_forwards.json",
    "content": "{\n  \"name\": \"example.com\",\n  \"domainConnect\": true,\n  \"records\": [\n    {\n      \"id\": 1000,\n      \"type\": \"A\",\n      \"host\": \"@\",\n      \"ttl\": 600,\n      \"rdata\": \"string\",\n      \"updated\": true,\n      \"locked\": true,\n      \"isDynDns\": true,\n      \"proxy\": \"ON\"\n    },\n    {\n      \"type\": \"TXT\",\n      \"host\": \"_acme-challenge\",\n      \"ttl\": 600,\n      \"rdata\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n      \"updated\": null,\n      \"locked\": null,\n      \"isDynDns\": null\n    }\n  ],\n  \"urlForwards\": [],\n  \"mailForwards\": []\n}\n"
  },
  {
    "path": "providers/dns/eurodns/internal/fixtures/zone_add_validate_ko.json",
    "content": "{\n  \"name\": \"example.com\",\n  \"domainConnect\": true,\n  \"records\": [\n    {\n      \"id\": 1000,\n      \"type\": \"A\",\n      \"host\": \"@\",\n      \"ttl\": 600,\n      \"rdata\": \"string\",\n      \"updated\": true,\n      \"locked\": true,\n      \"isDynDns\": true,\n      \"proxy\": \"ON\"\n    },\n    {\n      \"type\": \"TXT\",\n      \"host\": \"_acme-challenge\",\n      \"ttl\": 600,\n      \"rdata\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n      \"updated\": null,\n      \"locked\": null,\n      \"isDynDns\": null\n    }\n  ],\n  \"urlForwards\": [\n    {\n      \"id\": 2000,\n      \"forwardType\": \"FRAME\",\n      \"host\": \"string\",\n      \"url\": \"string\",\n      \"title\": \"string\",\n      \"keywords\": \"string\",\n      \"description\": \"string\",\n      \"updated\": true\n    }\n  ],\n  \"mailForwards\": [\n    {\n      \"id\": 3000,\n      \"source\": \"string\",\n      \"destination\": \"string\",\n      \"updated\": true\n    }\n  ],\n  \"report\": {\n    \"isValid\": false,\n    \"recordErrors\": [\n      {\n        \"messages\": [\n          \"\\\"120\\\" is not a valid TTL\"\n        ],\n        \"record\": {\n          \"id\": 1000,\n          \"type\": \"A\",\n          \"host\": \"@\",\n          \"ttl\": 600,\n          \"rdata\": \"string\",\n          \"updated\": true,\n          \"locked\": true,\n          \"isDynDns\": true,\n          \"proxy\": \"ON\"\n        },\n        \"severity\": \"ERROR\"\n      }\n    ],\n    \"urlForwardErrors\": [\n      {\n        \"messages\": [\n          \"string\"\n        ],\n        \"urlForward\": {\n          \"id\": 2000,\n          \"forwardType\": \"FRAME\",\n          \"host\": \"string\",\n          \"url\": \"string\",\n          \"title\": \"string\",\n          \"keywords\": \"string\",\n          \"description\": \"string\",\n          \"updated\": true\n        },\n        \"severity\": \"ERROR\"\n      }\n    ],\n    \"mailForwardErrors\": [\n      {\n        \"messages\": [\n          \"string\"\n        ],\n        \"mailForward\": {\n          \"id\": 3000,\n          \"source\": \"string\",\n          \"destination\": \"string\",\n          \"updated\": true\n        },\n        \"severity\": \"ERROR\"\n      }\n    ],\n    \"zoneErrors\": [\n      {\n        \"message\": \"string\",\n        \"records\": [\n          {\n            \"id\": 1000,\n            \"type\": \"A\",\n            \"host\": \"@\",\n            \"ttl\": 600,\n            \"rdata\": \"string\",\n            \"updated\": true,\n            \"locked\": true,\n            \"isDynDns\": true,\n            \"proxy\": \"ON\"\n          }\n        ],\n        \"urlForwards\": [\n          {\n            \"id\": 2000,\n            \"forwardType\": \"FRAME\",\n            \"host\": \"string\",\n            \"url\": \"string\",\n            \"title\": \"string\",\n            \"keywords\": \"string\",\n            \"description\": \"string\",\n            \"updated\": true\n          }\n        ],\n        \"mailForwards\": [\n          {\n            \"id\": 3000,\n            \"source\": \"string\",\n            \"destination\": \"string\",\n            \"updated\": true\n          }\n        ],\n        \"severity\": \"ERROR\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/eurodns/internal/fixtures/zone_add_validate_ok.json",
    "content": "{\n  \"name\": \"example.com\",\n  \"domainConnect\": true,\n  \"records\": [\n    {\n      \"id\": 1000,\n      \"type\": \"A\",\n      \"host\": \"@\",\n      \"ttl\": 600,\n      \"rdata\": \"string\",\n      \"updated\": true,\n      \"locked\": true,\n      \"isDynDns\": true,\n      \"proxy\": \"ON\"\n    },\n    {\n      \"type\": \"TXT\",\n      \"host\": \"_acme-challenge\",\n      \"ttl\": 600,\n      \"rdata\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n      \"updated\": null,\n      \"locked\": null,\n      \"isDynDns\": null\n    }\n  ],\n  \"urlForwards\": [\n    {\n      \"id\": 2000,\n      \"forwardType\": \"FRAME\",\n      \"host\": \"string\",\n      \"url\": \"string\",\n      \"title\": \"string\",\n      \"keywords\": \"string\",\n      \"description\": \"string\",\n      \"updated\": true\n    }\n  ],\n  \"mailForwards\": [\n    {\n      \"id\": 3000,\n      \"source\": \"string\",\n      \"destination\": \"string\",\n      \"updated\": true\n    }\n  ],\n  \"report\": {\n    \"isValid\": true\n  }\n}\n"
  },
  {
    "path": "providers/dns/eurodns/internal/fixtures/zone_get.json",
    "content": "{\n  \"name\": \"example.com\",\n  \"domainConnect\": true,\n  \"records\": [\n    {\n      \"id\": 1000,\n      \"type\": \"A\",\n      \"host\": \"@\",\n      \"ttl\": 600,\n      \"rdata\": \"string\",\n      \"updated\": true,\n      \"locked\": true,\n      \"isDynDns\": true,\n      \"proxy\": \"ON\"\n    }\n  ],\n  \"urlForwards\": [\n    {\n      \"id\": 2000,\n      \"forwardType\": \"FRAME\",\n      \"host\": \"string\",\n      \"url\": \"string\",\n      \"title\": \"string\",\n      \"keywords\": \"string\",\n      \"description\": \"string\",\n      \"updated\": true\n    }\n  ],\n  \"mailForwards\": [\n    {\n      \"id\": 3000,\n      \"source\": \"string\",\n      \"destination\": \"string\",\n      \"updated\": true\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/eurodns/internal/fixtures/zone_remove.json",
    "content": "{\n  \"name\": \"example.com\",\n  \"domainConnect\": true,\n  \"records\": [\n    {\n      \"id\": 1000,\n      \"type\": \"A\",\n      \"host\": \"@\",\n      \"ttl\": 600,\n      \"rdata\": \"string\",\n      \"updated\": true,\n      \"locked\": true,\n      \"isDynDns\": true,\n      \"proxy\": \"ON\"\n    }\n  ],\n  \"urlForwards\": [\n    {\n      \"id\": 2000,\n      \"forwardType\": \"FRAME\",\n      \"host\": \"string\",\n      \"url\": \"string\",\n      \"title\": \"string\",\n      \"keywords\": \"string\",\n      \"description\": \"string\",\n      \"updated\": true\n    }\n  ],\n  \"mailForwards\": [\n    {\n      \"id\": 3000,\n      \"source\": \"string\",\n      \"destination\": \"string\",\n      \"updated\": true\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/eurodns/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype APIError struct {\n\tErrors []Error `json:\"errors\"`\n}\n\nfunc (a *APIError) Error() string {\n\tvar msg []string\n\n\tfor _, e := range a.Errors {\n\t\tmsg = append(msg, fmt.Sprintf(\"%s: %s\", e.Code, e.Title))\n\t}\n\n\treturn strings.Join(msg, \", \")\n}\n\ntype Error struct {\n\tCode  string `json:\"code\"`\n\tTitle string `json:\"title\"`\n}\n\ntype Zone struct {\n\tName          string        `json:\"name,omitempty\"`\n\tDomainConnect bool          `json:\"domainConnect,omitempty\"`\n\tRecords       []Record      `json:\"records\"`\n\tURLForwards   []URLForward  `json:\"urlForwards\"`\n\tMailForwards  []MailForward `json:\"mailForwards\"`\n\tReport        *Report       `json:\"report,omitempty\"`\n}\n\ntype Record struct {\n\tID       int    `json:\"id,omitempty\"`\n\tType     string `json:\"type,omitempty\"`\n\tHost     string `json:\"host,omitempty\"`\n\tTTL      int    `json:\"ttl,omitempty\"`\n\tRData    string `json:\"rdata,omitempty\"`\n\tUpdated  *bool  `json:\"updated\"`\n\tLocked   *bool  `json:\"locked\"`\n\tIsDynDNS *bool  `json:\"isDynDns\"`\n\tProxy    string `json:\"proxy,omitempty\"`\n}\n\ntype URLForward struct {\n\tID          int    `json:\"id,omitempty\"`\n\tForwardType string `json:\"forwardType,omitempty\"`\n\tHost        string `json:\"host,omitempty\"`\n\tURL         string `json:\"url,omitempty\"`\n\tTitle       string `json:\"title,omitempty\"`\n\tKeywords    string `json:\"keywords,omitempty\"`\n\tDescription string `json:\"description,omitempty\"`\n\tUpdated     *bool  `json:\"updated,omitempty\"`\n}\n\ntype MailForward struct {\n\tID          int    `json:\"id,omitempty\"`\n\tSource      string `json:\"source,omitempty\"`\n\tDestination string `json:\"destination,omitempty\"`\n\tUpdated     *bool  `json:\"updated,omitempty\"`\n}\n\ntype Report struct {\n\tIsValid           bool               `json:\"isValid,omitempty\"`\n\tRecordErrors      []RecordError      `json:\"recordErrors,omitempty\"`\n\tURLForwardErrors  []URLForwardError  `json:\"urlForwardErrors,omitempty\"`\n\tMailForwardErrors []MailForwardError `json:\"mailForwardErrors,omitempty\"`\n\tZoneErrors        []ZoneError        `json:\"zoneErrors,omitempty\"`\n}\n\nfunc (r *Report) Error() string {\n\tvar msg []string\n\n\tfor _, e := range r.RecordErrors {\n\t\tmsg = append(msg, e.Error())\n\t}\n\n\tfor _, e := range r.URLForwardErrors {\n\t\tmsg = append(msg, e.Error())\n\t}\n\n\tfor _, e := range r.MailForwardErrors {\n\t\tmsg = append(msg, e.Error())\n\t}\n\n\tfor _, e := range r.ZoneErrors {\n\t\tmsg = append(msg, e.Error())\n\t}\n\n\treturn strings.Join(msg, \", \")\n}\n\ntype RecordError struct {\n\tMessages []string `json:\"messages,omitempty\"`\n\tRecord   Record   `json:\"record\"`\n\tSeverity string   `json:\"severity,omitempty\"`\n}\n\nfunc (e *RecordError) Error() string {\n\treturn fmt.Sprintf(\"record error (%s): %s\", e.Severity, strings.Join(e.Messages, \", \"))\n}\n\ntype URLForwardError struct {\n\tMessages   []string   `json:\"messages,omitempty\"`\n\tURLForward URLForward `json:\"urlForward\"`\n\tSeverity   string     `json:\"severity,omitempty\"`\n}\n\nfunc (e *URLForwardError) Error() string {\n\treturn fmt.Sprintf(\"URL forward error (%s): %s\", e.Severity, strings.Join(e.Messages, \", \"))\n}\n\ntype MailForwardError struct {\n\tMessages    []string    `json:\"messages,omitempty\"`\n\tMailForward MailForward `json:\"mailForward\"`\n\tSeverity    string      `json:\"severity,omitempty\"`\n}\n\nfunc (e *MailForwardError) Error() string {\n\treturn fmt.Sprintf(\"mail forward error (%s): %s\", e.Severity, strings.Join(e.Messages, \", \"))\n}\n\ntype ZoneError struct {\n\tMessage      string        `json:\"message,omitempty\"`\n\tRecords      []Record      `json:\"records,omitempty\"`\n\tURLForwards  []URLForward  `json:\"urlForwards,omitempty\"`\n\tMailForwards []MailForward `json:\"mailForwards,omitempty\"`\n\tSeverity     string        `json:\"severity,omitempty\"`\n}\n\nfunc (e *ZoneError) Error() string {\n\treturn fmt.Sprintf(\"zone error (%s): %s\", e.Severity, e.Message)\n}\n"
  },
  {
    "path": "providers/dns/excedo/excedo.go",
    "content": "// Package excedo implements a DNS provider for solving the DNS-01 challenge using Excedo.\npackage excedo\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/excedo/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"EXCEDO_\"\n\n\tEnvAPIURL = envNamespace + \"API_URL\"\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIURL string\n\tAPIKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 60),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordsMu sync.Mutex\n\trecords   map[string]int64\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Excedo.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIURL, EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"excedo: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIURL = values[EnvAPIURL]\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Excedo.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"excedo: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.APIURL, config.APIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"excedo: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:  config,\n\t\tclient:  client,\n\t\trecords: make(map[string]int64),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"excedo: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"excedo: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tDomainName: dns01.UnFqdn(authZone),\n\t\tName:       subDomain,\n\t\tType:       \"TXT\",\n\t\tContent:    info.Value,\n\t\tTTL:        strconv.Itoa(d.config.TTL),\n\t}\n\n\trecordID, err := d.client.AddRecord(ctx, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"excedo: add record: %w\", err)\n\t}\n\n\td.recordsMu.Lock()\n\td.records[token] = recordID\n\td.recordsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"excedo: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\td.recordsMu.Lock()\n\trecordID, ok := d.records[token]\n\td.recordsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"excedo: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\terr = d.client.DeleteRecord(ctx, dns01.UnFqdn(authZone), strconv.FormatInt(recordID, 10))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"excedo: delete record: %w\", err)\n\t}\n\n\td.recordsMu.Lock()\n\tdelete(d.records, token)\n\td.recordsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/excedo/excedo.toml",
    "content": "Name = \"Excedo\"\nDescription = ''''''\nURL = \"https://excedo.se/\"\nCode = \"excedo\"\nSince = \"v4.33.0\"\n\nExample = '''\nEXCEDO_API_KEY=your-api-key \\\nEXCEDO_API_URL=your-base-url \\\nlego --dns excedo -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    EXCEDO_API_KEY = \"API key\"\n    EXCEDO_API_URL = \"API base URL\"\n  [Configuration.Additional]\n    EXCEDO_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    EXCEDO_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 300)\"\n    EXCEDO_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    EXCEDO_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"none\"\n"
  },
  {
    "path": "providers/dns/excedo/excedo_test.go",
    "content": "package excedo\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIURL, EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIURL: \"https://example.com\",\n\t\t\t\tEnvAPIKey: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing the API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIURL: \"https://example.com\",\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"excedo: some credentials information are missing: EXCEDO_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing the API URL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIURL: \"\",\n\t\t\t\tEnvAPIKey: \"secret\",\n\t\t\t},\n\t\t\texpected: \"excedo: some credentials information are missing: EXCEDO_API_URL\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"excedo: some credentials information are missing: EXCEDO_API_URL,EXCEDO_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiURL   string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiURL: \"https://example.com\",\n\t\t\tapiKey: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing the API key\",\n\t\t\tapiURL:   \"https://example.com\",\n\t\t\texpected: \"excedo: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing the API URL\",\n\t\t\tapiKey:   \"secret\",\n\t\t\texpected: \"excedo: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"excedo: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIURL = test.apiURL\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIURL = server.URL\n\t\t\tconfig.APIKey = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn p, nil\n\t\t},\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /authenticate/login/\",\n\t\t\tservermock.ResponseFromInternal(\"login.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer secret\"),\n\t\t).\n\t\tRoute(\"POST /dns/addrecord/\",\n\t\t\tservermock.ResponseFromInternal(\"addrecord.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer session-token\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"content\", \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\").\n\t\t\t\tWith(\"domainName\", \"example.com\").\n\t\t\t\tWith(\"name\", \"_acme-challenge\").\n\t\t\t\tWith(\"ttl\", \"60\").\n\t\t\t\tWith(\"type\", \"TXT\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /authenticate/login/\",\n\t\t\tservermock.ResponseFromInternal(\"login.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer secret\"),\n\t\t).\n\t\tRoute(\"POST /dns/deleterecord/\",\n\t\t\tservermock.ResponseFromInternal(\"deleterecord.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer session-token\"),\n\t\t).\n\t\tBuild(t)\n\n\tprovider.records[\"abc\"] = 19695822\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/excedo/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\ntype responseChecker interface {\n\tCheck() error\n}\n\n// Client the Excedo API client.\ntype Client struct {\n\tapiKey string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n\n\ttoken   *ExpirableToken\n\tmuToken sync.Mutex\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiURL, apiKey string) (*Client, error) {\n\tif apiURL == \"\" || apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, err := url.Parse(apiURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) AddRecord(ctx context.Context, record Record) (int64, error) {\n\tpayload, err := querystring.Values(record)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tendpoint := c.baseURL.JoinPath(\"/dns/addrecord/\")\n\n\treq, err := newFormRequest(ctx, http.MethodPost, endpoint, payload)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tresult := new(AddRecordResponse)\n\n\terr = c.doAuthenticated(ctx, req, result)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn result.RecordID, nil\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error {\n\tendpoint := c.baseURL.JoinPath(\"/dns/deleterecord/\")\n\n\tdata := map[string]string{\n\t\t\"domainname\": dns01.UnFqdn(zone),\n\t\t\"recordid\":   recordID,\n\t}\n\n\treq, err := newMultipartRequest(ctx, http.MethodPost, endpoint, data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresult := new(BaseResponse)\n\n\terr = c.doAuthenticated(ctx, req, result)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) GetRecords(ctx context.Context, zone string) (map[string]Zone, error) {\n\tendpoint := c.baseURL.JoinPath(\"/dns/getrecords/\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"domainname\", zone)\n\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newFormRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := new(GetRecordsResponse)\n\n\terr = c.doAuthenticated(ctx, req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.DNS, nil\n}\n\nfunc (c *Client) do(req *http.Request, result responseChecker) error {\n\tuseragent.SetHeader(req.Header)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\traw, _ := io.ReadAll(resp.Body)\n\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn result.Check()\n}\n\nfunc newMultipartRequest(ctx context.Context, method string, endpoint *url.URL, data map[string]string) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\twriter := multipart.NewWriter(buf)\n\n\tfor k, v := range data {\n\t\terr := writer.WriteField(k, v)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\terr := writer.Close()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbody := bytes.NewReader(buf.Bytes())\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\n\treturn req, nil\n}\n\nfunc newFormRequest(ctx context.Context, method string, endpoint *url.URL, form url.Values) (*http.Request, error) {\n\tvar body io.Reader\n\n\tif len(form) > 0 {\n\t\tbody = bytes.NewReader([]byte(form.Encode()))\n\t} else {\n\t\tbody = http.NoBody\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tif method == http.MethodPost {\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/excedo/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(server.URL, \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/addrecord/\",\n\t\t\tservermock.ResponseFromFixture(\"addrecord.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer session-token\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"content\", \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\").\n\t\t\t\tWith(\"domainName\", \"example.com\").\n\t\t\t\tWith(\"name\", \"_acme-challenge\").\n\t\t\t\tWith(\"ttl\", \"60\").\n\t\t\t\tWith(\"type\", \"TXT\"),\n\t\t).\n\t\tBuild(t)\n\n\tclient.token = &ExpirableToken{\n\t\tToken:   \"session-token\",\n\t\tExpires: time.Now().Add(6 * time.Hour),\n\t}\n\n\trecord := Record{\n\t\tDomainName: \"example.com\",\n\t\tName:       \"_acme-challenge\",\n\t\tType:       \"TXT\",\n\t\tContent:    \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:        \"60\",\n\t}\n\n\trecordID, err := client.AddRecord(t.Context(), record)\n\trequire.NoError(t, err)\n\n\tassert.EqualValues(t, 19695822, recordID)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/addrecord/\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\"),\n\t\t).\n\t\tBuild(t)\n\n\tclient.token = &ExpirableToken{\n\t\tToken:   \"session-token\",\n\t\tExpires: time.Now().Add(6 * time.Hour),\n\t}\n\n\trecord := Record{\n\t\tDomainName: \"example.com\",\n\t\tName:       \"_acme-challenge\",\n\t\tType:       \"TXT\",\n\t\tContent:    \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:        \"60\",\n\t}\n\n\t_, err := client.AddRecord(t.Context(), record)\n\trequire.EqualError(t, err, \"2003: Required parameter missing\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/deleterecord/\",\n\t\t\tservermock.ResponseFromFixture(\"deleterecord.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer session-token\"),\n\t\t).\n\t\tBuild(t)\n\n\tclient.token = &ExpirableToken{\n\t\tToken:   \"session-token\",\n\t\tExpires: time.Now().Add(6 * time.Hour),\n\t}\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", \"19695822\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/getrecords/\",\n\t\t\tservermock.ResponseFromFixture(\"getrecords.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer session-token\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"domainname\", \"example.com\"),\n\t\t).\n\t\tBuild(t)\n\n\tclient.token = &ExpirableToken{\n\t\tToken:   \"session-token\",\n\t\tExpires: time.Now().Add(6 * time.Hour),\n\t}\n\n\tzones, err := client.GetRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := map[string]Zone{\n\t\t\"example.com\": {\n\t\t\tDNSType: \"type\",\n\t\t\tRecords: []Record{{\n\t\t\t\tRecordID: \"1234\",\n\t\t\t\tName:     \"_acme-challenge.example.com\",\n\t\t\t\tType:     \"TXT\",\n\t\t\t\tContent:  \"txt-value\",\n\t\t\t\tTTL:      \"60\",\n\t\t\t}},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zones)\n}\n"
  },
  {
    "path": "providers/dns/excedo/internal/fixtures/addrecord.json",
    "content": "{\n  \"code\": 1000,\n  \"desc\": \"Command completed successfully\",\n  \"recordid\": 19695822,\n  \"session\": {\n    \"accID\": \"1234\",\n    \"usrID\": \"1234\",\n    \"status\": \"active\",\n    \"expire\": {\n      \"date\": \"2026-03-10 19:03:18\",\n      \"seconds\": 5678\n    }\n  },\n  \"runtime\": 0.2852\n}\n"
  },
  {
    "path": "providers/dns/excedo/internal/fixtures/deleterecord.json",
    "content": "{\n  \"code\": 1000,\n  \"desc\": \"Command completed successfully\",\n  \"session\": {\n    \"accID\": \"1234\",\n    \"usrID\": \"1234\",\n    \"status\": \"active\",\n    \"expire\": {\n      \"date\": \"2026-03-10 19:03:18\",\n      \"seconds\": 5678\n    }\n  },\n  \"runtime\": 0.2852\n}\n"
  },
  {
    "path": "providers/dns/excedo/internal/fixtures/error.json",
    "content": "{\n  \"code\": 2003,\n  \"desc\": \"Required parameter missing\",\n  \"missing\": [\n    \"domainname\",\n    \"recordid\"\n  ],\n  \"session\": {\n    \"accID\": \"1234\",\n    \"usrID\": \"1234\",\n    \"status\": \"active\",\n    \"expire\": {\n      \"date\": \"2026-03-10 19:03:18\",\n      \"seconds\": 5485\n    }\n  },\n  \"runtime\": 0.0534\n}\n"
  },
  {
    "path": "providers/dns/excedo/internal/fixtures/getrecords.json",
    "content": "{\n  \"code\": 1000,\n  \"desc\": \"Command completed successfully\",\n  \"dns\": {\n    \"example.com\": {\n      \"dnstype\": \"type\",\n      \"recordusage\": {\n        \"used\": 74\n      },\n      \"records\": [\n        {\n          \"recordid\": \"1234\",\n          \"name\": \"_acme-challenge.example.com\",\n          \"type\": \"TXT\",\n          \"content\": \"txt-value\",\n          \"ttl\": \"60\",\n          \"prio\": null,\n          \"change_date\": null\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/excedo/internal/fixtures/login.json",
    "content": "{\n  \"code\": 1000,\n  \"desc\": \"Command completed successfully\",\n  \"parameters\": {\n    \"token\": \"session-token\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/excedo/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n)\n\ntype ExpirableToken struct {\n\tToken   string\n\tExpires time.Time\n}\n\nfunc (t *ExpirableToken) IsExpired() bool {\n\treturn time.Now().After(t.Expires)\n}\n\nfunc (c *Client) Login(ctx context.Context) (string, error) {\n\tendpoint := c.baseURL.JoinPath(\"/authenticate/login/\")\n\n\treq, err := newFormRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n\n\tresult := new(LoginResponse)\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif result.Code != 1000 && result.Code != 1300 {\n\t\treturn \"\", fmt.Errorf(\"%d: %s\", result.Code, result.Description)\n\t}\n\n\treturn result.Parameters.Token, nil\n}\n\nfunc (c *Client) authenticate(ctx context.Context) (string, error) {\n\tc.muToken.Lock()\n\tdefer c.muToken.Unlock()\n\n\tif c.token == nil || c.token.IsExpired() {\n\t\ttoken, err := c.Login(ctx)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tc.token = &ExpirableToken{\n\t\t\tToken:   token,\n\t\t\tExpires: time.Now().Add(2*time.Hour - time.Minute),\n\t\t}\n\n\t\treturn token, nil\n\t}\n\n\treturn c.token.Token, nil\n}\n\nfunc (c *Client) doAuthenticated(ctx context.Context, req *http.Request, result responseChecker) error {\n\ttoken, err := c.authenticate(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif token != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t}\n\n\treturn c.do(req, result)\n}\n"
  },
  {
    "path": "providers/dns/excedo/internal/identity_test.go",
    "content": "package internal\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestClient_Login(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /authenticate/login/\",\n\t\t\tservermock.ResponseFromFixture(\"login.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer secret\"),\n\t\t).\n\t\tBuild(t)\n\n\ttoken, err := client.Login(t.Context())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"session-token\", token)\n}\n\nfunc TestClient_Login_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /authenticate/login/\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\"),\n\t\t).\n\t\tBuild(t)\n\n\t_, err := client.Login(t.Context())\n\trequire.EqualError(t, err, \"2003: Required parameter missing\")\n}\n"
  },
  {
    "path": "providers/dns/excedo/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype BaseResponse struct {\n\tCode        int    `json:\"code\"`\n\tDescription string `json:\"desc\"`\n}\n\nfunc (r BaseResponse) Check() error {\n\t// Response codes:\n\t// - 1000: Command completed successfully\n\t// - 1300: Command completed successfully; no messages\n\t// - 2001: Command syntax error\n\t// - 2002: Command use error\n\t// - 2003: Required parameter missing\n\t// - 2004: Parameter value range error\n\t// - 2104: Billing failure\n\t// - 2200: Authentication error\n\t// - 2201: Authorization error\n\t// - 2303: Object does not exist\n\t// - 2304: Object status prohibits operation\n\t// - 2309: Object duplicate found\n\t// - 2400: Command failed\n\t// - 2500: Command failed; server closing connection\n\tif r.Code != 1000 && r.Code != 1300 {\n\t\treturn fmt.Errorf(\"%d: %s\", r.Code, r.Description)\n\t}\n\n\treturn nil\n}\n\ntype GetRecordsResponse struct {\n\tBaseResponse\n\n\tDNS map[string]Zone `json:\"dns\"`\n}\n\ntype Zone struct {\n\tDNSType string   `json:\"dnstype\"`\n\tRecords []Record `json:\"records\"`\n}\n\ntype Record struct {\n\tDomainName string `json:\"domainName,omitempty\" url:\"domainName,omitempty\"`\n\tRecordID   string `json:\"recordid,omitempty\" url:\"recordid,omitempty\"`\n\tName       string `json:\"name,omitempty\" url:\"name,omitempty\"`\n\tType       string `json:\"type,omitempty\" url:\"type,omitempty\"`\n\tContent    string `json:\"content,omitempty\" url:\"content,omitempty\"`\n\tTTL        string `json:\"ttl,omitempty\" url:\"ttl,omitempty\"`\n}\n\ntype AddRecordResponse struct {\n\tBaseResponse\n\n\tRecordID int64 `json:\"recordid\"`\n}\n\ntype LoginResponse struct {\n\tBaseResponse\n\n\tParameters struct {\n\t\tToken string `json:\"token\"`\n\t} `json:\"parameters\"`\n}\n"
  },
  {
    "path": "providers/dns/exec/exec.go",
    "content": "// Package exec implements a DNS provider which runs a program for adding/removing the DNS record.\npackage exec\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"EXEC_\"\n\n\tEnvPath = envNamespace + \"PATH\"\n\tEnvMode = envNamespace + \"MODE\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config Provider configuration.\ntype Config struct {\n\tProgram            string\n\tMode               string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n}\n\n// NewDNSProvider returns a new DNS provider which runs the program in the\n// environment variable EXEC_PATH for adding and removing the DNS record.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"exec: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Program = values[EnvPath]\n\tconfig.Mode = os.Getenv(EnvMode)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig returns a new DNS provider which runs the given configuration\n// for adding and removing the DNS record.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"exec: the configuration is nil\")\n\t}\n\n\treturn &DNSProvider{config: config}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.run(context.Background(), \"present\", domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"exec: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.run(context.Background(), \"cleanup\", domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"exec: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n\nfunc (d *DNSProvider) run(ctx context.Context, command, domain, token, keyAuth string) error {\n\tvar args []string\n\tif d.config.Mode == \"RAW\" {\n\t\targs = []string{command, \"--\", domain, token, keyAuth}\n\t} else {\n\t\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\t\targs = []string{command, info.EffectiveFQDN, info.Value}\n\t}\n\n\tcmd := exec.CommandContext(ctx, d.config.Program, args...)\n\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create pipe: %w\", err)\n\t}\n\n\tcmd.Stderr = cmd.Stdout\n\n\terr = cmd.Start()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"start command: %w\", err)\n\t}\n\n\tscanner := bufio.NewScanner(stdout)\n\tfor scanner.Scan() {\n\t\tlog.Println(scanner.Text())\n\t}\n\n\terr = cmd.Wait()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wait command: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/exec/exec.toml",
    "content": "Name = \"External program\"\nDescription = \"Solving the DNS-01 challenge using an external program.\"\nURL = \"/dns/exec\"\nCode = \"exec\"\nSince = \"v0.5.0\"\n\nExample = '''\nEXEC_PATH=/the/path/to/myscript.sh \\\nlego --dns exec -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n\n## Base Configuration\n\n| Environment Variable Name | Description                           |\n|---------------------------|---------------------------------------|\n| `EXEC_MODE`               | `RAW`, none                           |\n| `EXEC_PATH`               | The path of the the external program. |\n\n\n## Additional Configuration\n\n| Environment Variable Name  | Description                                                        |\n|----------------------------|--------------------------------------------------------------------|\n| `EXEC_POLLING_INTERVAL`    | Time between DNS propagation check in seconds (Default: 3).        |\n| `EXEC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60). |\n| `EXEC_SEQUENCE_INTERVAL`   | Time between sequential requests in seconds (Default: 60).         |\n\n\n## Description\n\nThe file name of the external program is specified in the environment variable `EXEC_PATH`.\n\nWhen it is run by lego, three command-line parameters are passed to it:\nThe action (\"present\" or \"cleanup\"), the fully-qualified domain name and the value for the record.\n\nFor example, requesting a certificate for the domain 'my.example.org' can be achieved by calling lego as follows:\n\n```bash\nEXEC_PATH=./update-dns.sh \\\nlego --dns exec --d my.example.org run\n```\n\nIt will then call the program './update-dns.sh' with like this:\n\n```bash\n./update-dns.sh \"present\" \"_acme-challenge.my.example.org.\" \"MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI\"\n```\n\nThe program then needs to make sure the record is inserted.\nWhen it returns an error via a non-zero exit code, lego aborts.\n\nWhen the record is to be removed again,\nthe program is called with the first command-line parameter set to `cleanup` instead of `present`.\n\nIf you want to use the raw domain, token, and keyAuth values with your program, you can set `EXEC_MODE=RAW`:\n\n```bash\nEXEC_MODE=RAW \\\nEXEC_PATH=./update-dns.sh \\\nlego --dns exec -d my.example.org run\n```\n\nIt will then call the program `./update-dns.sh` like this:\n\n```bash\n./update-dns.sh \"present\" \"--\" \"my.example.org.\" \"some-token\" \"KxAy-J3NwUmg9ZQuM-gP_Mq1nStaYSaP9tYQs5_-YsE.ksT-qywTd8058G-SHHWA3RAN72Pr0yWtPYmmY5UBpQ8\"\n```\n\n## Commands\n\n{{% notice note %}}\nThe `--` is because the token MAY start with a `-`, and the called program may try and interpret a `-` as indicating a flag.\nIn the case of urfave, which is commonly used,\nyou can use the `--` delimiter to specify the start of positional arguments, and handle such a string safely.\n{{% /notice %}}\n\n### Present\n\n| Mode    | Command                                            |\n|---------|----------------------------------------------------|\n| default | `myprogram present <FQDN> <record>`                |\n| `RAW`   | `myprogram present -- <domain> <token> <key_auth>` |\n\n### Cleanup\n\n| Mode    | Command                                            |\n|---------|----------------------------------------------------|\n| default | `myprogram cleanup <FQDN> <record>`                |\n| `RAW`   | `myprogram cleanup -- <domain> <token> <key_auth>` |\n\n'''\n"
  },
  {
    "path": "providers/dns/exec/exec_test.go",
    "content": "package exec\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tbackupLogger := log.Logger\n\n\tdefer func() {\n\t\tlog.Logger = backupLogger\n\t}()\n\n\tlogRecorder := &LogRecorder{}\n\tlog.Logger = logRecorder\n\n\ttype expected struct {\n\t\targs  string\n\t\terror bool\n\t}\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tconfig   *Config\n\t\texpected expected\n\t}{\n\t\t{\n\t\t\tdesc: \"Standard mode\",\n\t\t\tconfig: &Config{\n\t\t\t\tProgram: \"echo\",\n\t\t\t\tMode:    \"\",\n\t\t\t},\n\t\t\texpected: expected{\n\t\t\t\targs: \"present _acme-challenge.domain. pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"program error\",\n\t\t\tconfig: &Config{\n\t\t\t\tProgram: \"ogellego\",\n\t\t\t\tMode:    \"\",\n\t\t\t},\n\t\t\texpected: expected{error: true},\n\t\t},\n\t\t{\n\t\t\tdesc: \"Raw mode\",\n\t\t\tconfig: &Config{\n\t\t\t\tProgram: \"echo\",\n\t\t\t\tMode:    \"RAW\",\n\t\t\t},\n\t\t\texpected: expected{\n\t\t\t\targs: \"present -- domain token keyAuth\",\n\t\t\t},\n\t\t},\n\t}\n\n\tvar message string\n\n\tlogRecorder.On(\"Println\", mock.Anything).Run(func(args mock.Arguments) {\n\t\tmessage = args.String(0)\n\t\tfmt.Fprintln(os.Stdout, \"XXX\", message)\n\t})\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tmessage = \"\"\n\n\t\t\tprovider, err := NewDNSProviderConfig(test.config)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = provider.Present(\"domain\", \"token\", \"keyAuth\")\n\t\t\tif test.expected.error {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.expected.args, strings.TrimSpace(message))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tbackupLogger := log.Logger\n\n\tdefer func() {\n\t\tlog.Logger = backupLogger\n\t}()\n\n\tlogRecorder := &LogRecorder{}\n\tlog.Logger = logRecorder\n\n\ttype expected struct {\n\t\targs  string\n\t\terror bool\n\t}\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tconfig   *Config\n\t\texpected expected\n\t}{\n\t\t{\n\t\t\tdesc: \"Standard mode\",\n\t\t\tconfig: &Config{\n\t\t\t\tProgram: \"echo\",\n\t\t\t\tMode:    \"\",\n\t\t\t},\n\t\t\texpected: expected{\n\t\t\t\targs: \"cleanup _acme-challenge.domain. pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"program error\",\n\t\t\tconfig: &Config{\n\t\t\t\tProgram: \"ogellego\",\n\t\t\t\tMode:    \"\",\n\t\t\t},\n\t\t\texpected: expected{error: true},\n\t\t},\n\t\t{\n\t\t\tdesc: \"Raw mode\",\n\t\t\tconfig: &Config{\n\t\t\t\tProgram: \"echo\",\n\t\t\t\tMode:    \"RAW\",\n\t\t\t},\n\t\t\texpected: expected{\n\t\t\t\targs: \"cleanup -- domain token keyAuth\",\n\t\t\t},\n\t\t},\n\t}\n\n\tvar message string\n\n\tlogRecorder.On(\"Println\", mock.Anything).Run(func(args mock.Arguments) {\n\t\tmessage = args.String(0)\n\t\tfmt.Fprintln(os.Stdout, \"XXX\", message)\n\t})\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tmessage = \"\"\n\n\t\t\tprovider, err := NewDNSProviderConfig(test.config)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = provider.CleanUp(\"domain\", \"token\", \"keyAuth\")\n\t\t\tif test.expected.error {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.expected.args, strings.TrimSpace(message))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/exec/log_mock_test.go",
    "content": "package exec\n\nimport \"github.com/stretchr/testify/mock\"\n\ntype LogRecorder struct {\n\tmock.Mock\n}\n\nfunc (*LogRecorder) Fatal(args ...any) {\n\tpanic(\"implement me\")\n}\n\nfunc (*LogRecorder) Fatalln(args ...any) {\n\tpanic(\"implement me\")\n}\n\nfunc (*LogRecorder) Fatalf(format string, args ...any) {\n\tpanic(\"implement me\")\n}\n\nfunc (*LogRecorder) Print(args ...any) {\n\tpanic(\"implement me\")\n}\n\nfunc (l *LogRecorder) Println(args ...any) {\n\tl.Called(args...)\n}\n\nfunc (*LogRecorder) Printf(format string, args ...any) {\n\tpanic(\"implement me\")\n}\n"
  },
  {
    "path": "providers/dns/exoscale/exoscale.go",
    "content": "// Package exoscale implements a DNS provider for solving the DNS-01 challenge using Exoscale DNS.\npackage exoscale\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\tegoscale \"github.com/exoscale/egoscale/v3\"\n\t\"github.com/exoscale/egoscale/v3/credentials\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"EXOSCALE_\"\n\n\tEnvAPISecret = envNamespace + \"API_SECRET\"\n\tEnvAPIKey    = envNamespace + \"API_KEY\"\n\tEnvEndpoint  = envNamespace + \"ENDPOINT\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tAPISecret          string\n\tEndpoint           string\n\tHTTPTimeout        time.Duration\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int64\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                int64(env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL)),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *egoscale.Client\n}\n\n// NewDNSProvider Credentials must be passed in the environment variables:\n// EXOSCALE_API_KEY, EXOSCALE_API_SECRET, EXOSCALE_ENDPOINT.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey, EnvAPISecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"exoscale: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.APISecret = values[EnvAPISecret]\n\tconfig.Endpoint = env.GetOrDefaultString(EnvEndpoint, string(egoscale.CHGva2))\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Exoscale.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"exoscale: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" || config.APISecret == \"\" {\n\t\treturn nil, errors.New(\"exoscale: credentials missing\")\n\t}\n\n\tclient, err := egoscale.NewClient(\n\t\tcredentials.NewStaticCredentials(config.APIKey, config.APISecret),\n\t\tegoscale.ClientOptWithEndpoint(egoscale.Endpoint(config.Endpoint)),\n\t\tegoscale.ClientOptWithHTTPClient(clientdebug.Wrap(&http.Client{Timeout: config.HTTPTimeout})),\n\t\tegoscale.ClientOptWithUserAgent(useragent.Get()),\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"exoscale: initializing client: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tclient: client,\n\t\tconfig: config,\n\t}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneName, recordName, err := d.findZoneAndRecordName(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"exoscale: %w\", err)\n\t}\n\n\tzone, err := d.findExistingZone(ctx, zoneName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"exoscale: %w\", err)\n\t}\n\n\tif zone == nil {\n\t\treturn fmt.Errorf(\"exoscale: zone %q not found\", zoneName)\n\t}\n\n\trecordRequest := egoscale.CreateDNSDomainRecordRequest{\n\t\tName:    recordName,\n\t\tTtl:     d.config.TTL,\n\t\tContent: info.Value,\n\t\tType:    egoscale.CreateDNSDomainRecordRequestTypeTXT,\n\t}\n\n\top, err := d.client.CreateDNSDomainRecord(ctx, zone.ID, recordRequest)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"exoscale: error while creating DNS record: %w\", err)\n\t}\n\n\t_, err = d.client.Wait(ctx, op, egoscale.OperationStateSuccess)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"exoscale: error while creating DNS record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneName, recordName, err := d.findZoneAndRecordName(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"exoscale: %w\", err)\n\t}\n\n\tzone, err := d.findExistingZone(ctx, zoneName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"exoscale: %w\", err)\n\t}\n\n\tif zone == nil {\n\t\treturn fmt.Errorf(\"exoscale: zone %q not found\", zoneName)\n\t}\n\n\trecordID, err := d.findExistingRecordID(ctx, zone.ID, recordName, info.Value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif recordID == \"\" {\n\t\treturn nil\n\t}\n\n\top, err := d.client.DeleteDNSDomainRecord(ctx, zone.ID, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"exoscale: error while deleting DNS record: %w\", err)\n\t}\n\n\t_, err = d.client.Wait(ctx, op, egoscale.OperationStateSuccess)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"exoscale: error while creating DNS record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// findExistingZone Query Exoscale to find an existing zone for this name.\n// Returns nil result if no zone could be found.\nfunc (d *DNSProvider) findExistingZone(ctx context.Context, zoneName string) (*egoscale.DNSDomain, error) {\n\tzones, err := d.client.ListDNSDomains(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error while retrieving DNS zones: %w\", err)\n\t}\n\n\tfor _, zone := range zones.DNSDomains {\n\t\tif zone.UnicodeName == zoneName {\n\t\t\treturn &zone, nil\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n\n// findExistingRecordID Query Exoscale to find an existing record for this name.\n// Returns empty result if no record could be found.\nfunc (d *DNSProvider) findExistingRecordID(ctx context.Context, zoneID egoscale.UUID, recordName, value string) (egoscale.UUID, error) {\n\trecords, err := d.client.ListDNSDomainRecords(ctx, zoneID)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error while retrieving DNS records: %w\", err)\n\t}\n\n\tfor _, record := range records.DNSDomainRecords {\n\t\tif record.Name == recordName && record.Type == egoscale.DNSDomainRecordTypeTXT &&\n\t\t\t(record.Content == value || record.Content == strconv.Quote(value)) {\n\t\t\treturn record.ID, nil\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\n// findZoneAndRecordName Extract DNS zone and DNS entry name.\nfunc (d *DNSProvider) findZoneAndRecordName(fqdn string) (string, string, error) {\n\tzone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tzone = dns01.UnFqdn(zone)\n\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, zone)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\treturn zone, subDomain, nil\n}\n"
  },
  {
    "path": "providers/dns/exoscale/exoscale.toml",
    "content": "Name = \"Exoscale\"\nDescription = ''''''\nURL = \"https://www.exoscale.com/\"\nCode = \"exoscale\"\nSince = \"v0.4.0\"\n\nExample = '''\nEXOSCALE_API_KEY=abcdefghijklmnopqrstuvwx \\\nEXOSCALE_API_SECRET=xxxxxxx \\\nlego --dns exoscale -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    EXOSCALE_API_KEY = \"API key\"\n    EXOSCALE_API_SECRET = \"API secret\"\n  [Configuration.Additional]\n    EXOSCALE_ENDPOINT = \"API endpoint URL\"\n    EXOSCALE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    EXOSCALE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    EXOSCALE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    EXOSCALE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 60)\"\n\n[Links]\n  API = \"https://openapi-v2.exoscale.com/#endpoint-dns\"\n  GoClient = \"https://github.com/exoscale/egoscale\"\n"
  },
  {
    "path": "providers/dns/exoscale/exoscale_test.go",
    "content": "package exoscale\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPISecret,\n\tEnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"123\",\n\t\t\t\tEnvAPISecret: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"\",\n\t\t\t\tEnvAPISecret: \"\",\n\t\t\t},\n\t\t\texpected: \"exoscale: some credentials information are missing: EXOSCALE_API_KEY,EXOSCALE_API_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"\",\n\t\t\t\tEnvAPISecret: \"456\",\n\t\t\t},\n\t\t\texpected: \"exoscale: some credentials information are missing: EXOSCALE_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"123\",\n\t\t\t\tEnvAPISecret: \"\",\n\t\t\t},\n\t\t\texpected: \"exoscale: some credentials information are missing: EXOSCALE_API_SECRET\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tapiKey    string\n\t\tapiSecret string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tapiKey:    \"123\",\n\t\t\tapiSecret: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"exoscale: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing api key\",\n\t\t\tapiSecret: \"456\",\n\t\t\texpected:  \"exoscale: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret key\",\n\t\t\tapiKey:   \"123\",\n\t\t\texpected: \"exoscale: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.APISecret = test.apiSecret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_FindZoneAndRecordName(t *testing.T) {\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = \"example@example.com\"\n\tconfig.APISecret = \"123\"\n\n\tprovider, err := NewDNSProviderConfig(config)\n\trequire.NoError(t, err)\n\n\ttype expected struct {\n\t\tzone       string\n\t\trecordName string\n\t}\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tfqdn     string\n\t\texpected expected\n\t}{\n\t\t{\n\t\t\tdesc: \"Extract root record name\",\n\t\t\tfqdn: \"_acme-challenge.example.com.\",\n\t\t\texpected: expected{\n\t\t\t\tzone:       \"example.com\",\n\t\t\t\trecordName: \"_acme-challenge\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"Extract sub record name\",\n\t\t\tfqdn: \"_acme-challenge.foo.example.com.\",\n\t\t\texpected: expected{\n\t\t\t\tzone:       \"example.com\",\n\t\t\t\trecordName: \"_acme-challenge.foo\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tzone, recordName, err := provider.findZoneAndRecordName(test.fqdn)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, test.expected.zone, zone)\n\t\t\tassert.Equal(t, test.expected.recordName, recordName)\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n\n\t// Present Twice to handle create / update\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/f5xc/f5xc.go",
    "content": "// Package f5xc implements a DNS provider for solving the DNS-01 challenge using F5 XC.\npackage f5xc\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n\t\"github.com/go-acme/lego/v4/providers/dns/f5xc/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"F5XC_\"\n\n\tEnvToken      = envNamespace + \"API_TOKEN\"\n\tEnvTenantName = envNamespace + \"TENANT_NAME\"\n\tEnvServer     = envNamespace + \"SERVER\"\n\tEnvGroupName  = envNamespace + \"GROUP_NAME\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIToken   string\n\tTenantName string\n\tServer     string\n\tGroupName  string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for F5 XC.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken, EnvTenantName, EnvGroupName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"f5xc: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIToken = values[EnvToken]\n\tconfig.TenantName = values[EnvTenantName]\n\tconfig.GroupName = values[EnvGroupName]\n\tconfig.Server = env.GetOrFile(EnvServer)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for F5 XC.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"f5xc: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.GroupName == \"\" {\n\t\treturn nil, errors.New(\"f5xc: missing group name\")\n\t}\n\n\tclient, err := internal.NewClient(config.APIToken, config.TenantName, config.Server)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"f5xc: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"f5xc: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"f5xc: %w\", err)\n\t}\n\n\texistingRRSet, err := d.client.GetRRSet(ctx, dns01.UnFqdn(authZone), d.config.GroupName, subDomain, \"TXT\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"f5xc: get RR Set: %w\", err)\n\t}\n\n\t// New RRSet.\n\tif existingRRSet == nil || existingRRSet.RRSet.TXTRecord == nil {\n\t\trrSet := internal.RRSet{\n\t\t\tDescription: \"lego\",\n\t\t\tTTL:         d.config.TTL,\n\t\t\tTXTRecord: &internal.TXTRecord{\n\t\t\t\tName:   subDomain,\n\t\t\t\tValues: []string{info.Value},\n\t\t\t},\n\t\t}\n\n\t\treturn d.waitFor(ctx, func() error {\n\t\t\t_, err = d.client.CreateRRSet(ctx, dns01.UnFqdn(authZone), d.config.GroupName, rrSet)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"create RR set: %w\", err)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// Update RRSet.\n\texistingRRSet.RRSet.TXTRecord.Values = append(existingRRSet.RRSet.TXTRecord.Values, info.Value)\n\n\treturn d.waitFor(ctx, func() error {\n\t\t_, err = d.client.ReplaceRRSet(ctx, dns01.UnFqdn(authZone), d.config.GroupName, subDomain, \"TXT\", existingRRSet.RRSet)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"replace RR set: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\nfunc (d *DNSProvider) waitFor(ctx context.Context, operation func() error) error {\n\terr := wait.Retry(ctx, operation,\n\t\tbackoff.WithBackOff(backoff.NewConstantBackOff(2*time.Second)),\n\t\tbackoff.WithMaxElapsedTime(60*time.Second),\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"f5xc: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"f5xc: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"f5xc: %w\", err)\n\t}\n\n\t_, err = d.client.DeleteRRSet(context.Background(), dns01.UnFqdn(authZone), d.config.GroupName, subDomain, \"TXT\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"f5xc: delete RR set: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/f5xc/f5xc.toml",
    "content": "Name = \"F5 XC\"\nDescription = ''''''\nURL = \"https://www.f5.com/products/distributed-cloud-services\"\nCode = \"f5xc\"\nSince = \"v4.23.0\"\n\nExample = '''\nF5XC_API_TOKEN=\"xxx\" \\\nF5XC_TENANT_NAME=\"yyy\" \\\nF5XC_GROUP_NAME=\"zzz\" \\\nlego --dns f5xc -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    F5XC_API_TOKEN = \"API token\"\n    F5XC_TENANT_NAME = \"XC Tenant shortname\"\n    F5XC_GROUP_NAME = \"Group name\"\n  [Configuration.Additional]\n    F5XC_SERVER = \"Server domain (Default: console.ves.volterra.io)\"\n    F5XC_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    F5XC_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    F5XC_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    F5XC_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset\"\n  Documentation = \"https://my.f5.com/manage/s/article/K000147937\"\n"
  },
  {
    "path": "providers/dns/f5xc/f5xc_test.go",
    "content": "package f5xc\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvToken,\n\tEnvTenantName,\n\tEnvServer,\n\tEnvGroupName,\n).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken:      \"secret\",\n\t\t\t\tEnvTenantName: \"shortname\",\n\t\t\t\tEnvGroupName:  \"group\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken:      \"\",\n\t\t\t\tEnvTenantName: \"shortname\",\n\t\t\t\tEnvGroupName:  \"group\",\n\t\t\t},\n\t\t\texpected: \"f5xc: some credentials information are missing: F5XC_API_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing tenant name\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken:      \"secret\",\n\t\t\t\tEnvTenantName: \"\",\n\t\t\t\tEnvGroupName:  \"group\",\n\t\t\t},\n\t\t\texpected: \"f5xc: some credentials information are missing: F5XC_TENANT_NAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing group name\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken:      \"secret\",\n\t\t\t\tEnvTenantName: \"shortname\",\n\t\t\t\tEnvGroupName:  \"\",\n\t\t\t},\n\t\t\texpected: \"f5xc: some credentials information are missing: F5XC_GROUP_NAME\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"f5xc: some credentials information are missing: F5XC_API_TOKEN,F5XC_TENANT_NAME,F5XC_GROUP_NAME\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc       string\n\t\tapiToken   string\n\t\ttenantName string\n\t\tgroupName  string\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tdesc:       \"success\",\n\t\t\tapiToken:   \"secret\",\n\t\t\ttenantName: \"shortname\",\n\t\t\tgroupName:  \"group\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"missing API token\",\n\t\t\ttenantName: \"shortname\",\n\t\t\tgroupName:  \"group\",\n\t\t\texpected:   \"f5xc: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing tenant name\",\n\t\t\tapiToken:  \"secret\",\n\t\t\tgroupName: \"group\",\n\t\t\texpected:  \"f5xc: missing tenant name\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"missing group name\",\n\t\t\tapiToken:   \"secret\",\n\t\t\ttenantName: \"shortname\",\n\t\t\texpected:   \"f5xc: missing group name\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"f5xc: missing group name\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = test.apiToken\n\t\t\tconfig.TenantName = test.tenantName\n\t\t\tconfig.GroupName = test.groupName\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/f5xc/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultServer = \"console.ves.volterra.io\"\n\nconst authorizationHeader = \"Authorization\"\n\n// Client the F5 XC API client.\ntype Client struct {\n\tapiToken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiToken, tenantName, server string) (*Client, error) {\n\tif apiToken == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, err := createBaseURL(tenantName, server)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\tapiToken:   apiToken,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// CreateRRSet creates RRSet.\n// https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Create\nfunc (c *Client) CreateRRSet(ctx context.Context, dnsZoneName, groupName string, rrSet RRSet) (*APIRRSet, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"config\", \"dns\", \"namespaces\", \"system\", \"dns_zones\", dnsZoneName, \"rrsets\", groupName)\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, APIRRSet{\n\t\tDNSZoneName: dnsZoneName,\n\t\tGroupName:   groupName,\n\t\tRRSet:       rrSet,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &APIRRSet{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// GetRRSet gets RRSets.\n// https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Get\nfunc (c *Client) GetRRSet(ctx context.Context, dnsZoneName, groupName, recordName, recordType string) (*APIRRSet, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"config\", \"dns\", \"namespaces\", \"system\", \"dns_zones\", dnsZoneName, \"rrsets\", groupName, recordName, recordType)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &APIRRSet{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\tusce := &APIError{}\n\t\tif errors.As(err, &usce) && usce.StatusCode == http.StatusNotFound {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// DeleteRRSet deletes RRSet.\n// https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Delete\nfunc (c *Client) DeleteRRSet(ctx context.Context, dnsZoneName, groupName, recordName, recordType string) (*APIRRSet, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"config\", \"dns\", \"namespaces\", \"system\", \"dns_zones\", dnsZoneName, \"rrsets\", groupName, recordName, recordType)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &APIRRSet{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// ReplaceRRSet replaces RRSet.\n// https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Replace\nfunc (c *Client) ReplaceRRSet(ctx context.Context, dnsZoneName, groupName, recordName, recordType string, rrSet RRSet) (*APIRRSet, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"config\", \"dns\", \"namespaces\", \"system\", \"dns_zones\", dnsZoneName, \"rrsets\", groupName, recordName, recordType)\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, APIRRSet{\n\t\tDNSZoneName: dnsZoneName,\n\t\tGroupName:   groupName,\n\t\tRRSet:       rrSet,\n\t\tType:        recordType,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &APIRRSet{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(authorizationHeader, \"APIToken \"+c.apiToken)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tapiErr := APIError{StatusCode: resp.StatusCode}\n\n\terr := json.Unmarshal(raw, &apiErr)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &apiErr\n}\n\nfunc createBaseURL(tenant, server string) (*url.URL, error) {\n\tif tenant == \"\" {\n\t\treturn nil, errors.New(\"missing tenant name\")\n\t}\n\n\tif server == \"\" {\n\t\tserver = defaultServer\n\t}\n\n\tbaseURL, err := url.Parse(fmt.Sprintf(\"https://%s.%s\", tenant, server))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parse base URL: %w\", err)\n\t}\n\n\treturn baseURL, nil\n}\n"
  },
  {
    "path": "providers/dns/f5xc/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\", \"shortname\", \"\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"APIToken secret\"))\n}\n\nfunc TestClient_CreateRRSet(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA\",\n\t\t\tservermock.ResponseFromFixture(\"create.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"dns_zone_name\":\"example.com\",\"group_name\":\"groupA\",\"rrset\":{\"description\":\"lego\",\"ttl\":60,\"txt_record\":{\"name\":\"wwww\",\"values\":[\"txt\"]}}}`)).\n\t\tBuild(t)\n\n\trrSet := RRSet{\n\t\tDescription: \"lego\",\n\t\tTTL:         60,\n\t\tTXTRecord: &TXTRecord{\n\t\t\tName:   \"wwww\",\n\t\t\tValues: []string{\"txt\"},\n\t\t},\n\t}\n\n\tresult, err := client.CreateRRSet(t.Context(), \"example.com\", \"groupA\", rrSet)\n\trequire.NoError(t, err)\n\n\texpected := &APIRRSet{\n\t\tDNSZoneName: \"string\",\n\t\tGroupName:   \"string\",\n\t\tRRSet: RRSet{\n\t\t\tDescription: \"string\",\n\t\t\tTXTRecord: &TXTRecord{\n\t\t\t\tName:   \"string\",\n\t\t\t\tValues: []string{\"string\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_CreateRRSet_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\trrSet := RRSet{\n\t\tDescription: \"lego\",\n\t\tTTL:         60,\n\t\tTXTRecord: &TXTRecord{\n\t\t\tName:   \"wwww\",\n\t\t\tValues: []string{\"txt\"},\n\t\t},\n\t}\n\n\t_, err := client.CreateRRSet(t.Context(), \"example.com\", \"groupA\", rrSet)\n\trequire.Error(t, err)\n}\n\nfunc TestClient_GetRRSet(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT\",\n\t\t\tservermock.ResponseFromFixture(\"get.json\")).\n\t\tBuild(t)\n\n\tresult, err := client.GetRRSet(t.Context(), \"example.com\", \"groupA\", \"www\", \"TXT\")\n\trequire.NoError(t, err)\n\n\texpected := &APIRRSet{\n\t\tDNSZoneName: \"string\",\n\t\tGroupName:   \"string\",\n\t\tNamespace:   \"string\",\n\t\tRecordName:  \"string\",\n\t\tType:        \"string\",\n\t\tRRSet: RRSet{\n\t\t\tDescription: \"string\",\n\t\t\tTXTRecord: &TXTRecord{\n\t\t\t\tName:   \"string\",\n\t\t\t\tValues: []string{\"string\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_GetRRSet_not_found(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT\",\n\t\t\tservermock.ResponseFromFixture(\"error_404.json\").WithStatusCode(http.StatusNotFound)).\n\t\tBuild(t)\n\n\tresult, err := client.GetRRSet(t.Context(), \"example.com\", \"groupA\", \"www\", \"TXT\")\n\trequire.NoError(t, err)\n\n\tassert.Nil(t, result)\n}\n\nfunc TestClient_GetRRSet_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\t_, err := client.GetRRSet(t.Context(), \"example.com\", \"groupA\", \"www\", \"TXT\")\n\trequire.Error(t, err)\n}\n\nfunc TestClient_DeleteRRSet(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT\",\n\t\t\tservermock.ResponseFromFixture(\"get.json\")).\n\t\tBuild(t)\n\n\tresult, err := client.DeleteRRSet(t.Context(), \"example.com\", \"groupA\", \"www\", \"TXT\")\n\trequire.NoError(t, err)\n\n\texpected := &APIRRSet{\n\t\tDNSZoneName: \"string\",\n\t\tGroupName:   \"string\",\n\t\tNamespace:   \"string\",\n\t\tRecordName:  \"string\",\n\t\tType:        \"string\",\n\t\tRRSet: RRSet{\n\t\t\tDescription: \"string\",\n\t\t\tTXTRecord: &TXTRecord{\n\t\t\t\tName:   \"string\",\n\t\t\t\tValues: []string{\"string\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_DeleteRRSet_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\t_, err := client.DeleteRRSet(t.Context(), \"example.com\", \"groupA\", \"www\", \"TXT\")\n\trequire.Error(t, err)\n}\n\nfunc TestClient_ReplaceRRSet(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT\",\n\t\t\tservermock.ResponseFromFixture(\"get.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"dns_zone_name\":\"example.com\",\"group_name\":\"groupA\",\"type\":\"TXT\",\"rrset\":{\"description\":\"lego\",\"ttl\":60,\"txt_record\":{\"name\":\"wwww\",\"values\":[\"txt\"]}}}`)).\n\t\tBuild(t)\n\n\trrSet := RRSet{\n\t\tDescription: \"lego\",\n\t\tTTL:         60,\n\t\tTXTRecord: &TXTRecord{\n\t\t\tName:   \"wwww\",\n\t\t\tValues: []string{\"txt\"},\n\t\t},\n\t}\n\n\tresult, err := client.ReplaceRRSet(t.Context(), \"example.com\", \"groupA\", \"www\", \"TXT\", rrSet)\n\trequire.NoError(t, err)\n\n\texpected := &APIRRSet{\n\t\tDNSZoneName: \"string\",\n\t\tGroupName:   \"string\",\n\t\tNamespace:   \"string\",\n\t\tRecordName:  \"string\",\n\t\tType:        \"string\",\n\t\tRRSet: RRSet{\n\t\t\tDescription: \"string\",\n\t\t\tTXTRecord: &TXTRecord{\n\t\t\t\tName:   \"string\",\n\t\t\t\tValues: []string{\"string\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_ReplaceRRSet_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\trrSet := RRSet{\n\t\tDescription: \"lego\",\n\t\tTTL:         60,\n\t\tTXTRecord: &TXTRecord{\n\t\t\tName:   \"wwww\",\n\t\t\tValues: []string{\"txt\"},\n\t\t},\n\t}\n\n\t_, err := client.ReplaceRRSet(t.Context(), \"example.com\", \"groupA\", \"www\", \"TXT\", rrSet)\n\trequire.Error(t, err)\n}\n\nfunc Test_createBaseURL(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttenant   string\n\t\tserver   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"only tenant\",\n\t\t\ttenant:   \"foo\",\n\t\t\texpected: \"https://foo.console.ves.volterra.io\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"custom server\",\n\t\t\ttenant:   \"foo\",\n\t\t\tserver:   \"example.com\",\n\t\t\texpected: \"https://foo.example.com\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tbaseURL, err := createBaseURL(test.tenant, test.server)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.expected, baseURL.String())\n\t\t})\n\t}\n}\n\nfunc Test_createBaseURL_error(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttenant   string\n\t\tserver   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"no tenant\",\n\t\t\ttenant:   \"\",\n\t\t\texpected: \"missing tenant name\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"invalid tenant\",\n\t\t\ttenant:   \"%31\",\n\t\t\texpected: `parse base URL: parse \"https://%31.console.ves.volterra.io\": invalid URL escape \"%31\"`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"invalid host\",\n\t\t\ttenant:   \"foo\",\n\t\t\tserver:   \"192.168.0.%31\",\n\t\t\texpected: `parse base URL: parse \"https://foo.192.168.0.%31\": invalid URL escape \"%31\"`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t_, err := createBaseURL(test.tenant, test.server)\n\t\t\trequire.EqualError(t, err, test.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/f5xc/internal/fixtures/create.json",
    "content": "{\n  \"dns_zone_name\": \"string\",\n  \"group_name\": \"string\",\n  \"rrset\": {\n    \"a_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"aaaa_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"afsdb_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"hostname\": \"string\",\n          \"subtype\": \"NONE\"\n        }\n      ]\n    },\n    \"alias_record\": {\n      \"value\": \"string\"\n    },\n    \"caa_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"flags\": 0,\n          \"tag\": \"string\",\n          \"value\": \"string\"\n        }\n      ]\n    },\n    \"cds_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"ds_key_algorithm\": \"UNSPECIFIED\",\n          \"key_tag\": 0,\n          \"sha1_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstri\"\n          },\n          \"sha256_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstri\"\n          },\n          \"sha384_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring\"\n          }\n        }\n      ]\n    },\n    \"cert_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"algorithm\": \"RESERVEDALGORITHM\",\n          \"cert_key_tag\": 0,\n          \"cert_type\": \"INVALIDCERTTYPE\",\n          \"certificate\": \"string\"\n        }\n      ]\n    },\n    \"cname_record\": {\n      \"name\": \"string\",\n      \"value\": \"string\"\n    },\n    \"description\": \"string\",\n    \"ds_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"ds_key_algorithm\": \"UNSPECIFIED\",\n          \"key_tag\": 0,\n          \"sha1_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstri\"\n          },\n          \"sha256_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstri\"\n          },\n          \"sha384_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring\"\n          }\n        }\n      ]\n    },\n    \"eui48_record\": {\n      \"name\": \"string\",\n      \"value\": \"stringstringstrin\"\n    },\n    \"eui64_record\": {\n      \"name\": \"string\",\n      \"value\": \"stringstringstringstrin\"\n    },\n    \"lb_record\": {\n      \"name\": \"string\",\n      \"value\": {\n        \"name\": \"string\",\n        \"namespace\": \"string\",\n        \"tenant\": \"string\"\n      }\n    },\n    \"loc_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"altitude\": 0.1,\n          \"horizontal_precision\": 0.1,\n          \"latitude_degree\": 0,\n          \"latitude_hemisphere\": \"N\",\n          \"latitude_minute\": 0,\n          \"latitude_second\": 0.1,\n          \"location_diameter\": 0.1,\n          \"longitude_degree\": 0,\n          \"longitude_hemisphere\": \"E\",\n          \"longitude_minute\": 0,\n          \"longitude_second\": 0.1,\n          \"vertical_precision\": 0.1\n        }\n      ]\n    },\n    \"mx_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"domain\": \"string\",\n          \"priority\": 0\n        }\n      ]\n    },\n    \"naptr_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"flags\": \"string\",\n          \"order\": 0,\n          \"preference\": 0,\n          \"regexp\": \"string\",\n          \"replacement\": \"string\",\n          \"service\": \"string\"\n        }\n      ]\n    },\n    \"ns_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"ptr_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"srv_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"port\": 0,\n          \"priority\": 0,\n          \"target\": \"string\",\n          \"weight\": 0\n        }\n      ]\n    },\n    \"sshfp_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"algorithm\": \"UNSPECIFIEDALGORITHM\",\n          \"sha1_fingerprint\": {\n            \"fingerprint\": \"stringstringstringstringstringstringstri\"\n          },\n          \"sha256_fingerprint\": {\n            \"fingerprint\": \"stringstringstringstringstringstringstringstringstringstringstri\"\n          }\n        }\n      ]\n    },\n    \"tlsa_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"certificate_association_data\": \"string\",\n          \"certificate_usage\": \"CertificateAuthorityConstraint\",\n          \"matching_type\": \"NoHash\",\n          \"selector\": \"FullCertificate\"\n        }\n      ]\n    },\n    \"ttl\": 0,\n    \"txt_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/f5xc/internal/fixtures/delete.json",
    "content": "{\n  \"dns_zone_name\": \"string\",\n  \"group_name\": \"string\",\n  \"namespace\": \"string\",\n  \"record_name\": \"string\",\n  \"rrset\": {\n    \"a_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"aaaa_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"afsdb_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"hostname\": \"string\",\n          \"subtype\": \"NONE\"\n        }\n      ]\n    },\n    \"alias_record\": {\n      \"value\": \"string\"\n    },\n    \"caa_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"flags\": 0,\n          \"tag\": \"string\",\n          \"value\": \"string\"\n        }\n      ]\n    },\n    \"cds_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"ds_key_algorithm\": \"UNSPECIFIED\",\n          \"key_tag\": 0,\n          \"sha1_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstri\"\n          },\n          \"sha256_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstri\"\n          },\n          \"sha384_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring\"\n          }\n        }\n      ]\n    },\n    \"cert_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"algorithm\": \"RESERVEDALGORITHM\",\n          \"cert_key_tag\": 0,\n          \"cert_type\": \"INVALIDCERTTYPE\",\n          \"certificate\": \"string\"\n        }\n      ]\n    },\n    \"cname_record\": {\n      \"name\": \"string\",\n      \"value\": \"string\"\n    },\n    \"description\": \"string\",\n    \"ds_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"ds_key_algorithm\": \"UNSPECIFIED\",\n          \"key_tag\": 0,\n          \"sha1_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstri\"\n          },\n          \"sha256_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstri\"\n          },\n          \"sha384_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring\"\n          }\n        }\n      ]\n    },\n    \"eui48_record\": {\n      \"name\": \"string\",\n      \"value\": \"stringstringstrin\"\n    },\n    \"eui64_record\": {\n      \"name\": \"string\",\n      \"value\": \"stringstringstringstrin\"\n    },\n    \"lb_record\": {\n      \"name\": \"string\",\n      \"value\": {\n        \"name\": \"string\",\n        \"namespace\": \"string\",\n        \"tenant\": \"string\"\n      }\n    },\n    \"loc_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"altitude\": 0.1,\n          \"horizontal_precision\": 0.1,\n          \"latitude_degree\": 0,\n          \"latitude_hemisphere\": \"N\",\n          \"latitude_minute\": 0,\n          \"latitude_second\": 0.1,\n          \"location_diameter\": 0.1,\n          \"longitude_degree\": 0,\n          \"longitude_hemisphere\": \"E\",\n          \"longitude_minute\": 0,\n          \"longitude_second\": 0.1,\n          \"vertical_precision\": 0.1\n        }\n      ]\n    },\n    \"mx_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"domain\": \"string\",\n          \"priority\": 0\n        }\n      ]\n    },\n    \"naptr_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"flags\": \"string\",\n          \"order\": 0,\n          \"preference\": 0,\n          \"regexp\": \"string\",\n          \"replacement\": \"string\",\n          \"service\": \"string\"\n        }\n      ]\n    },\n    \"ns_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"ptr_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"srv_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"port\": 0,\n          \"priority\": 0,\n          \"target\": \"string\",\n          \"weight\": 0\n        }\n      ]\n    },\n    \"sshfp_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"algorithm\": \"UNSPECIFIEDALGORITHM\",\n          \"sha1_fingerprint\": {\n            \"fingerprint\": \"stringstringstringstringstringstringstri\"\n          },\n          \"sha256_fingerprint\": {\n            \"fingerprint\": \"stringstringstringstringstringstringstringstringstringstringstri\"\n          }\n        }\n      ]\n    },\n    \"tlsa_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"certificate_association_data\": \"string\",\n          \"certificate_usage\": \"CertificateAuthorityConstraint\",\n          \"matching_type\": \"NoHash\",\n          \"selector\": \"FullCertificate\"\n        }\n      ]\n    },\n    \"ttl\": 0,\n    \"txt_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    }\n  },\n  \"type\": \"string\"\n}\n"
  },
  {
    "path": "providers/dns/f5xc/internal/fixtures/error_404.json",
    "content": "{\n  \"code\": 5,\n  \"details\": [],\n  \"message\": \"the requested resource record was not found: (group,name,type) (acme-records,_acme-challenge,TXT)\"\n}\n"
  },
  {
    "path": "providers/dns/f5xc/internal/fixtures/error_503.json",
    "content": "{\n  \"code\": 14,\n  \"details\": [],\n  \"message\": \"Previous DNS zone change is pending. Try again later\"\n}\n"
  },
  {
    "path": "providers/dns/f5xc/internal/fixtures/get.json",
    "content": "{\n  \"dns_zone_name\": \"string\",\n  \"group_name\": \"string\",\n  \"namespace\": \"string\",\n  \"record_name\": \"string\",\n  \"rrset\": {\n    \"a_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"aaaa_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"afsdb_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"hostname\": \"string\",\n          \"subtype\": \"NONE\"\n        }\n      ]\n    },\n    \"alias_record\": {\n      \"value\": \"string\"\n    },\n    \"caa_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"flags\": 0,\n          \"tag\": \"string\",\n          \"value\": \"string\"\n        }\n      ]\n    },\n    \"cds_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"ds_key_algorithm\": \"UNSPECIFIED\",\n          \"key_tag\": 0,\n          \"sha1_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstri\"\n          },\n          \"sha256_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstri\"\n          },\n          \"sha384_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring\"\n          }\n        }\n      ]\n    },\n    \"cert_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"algorithm\": \"RESERVEDALGORITHM\",\n          \"cert_key_tag\": 0,\n          \"cert_type\": \"INVALIDCERTTYPE\",\n          \"certificate\": \"string\"\n        }\n      ]\n    },\n    \"cname_record\": {\n      \"name\": \"string\",\n      \"value\": \"string\"\n    },\n    \"description\": \"string\",\n    \"ds_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"ds_key_algorithm\": \"UNSPECIFIED\",\n          \"key_tag\": 0,\n          \"sha1_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstri\"\n          },\n          \"sha256_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstri\"\n          },\n          \"sha384_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring\"\n          }\n        }\n      ]\n    },\n    \"eui48_record\": {\n      \"name\": \"string\",\n      \"value\": \"stringstringstrin\"\n    },\n    \"eui64_record\": {\n      \"name\": \"string\",\n      \"value\": \"stringstringstringstrin\"\n    },\n    \"lb_record\": {\n      \"name\": \"string\",\n      \"value\": {\n        \"name\": \"string\",\n        \"namespace\": \"string\",\n        \"tenant\": \"string\"\n      }\n    },\n    \"loc_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"altitude\": 0.1,\n          \"horizontal_precision\": 0.1,\n          \"latitude_degree\": 0,\n          \"latitude_hemisphere\": \"N\",\n          \"latitude_minute\": 0,\n          \"latitude_second\": 0.1,\n          \"location_diameter\": 0.1,\n          \"longitude_degree\": 0,\n          \"longitude_hemisphere\": \"E\",\n          \"longitude_minute\": 0,\n          \"longitude_second\": 0.1,\n          \"vertical_precision\": 0.1\n        }\n      ]\n    },\n    \"mx_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"domain\": \"string\",\n          \"priority\": 0\n        }\n      ]\n    },\n    \"naptr_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"flags\": \"string\",\n          \"order\": 0,\n          \"preference\": 0,\n          \"regexp\": \"string\",\n          \"replacement\": \"string\",\n          \"service\": \"string\"\n        }\n      ]\n    },\n    \"ns_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"ptr_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"srv_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"port\": 0,\n          \"priority\": 0,\n          \"target\": \"string\",\n          \"weight\": 0\n        }\n      ]\n    },\n    \"sshfp_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"algorithm\": \"UNSPECIFIEDALGORITHM\",\n          \"sha1_fingerprint\": {\n            \"fingerprint\": \"stringstringstringstringstringstringstri\"\n          },\n          \"sha256_fingerprint\": {\n            \"fingerprint\": \"stringstringstringstringstringstringstringstringstringstringstri\"\n          }\n        }\n      ]\n    },\n    \"tlsa_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"certificate_association_data\": \"string\",\n          \"certificate_usage\": \"CertificateAuthorityConstraint\",\n          \"matching_type\": \"NoHash\",\n          \"selector\": \"FullCertificate\"\n        }\n      ]\n    },\n    \"ttl\": 0,\n    \"txt_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    }\n  },\n  \"type\": \"string\"\n}\n"
  },
  {
    "path": "providers/dns/f5xc/internal/fixtures/replace.json",
    "content": "{\n  \"dns_zone_name\": \"string\",\n  \"group_name\": \"string\",\n  \"record_name\": \"string\",\n  \"rrset\": {\n    \"a_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"aaaa_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"afsdb_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"hostname\": \"string\",\n          \"subtype\": \"NONE\"\n        }\n      ]\n    },\n    \"alias_record\": {\n      \"value\": \"string\"\n    },\n    \"caa_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"flags\": 0,\n          \"tag\": \"string\",\n          \"value\": \"string\"\n        }\n      ]\n    },\n    \"cds_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"ds_key_algorithm\": \"UNSPECIFIED\",\n          \"key_tag\": 0,\n          \"sha1_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstri\"\n          },\n          \"sha256_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstri\"\n          },\n          \"sha384_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring\"\n          }\n        }\n      ]\n    },\n    \"cert_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"algorithm\": \"RESERVEDALGORITHM\",\n          \"cert_key_tag\": 0,\n          \"cert_type\": \"INVALIDCERTTYPE\",\n          \"certificate\": \"string\"\n        }\n      ]\n    },\n    \"cname_record\": {\n      \"name\": \"string\",\n      \"value\": \"string\"\n    },\n    \"description\": \"string\",\n    \"ds_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"ds_key_algorithm\": \"UNSPECIFIED\",\n          \"key_tag\": 0,\n          \"sha1_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstri\"\n          },\n          \"sha256_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstri\"\n          },\n          \"sha384_digest\": {\n            \"digest\": \"stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring\"\n          }\n        }\n      ]\n    },\n    \"eui48_record\": {\n      \"name\": \"string\",\n      \"value\": \"stringstringstrin\"\n    },\n    \"eui64_record\": {\n      \"name\": \"string\",\n      \"value\": \"stringstringstringstrin\"\n    },\n    \"lb_record\": {\n      \"name\": \"string\",\n      \"value\": {\n        \"name\": \"string\",\n        \"namespace\": \"string\",\n        \"tenant\": \"string\"\n      }\n    },\n    \"loc_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"altitude\": 0.1,\n          \"horizontal_precision\": 0.1,\n          \"latitude_degree\": 0,\n          \"latitude_hemisphere\": \"N\",\n          \"latitude_minute\": 0,\n          \"latitude_second\": 0.1,\n          \"location_diameter\": 0.1,\n          \"longitude_degree\": 0,\n          \"longitude_hemisphere\": \"E\",\n          \"longitude_minute\": 0,\n          \"longitude_second\": 0.1,\n          \"vertical_precision\": 0.1\n        }\n      ]\n    },\n    \"mx_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"domain\": \"string\",\n          \"priority\": 0\n        }\n      ]\n    },\n    \"naptr_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"flags\": \"string\",\n          \"order\": 0,\n          \"preference\": 0,\n          \"regexp\": \"string\",\n          \"replacement\": \"string\",\n          \"service\": \"string\"\n        }\n      ]\n    },\n    \"ns_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"ptr_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    },\n    \"srv_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"port\": 0,\n          \"priority\": 0,\n          \"target\": \"string\",\n          \"weight\": 0\n        }\n      ]\n    },\n    \"sshfp_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"algorithm\": \"UNSPECIFIEDALGORITHM\",\n          \"sha1_fingerprint\": {\n            \"fingerprint\": \"stringstringstringstringstringstringstri\"\n          },\n          \"sha256_fingerprint\": {\n            \"fingerprint\": \"stringstringstringstringstringstringstringstringstringstringstri\"\n          }\n        }\n      ]\n    },\n    \"tlsa_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        {\n          \"certificate_association_data\": \"string\",\n          \"certificate_usage\": \"CertificateAuthorityConstraint\",\n          \"matching_type\": \"NoHash\",\n          \"selector\": \"FullCertificate\"\n        }\n      ]\n    },\n    \"ttl\": 0,\n    \"txt_record\": {\n      \"name\": \"string\",\n      \"values\": [\n        \"string\"\n      ]\n    }\n  },\n  \"type\": \"string\"\n}\n"
  },
  {
    "path": "providers/dns/f5xc/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype APIError struct {\n\tStatusCode int      `json:\"-\"`\n\tCode       int      `json:\"code\"`\n\tDetails    []string `json:\"details\"`\n\tMessage    string   `json:\"message\"`\n}\n\nfunc (a *APIError) Error() string {\n\tvar details string\n\tif len(a.Details) > 0 {\n\t\tdetails = \" \" + strings.Join(a.Details, \", \")\n\t}\n\n\treturn fmt.Sprintf(\"code: %d, message: %s%s\", a.Code, a.Message, details)\n}\n\ntype APIRRSet struct {\n\tDNSZoneName string `json:\"dns_zone_name,omitempty\"`\n\tGroupName   string `json:\"group_name,omitempty\"`\n\tNamespace   string `json:\"namespace,omitempty\"`\n\tRecordName  string `json:\"record_name,omitempty\"`\n\tType        string `json:\"type,omitempty\"`\n\tRRSet       RRSet  `json:\"rrset\"`\n}\n\ntype RRSetRequest struct {\n\tDNSZoneName string `json:\"dns_zone_name,omitempty\"`\n\tGroupName   string `json:\"group_name,omitempty\"`\n\tRRSet       RRSet  `json:\"rrset\"`\n}\n\ntype RRSet struct {\n\tDescription string     `json:\"description,omitempty\"`\n\tTTL         int        `json:\"ttl,omitempty\"`\n\tTXTRecord   *TXTRecord `json:\"txt_record,omitempty\"`\n}\n\ntype TXTRecord struct {\n\tName   string   `json:\"name,omitempty\"`\n\tValues []string `json:\"values,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/freemyip/freemyip.go",
    "content": "// Package freemyip implements a DNS provider for solving the DNS-01 challenge using freemyip.com.\npackage freemyip\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/nrdcg/freemyip\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"FREEMYIP_\"\n\n\tEnvToken = envNamespace + \"TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken              string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 3600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *freemyip.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for freemyip.com.\n// Credentials must be passed in the environment variable: FREEMYIP_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"freemyip: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for freemyip.com.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"freemyip: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"freemyip: missing credentials\")\n\t}\n\n\tclient := freemyip.New(config.Token, true)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, freemyip.RootDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"freemyip: %w\", err)\n\t}\n\n\t_, err = d.client.EditTXTRecord(context.Background(), subDomain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"freemyip: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, freemyip.RootDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"freemyip: %w\", err)\n\t}\n\n\t_, err = d.client.DeleteTXTRecord(context.Background(), subDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"freemyip: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/freemyip/freemyip.toml",
    "content": "Name = \"freemyip.com\"\nDescription = ''''''\nURL = \"https://freemyip.com/\"\nCode = \"freemyip\"\nSince = \"v4.5.0\"\n\nExample = '''\nFREEMYIP_TOKEN=xxxxxx \\\nlego --dns freemyip -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    FREEMYIP_TOKEN = \"Account token\"\n  [Configuration.Additional]\n    FREEMYIP_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    FREEMYIP_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    FREEMYIP_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)\"\n    FREEMYIP_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n    FREEMYIP_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n\n[Links]\n  API = \"https://freemyip.com/help\"\n"
  },
  {
    "path": "providers/dns/freemyip/freemyip_test.go",
    "content": "package freemyip\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvToken).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"\",\n\t\t\t},\n\t\t\texpected: \"freemyip: some credentials information are missing: FREEMYIP_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"freemyip: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/gandi/gandi.go",
    "content": "// Package gandi implements a DNS provider for solving the DNS-01 challenge using Gandi DNS.\npackage gandi\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/gandi/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"GANDI_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst minTTL = 300\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL            string\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 40*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 60*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second),\n\t\t},\n\t}\n}\n\n// inProgressInfo contains information about an in-progress challenge.\ntype inProgressInfo struct {\n\tzoneID    int    // zoneID of gandi zone to restore in CleanUp\n\tnewZoneID int    // zoneID of temporary gandi zone containing TXT record\n\tauthZone  string // the domain name registered at gandi with trailing \".\"\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\tinProgressFQDNs     map[string]inProgressInfo\n\tinProgressAuthZones map[string]struct{}\n\tinProgressMu        sync.Mutex\n\n\t// findZoneByFqdn determines the DNS zone of a FQDN.\n\t// It is overridden during tests.\n\t// only for testing purpose.\n\tfindZoneByFqdn func(fqdn string) (string, error)\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Gandi.\n// Credentials must be passed in the environment variable: GANDI_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"gandi: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Gandi.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"gandi: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"gandi: no API Key given\")\n\t}\n\n\tclient := internal.NewClient(config.APIKey)\n\n\tif config.BaseURL != \"\" {\n\t\tclient.BaseURL = config.BaseURL\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:              config,\n\t\tclient:              client,\n\t\tinProgressFQDNs:     make(map[string]inProgressInfo),\n\t\tinProgressAuthZones: make(map[string]struct{}),\n\t\tfindZoneByFqdn:      dns01.FindZoneByFqdn,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters. It\n// does this by creating and activating a new temporary Gandi DNS\n// zone. This new zone contains the TXT record.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tif d.config.TTL < minTTL {\n\t\td.config.TTL = minTTL // 300 is gandi minimum value for ttl\n\t}\n\n\t// find authZone and Gandi zone_id for fqdn\n\tauthZone, err := d.findZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gandi: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tzoneID, err := d.client.GetZoneID(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gandi: %w\", err)\n\t}\n\n\t// determine name of TXT record\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gandi: %w\", err)\n\t}\n\n\t// acquire lock and check there is not a challenge already in\n\t// progress for this value of authZone\n\td.inProgressMu.Lock()\n\tdefer d.inProgressMu.Unlock()\n\n\tif _, ok := d.inProgressAuthZones[authZone]; ok {\n\t\treturn fmt.Errorf(\"gandi: challenge already in progress for authZone %s\", authZone)\n\t}\n\n\t// perform API actions to create and activate new gandi zone\n\t// containing the required TXT record\n\tnewZoneName := fmt.Sprintf(\"%s [ACME Challenge %s]\", dns01.UnFqdn(authZone), time.Now().Format(time.RFC822Z))\n\n\tnewZoneID, err := d.client.CloneZone(ctx, zoneID, newZoneName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnewZoneVersion, err := d.client.NewZoneVersion(ctx, newZoneID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gandi: %w\", err)\n\t}\n\n\terr = d.client.AddTXTRecord(ctx, newZoneID, newZoneVersion, subDomain, info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gandi: %w\", err)\n\t}\n\n\terr = d.client.SetZoneVersion(ctx, newZoneID, newZoneVersion)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gandi: %w\", err)\n\t}\n\n\terr = d.client.SetZone(ctx, authZone, newZoneID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gandi: %w\", err)\n\t}\n\n\t// save data necessary for CleanUp\n\td.inProgressFQDNs[info.EffectiveFQDN] = inProgressInfo{\n\t\tzoneID:    zoneID,\n\t\tnewZoneID: newZoneID,\n\t\tauthZone:  authZone,\n\t}\n\td.inProgressAuthZones[authZone] = struct{}{}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified\n// parameters. It does this by restoring the old Gandi DNS zone and\n// removing the temporary one created by Present.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// acquire lock and retrieve zoneID, newZoneID and authZone\n\td.inProgressMu.Lock()\n\tdefer d.inProgressMu.Unlock()\n\n\tif _, ok := d.inProgressFQDNs[info.EffectiveFQDN]; !ok {\n\t\t// if there is no cleanup information then just return\n\t\treturn nil\n\t}\n\n\tzoneID := d.inProgressFQDNs[info.EffectiveFQDN].zoneID\n\tnewZoneID := d.inProgressFQDNs[info.EffectiveFQDN].newZoneID\n\tauthZone := d.inProgressFQDNs[info.EffectiveFQDN].authZone\n\tdelete(d.inProgressFQDNs, info.EffectiveFQDN)\n\tdelete(d.inProgressAuthZones, authZone)\n\n\tctx := context.Background()\n\n\t// perform API actions to restore old gandi zone for authZone\n\terr := d.client.SetZone(ctx, authZone, zoneID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gandi: %w\", err)\n\t}\n\n\treturn d.client.DeleteZone(ctx, newZoneID)\n}\n\n// Timeout returns the values (40*time.Minute, 60*time.Second) which\n// are used by the acme package as timeout and check interval values\n// when checking for DNS record propagation with Gandi.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/gandi/gandi.toml",
    "content": "Name = \"Gandi\"\nDescription = \"\"\"\"\"\"\nURL = \"https://www.gandi.net\"\nCode = \"gandi\"\nSince = \"v0.3.0\"\n\nExample = '''\nGANDI_API_KEY=abcdefghijklmnopqrstuvwx \\\nlego --dns gandi -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    GANDI_API_KEY = \"API key\"\n  [Configuration.Additional]\n    GANDI_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 60)\"\n    GANDI_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 2400)\"\n    GANDI_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    GANDI_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 60)\"\n\n[Links]\n  API = \"https://doc.rpc.gandi.net/index.html\"\n"
  },
  {
    "path": "providers/dns/gandi/gandi_mock_test.go",
    "content": "package gandi\n\n// CleanUp Request->Response 1 (setZone).\nconst cleanupSetZoneRequestMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>domain.zone.set</methodName>\n  <param>\n    <value>\n      <string>123412341234123412341234</string>\n    </value>\n  </param>\n  <param>\n    <value>\n      <string>example.com.</string>\n    </value>\n  </param>\n  <param>\n    <value>\n      <int>1234567</int>\n    </value>\n  </param>\n</methodCall>`\n\n// CleanUp Request->Response 1 (setZone).\nconst cleanupSetZoneResponseMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodResponse>\n<params>\n<param>\n<value><struct>\n<member>\n<name>date_updated</name>\n<value><dateTime.iso8601>20160216T16:24:38</dateTime.iso8601></value>\n</member>\n<member>\n<name>date_delete</name>\n<value><dateTime.iso8601>20170331T16:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>is_premium</name>\n<value><boolean>0</boolean></value>\n</member>\n<member>\n<name>date_hold_begin</name>\n<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>date_registry_end</name>\n<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>authinfo_expiration_date</name>\n<value><dateTime.iso8601>20161211T21:31:20</dateTime.iso8601></value>\n</member>\n<member>\n<name>contacts</name>\n<value><struct>\n<member>\n<name>owner</name>\n<value><struct>\n<member>\n<name>handle</name>\n<value><string>LEGO-GANDI</string></value>\n</member>\n<member>\n<name>id</name>\n<value><int>111111</int></value>\n</member>\n</struct></value>\n</member>\n<member>\n<name>admin</name>\n<value><struct>\n<member>\n<name>handle</name>\n<value><string>LEGO-GANDI</string></value>\n</member>\n<member>\n<name>id</name>\n<value><int>111111</int></value>\n</member>\n</struct></value>\n</member>\n<member>\n<name>bill</name>\n<value><struct>\n<member>\n<name>handle</name>\n<value><string>LEGO-GANDI</string></value>\n</member>\n<member>\n<name>id</name>\n<value><int>111111</int></value>\n</member>\n</struct></value>\n</member>\n<member>\n<name>tech</name>\n<value><struct>\n<member>\n<name>handle</name>\n<value><string>LEGO-GANDI</string></value>\n</member>\n<member>\n<name>id</name>\n<value><int>111111</int></value>\n</member>\n</struct></value>\n</member>\n<member>\n<name>reseller</name>\n<value><nil/></value></member>\n</struct></value>\n</member>\n<member>\n<name>nameservers</name>\n<value><array><data>\n<value><string>a.dns.gandi.net</string></value>\n<value><string>b.dns.gandi.net</string></value>\n<value><string>c.dns.gandi.net</string></value>\n</data></array></value>\n</member>\n<member>\n<name>date_restore_end</name>\n<value><dateTime.iso8601>20170501T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>id</name>\n<value><int>2222222</int></value>\n</member>\n<member>\n<name>authinfo</name>\n<value><string>ABCDABCDAB</string></value>\n</member>\n<member>\n<name>status</name>\n<value><array><data>\n<value><string>clientTransferProhibited</string></value>\n<value><string>serverTransferProhibited</string></value>\n</data></array></value>\n</member>\n<member>\n<name>tags</name>\n<value><array><data>\n</data></array></value>\n</member>\n<member>\n<name>date_hold_end</name>\n<value><dateTime.iso8601>20170401T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>services</name>\n<value><array><data>\n<value><string>gandidns</string></value>\n<value><string>gandimail</string></value>\n</data></array></value>\n</member>\n<member>\n<name>date_pending_delete_end</name>\n<value><dateTime.iso8601>20170506T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>zone_id</name>\n<value><int>1234567</int></value>\n</member>\n<member>\n<name>date_renew_begin</name>\n<value><dateTime.iso8601>20120101T00:00:00</dateTime.iso8601></value>\n</member>\n<member>\n<name>fqdn</name>\n<value><string>example.com</string></value>\n</member>\n<member>\n<name>autorenew</name>\n<value><nil/></value></member>\n<member>\n<name>date_registry_creation</name>\n<value><dateTime.iso8601>20150215T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>tld</name>\n<value><string>org</string></value>\n</member>\n<member>\n<name>date_created</name>\n<value><dateTime.iso8601>20150215T03:04:06</dateTime.iso8601></value>\n</member>\n</struct></value>\n</param>\n</params>\n</methodResponse>\n`\n\n// CleanUp Request->Response 2 (deleteZone).\nconst cleanupDeleteZoneRequestMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>domain.zone.delete</methodName>\n  <param>\n    <value>\n      <string>123412341234123412341234</string>\n    </value>\n  </param>\n  <param>\n    <value>\n      <int>7654321</int>\n    </value>\n  </param>\n</methodCall>`\n\n// CleanUp Request->Response 2 (deleteZone).\nconst cleanupDeleteZoneResponseMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodResponse>\n<params>\n<param>\n<value><boolean>1</boolean></value>\n</param>\n</params>\n</methodResponse>\n`\n\n// Present Request->Response 1 (getZoneID).\nconst presentGetZoneIDRequestMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>domain.info</methodName>\n  <param>\n    <value>\n      <string>123412341234123412341234</string>\n    </value>\n  </param>\n  <param>\n    <value>\n      <string>example.com.</string>\n    </value>\n  </param>\n</methodCall>`\n\n// Present Request->Response 1 (getZoneID).\nconst presentGetZoneIDResponseMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodResponse>\n<params>\n<param>\n<value><struct>\n<member>\n<name>date_updated</name>\n<value><dateTime.iso8601>20160216T16:14:23</dateTime.iso8601></value>\n</member>\n<member>\n<name>date_delete</name>\n<value><dateTime.iso8601>20170331T16:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>is_premium</name>\n<value><boolean>0</boolean></value>\n</member>\n<member>\n<name>date_hold_begin</name>\n<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>date_registry_end</name>\n<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>authinfo_expiration_date</name>\n<value><dateTime.iso8601>20161211T21:31:20</dateTime.iso8601></value>\n</member>\n<member>\n<name>contacts</name>\n<value><struct>\n<member>\n<name>owner</name>\n<value><struct>\n<member>\n<name>handle</name>\n<value><string>LEGO-GANDI</string></value>\n</member>\n<member>\n<name>id</name>\n<value><int>111111</int></value>\n</member>\n</struct></value>\n</member>\n<member>\n<name>admin</name>\n<value><struct>\n<member>\n<name>handle</name>\n<value><string>LEGO-GANDI</string></value>\n</member>\n<member>\n<name>id</name>\n<value><int>111111</int></value>\n</member>\n</struct></value>\n</member>\n<member>\n<name>bill</name>\n<value><struct>\n<member>\n<name>handle</name>\n<value><string>LEGO-GANDI</string></value>\n</member>\n<member>\n<name>id</name>\n<value><int>111111</int></value>\n</member>\n</struct></value>\n</member>\n<member>\n<name>tech</name>\n<value><struct>\n<member>\n<name>handle</name>\n<value><string>LEGO-GANDI</string></value>\n</member>\n<member>\n<name>id</name>\n<value><int>111111</int></value>\n</member>\n</struct></value>\n</member>\n<member>\n<name>reseller</name>\n<value><nil/></value></member>\n</struct></value>\n</member>\n<member>\n<name>nameservers</name>\n<value><array><data>\n<value><string>a.dns.gandi.net</string></value>\n<value><string>b.dns.gandi.net</string></value>\n<value><string>c.dns.gandi.net</string></value>\n</data></array></value>\n</member>\n<member>\n<name>date_restore_end</name>\n<value><dateTime.iso8601>20170501T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>id</name>\n<value><int>2222222</int></value>\n</member>\n<member>\n<name>authinfo</name>\n<value><string>ABCDABCDAB</string></value>\n</member>\n<member>\n<name>status</name>\n<value><array><data>\n<value><string>clientTransferProhibited</string></value>\n<value><string>serverTransferProhibited</string></value>\n</data></array></value>\n</member>\n<member>\n<name>tags</name>\n<value><array><data>\n</data></array></value>\n</member>\n<member>\n<name>date_hold_end</name>\n<value><dateTime.iso8601>20170401T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>services</name>\n<value><array><data>\n<value><string>gandidns</string></value>\n<value><string>gandimail</string></value>\n</data></array></value>\n</member>\n<member>\n<name>date_pending_delete_end</name>\n<value><dateTime.iso8601>20170506T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>zone_id</name>\n<value><int>1234567</int></value>\n</member>\n<member>\n<name>date_renew_begin</name>\n<value><dateTime.iso8601>20120101T00:00:00</dateTime.iso8601></value>\n</member>\n<member>\n<name>fqdn</name>\n<value><string>example.com</string></value>\n</member>\n<member>\n<name>autorenew</name>\n<value><nil/></value></member>\n<member>\n<name>date_registry_creation</name>\n<value><dateTime.iso8601>20150215T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>tld</name>\n<value><string>org</string></value>\n</member>\n<member>\n<name>date_created</name>\n<value><dateTime.iso8601>20150215T03:04:06</dateTime.iso8601></value>\n</member>\n</struct></value>\n</param>\n</params>\n</methodResponse>\n`\n\n// Present Request->Response 2 (cloneZone).\nconst presentCloneZoneRequestMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>domain.zone.clone</methodName>\n  <param>\n    <value>\n      <string>123412341234123412341234</string>\n    </value>\n  </param>\n  <param>\n    <value>\n      <int>1234567</int>\n    </value>\n  </param>\n  <param>\n    <value>\n      <int>0</int>\n    </value>\n  </param>\n  <param>\n    <value>\n      <struct>\n        <member>\n          <name>name</name>\n          <value>\n            <string>example.com [ACME Challenge 01 Jan 16 00:00 +0000]</string>\n          </value>\n        </member>\n      </struct>\n    </value>\n  </param>\n</methodCall>`\n\n// Present Request->Response 2 (cloneZone).\nconst presentCloneZoneResponseMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodResponse>\n<params>\n<param>\n<value><struct>\n<member>\n<name>name</name>\n<value><string>example.com [ACME Challenge 01 Jan 16 00:00 +0000]</string></value>\n</member>\n<member>\n<name>versions</name>\n<value><array><data>\n<value><int>1</int></value>\n</data></array></value>\n</member>\n<member>\n<name>date_updated</name>\n<value><dateTime.iso8601>20160216T16:24:29</dateTime.iso8601></value>\n</member>\n<member>\n<name>id</name>\n<value><int>7654321</int></value>\n</member>\n<member>\n<name>owner</name>\n<value><string>LEGO-GANDI</string></value>\n</member>\n<member>\n<name>version</name>\n<value><int>1</int></value>\n</member>\n<member>\n<name>domains</name>\n<value><int>0</int></value>\n</member>\n<member>\n<name>public</name>\n<value><boolean>0</boolean></value>\n</member>\n</struct></value>\n</param>\n</params>\n</methodResponse>\n`\n\n// Present Request->Response 3 (newZoneVersion).\nconst presentNewZoneVersionRequestMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>domain.zone.version.new</methodName>\n  <param>\n    <value>\n      <string>123412341234123412341234</string>\n    </value>\n  </param>\n  <param>\n    <value>\n      <int>7654321</int>\n    </value>\n  </param>\n</methodCall>`\n\n// Present Request->Response 3 (newZoneVersion).\nconst presentNewZoneVersionResponseMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodResponse>\n<params>\n<param>\n<value><int>2</int></value>\n</param>\n</params>\n</methodResponse>\n`\n\n// Present Request->Response 4 (addTXTRecord).\nconst presentAddTXTRecordRequestMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>domain.zone.record.add</methodName>\n  <param>\n    <value>\n      <string>123412341234123412341234</string>\n    </value>\n  </param>\n  <param>\n    <value>\n      <int>7654321</int>\n    </value>\n  </param>\n  <param>\n    <value>\n      <int>2</int>\n    </value>\n  </param>\n  <param>\n    <value>\n      <struct>\n        <member>\n          <name>type</name>\n          <value>\n            <string>TXT</string>\n          </value>\n        </member>\n        <member>\n          <name>name</name>\n          <value>\n            <string>_acme-challenge.abc.def</string>\n          </value>\n        </member>\n        <member>\n          <name>value</name>\n          <value>\n            <string>ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ</string>\n          </value>\n        </member>\n        <member>\n          <name>ttl</name>\n          <value>\n            <int>300</int>\n          </value>\n        </member>\n      </struct>\n    </value>\n  </param>\n</methodCall>`\n\n// Present Request->Response 4 (addTXTRecord).\nconst presentAddTXTRecordResponseMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodResponse>\n<params>\n<param>\n<value><struct>\n<member>\n<name>name</name>\n<value><string>_acme-challenge.abc.def</string></value>\n</member>\n<member>\n<name>type</name>\n<value><string>TXT</string></value>\n</member>\n<member>\n<name>id</name>\n<value><int>333333333</int></value>\n</member>\n<member>\n<name>value</name>\n<value><string>\"ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ\"</string></value>\n</member>\n<member>\n<name>ttl</name>\n<value><int>300</int></value>\n</member>\n</struct></value>\n</param>\n</params>\n</methodResponse>\n`\n\n// Present Request->Response 5 (setZoneVersion).\nconst presentSetZoneVersionRequestMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>domain.zone.version.set</methodName>\n  <param>\n    <value>\n      <string>123412341234123412341234</string>\n    </value>\n  </param>\n  <param>\n    <value>\n      <int>7654321</int>\n    </value>\n  </param>\n  <param>\n    <value>\n      <int>2</int>\n    </value>\n  </param>\n</methodCall>`\n\n// Present Request->Response 5 (setZoneVersion).\nconst presentSetZoneVersionResponseMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodResponse>\n<params>\n<param>\n<value><boolean>1</boolean></value>\n</param>\n</params>\n</methodResponse>\n`\n\n// Present Request->Response 6 (setZone).\nconst presentSetZoneRequestMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>domain.zone.set</methodName>\n  <param>\n    <value>\n      <string>123412341234123412341234</string>\n    </value>\n  </param>\n  <param>\n    <value>\n      <string>example.com.</string>\n    </value>\n  </param>\n  <param>\n    <value>\n      <int>7654321</int>\n    </value>\n  </param>\n</methodCall>`\n\n// Present Request->Response 6 (setZone).\nconst presentSetZoneResponseMock = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodResponse>\n<params>\n<param>\n<value><struct>\n<member>\n<name>date_updated</name>\n<value><dateTime.iso8601>20160216T16:14:23</dateTime.iso8601></value>\n</member>\n<member>\n<name>date_delete</name>\n<value><dateTime.iso8601>20170331T16:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>is_premium</name>\n<value><boolean>0</boolean></value>\n</member>\n<member>\n<name>date_hold_begin</name>\n<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>date_registry_end</name>\n<value><dateTime.iso8601>20170215T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>authinfo_expiration_date</name>\n<value><dateTime.iso8601>20161211T21:31:20</dateTime.iso8601></value>\n</member>\n<member>\n<name>contacts</name>\n<value><struct>\n<member>\n<name>owner</name>\n<value><struct>\n<member>\n<name>handle</name>\n<value><string>LEGO-GANDI</string></value>\n</member>\n<member>\n<name>id</name>\n<value><int>111111</int></value>\n</member>\n</struct></value>\n</member>\n<member>\n<name>admin</name>\n<value><struct>\n<member>\n<name>handle</name>\n<value><string>LEGO-GANDI</string></value>\n</member>\n<member>\n<name>id</name>\n<value><int>111111</int></value>\n</member>\n</struct></value>\n</member>\n<member>\n<name>bill</name>\n<value><struct>\n<member>\n<name>handle</name>\n<value><string>LEGO-GANDI</string></value>\n</member>\n<member>\n<name>id</name>\n<value><int>111111</int></value>\n</member>\n</struct></value>\n</member>\n<member>\n<name>tech</name>\n<value><struct>\n<member>\n<name>handle</name>\n<value><string>LEGO-GANDI</string></value>\n</member>\n<member>\n<name>id</name>\n<value><int>111111</int></value>\n</member>\n</struct></value>\n</member>\n<member>\n<name>reseller</name>\n<value><nil/></value></member>\n</struct></value>\n</member>\n<member>\n<name>nameservers</name>\n<value><array><data>\n<value><string>a.dns.gandi.net</string></value>\n<value><string>b.dns.gandi.net</string></value>\n<value><string>c.dns.gandi.net</string></value>\n</data></array></value>\n</member>\n<member>\n<name>date_restore_end</name>\n<value><dateTime.iso8601>20170501T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>id</name>\n<value><int>2222222</int></value>\n</member>\n<member>\n<name>authinfo</name>\n<value><string>ABCDABCDAB</string></value>\n</member>\n<member>\n<name>status</name>\n<value><array><data>\n<value><string>clientTransferProhibited</string></value>\n<value><string>serverTransferProhibited</string></value>\n</data></array></value>\n</member>\n<member>\n<name>tags</name>\n<value><array><data>\n</data></array></value>\n</member>\n<member>\n<name>date_hold_end</name>\n<value><dateTime.iso8601>20170401T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>services</name>\n<value><array><data>\n<value><string>gandidns</string></value>\n<value><string>gandimail</string></value>\n</data></array></value>\n</member>\n<member>\n<name>date_pending_delete_end</name>\n<value><dateTime.iso8601>20170506T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>zone_id</name>\n<value><int>7654321</int></value>\n</member>\n<member>\n<name>date_renew_begin</name>\n<value><dateTime.iso8601>20120101T00:00:00</dateTime.iso8601></value>\n</member>\n<member>\n<name>fqdn</name>\n<value><string>example.com</string></value>\n</member>\n<member>\n<name>autorenew</name>\n<value><nil/></value></member>\n<member>\n<name>date_registry_creation</name>\n<value><dateTime.iso8601>20150215T02:04:06</dateTime.iso8601></value>\n</member>\n<member>\n<name>tld</name>\n<value><string>org</string></value>\n</member>\n<member>\n<name>date_created</name>\n<value><dateTime.iso8601>20150215T03:04:06</dateTime.iso8601></value>\n</member>\n</struct></value>\n</param>\n</params>\n</methodResponse>\n`\n"
  },
  {
    "path": "providers/dns/gandi/gandi_test.go",
    "content": "package gandi\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar envTest = tester.NewEnvTest(EnvAPIKey)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"gandi: some credentials information are missing: GANDI_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.inProgressFQDNs)\n\t\t\t\trequire.NotNil(t, p.inProgressAuthZones)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"gandi: no API Key given\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.inProgressFQDNs)\n\t\t\t\trequire.NotNil(t, p.inProgressAuthZones)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestDNSProvider runs Present and CleanUp against a fake Gandi RPC\n// Server, whose responses are predetermined for particular requests.\nfunc TestDNSProvider(t *testing.T) {\n\t// serverResponses is the XML-RPC Request->Response map used by the\n\t// fake RPC server. It was generated by recording a real RPC session\n\t// which resulted in the successful issue of a cert, and then\n\t// anonymizing the RPC data.\n\tserverResponses := map[string]string{\n\t\t// Present Request->Response 1 (getZoneID)\n\t\tpresentGetZoneIDRequestMock: presentGetZoneIDResponseMock,\n\t\t// Present Request->Response 2 (cloneZone)\n\t\tpresentCloneZoneRequestMock: presentCloneZoneResponseMock,\n\t\t// Present Request->Response 3 (newZoneVersion)\n\t\tpresentNewZoneVersionRequestMock: presentNewZoneVersionResponseMock,\n\t\t// Present Request->Response 4 (addTXTRecord)\n\t\tpresentAddTXTRecordRequestMock: presentAddTXTRecordResponseMock,\n\t\t// Present Request->Response 5 (setZoneVersion)\n\t\tpresentSetZoneVersionRequestMock: presentSetZoneVersionResponseMock,\n\t\t// Present Request->Response 6 (setZone)\n\t\tpresentSetZoneRequestMock: presentSetZoneResponseMock,\n\t\t// CleanUp Request->Response 1 (setZone)\n\t\tcleanupSetZoneRequestMock: cleanupSetZoneResponseMock,\n\t\t// CleanUp Request->Response 2 (deleteZone)\n\t\tcleanupDeleteZoneRequestMock: cleanupDeleteZoneResponseMock,\n\t}\n\n\tregexpDate := regexp.MustCompile(`\\[ACME Challenge [^\\]:]*:[^\\]]*\\]`)\n\n\tprovider := servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.BaseURL = server.URL + \"/\"\n\t\t\tconfig.HTTPClient = server.Client()\n\t\t\tconfig.APIKey = \"123412341234123412341234\"\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t},\n\t\tservermock.CheckHeader().WithContentType(\"text/xml\"),\n\t).\n\t\tRoute(\"POST /\", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\trequire.Equal(t, \"text/xml\", req.Header.Get(\"Content-Type\"), \"invalid content type\")\n\n\t\t\tbody, errS := io.ReadAll(req.Body)\n\t\t\trequire.NoError(t, errS)\n\n\t\t\tbody = regexpDate.ReplaceAllLiteral(body, []byte(`[ACME Challenge 01 Jan 16 00:00 +0000]`))\n\t\t\tresp, ok := serverResponses[string(body)]\n\t\t\trequire.Truef(t, ok, \"Server response for request not found: %s\", string(body))\n\n\t\t\t_, errS = io.Copy(rw, strings.NewReader(resp))\n\t\t\trequire.NoError(t, errS)\n\t\t})).\n\t\tBuild(t)\n\n\tfakeKeyAuth := \"XXXX\"\n\n\t// define function to override findZoneByFqdn with\n\tfakeFindZoneByFqdn := func(fqdn string) (string, error) {\n\t\treturn \"example.com.\", nil\n\t}\n\n\t// override findZoneByFqdn function\n\tsavedFindZoneByFqdn := provider.findZoneByFqdn\n\n\tt.Cleanup(func() {\n\t\tprovider.findZoneByFqdn = savedFindZoneByFqdn\n\t})\n\n\tprovider.findZoneByFqdn = fakeFindZoneByFqdn\n\n\t// run Present\n\terr := provider.Present(\"abc.def.example.com\", \"\", fakeKeyAuth)\n\trequire.NoError(t, err)\n\n\t// run CleanUp\n\terr = provider.CleanUp(\"abc.def.example.com\", \"\", fakeKeyAuth)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/gandi/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// defaultBaseURL Gandi XML-RPC endpoint used by Present and CleanUp.\nconst defaultBaseURL = \"https://rpc.gandi.net/xmlrpc/\"\n\n// Client the Gandi API client.\ntype Client struct {\n\tapiKey string\n\n\tBaseURL    string\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(apiKey string) *Client {\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tBaseURL:    defaultBaseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\nfunc (c *Client) GetZoneID(ctx context.Context, domain string) (int, error) {\n\tcall := &methodCall{\n\t\tMethodName: \"domain.info\",\n\t\tParams: []param{\n\t\t\tparamString{Value: c.apiKey},\n\t\t\tparamString{Value: domain},\n\t\t},\n\t}\n\n\tresp := &responseStruct{}\n\n\terr := c.rpcCall(ctx, call, resp)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvar zoneID int\n\n\tfor _, member := range resp.StructMembers {\n\t\tif member.Name == \"zone_id\" {\n\t\t\tzoneID = member.ValueInt\n\t\t}\n\t}\n\n\tif zoneID == 0 {\n\t\treturn 0, fmt.Errorf(\"could not find zone_id for %s\", domain)\n\t}\n\n\treturn zoneID, nil\n}\n\nfunc (c *Client) CloneZone(ctx context.Context, zoneID int, name string) (int, error) {\n\tcall := &methodCall{\n\t\tMethodName: \"domain.zone.clone\",\n\t\tParams: []param{\n\t\t\tparamString{Value: c.apiKey},\n\t\t\tparamInt{Value: zoneID},\n\t\t\tparamInt{Value: 0},\n\t\t\tparamStruct{\n\t\t\t\tStructMembers: []structMember{\n\t\t\t\t\tstructMemberString{\n\t\t\t\t\t\tName:  \"name\",\n\t\t\t\t\t\tValue: name,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tresp := &responseStruct{}\n\n\terr := c.rpcCall(ctx, call, resp)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvar newZoneID int\n\n\tfor _, member := range resp.StructMembers {\n\t\tif member.Name == \"id\" {\n\t\t\tnewZoneID = member.ValueInt\n\t\t}\n\t}\n\n\tif newZoneID == 0 {\n\t\treturn 0, errors.New(\"could not determine cloned zone_id\")\n\t}\n\n\treturn newZoneID, nil\n}\n\nfunc (c *Client) NewZoneVersion(ctx context.Context, zoneID int) (int, error) {\n\tcall := &methodCall{\n\t\tMethodName: \"domain.zone.version.new\",\n\t\tParams: []param{\n\t\t\tparamString{Value: c.apiKey},\n\t\t\tparamInt{Value: zoneID},\n\t\t},\n\t}\n\n\tresp := &responseInt{}\n\n\terr := c.rpcCall(ctx, call, resp)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif resp.Value == 0 {\n\t\treturn 0, errors.New(\"could not create new zone version\")\n\t}\n\n\treturn resp.Value, nil\n}\n\nfunc (c *Client) AddTXTRecord(ctx context.Context, zoneID, version int, name, value string, ttl int) error {\n\tcall := &methodCall{\n\t\tMethodName: \"domain.zone.record.add\",\n\t\tParams: []param{\n\t\t\tparamString{Value: c.apiKey},\n\t\t\tparamInt{Value: zoneID},\n\t\t\tparamInt{Value: version},\n\t\t\tparamStruct{\n\t\t\t\tStructMembers: []structMember{\n\t\t\t\t\tstructMemberString{\n\t\t\t\t\t\tName:  \"type\",\n\t\t\t\t\t\tValue: \"TXT\",\n\t\t\t\t\t}, structMemberString{\n\t\t\t\t\t\tName:  \"name\",\n\t\t\t\t\t\tValue: name,\n\t\t\t\t\t}, structMemberString{\n\t\t\t\t\t\tName:  \"value\",\n\t\t\t\t\t\tValue: value,\n\t\t\t\t\t}, structMemberInt{\n\t\t\t\t\t\tName:  \"ttl\",\n\t\t\t\t\t\tValue: ttl,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tresp := &responseStruct{}\n\n\treturn c.rpcCall(ctx, call, resp)\n}\n\nfunc (c *Client) SetZoneVersion(ctx context.Context, zoneID, version int) error {\n\tcall := &methodCall{\n\t\tMethodName: \"domain.zone.version.set\",\n\t\tParams: []param{\n\t\t\tparamString{Value: c.apiKey},\n\t\t\tparamInt{Value: zoneID},\n\t\t\tparamInt{Value: version},\n\t\t},\n\t}\n\n\tresp := &responseBool{}\n\n\terr := c.rpcCall(ctx, call, resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !resp.Value {\n\t\treturn errors.New(\"could not set zone version\")\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) SetZone(ctx context.Context, domain string, zoneID int) error {\n\tcall := &methodCall{\n\t\tMethodName: \"domain.zone.set\",\n\t\tParams: []param{\n\t\t\tparamString{Value: c.apiKey},\n\t\t\tparamString{Value: domain},\n\t\t\tparamInt{Value: zoneID},\n\t\t},\n\t}\n\n\tresp := &responseStruct{}\n\n\terr := c.rpcCall(ctx, call, resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar respZoneID int\n\n\tfor _, member := range resp.StructMembers {\n\t\tif member.Name == \"zone_id\" {\n\t\t\trespZoneID = member.ValueInt\n\t\t}\n\t}\n\n\tif respZoneID != zoneID {\n\t\treturn fmt.Errorf(\"could not set new zone_id for %s\", domain)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) DeleteZone(ctx context.Context, zoneID int) error {\n\tcall := &methodCall{\n\t\tMethodName: \"domain.zone.delete\",\n\t\tParams: []param{\n\t\t\tparamString{Value: c.apiKey},\n\t\t\tparamInt{Value: zoneID},\n\t\t},\n\t}\n\n\tresp := &responseBool{}\n\n\terr := c.rpcCall(ctx, call, resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !resp.Value {\n\t\treturn errors.New(\"could not delete zone_id\")\n\t}\n\n\treturn nil\n}\n\n// rpcCall makes an XML-RPC call to Gandi's RPC endpoint by marshaling the data given in the call argument to XML\n// and sending  that via HTTP Post to Gandi.\n// The response is then unmarshalled into the resp argument.\nfunc (c *Client) rpcCall(ctx context.Context, call *methodCall, result response) error {\n\treq, err := newXMLRequest(ctx, c.BaseURL, call)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = xml.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unmarshal error: %w\", err)\n\t}\n\n\tif result.faultCode() != 0 {\n\t\treturn RPCError{\n\t\t\tFaultCode:   result.faultCode(),\n\t\t\tFaultString: result.faultString(),\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc newXMLRequest(ctx context.Context, endpoint string, payload *methodCall) (*http.Request, error) {\n\tbody := new(bytes.Buffer)\n\tbody.WriteString(xml.Header)\n\n\tencoder := xml.NewEncoder(body)\n\tencoder.Indent(\"\", \"  \")\n\n\terr := encoder.Encode(payload)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"text/xml\")\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/gandi/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"secret\")\n\t\t\tclient.BaseURL = server.URL\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithContentType(\"text/xml\"),\n\t)\n}\n\nfunc TestClient_GetZoneID(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"get_zone_id.xml\"),\n\t\t\tservermock.CheckRequestBodyFromFixture(\"get_zone_id-request.xml\").IgnoreWhitespace()).\n\t\tBuild(t)\n\n\tzoneID, err := client.GetZoneID(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 1, zoneID)\n}\n\nfunc TestClient_CloneZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"clone_zone.xml\"),\n\t\t\tservermock.CheckRequestBodyFromFixture(\"clone_zone-request.xml\").IgnoreWhitespace()).\n\t\tBuild(t)\n\n\tzoneID, err := client.CloneZone(t.Context(), 6, \"foo\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 1, zoneID)\n}\n\nfunc TestClient_NewZoneVersion(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"new_zone_version.xml\"),\n\t\t\tservermock.CheckRequestBodyFromFixture(\"new_zone_version-request.xml\").IgnoreWhitespace()).\n\t\tBuild(t)\n\n\tzoneID, err := client.NewZoneVersion(t.Context(), 6)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 1, zoneID)\n}\n\nfunc TestClient_AddTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"empty.xml\"),\n\t\t\tservermock.CheckRequestBodyFromFixture(\"add_txt_record-request.xml\").IgnoreWhitespace()).\n\t\tBuild(t)\n\n\terr := client.AddTXTRecord(t.Context(), 1, 123, \"foo\", \"content\", 120)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_SetZoneVersion(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"set_zone_version.xml\"),\n\t\t\tservermock.CheckRequestBodyFromFixture(\"set_zone_version-request.xml\").IgnoreWhitespace()).\n\t\tBuild(t)\n\n\terr := client.SetZoneVersion(t.Context(), 1, 123)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_SetZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"set_zone.xml\"),\n\t\t\tservermock.CheckRequestBodyFromFixture(\"set_zone-request.xml\").IgnoreWhitespace()).\n\t\tBuild(t)\n\n\terr := client.SetZone(t.Context(), \"example.com\", 1)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"delete_zone.xml\"),\n\t\t\tservermock.CheckRequestBodyFromFixture(\"delete_zone-request.xml\").IgnoreWhitespace()).\n\t\tBuild(t)\n\n\terr := client.DeleteZone(t.Context(), 1)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/gandi/internal/fixtures/add_txt_record-request.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n    <methodName>domain.zone.record.add</methodName>\n    <param>\n        <value>\n            <string>secret</string>\n        </value>\n    </param>\n    <param>\n        <value>\n            <int>1</int>\n        </value>\n    </param>\n    <param>\n        <value>\n            <int>123</int>\n        </value>\n    </param>\n    <param>\n        <value>\n            <struct>\n                <member>\n                    <name>type</name>\n                    <value>\n                        <string>TXT</string>\n                    </value>\n                </member>\n                <member>\n                    <name>name</name>\n                    <value>\n                        <string>foo</string>\n                    </value>\n                </member>\n                <member>\n                    <name>value</name>\n                    <value>\n                        <string>content</string>\n                    </value>\n                </member>\n                <member>\n                    <name>ttl</name>\n                    <value>\n                        <int>120</int>\n                    </value>\n                </member>\n            </struct>\n        </value>\n    </param>\n</methodCall>\n"
  },
  {
    "path": "providers/dns/gandi/internal/fixtures/clone_zone-request.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n    <methodName>domain.zone.clone</methodName>\n    <param>\n        <value>\n            <string>secret</string>\n        </value>\n    </param>\n    <param>\n        <value>\n            <int>6</int>\n        </value>\n    </param>\n    <param>\n        <value>\n            <int>0</int>\n        </value>\n    </param>\n    <param>\n        <value>\n            <struct>\n                <member>\n                    <name>name</name>\n                    <value>\n                        <string>foo</string>\n                    </value>\n                </member>\n            </struct>\n        </value>\n    </param>\n</methodCall>\n"
  },
  {
    "path": "providers/dns/gandi/internal/fixtures/clone_zone.xml",
    "content": "<responseStruct>\n    <params>\n        <param>\n            <value>\n                <struct>\n                    <member>\n                        <name>id</name>\n                        <value>\n                            <int>1</int>\n                        </value>\n                    </member>\n                    <member>\n                        <name>foo</name>\n                        <value>\n                            <int>2</int>\n                        </value>\n                    </member>\n                </struct>\n            </value>\n        </param>\n    </params>\n</responseStruct>\n"
  },
  {
    "path": "providers/dns/gandi/internal/fixtures/delete_zone-request.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n    <methodName>domain.zone.delete</methodName>\n    <param>\n        <value>\n            <string>secret</string>\n        </value>\n    </param>\n    <param>\n        <value>\n            <int>1</int>\n        </value>\n    </param>\n</methodCall>\n"
  },
  {
    "path": "providers/dns/gandi/internal/fixtures/delete_zone.xml",
    "content": "<responseBool>\n    <params>\n        <param>\n            <value>\n                <boolean>true</boolean>\n            </value>\n        </param>\n    </params>\n</responseBool>\n"
  },
  {
    "path": "providers/dns/gandi/internal/fixtures/empty.xml",
    "content": "<responseStruct>\n</responseStruct>\n"
  },
  {
    "path": "providers/dns/gandi/internal/fixtures/get_zone_id-request.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n\t<methodName>domain.info</methodName>\n\t<param>\n\t\t<value>\n\t\t\t<string>secret</string>\n\t\t</value>\n\t</param>\n\t<param>\n\t\t<value>\n\t\t\t<string>example.com</string>\n\t\t</value>\n\t</param>\n</methodCall>\n"
  },
  {
    "path": "providers/dns/gandi/internal/fixtures/get_zone_id.xml",
    "content": "<responseStruct>\n    <params>\n        <param>\n            <value>\n                <struct>\n                    <member>\n                        <name>zone_id</name>\n                        <value>\n                            <int>1</int>\n                        </value>\n                    </member>\n                    <member>\n                        <name>foo</name>\n                        <value>\n                            <int>2</int>\n                        </value>\n                    </member>\n                </struct>\n            </value>\n        </param>\n    </params>\n</responseStruct>\n"
  },
  {
    "path": "providers/dns/gandi/internal/fixtures/new_zone_version-request.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n    <methodName>domain.zone.version.new</methodName>\n    <param>\n        <value>\n            <string>secret</string>\n        </value>\n    </param>\n    <param>\n        <value>\n            <int>6</int>\n        </value>\n    </param>\n</methodCall>\n"
  },
  {
    "path": "providers/dns/gandi/internal/fixtures/new_zone_version.xml",
    "content": "<responseInt>\n    <params>\n        <param>\n            <value>\n                <int>1</int>\n            </value>\n        </param>\n    </params>\n</responseInt>\n"
  },
  {
    "path": "providers/dns/gandi/internal/fixtures/set_zone-request.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n    <methodName>domain.zone.set</methodName>\n    <param>\n        <value>\n            <string>secret</string>\n        </value>\n    </param>\n    <param>\n        <value>\n            <string>example.com</string>\n        </value>\n    </param>\n    <param>\n        <value>\n            <int>1</int>\n        </value>\n    </param>\n</methodCall>\n"
  },
  {
    "path": "providers/dns/gandi/internal/fixtures/set_zone.xml",
    "content": "<responseStruct>\n    <params>\n        <param>\n            <value>\n                <struct>\n                    <member>\n                        <name>zone_id</name>\n                        <value>\n                            <int>1</int>\n                        </value>\n                    </member>\n                    <member>\n                        <name>foo</name>\n                        <value>\n                            <int>2</int>\n                        </value>\n                    </member>\n                </struct>\n            </value>\n        </param>\n    </params>\n</responseStruct>\n"
  },
  {
    "path": "providers/dns/gandi/internal/fixtures/set_zone_version-request.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n    <methodName>domain.zone.version.set</methodName>\n    <param>\n        <value>\n            <string>secret</string>\n        </value>\n    </param>\n    <param>\n        <value>\n            <int>1</int>\n        </value>\n    </param>\n    <param>\n        <value>\n            <int>123</int>\n        </value>\n    </param>\n</methodCall>\n"
  },
  {
    "path": "providers/dns/gandi/internal/fixtures/set_zone_version.xml",
    "content": "<responseBool>\n    <params>\n        <param>\n            <value>\n                <boolean>true</boolean>\n            </value>\n        </param>\n    </params>\n</responseBool>\n"
  },
  {
    "path": "providers/dns/gandi/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"encoding/xml\"\n\t\"fmt\"\n)\n\n// types for XML-RPC method calls and parameters\n\ntype param interface {\n\tparam()\n}\n\ntype paramString struct {\n\tXMLName xml.Name `xml:\"param\"`\n\tValue   string   `xml:\"value>string\"`\n}\n\ntype paramInt struct {\n\tXMLName xml.Name `xml:\"param\"`\n\tValue   int      `xml:\"value>int\"`\n}\n\ntype structMember interface {\n\tstructMember()\n}\n\ntype structMemberString struct {\n\tName  string `xml:\"name\"`\n\tValue string `xml:\"value>string\"`\n}\n\ntype structMemberInt struct {\n\tName  string `xml:\"name\"`\n\tValue int    `xml:\"value>int\"`\n}\n\ntype paramStruct struct {\n\tXMLName       xml.Name       `xml:\"param\"`\n\tStructMembers []structMember `xml:\"value>struct>member\"`\n}\n\nfunc (p paramString) param()               {}\nfunc (p paramInt) param()                  {}\nfunc (m structMemberString) structMember() {}\nfunc (m structMemberInt) structMember()    {}\nfunc (p paramStruct) param()               {}\n\ntype methodCall struct {\n\tXMLName    xml.Name `xml:\"methodCall\"`\n\tMethodName string   `xml:\"methodName\"`\n\tParams     []param  `xml:\"params\"`\n}\n\n// types for XML-RPC responses\n\ntype response interface {\n\tfaultCode() int\n\tfaultString() string\n}\n\ntype responseFault struct {\n\tFaultCode   int    `xml:\"fault>value>struct>member>value>int\"`\n\tFaultString string `xml:\"fault>value>struct>member>value>string\"`\n}\n\nfunc (r responseFault) faultCode() int      { return r.FaultCode }\nfunc (r responseFault) faultString() string { return r.FaultString }\n\ntype responseStruct struct {\n\tresponseFault\n\n\tStructMembers []struct {\n\t\tName     string `xml:\"name\"`\n\t\tValueInt int    `xml:\"value>int\"`\n\t} `xml:\"params>param>value>struct>member\"`\n}\n\ntype responseInt struct {\n\tresponseFault\n\n\tValue int `xml:\"params>param>value>int\"`\n}\n\ntype responseBool struct {\n\tresponseFault\n\n\tValue bool `xml:\"params>param>value>boolean\"`\n}\n\ntype RPCError struct {\n\tFaultCode   int\n\tFaultString string\n}\n\nfunc (e RPCError) Error() string {\n\treturn fmt.Sprintf(\"Gandi DNS: RPC Error: (%d) %s\", e.FaultCode, e.FaultString)\n}\n"
  },
  {
    "path": "providers/dns/gandiv5/gandiv5.go",
    "content": "// Package gandiv5 implements a DNS provider for solving the DNS-01 challenge using Gandi LiveDNS api.\npackage gandiv5\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/gandiv5/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"GANDIV5_\"\n\n\tEnvAPIKey              = envNamespace + \"API_KEY\"\n\tEnvPersonalAccessToken = envNamespace + \"PERSONAL_ACCESS_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst minTTL = 300\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// inProgressInfo contains information about an in-progress challenge.\ntype inProgressInfo struct {\n\tfieldName string\n\tauthZone  string\n}\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL             string\n\tAPIKey              string // Deprecated use PersonalAccessToken\n\tPersonalAccessToken string\n\tPropagationTimeout  time.Duration\n\tPollingInterval     time.Duration\n\tTTL                 int\n\tHTTPClient          *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 20*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 20*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\tinProgressFQDNs map[string]inProgressInfo\n\tinProgressMu    sync.Mutex\n\n\t// findZoneByFqdn determines the DNS zone of a FQDN.\n\t// It is overridden during tests.\n\t// only for testing purpose.\n\tfindZoneByFqdn func(fqdn string) (string, error)\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Gandi.\n// Credentials must be passed in the environment variable: GANDIV5_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\t// TODO(ldez): rewrite this when APIKey will be removed.\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = env.GetOrFile(EnvAPIKey)\n\tconfig.PersonalAccessToken = env.GetOrFile(EnvPersonalAccessToken)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Gandi.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"gandiv5: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey != \"\" {\n\t\tlog.Print(\"gandiv5: API Key is deprecated, use Personal Access Token instead\")\n\t}\n\n\tif config.APIKey == \"\" && config.PersonalAccessToken == \"\" {\n\t\treturn nil, errors.New(\"gandiv5: credentials information are missing\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"gandiv5: invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\tclient := internal.NewClient(config.APIKey, config.PersonalAccessToken)\n\n\tif config.BaseURL != \"\" {\n\t\tbaseURL, err := url.Parse(config.BaseURL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"gandiv5: %w\", err)\n\t\t}\n\n\t\tclient.BaseURL = baseURL\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:          config,\n\t\tclient:          client,\n\t\tinProgressFQDNs: make(map[string]inProgressInfo),\n\t\tfindZoneByFqdn:  dns01.FindZoneByFqdn,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// find authZone\n\tauthZone, err := d.findZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gandiv5: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// determine name of TXT record\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gandiv5: %w\", err)\n\t}\n\n\t// acquire lock and check there is not a challenge already in\n\t// progress for this value of authZone\n\td.inProgressMu.Lock()\n\tdefer d.inProgressMu.Unlock()\n\n\t// add TXT record into authZone\n\terr = d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(authZone), subDomain, info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// save data necessary for CleanUp\n\td.inProgressFQDNs[info.EffectiveFQDN] = inProgressInfo{\n\t\tauthZone:  authZone,\n\t\tfieldName: subDomain,\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// acquire lock and retrieve authZone\n\td.inProgressMu.Lock()\n\tdefer d.inProgressMu.Unlock()\n\n\tif _, ok := d.inProgressFQDNs[info.EffectiveFQDN]; !ok {\n\t\t// if there is no cleanup information then just return\n\t\treturn nil\n\t}\n\n\tfieldName := d.inProgressFQDNs[info.EffectiveFQDN].fieldName\n\tauthZone := d.inProgressFQDNs[info.EffectiveFQDN].authZone\n\tdelete(d.inProgressFQDNs, info.EffectiveFQDN)\n\n\t// delete TXT record from authZone\n\terr := d.client.DeleteTXTRecord(context.Background(), dns01.UnFqdn(authZone), fieldName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gandiv5: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/gandiv5/gandiv5.toml",
    "content": "Name = \"Gandi Live DNS (v5)\"\nDescription = ''''''\nURL = \"https://www.gandi.net\"\nCode = \"gandiv5\"\nSince = \"v0.5.0\"\n\nExample = '''\nGANDIV5_PERSONAL_ACCESS_TOKEN=abcdefghijklmnopqrstuvwx \\\nlego --dns gandiv5 -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    GANDIV5_PERSONAL_ACCESS_TOKEN = \"Personal Access Token\"\n    GANDIV5_API_KEY = \"API key (Deprecated)\"\n  [Configuration.Additional]\n    GANDIV5_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 20)\"\n    GANDIV5_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 1200)\"\n    GANDIV5_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    GANDIV5_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://api.gandi.net/docs/livedns/\"\n"
  },
  {
    "path": "providers/dns/gandiv5/gandiv5_test.go",
    "content": "package gandiv5\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar envTest = tester.NewEnvTest(EnvAPIKey, EnvPersonalAccessToken)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"gandiv5: credentials information are missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.inProgressFQDNs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"gandiv5: credentials information are missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.inProgressFQDNs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestDNSProvider runs Present and CleanUp against a fake Gandi RPC\n// Server, whose responses are predetermined for particular requests.\nfunc TestDNSProvider(t *testing.T) {\n\tprovider := servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.PersonalAccessToken = \"123412341234123412341234\"\n\t\t\tconfig.BaseURL = server.URL\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer 123412341234123412341234\"),\n\t).\n\t\tRoute(\"GET /domains/example.com/records/_acme-challenge.abc.def/TXT\",\n\t\t\tservermock.RawStringResponse(`{\"rrset_ttl\":300,\"rrset_values\":[],\"rrset_name\":\"_acme-challenge.abc.def\",\"rrset_type\":\"TXT\"}`)).\n\t\tRoute(\"PUT /domains/example.com/records/_acme-challenge.abc.def/TXT\",\n\t\t\tservermock.RawStringResponse(`{\"message\": \"Zone Record Created\"}`),\n\t\t\tservermock.CheckRequestJSONBody(`{\"rrset_ttl\":300,\"rrset_values\":[\"ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ\"]}`)).\n\t\tRoute(\"DELETE /domains/example.com/records/_acme-challenge.abc.def/TXT\", nil).\n\t\tBuild(t)\n\n\tfakeKeyAuth := \"XXXX\"\n\n\t// define function to override findZoneByFqdn with\n\tfakeFindZoneByFqdn := func(fqdn string) (string, error) {\n\t\treturn \"example.com.\", nil\n\t}\n\n\t// override findZoneByFqdn function\n\tsavedFindZoneByFqdn := provider.findZoneByFqdn\n\n\tdefer func() {\n\t\tprovider.findZoneByFqdn = savedFindZoneByFqdn\n\t}()\n\n\tprovider.findZoneByFqdn = fakeFindZoneByFqdn\n\n\t// run Present\n\terr := provider.Present(\"abc.def.example.com\", \"\", fakeKeyAuth)\n\trequire.NoError(t, err)\n\n\t// run CleanUp\n\terr = provider.CleanUp(\"abc.def.example.com\", \"\", fakeKeyAuth)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/gandiv5/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// defaultBaseURL endpoint is the Gandi API endpoint used by Present and CleanUp.\nconst defaultBaseURL = \"https://api.gandi.net/v5/livedns\"\n\n// Related to Personal Access Token.\nconst authorizationHeader = \"Authorization\"\n\n// Client the Gandi API v5 client.\ntype Client struct {\n\tapiKey string\n\tpat    string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(apiKey, pat string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tpat:        pat,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\nfunc (c *Client) AddTXTRecord(ctx context.Context, domain, name, value string, ttl int) error {\n\t// Get exiting values for the TXT records\n\t// Needed to create challenges for both wildcard and base name domains\n\ttxtRecord, err := c.getTXTRecord(ctx, domain, name)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvalues := []string{value}\n\tif len(txtRecord.RRSetValues) > 0 {\n\t\tvalues = append(values, txtRecord.RRSetValues...)\n\t}\n\n\tnewRecord := &Record{RRSetTTL: ttl, RRSetValues: values}\n\n\terr = c.addTXTRecord(ctx, domain, name, newRecord)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) getTXTRecord(ctx context.Context, domain, name string) (*Record, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", domain, \"records\", name, \"TXT\")\n\n\t// Get exiting values for the TXT records\n\t// Needed to create challenges for both wildcard and base name domains\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttxtRecord := &Record{}\n\n\terr = c.do(req, txtRecord)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to get TXT records for domain %s and name %s: %w\", domain, name, err)\n\t}\n\n\treturn txtRecord, nil\n}\n\nfunc (c *Client) addTXTRecord(ctx context.Context, domain, name string, newRecord *Record) error {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", domain, \"records\", name, \"TXT\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, newRecord)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmessage := apiResponse{}\n\n\terr = c.do(req, &message)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create TXT record for domain %s and name %s: %w\", domain, name, err)\n\t}\n\n\tif message.Message != \"\" {\n\t\tlog.Infof(\"API response: %s\", message.Message)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) DeleteTXTRecord(ctx context.Context, domain, name string) error {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", domain, \"records\", name, \"TXT\")\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmessage := apiResponse{}\n\n\terr = c.do(req, &message)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to delete TXT record for domain %s and name %s: %w\", domain, name, err)\n\t}\n\n\tif message.Message != \"\" {\n\t\tlog.Infof(\"API response: %s\", message.Message)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tif c.apiKey != \"\" {\n\t\treq.Header.Set(authorizationHeader, \"Apikey \"+c.apiKey)\n\t}\n\n\tif c.pat != \"\" {\n\t\treq.Header.Set(authorizationHeader, \"Bearer \"+c.pat)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\terr = checkResponse(req, resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif len(raw) > 0 {\n\t\terr = json.Unmarshal(raw, result)\n\t\tif err != nil {\n\t\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc checkResponse(req *http.Request, resp *http.Response) error {\n\tif resp.StatusCode == http.StatusNotFound && resp.Request.Method == http.MethodGet {\n\t\treturn nil\n\t}\n\n\tif resp.StatusCode < http.StatusBadRequest {\n\t\treturn nil\n\t}\n\n\treturn parseError(req, resp)\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tresponse := apiResponse{}\n\n\terr := json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"%d: request failed: %s\", resp.StatusCode, response.Message)\n}\n"
  },
  {
    "path": "providers/dns/gandiv5/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder(apiKey, pat string) *servermock.Builder[*Client] {\n\tcheckHeaders := servermock.CheckHeader().WithJSONHeaders()\n\n\tif apiKey != \"\" {\n\t\tcheckHeaders = checkHeaders.WithAuthorization(\"Apikey secret-apikey\")\n\t} else {\n\t\tcheckHeaders = checkHeaders.WithAuthorization(\"Bearer secret-pat\")\n\t}\n\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(apiKey, pat)\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tcheckHeaders,\n\t)\n}\n\nfunc TestClient_AddTXTRecord(t *testing.T) {\n\tclient := mockBuilder(\"secret-apikey\", \"\").\n\t\tRoute(\"GET /domains/example.com/records/foo/TXT\",\n\t\t\tservermock.ResponseFromFixture(\"add_txt_record_get.json\")).\n\t\tRoute(\"PUT /domains/example.com/records/foo/TXT\",\n\t\t\tservermock.ResponseFromFixture(\"api_response.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"rrset_ttl\":120,\"rrset_values\":[\"content\",\"value1\"]}`)).\n\t\tBuild(t)\n\n\terr := client.AddTXTRecord(t.Context(), \"example.com\", \"foo\", \"content\", 120)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteTXTRecord(t *testing.T) {\n\tclient := mockBuilder(\"\", \"secret-pat\").\n\t\tRoute(\"DELETE /domains/example.com/records/foo/TXT\",\n\t\t\tservermock.ResponseFromFixture(\"api_response.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteTXTRecord(t.Context(), \"example.com\", \"foo\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/gandiv5/internal/fixtures/add_txt_record_get.json",
    "content": "{\n  \"rrset_ttl\": 120,\n  \"rrset_values\": [\n    \"value1\"\n  ],\n  \"rrset_name\": \"foo\",\n  \"rrset_type\": \"TXT\"\n}\n"
  },
  {
    "path": "providers/dns/gandiv5/internal/fixtures/api_response.json",
    "content": "{\n  \"message\":  \"test\",\n  \"uuid\": \"123456789\"\n}\n"
  },
  {
    "path": "providers/dns/gandiv5/internal/types.go",
    "content": "package internal\n\n// types for JSON responses with only a message.\ntype apiResponse struct {\n\tMessage string `json:\"message\"`\n\tUUID    string `json:\"uuid,omitempty\"`\n}\n\n// Record TXT record representation.\ntype Record struct {\n\tRRSetTTL    int      `json:\"rrset_ttl\"`\n\tRRSetValues []string `json:\"rrset_values\"`\n\tRRSetName   string   `json:\"rrset_name,omitempty\"`\n\tRRSetType   string   `json:\"rrset_type,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/gcloud/fixtures/gce_account_service_file.json",
    "content": "{\n  \"project_id\": \"A\",\n  \"type\": \"service_account\",\n  \"client_email\": \"foo@bar.com\",\n  \"private_key_id\": \"pki\",\n  \"private_key\": \"pk\",\n  \"token_uri\": \"/token\",\n  \"client_secret\": \"secret\",\n  \"client_id\": \"C\",\n  \"refresh_token\": \"D\"\n}"
  },
  {
    "path": "providers/dns/gcloud/gcloud.toml",
    "content": "Name = \"Google Cloud\"\nDescription = ''''''\nURL = \"https://cloud.google.com\"\nCode = \"gcloud\"\nSince = \"v0.3.0\"\n\nExample = '''\n# Using a service account file\nGCE_PROJECT=\"gc-project-id\" \\\nGCE_SERVICE_ACCOUNT_FILE=\"/path/to/svc/account/file.json\" \\\nlego --dns gcloud -d '*.example.com' -d example.com run\n\n# Using default credentials with impersonation\nGCE_PROJECT=\"gc-project-id\" \\\nGCE_IMPERSONATE_SERVICE_ACCOUNT=\"target-sa@gc-project-id.iam.gserviceaccount.com\" \\\nlego --dns gcloud -d '*.example.com' -d example.com run\n\n# Using service account key with impersonation\nGCE_PROJECT=\"gc-project-id\" \\\nGCE_SERVICE_ACCOUNT_FILE=\"/path/to/svc/account/file.json\" \\\nGCE_IMPERSONATE_SERVICE_ACCOUNT=\"target-sa@gc-project-id.iam.gserviceaccount.com\" \\\nlego --dns gcloud -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\nSupports service account impersonation to access Google Cloud DNS resources across different projects or with restricted permissions.\n\nWhen using impersonation, the source service account must have:\n1. The \"Service Account Token Creator\" role on the source service account\n2. The \"https://www.googleapis.com/auth/cloud-platform\" scope\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    GCE_PROJECT = \"Project name (by default, the project name is auto-detected by using the metadata service)\"\n    'Application Default Credentials' = \"[Documentation](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application)\"\n    GCE_SERVICE_ACCOUNT_FILE = \"Account file path\"\n    GCE_SERVICE_ACCOUNT = \"Account\"\n  [Configuration.Additional]\n    GCE_ALLOW_PRIVATE_ZONE = \"Allows requested domain to be in private DNS zone, works only with a private ACME server (by default: false)\"\n    GCE_ZONE_ID = \"Allows to skip the automatic detection of the zone\"\n    GCE_IMPERSONATE_SERVICE_ACCOUNT = \"Service account email to impersonate\"\n    GCE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 5)\"\n    GCE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 180)\"\n    GCE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n\n[Links]\n  API = \"https://cloud.google.com/dns/api/v1/\"\n  GoClient = \"https://github.com/googleapis/google-api-go-client\"\n"
  },
  {
    "path": "providers/dns/gcloud/googlecloud.go",
    "content": "// Package gcloud implements a DNS provider for solving the DNS-01 challenge using Google Cloud DNS.\npackage gcloud\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"cloud.google.com/go/compute/metadata\"\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/miekg/dns\"\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/google\"\n\tgdns \"google.golang.org/api/dns/v1\"\n\t\"google.golang.org/api/googleapi\"\n\t\"google.golang.org/api/impersonate\"\n\t\"google.golang.org/api/option\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"GCE_\"\n\n\tEnvServiceAccount            = envNamespace + \"SERVICE_ACCOUNT\"\n\tEnvProject                   = envNamespace + \"PROJECT\"\n\tEnvZoneID                    = envNamespace + \"ZONE_ID\"\n\tEnvAllowPrivateZone          = envNamespace + \"ALLOW_PRIVATE_ZONE\"\n\tEnvDebug                     = envNamespace + \"DEBUG\"\n\tEnvImpersonateServiceAccount = envNamespace + \"IMPERSONATE_SERVICE_ACCOUNT\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nconst changeStatusDone = \"done\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tDebug                     bool\n\tProject                   string\n\tZoneID                    string\n\tAllowPrivateZone          bool\n\tImpersonateServiceAccount string\n\tPropagationTimeout        time.Duration\n\tPollingInterval           time.Duration\n\tTTL                       int\n\tHTTPClient                *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tDebug:                     env.GetOrDefaultBool(EnvDebug, false),\n\t\tZoneID:                    env.GetOrDefaultString(EnvZoneID, \"\"),\n\t\tAllowPrivateZone:          env.GetOrDefaultBool(EnvAllowPrivateZone, false),\n\t\tImpersonateServiceAccount: env.GetOrDefaultString(EnvImpersonateServiceAccount, \"\"),\n\t\tTTL:                       env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout:        env.GetOrDefaultSecond(EnvPropagationTimeout, 180*time.Second),\n\t\tPollingInterval:           env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *gdns.Service\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Google Cloud DNS.\n// By default, the project name is auto-detected by using the metadata service,\n// it can be overridden using the GCE_PROJECT environment variable.\n// A Service Account can be passed in the environment variable: GCE_SERVICE_ACCOUNT\n// or by specifying the keyfile location: GCE_SERVICE_ACCOUNT_FILE.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\t// Use a service account file if specified via environment variable.\n\tif saKey := env.GetOrFile(EnvServiceAccount); saKey != \"\" {\n\t\treturn NewDNSProviderServiceAccountKey([]byte(saKey))\n\t}\n\n\t// Use default credentials.\n\tproject := env.GetOrDefaultString(EnvProject, autodetectProjectID(context.Background()))\n\n\treturn NewDNSProviderCredentials(project)\n}\n\n// NewDNSProviderCredentials uses the supplied credentials\n// to return a DNSProvider instance configured for Google Cloud DNS.\nfunc NewDNSProviderCredentials(project string) (*DNSProvider, error) {\n\tif project == \"\" {\n\t\treturn nil, errors.New(\"googlecloud: project name missing\")\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Project = project\n\n\tvar err error\n\n\tconfig.HTTPClient, err = newClientFromCredentials(context.Background(), config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"googlecloud: %w\", err)\n\t}\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderServiceAccountKey uses the supplied service account JSON\n// to return a DNSProvider instance configured for Google Cloud DNS.\nfunc NewDNSProviderServiceAccountKey(saKey []byte) (*DNSProvider, error) {\n\tif len(saKey) == 0 {\n\t\treturn nil, errors.New(\"googlecloud: Service Account is missing\")\n\t}\n\n\t// If GCE_PROJECT is non-empty it overrides the project in the service\n\t// account file.\n\tproject := env.GetOrDefaultString(EnvProject, \"\")\n\tif project == \"\" {\n\t\t// read project id from service account file\n\t\tvar datJSON struct {\n\t\t\tProjectID string `json:\"project_id\"`\n\t\t}\n\n\t\terr := json.Unmarshal(saKey, &datJSON)\n\t\tif err != nil || datJSON.ProjectID == \"\" {\n\t\t\treturn nil, errors.New(\"googlecloud: project ID not found in Google Cloud Service Account file\")\n\t\t}\n\n\t\tproject = datJSON.ProjectID\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Project = project\n\n\tvar err error\n\n\tconfig.HTTPClient, err = newClientFromServiceAccountKey(context.Background(), config, saKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"googlecloud: %w\", err)\n\t}\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderServiceAccount uses the supplied service account JSON file\n// to return a DNSProvider instance configured for Google Cloud DNS.\nfunc NewDNSProviderServiceAccount(saFile string) (*DNSProvider, error) {\n\tif saFile == \"\" {\n\t\treturn nil, errors.New(\"googlecloud: Service Account file missing\")\n\t}\n\n\tsaKey, err := os.ReadFile(saFile)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"googlecloud: unable to read Service Account file: %w\", err)\n\t}\n\n\treturn NewDNSProviderServiceAccountKey(saKey)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Google Cloud DNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"googlecloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.HTTPClient == nil {\n\t\treturn nil, errors.New(\"googlecloud: unable to create Google Cloud DNS service: client is nil\")\n\t}\n\n\tsvc, err := gdns.NewService(context.Background(), option.WithHTTPClient(clientdebug.Wrap(config.HTTPClient)))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"googlecloud: unable to create Google Cloud DNS service: %w\", err)\n\t}\n\n\treturn &DNSProvider{config: config, client: svc}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getHostedZone(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"googlecloud: %w\", err)\n\t}\n\n\t// Look for existing records.\n\texistingRrSet, err := d.findTxtRecords(zone, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"googlecloud: %w\", err)\n\t}\n\n\tfor _, rrSet := range existingRrSet {\n\t\tvar rrd []string\n\n\t\tfor _, rr := range rrSet.Rrdatas {\n\t\t\tdata := mustUnquote(rr)\n\t\t\trrd = append(rrd, data)\n\n\t\t\tif data == info.Value {\n\t\t\t\tlog.Printf(\"skip: the record already exists: %s\", info.Value)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\trrSet.Rrdatas = rrd\n\t}\n\n\t// Attempt to delete the existing records before adding the new one.\n\tif len(existingRrSet) > 0 {\n\t\tif err = d.applyChanges(ctx, zone, &gdns.Change{Deletions: existingRrSet}); err != nil {\n\t\t\treturn fmt.Errorf(\"googlecloud: %w\", err)\n\t\t}\n\t}\n\n\trec := &gdns.ResourceRecordSet{\n\t\tName:    info.EffectiveFQDN,\n\t\tRrdatas: []string{info.Value},\n\t\tTtl:     int64(d.config.TTL),\n\t\tType:    \"TXT\",\n\t}\n\n\t// Append existing TXT record data to the new TXT record data\n\tfor _, rrSet := range existingRrSet {\n\t\tfor _, rr := range rrSet.Rrdatas {\n\t\t\tif rr != info.Value {\n\t\t\t\trec.Rrdatas = append(rec.Rrdatas, rr)\n\t\t\t}\n\t\t}\n\t}\n\n\tchange := &gdns.Change{\n\t\tAdditions: []*gdns.ResourceRecordSet{rec},\n\t}\n\n\tif err = d.applyChanges(ctx, zone, change); err != nil {\n\t\treturn fmt.Errorf(\"googlecloud: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) applyChanges(ctx context.Context, zone string, change *gdns.Change) error {\n\tif d.config.Debug {\n\t\tdata, _ := json.Marshal(change)\n\t\tlog.Printf(\"change (Create): %s\", string(data))\n\t}\n\n\tchg, err := d.client.Changes.Create(d.config.Project, zone, change).Do()\n\tif err != nil {\n\t\tvar v *googleapi.Error\n\t\tif errors.As(err, &v) && v.Code == http.StatusNotFound {\n\t\t\treturn nil\n\t\t}\n\n\t\tdata, _ := json.Marshal(change)\n\n\t\treturn fmt.Errorf(\"failed to perform changes [zone %s, change %s]: %w\", zone, string(data), err)\n\t}\n\n\tif chg.Status == changeStatusDone {\n\t\treturn nil\n\t}\n\n\tchgID := chg.Id\n\n\t// wait for change to be acknowledged\n\treturn wait.Retry(ctx,\n\t\tfunc() error {\n\t\t\tif d.config.Debug {\n\t\t\t\tdata, _ := json.Marshal(change)\n\t\t\t\tlog.Printf(\"change (Get): %s\", string(data))\n\t\t\t}\n\n\t\t\tchg, err = d.client.Changes.Get(d.config.Project, zone, chgID).Do()\n\t\t\tif err != nil {\n\t\t\t\tdata, _ := json.Marshal(change)\n\t\t\t\treturn fmt.Errorf(\"failed to get changes [zone %s, change %s]: %w\", zone, string(data), err)\n\t\t\t}\n\n\t\t\tif chg.Status != changeStatusDone {\n\t\t\t\treturn fmt.Errorf(\"status: %s\", chg.Status)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t\tbackoff.WithBackOff(backoff.NewConstantBackOff(3*time.Second)),\n\t\tbackoff.WithMaxElapsedTime(30*time.Second),\n\t)\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getHostedZone(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"googlecloud: %w\", err)\n\t}\n\n\trecords, err := d.findTxtRecords(zone, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"googlecloud: %w\", err)\n\t}\n\n\tif len(records) == 0 {\n\t\treturn nil\n\t}\n\n\t_, err = d.client.Changes.Create(d.config.Project, zone, &gdns.Change{Deletions: records}).Do()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"googlecloud: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout customizes the timeout values used by the ACME package for checking\n// DNS record validity.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// getHostedZone returns the managed-zone.\nfunc (d *DNSProvider) getHostedZone(domain string) (string, error) {\n\tauthZone, zones, err := d.lookupHostedZoneID(domain)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(zones) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no matching domain found for domain %s\", authZone)\n\t}\n\n\tfor _, z := range zones {\n\t\tif z.Visibility == \"public\" || z.Visibility == \"\" || (z.Visibility == \"private\" && d.config.AllowPrivateZone) {\n\t\t\treturn z.Name, nil\n\t\t}\n\t}\n\n\tif d.config.AllowPrivateZone {\n\t\treturn \"\", fmt.Errorf(\"no public or private zone found for domain %s\", authZone)\n\t}\n\n\treturn \"\", fmt.Errorf(\"no public zone found for domain %s\", authZone)\n}\n\n// lookupHostedZoneID finds the managed zone ID in Google.\n//\n// Be careful here.\n// An automated system might run in a GCloud Service Account, with access to edit the zone\n//\n//\t(gcloud dns managed-zones get-iam-policy $zone_id) (role roles/dns.admin)\n//\n// but not with project-wide access to list all zones\n//\n//\t(gcloud projects get-iam-policy $project_id) (a role with permission dns.managedZones.list)\n//\n// If we force a zone list to succeed, we demand more permissions than needed.\nfunc (d *DNSProvider) lookupHostedZoneID(domain string) (string, []*gdns.ManagedZone, error) {\n\t// GCE_ZONE_ID override for service accounts to avoid needing zones-list permission\n\tif d.config.ZoneID != \"\" {\n\t\tzone, err := d.client.ManagedZones.Get(d.config.Project, d.config.ZoneID).Do()\n\t\tif err != nil {\n\t\t\treturn \"\", nil, fmt.Errorf(\"API call ManagedZones.Get for explicit zone ID %q in project %q failed: %w\", d.config.ZoneID, d.config.Project, err)\n\t\t}\n\n\t\treturn zone.DnsName, []*gdns.ManagedZone{zone}, nil\n\t}\n\n\tauthZone, err := dns01.FindZoneByFqdn(dns.Fqdn(domain))\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tzones, err := d.client.ManagedZones.\n\t\tList(d.config.Project).\n\t\tDnsName(authZone).\n\t\tDo()\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"API call ManagedZones.List failed: %w\", err)\n\t}\n\n\treturn authZone, zones.ManagedZones, nil\n}\n\nfunc (d *DNSProvider) findTxtRecords(zone, fqdn string) ([]*gdns.ResourceRecordSet, error) {\n\trecs, err := d.client.ResourceRecordSets.List(d.config.Project, zone).Name(fqdn).Type(\"TXT\").Do()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn recs.Rrsets, nil\n}\n\nfunc newClientFromCredentials(ctx context.Context, config *Config) (*http.Client, error) {\n\tif config.ImpersonateServiceAccount != \"\" {\n\t\tts, err := google.DefaultTokenSource(ctx, \"https://www.googleapis.com/auth/cloud-platform\")\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to get default token source: %w\", err)\n\t\t}\n\n\t\treturn newImpersonateClient(ctx, config.ImpersonateServiceAccount, ts)\n\t}\n\n\tclient, err := google.DefaultClient(ctx, gdns.NdevClouddnsReadwriteScope)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to get Google Cloud client: %w\", err)\n\t}\n\n\treturn client, nil\n}\n\nfunc newClientFromServiceAccountKey(ctx context.Context, config *Config, saKey []byte) (*http.Client, error) {\n\tif config.ImpersonateServiceAccount != \"\" {\n\t\tconf, err := google.JWTConfigFromJSON(saKey, \"https://www.googleapis.com/auth/cloud-platform\")\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to acquire config: %w\", err)\n\t\t}\n\n\t\treturn newImpersonateClient(ctx, config.ImpersonateServiceAccount, conf.TokenSource(ctx))\n\t}\n\n\tconf, err := google.JWTConfigFromJSON(saKey, gdns.NdevClouddnsReadwriteScope)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to acquire config: %w\", err)\n\t}\n\n\treturn conf.Client(ctx), nil\n}\n\nfunc newImpersonateClient(ctx context.Context, impersonateServiceAccount string, ts oauth2.TokenSource) (*http.Client, error) {\n\timpersonatedTS, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{\n\t\tTargetPrincipal: impersonateServiceAccount,\n\t\tScopes:          []string{gdns.NdevClouddnsReadwriteScope},\n\t}, option.WithTokenSource(ts))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create impersonated credentials: %w\", err)\n\t}\n\n\treturn oauth2.NewClient(ctx, impersonatedTS), nil\n}\n\nfunc mustUnquote(raw string) string {\n\tclean, err := strconv.Unquote(raw)\n\tif err != nil {\n\t\treturn raw\n\t}\n\n\treturn clean\n}\n\nfunc autodetectProjectID(ctx context.Context) string {\n\tif pid, err := metadata.ProjectIDWithContext(ctx); err == nil {\n\t\treturn pid\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "providers/dns/gcloud/googlecloud_test.go",
    "content": "package gcloud\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sort\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/oauth2/google\"\n\t\"google.golang.org/api/dns/v1\"\n)\n\nconst (\n\tenvDomain = envNamespace + \"DOMAIN\"\n\n\tenvServiceAccountFile = envNamespace + \"SERVICE_ACCOUNT_FILE\"\n\tenvMetadataHost       = envNamespace + \"METADATA_HOST\"\n\n\tenvGoogleApplicationCredentials = \"GOOGLE_APPLICATION_CREDENTIALS\"\n)\n\nvar envTest = tester.NewEnvTest(\n\tEnvProject,\n\tenvServiceAccountFile,\n\tenvGoogleApplicationCredentials,\n\tenvMetadataHost,\n\tEnvServiceAccount,\n\tEnvImpersonateServiceAccount).\n\tWithDomain(envDomain).\n\tWithLiveTestExtra(func() bool {\n\t\t_, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope)\n\t\treturn err == nil\n\t})\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"invalid credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvProject:            \"123\",\n\t\t\t\tenvServiceAccountFile: \"\",\n\t\t\t\t// as Travis run on GCE, we have to alter env\n\t\t\t\tenvGoogleApplicationCredentials: \"not-a-secret-file\",\n\t\t\t\tenvMetadataHost:                 \"http://example.com\", // defined here to avoid the client cache.\n\t\t\t},\n\t\t\t// the error message varies according to the OS used.\n\t\t\texpected: \"googlecloud: unable to get Google Cloud client: google: error getting credentials using GOOGLE_APPLICATION_CREDENTIALS environment variable: \",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing project\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvProject:            \"\",\n\t\t\t\tenvServiceAccountFile: \"\",\n\t\t\t\t// as Travis run on GCE, we have to alter env\n\t\t\t\tenvMetadataHost: \"http://example.com\",\n\t\t\t},\n\t\t\texpected: \"googlecloud: project name missing\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"success key file\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvProject:            \"\",\n\t\t\t\tenvServiceAccountFile: \"fixtures/gce_account_service_file.json\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvProject:        \"\",\n\t\t\t\tEnvServiceAccount: `{\"project_id\": \"A\",\"type\": \"service_account\",\"client_email\": \"foo@bar.com\",\"private_key_id\": \"pki\",\"private_key\": \"pk\",\"token_uri\": \"/token\",\"client_secret\": \"secret\",\"client_id\": \"C\",\"refresh_token\": \"D\"}`,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tproject  string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"invalid project\",\n\t\t\tproject:  \"123\",\n\t\t\texpected: \"googlecloud: unable to create Google Cloud DNS service: client is nil\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing project\",\n\t\t\texpected: \"googlecloud: unable to create Google Cloud DNS service: client is nil\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Project = test.project\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPresentNoExistingRR(t *testing.T) {\n\tprovider := mockBuilder().\n\t\t// getHostedZone\n\t\tRoute(\"GET /dns/v1/projects/manhattan/managedZones\",\n\t\t\tservermock.JSONEncode(&dns.ManagedZonesListResponse{\n\t\t\t\tManagedZones: []*dns.ManagedZone{\n\t\t\t\t\t{Name: \"test\", Visibility: \"public\"},\n\t\t\t\t},\n\t\t\t}),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"dnsName\", \"example.com.\").\n\t\t\t\tWith(\"prettyPrint\", \"false\").\n\t\t\t\tWith(\"alt\", \"json\")).\n\t\t// findTxtRecords\n\t\tRoute(\"GET /dns/v1/projects/manhattan/managedZones/test/rrsets\",\n\t\t\tservermock.JSONEncode(&dns.ResourceRecordSetsListResponse{\n\t\t\t\tRrsets: []*dns.ResourceRecordSet{},\n\t\t\t}),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"_acme-challenge.example.com.\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"prettyPrint\", \"false\").\n\t\t\t\tWith(\"alt\", \"json\")).\n\t\t// applyChanges [Create]\n\t\tRoute(\"POST /dns/v1/projects/manhattan/managedZones/test/changes\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\tvar chgReq dns.Change\n\t\t\t\tif err := json.NewDecoder(req.Body).Decode(&chgReq); err != nil {\n\t\t\t\t\thttp.Error(rw, err.Error(), http.StatusBadRequest)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tchgResp := chgReq\n\t\t\t\tchgResp.Status = changeStatusDone\n\n\t\t\t\tif err := json.NewEncoder(rw).Encode(chgResp); err != nil {\n\t\t\t\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"prettyPrint\", \"false\").\n\t\t\t\tWith(\"alt\", \"json\")).\n\t\tBuild(t)\n\n\tdomain := \"example.com\"\n\n\terr := provider.Present(domain, \"\", \"\")\n\trequire.NoError(t, err)\n}\n\nfunc TestPresentWithExistingRR(t *testing.T) {\n\tprovider := mockBuilder().\n\t\t// getHostedZone\n\t\tRoute(\"GET /dns/v1/projects/manhattan/managedZones\",\n\t\t\tservermock.JSONEncode(&dns.ManagedZonesListResponse{\n\t\t\t\tManagedZones: []*dns.ManagedZone{\n\t\t\t\t\t{Name: \"test\", Visibility: \"public\"},\n\t\t\t\t},\n\t\t\t}),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"dnsName\", \"example.com.\").\n\t\t\t\tWith(\"prettyPrint\", \"false\").\n\t\t\t\tWith(\"alt\", \"json\")).\n\t\t// findTxtRecords\n\t\tRoute(\"GET /dns/v1/projects/manhattan/managedZones/test/rrsets\",\n\t\t\tservermock.JSONEncode(&dns.ResourceRecordSetsListResponse{\n\t\t\t\tRrsets: []*dns.ResourceRecordSet{{\n\t\t\t\t\tName:    \"_acme-challenge.example.com.\",\n\t\t\t\t\tRrdatas: []string{`\"X7DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\"`, `\"huji\"`},\n\t\t\t\t\tTtl:     120,\n\t\t\t\t\tType:    \"TXT\",\n\t\t\t\t}},\n\t\t\t}),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"_acme-challenge.example.com.\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"prettyPrint\", \"false\").\n\t\t\t\tWith(\"alt\", \"json\")).\n\t\t// applyChanges [Create]\n\t\tRoute(\"POST /dns/v1/projects/manhattan/managedZones/test/changes\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\tvar chgReq dns.Change\n\t\t\t\tif err := json.NewDecoder(req.Body).Decode(&chgReq); err != nil {\n\t\t\t\t\thttp.Error(rw, err.Error(), http.StatusBadRequest)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif len(chgReq.Additions) > 0 {\n\t\t\t\t\tsort.Strings(chgReq.Additions[0].Rrdatas)\n\t\t\t\t}\n\n\t\t\t\tvar prevVal string\n\n\t\t\t\tfor _, addition := range chgReq.Additions {\n\t\t\t\t\tfor _, value := range addition.Rrdatas {\n\t\t\t\t\t\tif prevVal == value {\n\t\t\t\t\t\t\thttp.Error(rw, fmt.Sprintf(\"The resource %s already exists\", value), http.StatusConflict)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tprevVal = value\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tchgResp := chgReq\n\t\t\t\tchgResp.Status = changeStatusDone\n\n\t\t\t\tif err := json.NewEncoder(rw).Encode(chgResp); err != nil {\n\t\t\t\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"prettyPrint\", \"false\").\n\t\t\t\tWith(\"alt\", \"json\")).\n\t\tBuild(t)\n\n\tdomain := \"example.com\"\n\n\terr := provider.Present(domain, \"\", \"\")\n\trequire.NoError(t, err)\n}\n\nfunc TestPresentSkipExistingRR(t *testing.T) {\n\tprovider := mockBuilder().\n\t\t// getHostedZone\n\t\tRoute(\"GET /dns/v1/projects/manhattan/managedZones\",\n\t\t\tservermock.JSONEncode(&dns.ManagedZonesListResponse{\n\t\t\t\tManagedZones: []*dns.ManagedZone{\n\t\t\t\t\t{Name: \"test\", Visibility: \"public\"},\n\t\t\t\t},\n\t\t\t}),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"dnsName\", \"example.com.\").\n\t\t\t\tWith(\"prettyPrint\", \"false\").\n\t\t\t\tWith(\"alt\", \"json\")).\n\t\t// findTxtRecords\n\t\tRoute(\"GET /dns/v1/projects/manhattan/managedZones/test/rrsets\",\n\t\t\tservermock.JSONEncode(&dns.ResourceRecordSetsListResponse{\n\t\t\t\tRrsets: []*dns.ResourceRecordSet{{\n\t\t\t\t\tName:    \"_acme-challenge.example.com.\",\n\t\t\t\t\tRrdatas: []string{`\"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\"`, `\"X7DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\"`, `\"huji\"`},\n\t\t\t\t\tTtl:     120,\n\t\t\t\t\tType:    \"TXT\",\n\t\t\t\t}},\n\t\t\t}),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"_acme-challenge.example.com.\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"prettyPrint\", \"false\").\n\t\t\t\tWith(\"alt\", \"json\")).\n\t\tBuild(t)\n\n\tdomain := \"example.com\"\n\n\terr := provider.Present(domain, \"\", \"\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\tprovider, err := NewDNSProviderCredentials(envTest.GetValue(EnvProject))\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLivePresentMultiple(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProviderCredentials(envTest.GetValue(EnvProject))\n\trequire.NoError(t, err)\n\n\t// Check that we're able to create multiple entries\n\terr = provider.Present(envTest.GetDomain(), \"1\", \"123d==\")\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"2\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProviderCredentials(envTest.GetValue(EnvProject))\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {\n\t\tconfig := NewDefaultConfig()\n\t\tconfig.HTTPClient = server.Client()\n\t\tconfig.Project = \"manhattan\"\n\n\t\tp, err := NewDNSProviderConfig(config)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tp.client.BasePath = server.URL\n\n\t\treturn p, err\n\t})\n}\n"
  },
  {
    "path": "providers/dns/gcore/gcore.go",
    "content": "// Package gcore implements a DNS provider for solving the DNS-01 challenge using G-Core.\npackage gcore\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/gcore\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"GCORE_\"\n\n\tEnvPermanentAPIToken = envNamespace + \"PERMANENT_API_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config for DNSProvider.\ntype Config = gcore.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, gcore.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, gcore.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider an implementation of challenge.Provider contract.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns an instance of DNSProvider configured for G-Core DNS API.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvPermanentAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"gcore: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIToken = values[EnvPermanentAPIToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for G-Core DNS API.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"gcore: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := gcore.NewDNSProviderConfig(config, \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"gcore: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gcore: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gcore: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n"
  },
  {
    "path": "providers/dns/gcore/gcore.toml",
    "content": "Name = \"G-Core\"\nDescription = ''''''\nURL = \"https://gcore.com/dns/\"\nCode = \"gcore\"\nSince = \"v4.5.0\"\n\nExample = '''\nGCORE_PERMANENT_API_TOKEN=xxxxx \\\nlego --dns gcore -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    GCORE_PERMANENT_API_TOKEN = \"Permanent API token (https://gcore.com/blog/permanent-api-token-explained/)\"\n  [Configuration.Additional]\n    GCORE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 20)\"\n    GCORE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 360)\"\n    GCORE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    GCORE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://api.gcore.com/docs/dns#tag/zones\"\n"
  },
  {
    "path": "providers/dns/gcore/gcore_test.go",
    "content": "package gcore\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar envTest = tester.NewEnvTest(EnvPermanentAPIToken).WithDomain(envNamespace + \"DOMAIN\")\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPermanentAPIToken: \"A\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPermanentAPIToken: \"\",\n\t\t\t},\n\t\t\texpected: \"gcore: some credentials information are missing: GCORE_PERMANENT_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiToken string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiToken: \"A\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"gcore: incomplete credentials provided\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = test.apiToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/gigahostno.go",
    "content": "// Package gigahostno implements a DNS provider for solving the DNS-01 challenge using Gigahost.no.\npackage gigahostno\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/gigahostno/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"GIGAHOSTNO_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\tEnvSecret   = envNamespace + \"SECRET\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername string\n\tPassword string\n\tSecret   string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\n\tidentifier *internal.Identifier\n\tclient     *internal.Client\n\n\ttokenMu sync.Mutex\n\ttoken   *internal.Token\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Gigahost.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"gigahostno: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\tconfig.Secret = env.GetOrFile(EnvSecret)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Gigahost.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"gigahostno: the configuration of the DNS provider is nil\")\n\t}\n\n\tidentifier, err := internal.NewIdentifier(config.Username, config.Password, config.Secret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"gigahostno: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tidentifier.HTTPClient = config.HTTPClient\n\t}\n\n\tidentifier.HTTPClient = clientdebug.Wrap(identifier.HTTPClient)\n\n\tclient := internal.NewClient()\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:     config,\n\t\tidentifier: identifier,\n\t\tclient:     client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.authenticate(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gigahostno: %w\", err)\n\t}\n\n\tctx = internal.WithContext(ctx, d.token.Token)\n\n\tzone, err := d.findZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gigahostno: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gigahostno: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName:  subDomain,\n\t\tType:  \"TXT\",\n\t\tValue: info.Value,\n\t\tTTL:   d.config.TTL,\n\t}\n\n\terr = d.client.CreateNewRecord(ctx, zone.ID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gigahostno: create new record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.authenticate(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gigahostno: %w\", err)\n\t}\n\n\tctx = internal.WithContext(ctx, d.token.Token)\n\n\tzone, err := d.findZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gigahostno: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gigahostno: %w\", err)\n\t}\n\n\trecords, err := d.client.GetZoneRecords(ctx, zone.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gigahostno: get zone records: %w\", err)\n\t}\n\n\tfor _, record := range records {\n\t\tif record.Type == \"TXT\" && record.Name == subDomain && record.Value == info.Value {\n\t\t\terr := d.client.DeleteRecord(ctx, zone.ID, record.ID, record.Name, record.Type)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"gigahostno: delete record: %w\", err)\n\t\t\t}\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) authenticate(ctx context.Context) error {\n\td.tokenMu.Lock()\n\tdefer d.tokenMu.Unlock()\n\n\tif !d.token.IsExpired() {\n\t\treturn nil\n\t}\n\n\ttok, err := d.identifier.Authenticate(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"authenticate: %w\", err)\n\t}\n\n\td.token = tok\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*internal.Zone, error) {\n\tzones, err := d.client.GetZones(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get zones: %w\", err)\n\t}\n\n\tfor d := range dns01.UnFqdnDomainsSeq(fqdn) {\n\t\tfor _, zone := range zones {\n\t\t\tif zone.Name == d && zone.Active == \"1\" {\n\t\t\t\treturn &zone, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"zone not found for %q\", fqdn)\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/gigahostno.toml",
    "content": "Name = \"Gigahost.no\"\nDescription = ''''''\nURL = \"https://gigahost.no/\"\nCode = \"gigahostno\"\nSince = \"v4.29.0\"\n\nExample = '''\nGIGAHOSTNO_USERNAME=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nGIGAHOSTNO_PASSWORD=\"yyyyyyyyyyyyyyyyyyyyy\" \\\nlego --dns gigahostno -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    GIGAHOSTNO_USERNAME = \"Username\"\n    GIGAHOSTNO_PASSWORD = \"Password\"\n  [Configuration.Additional]\n    GIGAHOSTNO_SECRET = \"TOTP secret\"\n    GIGAHOSTNO_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    GIGAHOSTNO_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    GIGAHOSTNO_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    GIGAHOSTNO_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://gigahost.no/api-dokumentasjon\"\n"
  },
  {
    "path": "providers/dns/gigahostno/gigahostno_test.go",
    "content": "package gigahostno\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-acme/lego/v4/providers/dns/gigahostno/internal\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvUsername,\n\tEnvPassword,\n\tEnvSecret,\n).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t\tEnvSecret:   \"super-secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing GIGAHOSTNO_USERNAME\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"gigahostno: some credentials information are missing: GIGAHOSTNO_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing GIGAHOSTNO_PASSWORD\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t},\n\t\t\texpected: \"gigahostno: some credentials information are missing: GIGAHOSTNO_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"gigahostno: some credentials information are missing: GIGAHOSTNO_USERNAME,GIGAHOSTNO_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\tsecret   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t\tsecret:   \"super-secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"gigahostno: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tusername: \"user\",\n\t\t\texpected: \"gigahostno: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"gigahostno: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\t\t\tconfig.Secret = test.secret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = \"user\"\n\t\t\tconfig.Password = \"secret\"\n\t\t\tconfig.Secret = \"JBSWY3DPEHPK3PXP\"\n\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\t\t\tp.identifier.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders(),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /authenticate\",\n\t\t\tservermock.ResponseFromInternal(\"authenticate.json\")).\n\t\tRoute(\"GET /dns/zones\",\n\t\t\tservermock.ResponseFromInternal(\"zones.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer secrettoken\")).\n\t\tRoute(\"POST /dns/zones/123/records\",\n\t\t\tservermock.ResponseFromInternal(\"create_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"create_record-request.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer secrettoken\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_Present_token_not_expired(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /dns/zones\",\n\t\t\tservermock.ResponseFromInternal(\"zones.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer secret-token\")).\n\t\tRoute(\"POST /dns/zones/123/records\",\n\t\t\tservermock.ResponseFromInternal(\"create_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"create_record-request.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer secret-token\")).\n\t\tBuild(t)\n\n\tprovider.token = &internal.Token{\n\t\tToken:       \"secret-token\",\n\t\tTokenExpire: 65322892800, // 2040-01-01\n\t\tCustomerID:  \"123\",\n\t}\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /authenticate\",\n\t\t\tservermock.ResponseFromInternal(\"authenticate.json\")).\n\t\tRoute(\"GET /dns/zones\",\n\t\t\tservermock.ResponseFromInternal(\"zones.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer secrettoken\")).\n\t\tRoute(\"GET /dns/zones/123/records\",\n\t\t\tservermock.ResponseFromInternal(\"zone_records.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer secrettoken\")).\n\t\tRoute(\"DELETE /dns/zones/123/records/jkl012\",\n\t\t\tservermock.ResponseFromInternal(\"delete_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"_acme-challenge\").\n\t\t\t\tWith(\"type\", \"TXT\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer secrettoken\")).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp_token_not_expired(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /dns/zones\",\n\t\t\tservermock.ResponseFromInternal(\"zones.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer secret-token\")).\n\t\tRoute(\"GET /dns/zones/123/records\",\n\t\t\tservermock.ResponseFromInternal(\"zone_records.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer secret-token\")).\n\t\tRoute(\"DELETE /dns/zones/123/records/jkl012\",\n\t\t\tservermock.ResponseFromInternal(\"delete_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"_acme-challenge\").\n\t\t\t\tWith(\"type\", \"TXT\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithAuthorization(\"Bearer secret-token\")).\n\t\tBuild(t)\n\n\tprovider.token = &internal.Token{\n\t\tToken:       \"secret-token\",\n\t\tTokenExpire: 65322892800, // 2040-01-01\n\t\tCustomerID:  \"123\",\n\t}\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\nconst defaultBaseURL = \"https://api.gigahost.no/api/v0\"\n\nconst authorizationHeader = \"Authorization\"\n\n// Client the Gigahost.no API client.\ntype Client struct {\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient() *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}\n}\n\n// GetZones returns all zones.\nfunc (c *Client) GetZones(ctx context.Context) ([]Zone, error) {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", \"zones\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result APIResponse[[]Zone]\n\n\terr = c.do(ctx, req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Data, nil\n}\n\n// GetZoneRecords returns all records for a zone.\nfunc (c *Client) GetZoneRecords(ctx context.Context, zoneID string) ([]Record, error) {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", \"zones\", zoneID, \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result APIResponse[[]Record]\n\n\terr = c.do(ctx, req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Data, nil\n}\n\n// CreateNewRecord creates a new record.\nfunc (c *Client) CreateNewRecord(ctx context.Context, zoneID string, record Record) error {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", \"zones\", zoneID, \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(ctx, req, nil)\n}\n\n// DeleteRecord deletes a record.\nfunc (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID, name, recordType string) error {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", \"zones\", zoneID, \"records\", recordID)\n\n\tquery := endpoint.Query()\n\tquery.Set(\"name\", name)\n\tquery.Set(\"type\", recordType)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(ctx, req, nil)\n}\n\nfunc (c *Client) do(ctx context.Context, req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\treq.Header.Set(authorizationHeader, \"Bearer \"+getToken(ctx))\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient()\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer secret\"),\n\t)\n}\n\nfunc TestClient_GetZones(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/zones\",\n\t\t\tservermock.ResponseFromFixture(\"zones.json\")).\n\t\tBuild(t)\n\n\tzones, err := client.GetZones(mockContext(t))\n\trequire.NoError(t, err)\n\n\texpected := []Zone{\n\t\t{\n\t\t\tID:          \"123\",\n\t\t\tName:        \"example.com\",\n\t\t\tNameDisplay: \"example.com\",\n\t\t\tType:        \"NATIVE\",\n\t\t\tActive:      \"1\",\n\t\t},\n\t\t{\n\t\t\tID:          \"226\",\n\t\t\tName:        \"example.org\",\n\t\t\tNameDisplay: \"example.org\",\n\t\t\tType:        \"NATIVE\",\n\t\t\tActive:      \"1\",\n\t\t},\n\t\t{\n\t\t\tID:          \"229\",\n\t\t\tName:        \"example.xn--zckzah\",\n\t\t\tNameDisplay: \"example.テスト\",\n\t\t\tType:        \"NATIVE\",\n\t\t\tActive:      \"1\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_GetZones_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/zones\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetZones(mockContext(t))\n\trequire.EqualError(t, err, \"401: 401 Unauthorized: 401 Unauthorized\")\n}\n\nfunc TestClient_GetZoneRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/zones/123/records\",\n\t\t\tservermock.ResponseFromFixture(\"zone_records.json\")).\n\t\tBuild(t)\n\n\tzones, err := client.GetZoneRecords(mockContext(t), \"123\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tID:    \"abc123\",\n\t\t\tName:  \"@\",\n\t\t\tType:  \"A\",\n\t\t\tValue: \"185.125.168.166\",\n\t\t\tTTL:   3600,\n\t\t},\n\t\t{\n\t\t\tID:    \"def456\",\n\t\t\tName:  \"www\",\n\t\t\tType:  \"A\",\n\t\t\tValue: \"185.125.168.166\",\n\t\t\tTTL:   3600,\n\t\t},\n\t\t{\n\t\t\tID:    \"ghi789\",\n\t\t\tName:  \"@\",\n\t\t\tType:  \"MX\",\n\t\t\tValue: \"mail.example.no\",\n\t\t\tTTL:   3600,\n\t\t},\n\t\t{\n\t\t\tID:    \"jkl012\",\n\t\t\tName:  \"_acme-challenge\",\n\t\t\tType:  \"TXT\",\n\t\t\tValue: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\t\tTTL:   120,\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_CreateNewRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/zones/example.com/records\",\n\t\t\tservermock.ResponseFromFixture(\"create_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:  \"_acme-challenge\",\n\t\tType:  \"TXT\",\n\t\tValue: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:   120,\n\t}\n\n\terr := client.CreateNewRecord(mockContext(t), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"/dns/zones/123/records/abc123\",\n\t\t\tservermock.ResponseFromFixture(\"delete_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"_acme-challenge\").\n\t\t\t\tWith(\"type\", \"TXT\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(mockContext(t), \"123\", \"abc123\", \"_acme-challenge\", \"TXT\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/internal/fixtures/authenticate-request.json",
    "content": "{\n  \"username\": \"user\",\n  \"password\": \"secret\"\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/internal/fixtures/authenticate.json",
    "content": "{\n  \"meta\": {\n    \"status\": 200,\n    \"status_message\": \"200 OK\",\n    \"maintenance\": false\n  },\n  \"data\": {\n    \"token\": \"secrettoken\",\n    \"token_expire\": 1577836800,\n    \"customer_id\": \"16030\",\n    \"contact_id\": \"15182\",\n    \"customer_name\": \"Cloudline AS\",\n    \"contact_username\": \"test@example.com\",\n    \"contact_access_level\": \"admin\",\n    \"customer_address\": \"Grønland 14\",\n    \"customer_zipcode\": \"5918\",\n    \"customer_city\": \"Frekhaug\",\n    \"customer_province\": \"Vestland\",\n    \"ga_secret\": \"ga_secret\",\n    \"ga_enabled\": \"1\",\n    \"vat\": 1\n  }\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/internal/fixtures/create_record-request.json",
    "content": "{\n  \"record_name\": \"_acme-challenge\",\n  \"record_type\": \"TXT\",\n  \"record_value\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n  \"record_ttl\": 120\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/internal/fixtures/create_record.json",
    "content": "{\n  \"meta\": {\n    \"status\": 201,\n    \"status_message\": \"201 Created\",\n    \"message\": \"Record created successfully.\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/internal/fixtures/delete_record.json",
    "content": "{\n  \"meta\": {\n    \"status\": 200,\n    \"status_message\": \"200 OK\",\n    \"message\": \"Record deleted successfully.\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/internal/fixtures/error.json",
    "content": "{\n  \"meta\": {\n    \"status\": 401,\n    \"status_message\": \"401 Unauthorized\",\n    \"maintenance\": false,\n    \"message\": \"401 Unauthorized\"\n  },\n  \"data\": []\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/internal/fixtures/zone_records.json",
    "content": "{\n  \"meta\": {\n    \"status\": 200,\n    \"status_message\": \"200 OK\"\n  },\n  \"data\": [\n    {\n      \"record_id\": \"abc123\",\n      \"record_name\": \"@\",\n      \"record_type\": \"A\",\n      \"record_value\": \"185.125.168.166\",\n      \"record_ttl\": 3600,\n      \"record_priority\": null\n    },\n    {\n      \"record_id\": \"def456\",\n      \"record_name\": \"www\",\n      \"record_type\": \"A\",\n      \"record_value\": \"185.125.168.166\",\n      \"record_ttl\": 3600,\n      \"record_priority\": null\n    },\n    {\n      \"record_id\": \"ghi789\",\n      \"record_name\": \"@\",\n      \"record_type\": \"MX\",\n      \"record_value\": \"mail.example.no\",\n      \"record_ttl\": 3600,\n      \"record_priority\": 10\n    },\n    {\n      \"record_id\": \"jkl012\",\n      \"record_name\": \"_acme-challenge\",\n      \"record_type\": \"TXT\",\n      \"record_value\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n      \"record_ttl\": 120\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/internal/fixtures/zones.json",
    "content": "{\n  \"meta\": {\n    \"status\": 200,\n    \"status_message\": \"200 OK\",\n    \"maintenance\": false,\n    \"message\": \"200 OK\"\n  },\n  \"data\": [\n    {\n      \"zone_id\": \"123\",\n      \"cust_id\": \"16030\",\n      \"order_id\": \"26117\",\n      \"zone_name\": \"example.com\",\n      \"zone_type\": \"NATIVE\",\n      \"zone_active\": \"1\",\n      \"zone_protected\": \"1\",\n      \"zone_is_registered\": \"1\",\n      \"domain_registrar\": \"norid\",\n      \"domain_status\": \"active\",\n      \"domain_registered_date\": \"2025-11-23 15:17:38\",\n      \"domain_expiry_date\": \"2026-11-23 15:17:38\",\n      \"domain_updated_date\": \"2025-11-23 16:17:38\",\n      \"domain_auto_renew\": \"1\",\n      \"domain_epp_id\": \"LEG2175D-NORID\",\n      \"domain_registrant_id\": \"CA19777O\",\n      \"domain_tech_id\": \"GH295R\",\n      \"domain_auth_info\": \"XXXXXXXXXXXXXXX\",\n      \"domain_locked\": \"0\",\n      \"domain_dnssec\": \"0\",\n      \"domain_dnssec_data\": null,\n      \"domain_protected_email\": null,\n      \"zone_created\": \"2025-11-23 16:17:29\",\n      \"zone_updated\": 1700000000,\n      \"external_dns\": \"0\",\n      \"record_count\": 4,\n      \"zone_name_display\": \"example.com\"\n    },\n    {\n      \"zone_id\": \"226\",\n      \"cust_id\": \"16030\",\n      \"order_id\": \"26114\",\n      \"zone_name\": \"example.org\",\n      \"zone_type\": \"NATIVE\",\n      \"zone_active\": \"1\",\n      \"zone_protected\": \"1\",\n      \"zone_is_registered\": \"1\",\n      \"domain_registrar\": \"norid\",\n      \"domain_status\": \"active\",\n      \"domain_registered_date\": \"2025-11-23 14:15:01\",\n      \"domain_expiry_date\": \"2026-11-23 14:15:01\",\n      \"domain_updated_date\": \"2025-11-23 15:15:02\",\n      \"domain_auto_renew\": \"1\",\n      \"domain_epp_id\": \"TEO218D-NORID\",\n      \"domain_registrant_id\": \"CA19774O\",\n      \"domain_tech_id\": \"GH295R\",\n      \"domain_auth_info\": \"XXXXXXXXXXXXXX\",\n      \"domain_locked\": \"0\",\n      \"domain_dnssec\": \"0\",\n      \"domain_dnssec_data\": null,\n      \"domain_protected_email\": null,\n      \"zone_created\": \"2025-11-23 15:13:27\",\n      \"zone_updated\": 1700000000,\n      \"external_dns\": \"0\",\n      \"record_count\": 5,\n      \"zone_name_display\": \"example.org\"\n    },\n    {\n      \"zone_id\": \"229\",\n      \"cust_id\": \"16030\",\n      \"order_id\": \"26119\",\n      \"zone_name\": \"example.xn--zckzah\",\n      \"zone_type\": \"NATIVE\",\n      \"zone_active\": \"1\",\n      \"zone_protected\": \"1\",\n      \"zone_is_registered\": \"1\",\n      \"domain_registrar\": \"norid\",\n      \"domain_status\": \"active\",\n      \"domain_registered_date\": \"2014-12-01 12:40:48\",\n      \"domain_expiry_date\": \"2026-12-01 12:40:48\",\n      \"domain_updated_date\": \"2025-11-23 15:37:36\",\n      \"domain_auto_renew\": \"1\",\n      \"domain_epp_id\": \"DIT1003D-NORID\",\n      \"domain_registrant_id\": \"DCA822O\",\n      \"domain_tech_id\": \"GH295R\",\n      \"domain_auth_info\": \"XXXXXXXXXXXXXX\",\n      \"domain_locked\": \"0\",\n      \"domain_dnssec\": \"0\",\n      \"domain_dnssec_data\": null,\n      \"domain_protected_email\": null,\n      \"zone_created\": \"2025-11-23 16:37:15\",\n      \"zone_updated\": 1700000000,\n      \"external_dns\": \"0\",\n      \"record_count\": 4,\n      \"zone_name_display\": \"example.\\u30C6\\u30B9\\u30C8\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\t\"github.com/pquerna/otp/totp\"\n)\n\ntype token string\n\nconst tokenKey token = \"token\"\n\ntype Identifier struct {\n\tusername string\n\tpassword string\n\tSecret   string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewIdentifier(username, password, secret string) (*Identifier, error) {\n\tif username == \"\" || password == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Identifier{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\tSecret:     secret,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Identifier) Authenticate(ctx context.Context) (*Token, error) {\n\tendpoint := c.BaseURL.JoinPath(\"authenticate\")\n\n\tauth := Auth{Username: c.username, Password: c.password}\n\n\tif c.Secret != \"\" {\n\t\ttan, err := totp.GenerateCode(c.Secret, time.Now())\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"generate TOTP: %w\", err)\n\t\t}\n\n\t\tauth.Code, err = strconv.Atoi(tan)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parse TOTP: %w\", err)\n\t\t}\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, auth)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result APIResponse[*Token]\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Data, nil\n}\n\nfunc (c *Identifier) do(req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc WithContext(ctx context.Context, credential string) context.Context {\n\treturn context.WithValue(ctx, tokenKey, credential)\n}\n\nfunc getToken(ctx context.Context) string {\n\tcredential, ok := ctx.Value(tokenKey).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn credential\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/internal/identity_test.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupIdentifierClient(server *httptest.Server) (*Identifier, error) {\n\tclient, err := NewIdentifier(\"user\", \"secret\", \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.BaseURL, _ = url.Parse(server.URL)\n\tclient.HTTPClient = server.Client()\n\n\treturn client, nil\n}\n\nfunc mockContext(t *testing.T) context.Context {\n\tt.Helper()\n\n\treturn context.WithValue(t.Context(), tokenKey, \"secret\")\n}\n\nfunc TestIdentifier_Authenticate(t *testing.T) {\n\tidentifier := servermock.NewBuilder[*Identifier](setupIdentifierClient).\n\t\tRoute(\"POST /authenticate\",\n\t\t\tservermock.ResponseFromFixture(\"authenticate.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"authenticate-request.json\")).\n\t\tBuild(t)\n\n\ttoken, err := identifier.Authenticate(context.Background())\n\trequire.NoError(t, err)\n\n\texpected := &Token{\n\t\tToken:              \"secrettoken\",\n\t\tTokenExpire:        1577836800,\n\t\tCustomerID:         \"16030\",\n\t\tContactID:          \"15182\",\n\t\tCustomerName:       \"Cloudline AS\",\n\t\tContactUsername:    \"test@example.com\",\n\t\tContactAccessLevel: \"admin\",\n\t\tCustomerAddress:    \"Grønland 14\",\n\t\tCustomerZipcode:    \"5918\",\n\t\tCustomerCity:       \"Frekhaug\",\n\t\tCustomerProvince:   \"Vestland\",\n\t\tGASecret:           \"ga_secret\",\n\t\tGAEnabled:          \"1\",\n\t\tVAT:                1,\n\t}\n\n\tassert.Equal(t, expected, token)\n}\n\nfunc TestToken_IsExpired(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc   string\n\t\ttoken  *Token\n\t\tassert assert.BoolAssertionFunc\n\t}{\n\t\t{\n\t\t\tdesc:   \"nil\",\n\t\t\tassert: assert.True,\n\t\t},\n\t\t{\n\t\t\tdesc:   \"empty\",\n\t\t\ttoken:  &Token{},\n\t\t\tassert: assert.True,\n\t\t},\n\t\t{\n\t\t\tdesc: \"not expired\",\n\t\t\ttoken: &Token{\n\t\t\t\tTokenExpire: 65322892800, // 2040-01-01\n\t\t\t},\n\t\t\tassert: assert.False,\n\t\t},\n\t\t{\n\t\t\tdesc: \"now\",\n\t\t\ttoken: &Token{\n\t\t\t\tTokenExpire: time.Now().Unix(),\n\t\t\t},\n\t\t\tassert: assert.True,\n\t\t},\n\t\t{\n\t\t\tdesc: \"now + 2 minutes\",\n\t\t\ttoken: &Token{\n\t\t\t\tTokenExpire: time.Now().Add(2 * time.Minute).Unix(),\n\t\t\t},\n\t\t\tassert: assert.False,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttest.assert(t, test.token.IsExpired())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/gigahostno/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\ntype APIError struct {\n\tMeta MetaData `json:\"meta\"`\n}\n\nfunc (a *APIError) Error() string {\n\treturn fmt.Sprintf(\"%d: %s: %s\", a.Meta.Status, a.Meta.StatusMessage, a.Meta.Message)\n}\n\ntype MetaData struct {\n\tStatus        int    `json:\"status,omitempty\"`\n\tStatusMessage string `json:\"status_message,omitempty\"`\n\tMaintenance   bool   `json:\"maintenance\"`\n\tMessage       string `json:\"message,omitempty\"`\n}\n\ntype APIResponse[T any] struct {\n\tMeta MetaData `json:\"meta\"`\n\tData T        `json:\"data,omitempty\"`\n}\n\ntype Zone struct {\n\tID          string `json:\"zone_id,omitempty\"`\n\tName        string `json:\"zone_name,omitempty\"`\n\tNameDisplay string `json:\"zone_name_display,omitempty\"`\n\tType        string `json:\"zone_type,omitempty\"`\n\tActive      string `json:\"zone_active,omitempty\"`\n}\n\ntype Record struct {\n\tID    string `json:\"record_id,omitempty\"`\n\tName  string `json:\"record_name,omitempty\"`\n\tType  string `json:\"record_type,omitempty\"`\n\tValue string `json:\"record_value,omitempty\"`\n\tTTL   int    `json:\"record_ttl,omitempty\"`\n}\n\ntype Auth struct {\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n\tCode     int    `json:\"code,omitempty\"`\n}\n\ntype Token struct {\n\tToken              string `json:\"token,omitempty\"`\n\tTokenExpire        int64  `json:\"token_expire,omitempty\"`\n\tCustomerID         string `json:\"customer_id,omitempty\"`\n\tContactID          string `json:\"contact_id,omitempty\"`\n\tCustomerName       string `json:\"customer_name,omitempty\"`\n\tContactUsername    string `json:\"contact_username,omitempty\"`\n\tContactAccessLevel string `json:\"contact_access_level,omitempty\"`\n\tCustomerAddress    string `json:\"customer_address,omitempty\"`\n\tCustomerZipcode    string `json:\"customer_zipcode,omitempty\"`\n\tCustomerCity       string `json:\"customer_city,omitempty\"`\n\tCustomerProvince   string `json:\"customer_province,omitempty\"`\n\tGASecret           string `json:\"ga_secret,omitempty\"`\n\tGAEnabled          string `json:\"ga_enabled,omitempty\"`\n\tVAT                int    `json:\"vat,omitempty\"`\n}\n\nfunc (t *Token) IsExpired() bool {\n\tif t == nil {\n\t\treturn true\n\t}\n\n\treturn time.Now().UTC().Add(1 * time.Minute).After(time.Unix(t.TokenExpire, 0).UTC())\n}\n"
  },
  {
    "path": "providers/dns/glesys/glesys.go",
    "content": "// Package glesys implements a DNS provider for solving the DNS-01 challenge using GleSYS api.\npackage glesys\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/glesys/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"GLESYS_\"\n\n\tEnvAPIUser = envNamespace + \"API_USER\"\n\tEnvAPIKey  = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst minTTL = 60\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIUser            string\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 20*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 20*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\tactiveRecords map[string]int\n\tinProgressMu  sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for GleSYS.\n// Credentials must be passed in the environment variables:\n// GLESYS_API_USER and GLESYS_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIUser, EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"glesys: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIUser = values[EnvAPIUser]\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for GleSYS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"glesys: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIUser == \"\" || config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"glesys: incomplete credentials provided\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"glesys: invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\tclient := internal.NewClient(config.APIUser, config.APIKey)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:        config,\n\t\tclient:        client,\n\t\tactiveRecords: make(map[string]int),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// find authZone\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"glesys: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"glesys: %w\", err)\n\t}\n\n\t// acquire lock and check there is not a challenge already in progress for this value of authZone\n\td.inProgressMu.Lock()\n\tdefer d.inProgressMu.Unlock()\n\n\t// add TXT record into authZone\n\trecordID, err := d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(authZone), subDomain, info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// save data necessary for CleanUp\n\td.activeRecords[info.EffectiveFQDN] = recordID\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// acquire lock and retrieve authZone\n\td.inProgressMu.Lock()\n\tdefer d.inProgressMu.Unlock()\n\n\tif _, ok := d.activeRecords[info.EffectiveFQDN]; !ok {\n\t\t// if there is no cleanup information then just return\n\t\treturn nil\n\t}\n\n\trecordID := d.activeRecords[info.EffectiveFQDN]\n\tdelete(d.activeRecords, info.EffectiveFQDN)\n\n\t// delete TXT record from authZone\n\treturn d.client.DeleteTXTRecord(context.Background(), recordID)\n}\n\n// Timeout returns the values (20*time.Minute, 20*time.Second) which\n// are used by the acme package as timeout and check interval values\n// when checking for DNS record propagation with GleSYS.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/glesys/glesys.toml",
    "content": "Name = \"Glesys\"\nDescription = ''''''\nURL = \"https://glesys.com/\"\nCode = \"glesys\"\nSince = \"v0.5.0\"\n\nExample = '''\nGLESYS_API_USER=xxxxx \\\nGLESYS_API_KEY=yyyyy \\\nlego --dns glesys -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    GLESYS_API_USER = \"API user\"\n    GLESYS_API_KEY = \"API key\"\n  [Configuration.Additional]\n    GLESYS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 20)\"\n    GLESYS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 1200)\"\n    GLESYS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    GLESYS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://github.com/GleSYS/API/wiki/API-Documentation\"\n"
  },
  {
    "path": "providers/dns/glesys/glesys_test.go",
    "content": "package glesys\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIUser,\n\tEnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser: \"A\",\n\t\t\t\tEnvAPIKey:  \"B\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser: \"\",\n\t\t\t\tEnvAPIKey:  \"\",\n\t\t\t},\n\t\t\texpected: \"glesys: some credentials information are missing: GLESYS_API_USER,GLESYS_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api user\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser: \"\",\n\t\t\t\tEnvAPIKey:  \"B\",\n\t\t\t},\n\t\t\texpected: \"glesys: some credentials information are missing: GLESYS_API_USER\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser: \"A\",\n\t\t\t\tEnvAPIKey:  \"\",\n\t\t\t},\n\t\t\texpected: \"glesys: some credentials information are missing: GLESYS_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.activeRecords)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiUser  string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:    \"success\",\n\t\t\tapiUser: \"A\",\n\t\t\tapiKey:  \"B\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"glesys: incomplete credentials provided\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api user\",\n\t\t\tapiUser:  \"\",\n\t\t\tapiKey:   \"B\",\n\t\t\texpected: \"glesys: incomplete credentials provided\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api key\",\n\t\t\tapiUser:  \"A\",\n\t\t\tapiKey:   \"\",\n\t\t\texpected: \"glesys: incomplete credentials provided\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.APIUser = test.apiUser\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.activeRecords)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/glesys/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// defaultBaseURL is the GleSYS API endpoint used by Present and CleanUp.\nconst defaultBaseURL = \"https://api.glesys.com/\"\n\ntype Client struct {\n\tapiUser string\n\tapiKey  string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(apiUser, apiKey string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiUser:    apiUser,\n\t\tapiKey:     apiKey,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// AddTXTRecord adds a dns record to a domain.\n// https://github.com/GleSYS/API/wiki/API-Documentation#domainaddrecord\nfunc (c *Client) AddTXTRecord(ctx context.Context, domain, name, value string, ttl int) (int, error) {\n\tendpoint := c.baseURL.JoinPath(\"domain\", \"addrecord\")\n\n\trequest := addRecordRequest{\n\t\tDomainName: domain,\n\t\tHost:       name,\n\t\tType:       \"TXT\",\n\t\tData:       value,\n\t\tTTL:        ttl,\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, request)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tresponse, err := c.do(req)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif response != nil && response.Response.Status.Code == http.StatusOK {\n\t\treturn response.Response.Record.RecordID, nil\n\t}\n\n\treturn 0, err\n}\n\n// DeleteTXTRecord removes a dns record from a domain.\n// https://github.com/GleSYS/API/wiki/API-Documentation#domaindeleterecord\nfunc (c *Client) DeleteTXTRecord(ctx context.Context, recordID int) error {\n\tendpoint := c.baseURL.JoinPath(\"domain\", \"deleterecord\")\n\n\trequest := deleteRecordRequest{RecordID: recordID}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, request)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = c.do(req)\n\n\treturn err\n}\n\nfunc (c *Client) do(req *http.Request) (*apiResponse, error) {\n\treq.SetBasicAuth(c.apiUser, c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar response apiResponse\n\n\terr = json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn &response, nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/glesys/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithBasicAuth(\"user\", \"secret\"),\n\t)\n}\n\nfunc TestClient_AddTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domain/addrecord\",\n\t\t\tservermock.ResponseFromFixture(\"add-record.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"domainname\":\"example.com\",\"host\":\"foo\",\"type\":\"TXT\",\"data\":\"txt\",\"ttl\":120}`)).\n\t\tBuild(t)\n\n\trecordID, err := client.AddTXTRecord(t.Context(), \"example.com\", \"foo\", \"txt\", 120)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 123, recordID)\n}\n\nfunc TestClient_DeleteTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domain/deleterecord\",\n\t\t\tservermock.ResponseFromFixture(\"delete-record.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"recordid\":123}`)).\n\t\tBuild(t)\n\n\terr := client.DeleteTXTRecord(t.Context(), 123)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/glesys/internal/fixtures/add-record.json",
    "content": "{\n  \"response\": {\n    \"status\": {\n      \"code\": 200\n    },\n    \"record\": {\n      \"recordid\": 123\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/glesys/internal/fixtures/delete-record.json",
    "content": "{\n  \"response\": {\n    \"status\": {\n      \"code\": 200\n    },\n    \"record\": {\n      \"recordid\": 123\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/glesys/internal/types.go",
    "content": "package internal\n\ntype addRecordRequest struct {\n\tDomainName string `json:\"domainname\"`\n\tHost       string `json:\"host\"`\n\tType       string `json:\"type\"`\n\tData       string `json:\"data\"`\n\tTTL        int    `json:\"ttl,omitempty\"`\n}\n\ntype deleteRecordRequest struct {\n\tRecordID int `json:\"recordid\"`\n}\n\ntype apiResponse struct {\n\tResponse Response `json:\"response\"`\n}\n\ntype Response struct {\n\tStatus Status `json:\"status\"`\n\tRecord Record `json:\"record\"`\n}\n\ntype Status struct {\n\tCode int `json:\"code\"`\n}\n\ntype Record struct {\n\tRecordID int `json:\"recordid\"`\n}\n"
  },
  {
    "path": "providers/dns/godaddy/godaddy.go",
    "content": "// Package godaddy implements a DNS provider for solving the DNS-01 challenge using godaddy DNS.\npackage godaddy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/godaddy/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"GODADDY_\"\n\n\tEnvAPIKey    = envNamespace + \"API_KEY\"\n\tEnvAPISecret = envNamespace + \"API_SECRET\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst minTTL = 600\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tAPISecret          string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for godaddy.\n// Credentials must be passed in the environment variables:\n// GODADDY_API_KEY and GODADDY_API_SECRET.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey, EnvAPISecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"godaddy: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.APISecret = values[EnvAPISecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for godaddy.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"godaddy: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" || config.APISecret == \"\" {\n\t\treturn nil, errors.New(\"godaddy: credentials missing\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"godaddy: invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\tclient := internal.NewClient(config.APIKey, config.APISecret)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"godaddy: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"godaddy: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\texistingRecords, err := d.client.GetRecords(ctx, authZone, \"TXT\", subDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"godaddy: failed to get TXT records: %w\", err)\n\t}\n\n\tvar newRecords []internal.DNSRecord\n\n\tfor _, record := range existingRecords {\n\t\tif record.Data != \"\" {\n\t\t\tnewRecords = append(newRecords, record)\n\t\t}\n\t}\n\n\trecord := internal.DNSRecord{\n\t\tType: \"TXT\",\n\t\tName: subDomain,\n\t\tData: info.Value,\n\t\tTTL:  d.config.TTL,\n\t}\n\tnewRecords = append(newRecords, record)\n\n\terr = d.client.UpdateTxtRecords(ctx, newRecords, authZone, subDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"godaddy: failed to add TXT record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"godaddy: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"godaddy: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\texistingRecords, err := d.client.GetRecords(ctx, authZone, \"TXT\", subDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"godaddy: failed to get all TXT records: %w\", err)\n\t}\n\n\tvar recordsToKeep []internal.DNSRecord\n\n\tfor _, record := range existingRecords {\n\t\tif record.Data != info.Value && record.Data != \"\" {\n\t\t\trecordsToKeep = append(recordsToKeep, record)\n\t\t}\n\t}\n\n\tif len(recordsToKeep) == 0 {\n\t\terr = d.client.DeleteTxtRecords(ctx, authZone, subDomain)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"godaddy: failed to delete TXT record: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\terr = d.client.UpdateTxtRecords(ctx, recordsToKeep, authZone, subDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"godaddy: failed to remove TXT record: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/godaddy/godaddy.toml",
    "content": "Name = \"Go Daddy\"\nDescription = ''''''\nURL = \"https://godaddy.com\"\nCode = \"godaddy\"\nSince = \"v0.5.0\"\n\nExample = '''\nGODADDY_API_KEY=xxxxxxxx \\\nGODADDY_API_SECRET=yyyyyyyy \\\nlego --dns godaddy -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\nGoDaddy has recently (2024-04) updated the account requirements to access parts of their production Domains API:\n\n- Availability API: Limited to accounts with 50 or more domains.\n- Management and DNS APIs: Limited to accounts with 10 or more domains and/or an active Discount Domain Club plan.\n\nhttps://community.letsencrypt.org/t/getting-unauthorized-url-error-while-trying-to-get-cert-for-subdomains/217329/12\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    GODADDY_API_KEY = \"API key\"\n    GODADDY_API_SECRET = \"API secret\"\n  [Configuration.Additional]\n    GODADDY_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    GODADDY_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    GODADDY_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)\"\n    GODADDY_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://developer.godaddy.com/doc/endpoint/domains\"\n"
  },
  {
    "path": "providers/dns/godaddy/godaddy_test.go",
    "content": "package godaddy\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIKey,\n\tEnvAPISecret).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"123\",\n\t\t\t\tEnvAPISecret: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"\",\n\t\t\t\tEnvAPISecret: \"\",\n\t\t\t},\n\t\t\texpected: \"godaddy: some credentials information are missing: GODADDY_API_KEY,GODADDY_API_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"\",\n\t\t\t\tEnvAPISecret: \"456\",\n\t\t\t},\n\t\t\texpected: \"godaddy: some credentials information are missing: GODADDY_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"123\",\n\t\t\t\tEnvAPISecret: \"\",\n\t\t\t},\n\t\t\texpected: \"godaddy: some credentials information are missing: GODADDY_API_SECRET\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tapiKey    string\n\t\tapiSecret string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tapiKey:    \"123\",\n\t\t\tapiSecret: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"godaddy: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing api key\",\n\t\t\tapiSecret: \"456\",\n\t\t\texpected:  \"godaddy: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret key\",\n\t\t\tapiKey:   \"123\",\n\t\t\texpected: \"godaddy: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.APISecret = test.apiSecret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/godaddy/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// DefaultBaseURL represents the API endpoint to call.\nconst DefaultBaseURL = \"https://api.godaddy.com\"\n\nconst authorizationHeader = \"Authorization\"\n\ntype Client struct {\n\tapiKey    string\n\tapiSecret string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(apiKey, apiSecret string) *Client {\n\tbaseURL, _ := url.Parse(DefaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tapiSecret:  apiSecret,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// GetRecords retrieves DNS Records for the specified Domain.\n// https://developer.godaddy.com/doc/endpoint/domains#/v1/recordGet\nfunc (c *Client) GetRecords(ctx context.Context, domainZone, rType, recordName string) ([]DNSRecord, error) {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"domains\", domainZone, \"records\", rType, recordName)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar records []DNSRecord\n\n\terr = c.do(req, &records)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn records, nil\n}\n\n// UpdateTxtRecords replaces all DNS Records for the specified Domain with the specified Type.\n// https://developer.godaddy.com/doc/endpoint/domains#/v1/recordReplaceType\nfunc (c *Client) UpdateTxtRecords(ctx context.Context, records []DNSRecord, domainZone, recordName string) error {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"domains\", domainZone, \"records\", \"TXT\", recordName)\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, records)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\n// DeleteTxtRecords deletes all DNS Records for the specified Domain with the specified Type and Name.\n// https://developer.godaddy.com/doc/endpoint/domains#/v1/recordDeleteTypeName\nfunc (c *Client) DeleteTxtRecords(ctx context.Context, domainZone, recordName string) error {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"domains\", domainZone, \"records\", \"TXT\", recordName)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(authorizationHeader, fmt.Sprintf(\"sso-key %s:%s\", c.apiKey, c.apiSecret))\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code: %d] %w\", resp.StatusCode, &errAPI)\n}\n"
  },
  {
    "path": "providers/dns/godaddy/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"key\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"sso-key key:secret\"))\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/domains/example.com/records/TXT/\", servermock.ResponseFromFixture(\"getrecords.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"example.com\", \"TXT\", \"\")\n\trequire.NoError(t, err)\n\n\texpected := []DNSRecord{\n\t\t{Name: \"_acme-challenge\", Type: \"TXT\", Data: \" \", TTL: 600},\n\t\t{Name: \"_acme-challenge.example\", Type: \"TXT\", Data: \"6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU\", TTL: 600},\n\t\t{Name: \"_acme-challenge.example\", Type: \"TXT\", Data: \"8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek\", TTL: 600},\n\t\t{Name: \"_acme-challenge.lego\", Type: \"TXT\", Data: \" \", TTL: 600},\n\t\t{Name: \"_acme-challenge.lego\", Type: \"TXT\", Data: \"0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A\", TTL: 600},\n\t\t{Name: \"_acme-challenge.lego\", Type: \"TXT\", Data: \"acme\", TTL: 600},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetRecords_errors(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/domains/example.com/records/TXT/\",\n\t\t\tservermock.ResponseFromFixture(\"errors.json\").WithStatusCode(http.StatusUnprocessableEntity)).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"example.com\", \"TXT\", \"\")\n\trequire.EqualError(t, err, \"[status code: 422] INVALID_BODY: Request body doesn't fulfill schema, see details in `fields`\")\n\tassert.Nil(t, records)\n}\n\nfunc TestClient_UpdateTxtRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /v1/domains/example.com/records/TXT/lego\", nil,\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"update_records-request.json\")).\n\t\tBuild(t)\n\n\trecords := []DNSRecord{\n\t\t{Name: \"_acme-challenge\", Type: \"TXT\", Data: \" \", TTL: 600},\n\t\t{Name: \"_acme-challenge.example\", Type: \"TXT\", Data: \"6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU\", TTL: 600},\n\t\t{Name: \"_acme-challenge.example\", Type: \"TXT\", Data: \"8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek\", TTL: 600},\n\t\t{Name: \"_acme-challenge.lego\", Type: \"TXT\", Data: \" \", TTL: 600},\n\t\t{Name: \"_acme-challenge.lego\", Type: \"TXT\", Data: \"0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A\", TTL: 600},\n\t\t{Name: \"_acme-challenge.lego\", Type: \"TXT\", Data: \"acme\", TTL: 600},\n\t}\n\n\terr := client.UpdateTxtRecords(t.Context(), records, \"example.com\", \"lego\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_UpdateTxtRecords_errors(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /v1/domains/example.com/records/TXT/lego\",\n\t\t\tservermock.ResponseFromFixture(\"errors.json\").WithStatusCode(http.StatusUnprocessableEntity),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"update_records-request.json\")).\n\t\tBuild(t)\n\n\trecords := []DNSRecord{\n\t\t{Name: \"_acme-challenge\", Type: \"TXT\", Data: \" \", TTL: 600},\n\t\t{Name: \"_acme-challenge.example\", Type: \"TXT\", Data: \"6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU\", TTL: 600},\n\t\t{Name: \"_acme-challenge.example\", Type: \"TXT\", Data: \"8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek\", TTL: 600},\n\t\t{Name: \"_acme-challenge.lego\", Type: \"TXT\", Data: \" \", TTL: 600},\n\t\t{Name: \"_acme-challenge.lego\", Type: \"TXT\", Data: \"0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A\", TTL: 600},\n\t\t{Name: \"_acme-challenge.lego\", Type: \"TXT\", Data: \"acme\", TTL: 600},\n\t}\n\n\terr := client.UpdateTxtRecords(t.Context(), records, \"example.com\", \"lego\")\n\trequire.EqualError(t, err, \"[status code: 422] INVALID_BODY: Request body doesn't fulfill schema, see details in `fields`\")\n}\n\nfunc TestClient_DeleteTxtRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v1/domains/example.com/records/TXT/foo\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\terr := client.DeleteTxtRecords(t.Context(), \"example.com\", \"foo\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteTxtRecords_errors(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v1/domains/example.com/records/TXT/foo\",\n\t\t\tservermock.ResponseFromFixture(\"error-extended.json\").WithStatusCode(http.StatusConflict)).\n\t\tBuild(t)\n\n\terr := client.DeleteTxtRecords(t.Context(), \"example.com\", \"foo\")\n\trequire.EqualError(t, err, \"[status code: 409] ACCESS_DENIED: Authenticated user is not allowed access [test: content (path=/foo) (pathRelated=/bar)]\")\n}\n"
  },
  {
    "path": "providers/dns/godaddy/internal/fixtures/error-extended.json",
    "content": "{\n  \"code\": \"ACCESS_DENIED\",\n  \"fields\": [\n    {\n      \"code\": \"test\",\n      \"message\": \"content\",\n      \"path\": \"/foo\",\n      \"pathRelated\": \"/bar\"\n    }\n  ],\n  \"message\": \"Authenticated user is not allowed access\"\n}\n"
  },
  {
    "path": "providers/dns/godaddy/internal/fixtures/errors.json",
    "content": "{\n  \"code\": \"INVALID_BODY\",\n  \"message\": \"Request body doesn't fulfill schema, see details in `fields`\"\n}\n"
  },
  {
    "path": "providers/dns/godaddy/internal/fixtures/getrecords.json",
    "content": "[\n  {\n    \"name\":\"_acme-challenge\",\n    \"type\":\"TXT\",\n    \"data\":\" \",\n    \"ttl\":600\n  },\n  {\n    \"name\":\"_acme-challenge.example\",\n    \"type\":\"TXT\",\n    \"data\":\"6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU\",\n    \"ttl\":600\n  },\n  {\n    \"name\":\"_acme-challenge.example\",\n    \"type\":\"TXT\",\n    \"data\":\"8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek\",\n    \"ttl\":600\n  },\n  {\n    \"name\":\"_acme-challenge.lego\",\n    \"type\":\"TXT\",\n    \"data\":\" \",\n    \"ttl\":600\n  },\n  {\n    \"name\":\"_acme-challenge.lego\",\n    \"type\":\"TXT\",\n    \"data\":\"0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A\",\n    \"ttl\":600\n  },\n  {\n    \"name\":\"_acme-challenge.lego\",\n    \"type\":\"TXT\",\n    \"data\":\"acme\",\n    \"ttl\":600\n  }\n]\n"
  },
  {
    "path": "providers/dns/godaddy/internal/fixtures/update_records-request.json",
    "content": "[\n  {\n    \"name\": \"_acme-challenge\",\n    \"type\": \"TXT\",\n    \"data\": \" \",\n    \"ttl\": 600\n  },\n  {\n    \"name\": \"_acme-challenge.example\",\n    \"type\": \"TXT\",\n    \"data\": \"6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU\",\n    \"ttl\": 600\n  },\n  {\n    \"name\": \"_acme-challenge.example\",\n    \"type\": \"TXT\",\n    \"data\": \"8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek\",\n    \"ttl\": 600\n  },\n  {\n    \"name\": \"_acme-challenge.lego\",\n    \"type\": \"TXT\",\n    \"data\": \" \",\n    \"ttl\": 600\n  },\n  {\n    \"name\": \"_acme-challenge.lego\",\n    \"type\": \"TXT\",\n    \"data\": \"0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A\",\n    \"ttl\": 600\n  },\n  {\n    \"name\": \"_acme-challenge.lego\",\n    \"type\": \"TXT\",\n    \"data\": \"acme\",\n    \"ttl\": 600\n  }\n]\n"
  },
  {
    "path": "providers/dns/godaddy/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// DNSRecord a DNS record.\ntype DNSRecord struct {\n\tName string `json:\"name,omitempty\"`\n\tType string `json:\"type,omitempty\"`\n\tData string `json:\"data\"`\n\tTTL  int    `json:\"ttl,omitempty\"`\n\n\tPriority int    `json:\"priority,omitempty\"`\n\tPort     int    `json:\"port,omitempty\"`\n\tProtocol string `json:\"protocol,omitempty\"`\n\tService  string `json:\"service,omitempty\"`\n\tWeight   int    `json:\"weight,omitempty\"`\n}\n\ntype APIError struct {\n\tCode    string  `json:\"code,omitempty\"`\n\tFields  []Field `json:\"fields,omitempty\"`\n\tMessage string  `json:\"message,omitempty\"`\n}\n\nfunc (a APIError) Error() string {\n\tmsg := new(strings.Builder)\n\n\t_, _ = fmt.Fprintf(msg, \"%s: %s\", a.Code, a.Message)\n\n\tfor _, field := range a.Fields {\n\t\tmsg.WriteString(\" \")\n\t\tmsg.WriteString(field.String())\n\t}\n\n\treturn msg.String()\n}\n\ntype Field struct {\n\tCode        string `json:\"code,omitempty\"`\n\tMessage     string `json:\"message,omitempty\"`\n\tPath        string `json:\"path,omitempty\"`\n\tPathRelated string `json:\"pathRelated,omitempty\"`\n}\n\nfunc (f Field) String() string {\n\tmsg := fmt.Sprintf(\"[%s: %s\", f.Code, f.Message)\n\n\tif f.Path != \"\" {\n\t\tmsg += fmt.Sprintf(\" (path=%s)\", f.Path)\n\t}\n\n\tif f.PathRelated != \"\" {\n\t\tmsg += fmt.Sprintf(\" (pathRelated=%s)\", f.PathRelated)\n\t}\n\n\tmsg += \"]\"\n\n\treturn msg\n}\n"
  },
  {
    "path": "providers/dns/googledomains/googledomains.go",
    "content": "// Package googledomains implements a DNS provider for solving the DNS-01 challenge using Google Domains DNS API.\npackage googledomains\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"GOOGLE_DOMAINS_\"\n\n\tEnvAccessToken        = envNamespace + \"ACCESS_TOKEN\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAccessToken        string\n\tPollingInterval    time.Duration\n\tPropagationTimeout time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{}\n}\n\ntype DNSProvider struct{}\n\n// NewDNSProvider returns the Google Domains DNS provider with a default configuration.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\treturn NewDNSProviderConfig(&Config{})\n}\n\n// NewDNSProviderConfig returns the Google Domains DNS provider with the provided config.\nfunc NewDNSProviderConfig(_ *Config) (*DNSProvider, error) {\n\treturn nil, errors.New(\"googledomains: provider has shut down\")\n}\n\nfunc (d *DNSProvider) Present(_, _, _ string) error {\n\treturn nil\n}\n\nfunc (d *DNSProvider) CleanUp(_, _, _ string) error {\n\treturn nil\n}\n\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn dns01.DefaultPropagationTimeout, dns01.DefaultPollingInterval\n}\n"
  },
  {
    "path": "providers/dns/googledomains/googledomains.toml",
    "content": "Name = \"Google Domains\"\nDescription = '''\nThe Google Domains DNS provider has shut down.\n'''\nURL = \"https://github.com/go-acme/lego/issues/2553\"\nCode = \"googledomains\"\nSince = \"v4.11.0\"\n\nExample = '''\nGOOGLE_DOMAINS_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns googledomains -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    GOOGLE_DOMAINS_ACCESS_TOKEN = \"Access token\"\n  [Configuration.Additional]\n    GOOGLE_DOMAINS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    GOOGLE_DOMAINS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    GOOGLE_DOMAINS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  GoClient = \"https://github.com/googleapis/google-api-go-client\"\n\n"
  },
  {
    "path": "providers/dns/gravity/gravity.go",
    "content": "// Package gravity implements a DNS provider for solving the DNS-01 challenge using Gravity.\npackage gravity\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/gravity/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/google/uuid\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"GRAVITY_\"\n\n\tEnvUsername  = envNamespace + \"USERNAME\"\n\tEnvPassword  = envNamespace + \"PASSWORD\"\n\tEnvServerURL = envNamespace + \"SERVER_URL\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername  string\n\tPassword  string\n\tServerURL string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, 1*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecords   map[string]internal.Record\n\trecordsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Gravity.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword, EnvServerURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"gravity: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\tconfig.ServerURL = values[EnvServerURL]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Gravity.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"gravity: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.ServerURL, config.Username, config.Password)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"gravity: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:  config,\n\t\tclient:  client,\n\t\trecords: make(map[string]internal.Record),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t_, err := d.client.Login(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gravity: login: %w\", err)\n\t}\n\n\tzone, err := d.findZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gravity: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gravity: %w\", err)\n\t}\n\n\tid := uuid.New()\n\n\trecord := internal.Record{\n\t\tData:     info.Value,\n\t\tHostname: subDomain,\n\t\tType:     \"TXT\",\n\t\tUID:      id.String(),\n\t}\n\n\terr = d.client.CreateDNSRecord(ctx, zone, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gravity: create DNS record: %w\", err)\n\t}\n\n\td.recordsMu.Lock()\n\n\trecord.Fqdn = zone\n\td.records[token] = record\n\td.recordsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordsMu.Lock()\n\trecord, ok := d.records[token]\n\td.recordsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"gravity: unknown record for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\terr := d.client.DeleteDNSRecord(context.Background(), record.Fqdn, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"gravity: delete record: %w\", err)\n\t}\n\n\td.recordsMu.Lock()\n\tdelete(d.records, token)\n\td.recordsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential implements the [dns01.sequential] interface.\n// It changes the behavior of the provider to resolve DNS challenges sequentially.\n// Returns the interval between each iteration.\n//\n// Gravity supports adding multiple records for the same domain, but the DNS server doesn't work as expected:\n// if you call the DNS server, it will answer only the latest record instead of all of them.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n\nfunc (d *DNSProvider) findZone(ctx context.Context, effectiveFQDN string) (string, error) {\n\tvar zone string\n\n\tfor fqdn := range dns01.DomainsSeq(effectiveFQDN) {\n\t\tzones, err := d.client.GetDNSZones(ctx, fqdn)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"get DNS zones: %w\", err)\n\t\t}\n\n\t\tif len(zones) != 0 {\n\t\t\tzone = zones[0].Name\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif zone == \"\" {\n\t\treturn \"\", fmt.Errorf(\"could not find zone for %q\", effectiveFQDN)\n\t}\n\n\treturn zone, nil\n}\n"
  },
  {
    "path": "providers/dns/gravity/gravity.toml",
    "content": "Name = \"Gravity\"\nDescription = ''''''\nURL = \"https://gravity.beryju.io/\"\nCode = \"gravity\"\nSince = \"v4.30.0\"\n\nExample = '''\nGRAVITY_SERVER_URL=\"https://example.org:1234\" \\\nGRAVITY_USERNAME=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nGRAVITY_PASSWORD=\"yyyyyyyyyyyyyyyyyyyyy\" \\\nlego --dns gravity -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    GRAVITY_SERVER_URL = \"URL of the server\"\n    GRAVITY_USERNAME = \"Username\"\n    GRAVITY_PASSWORD = \"Password\"\n  [Configuration.Additional]\n    GRAVITY_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    GRAVITY_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    GRAVITY_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 1)\"\n    GRAVITY_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://gravity.beryju.io/docs/api/reference/\"\n"
  },
  {
    "path": "providers/dns/gravity/gravity_test.go",
    "content": "package gravity\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-acme/lego/v4/providers/dns/gravity/internal\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvUsername,\n\tEnvPassword,\n\tEnvServerURL,\n).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername:  \"user\",\n\t\t\t\tEnvPassword:  \"secret\",\n\t\t\t\tEnvServerURL: \"https://example.org:1234\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing EnvUsername\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername:  \"\",\n\t\t\t\tEnvPassword:  \"secret\",\n\t\t\t\tEnvServerURL: \"https://example.org:1234\",\n\t\t\t},\n\t\t\texpected: \"gravity: some credentials information are missing: GRAVITY_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing EnvPassword\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername:  \"user\",\n\t\t\t\tEnvPassword:  \"\",\n\t\t\t\tEnvServerURL: \"https://example.org:1234\",\n\t\t\t},\n\t\t\texpected: \"gravity: some credentials information are missing: GRAVITY_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing EnvServerURL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername:  \"user\",\n\t\t\t\tEnvPassword:  \"secret\",\n\t\t\t\tEnvServerURL: \"\",\n\t\t\t},\n\t\t\texpected: \"gravity: some credentials information are missing: GRAVITY_SERVER_URL\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"gravity: some credentials information are missing: GRAVITY_USERNAME,GRAVITY_PASSWORD,GRAVITY_SERVER_URL\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tusername  string\n\t\tpassword  string\n\t\tserverURL string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tusername:  \"user\",\n\t\t\tpassword:  \"secret\",\n\t\t\tserverURL: \"https://example.org:1234\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing username\",\n\t\t\tusername:  \"\",\n\t\t\tpassword:  \"secret\",\n\t\t\tserverURL: \"https://example.org:1234\",\n\t\t\texpected:  \"gravity: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing password\",\n\t\t\tusername:  \"user\",\n\t\t\tpassword:  \"\",\n\t\t\tserverURL: \"https://example.org:1234\",\n\t\t\texpected:  \"gravity: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing server URL\",\n\t\t\tusername:  \"user\",\n\t\t\tpassword:  \"secret\",\n\t\t\tserverURL: \"\",\n\t\t\texpected:  \"gravity: server URL missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"gravity: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\t\t\tconfig.ServerURL = test.serverURL\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\n\t\t\tconfig.Username = \"user\"\n\t\t\tconfig.Password = \"secret\"\n\t\t\tconfig.ServerURL = server.URL\n\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders(),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /api/v1/auth/login\",\n\t\t\tservermock.ResponseFromInternal(\"login.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"login-request.json\")).\n\t\tRoute(\"GET /api/v1/dns/\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\tif req.URL.Query().Get(\"name\") != \"example.com.\" {\n\t\t\t\t\tservermock.ResponseFromInternal(\"zones.json\").ServeHTTP(rw, req)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tservermock.ResponseFromInternal(\"zones_empty.json\").ServeHTTP(rw, req)\n\t\t\t}),\n\t\t).\n\t\tRoute(\"POST /api/v1/dns/zones/records\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"zone\", \"example.com.\").\n\t\t\t\tWithRegexp(\"uid\", `\\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12}`).\n\t\t\t\tWith(\"hostname\", \"_acme-challenge\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"DELETE /api/v1/dns/zones/records\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"zone\", \"example.com.\").\n\t\t\t\tWith(\"uid\", \"123\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"hostname\", \"_acme-challenge\")).\n\t\tBuild(t)\n\n\tprovider.records[\"abc\"] = internal.Record{\n\t\tFqdn:     \"example.com.\",\n\t\tHostname: \"_acme-challenge\",\n\t\tType:     \"TXT\",\n\t\tUID:      \"123\",\n\t}\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/gravity/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\t\"golang.org/x/net/publicsuffix\"\n)\n\n// Client the Gravity API client.\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(serverURL, username, password string) (*Client, error) {\n\tif username == \"\" || password == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tif serverURL == \"\" {\n\t\treturn nil, errors.New(\"server URL missing\")\n\t}\n\n\tbaseURL, err := url.Parse(serverURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) Login(ctx context.Context) (*Auth, error) {\n\tjar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc.HTTPClient.Jar = jar\n\n\tlogin := Login{\n\t\tUsername: c.username,\n\t\tPassword: c.password,\n\t}\n\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v1\", \"auth\", \"login\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, login)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &Auth{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\nfunc (c *Client) Me(ctx context.Context) (*UserInfo, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v1\", \"auth\", \"me\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &UserInfo{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, err\n}\n\nfunc (c *Client) GetDNSZones(ctx context.Context, name string) ([]Zone, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v1\", \"dns\", \"zones\")\n\n\tif name != \"\" {\n\t\tquery := endpoint.Query()\n\t\tquery.Set(\"name\", name)\n\t\tendpoint.RawQuery = query.Encode()\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := Zones{}\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Zones, nil\n}\n\nfunc (c *Client) CreateDNSRecord(ctx context.Context, zone string, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v1\", \"dns\", \"zones\", \"records\")\n\n\tquery := endpoint.Query()\n\n\tquery.Set(\"zone\", zone)\n\tquery.Set(\"hostname\", record.Hostname)\n\n\t// When the UID is the same as an existing one, the record is updated, else a new record is created.\n\t// An explicit UID is not required to create a record.\n\tif record.UID != \"\" {\n\t\tquery.Set(\"uid\", record.UID)\n\t}\n\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) DeleteDNSRecord(ctx context.Context, zone string, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v1\", \"dns\", \"zones\", \"records\")\n\n\tquery := endpoint.Query()\n\n\tquery.Set(\"zone\", zone)\n\tquery.Set(\"hostname\", record.Hostname)\n\tquery.Set(\"uid\", record.UID)\n\tquery.Set(\"type\", record.Type)\n\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/gravity/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(server.URL, \"user\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders(),\n\t)\n}\n\nfunc TestClient_Login(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /api/v1/auth/login\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\thttp.SetCookie(rw, &http.Cookie{\n\t\t\t\t\tName:  \"gravity_session\",\n\t\t\t\t\tValue: \"session_id\",\n\t\t\t\t\tPath:  \"/\",\n\t\t\t\t})\n\n\t\t\t\tservermock.ResponseFromFixture(\"login.json\").ServeHTTP(rw, req)\n\t\t\t}),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"login-request.json\")).\n\t\tBuild(t)\n\n\tauth, err := client.Login(t.Context())\n\trequire.NoError(t, err)\n\n\tcookies := client.HTTPClient.Jar.Cookies(client.baseURL)\n\n\trequire.Len(t, cookies, 1)\n\n\tassert.Equal(t, \"gravity_session\", cookies[0].Name)\n\tassert.Equal(t, \"session_id\", cookies[0].Value)\n\n\texpected := &Auth{Successful: true}\n\n\tassert.Equal(t, expected, auth)\n}\n\nfunc TestClient_Login_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /api/v1/auth/login\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.Login(t.Context())\n\trequire.EqualError(t, err, \"status: UNAUTHENTICATED, error: unauthenticated, additionalProp1: string\")\n}\n\nfunc TestClient_Me(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/v1/auth/me\",\n\t\t\tservermock.ResponseFromFixture(\"me.json\")).\n\t\tBuild(t)\n\n\tinfo, err := client.Me(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := &UserInfo{\n\t\tUsername:      \"admin\",\n\t\tAuthenticated: true,\n\t\tPermissions: []Permission{{\n\t\t\tMethods: []string{\"GET\", \"POST\", \"PUT\", \"HEAD\", \"DELETE\"},\n\t\t\tPath:    \"/*\",\n\t\t}},\n\t}\n\n\tassert.Equal(t, expected, info)\n}\n\nfunc TestClient_GetDNSZones(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/v1/dns/\",\n\t\t\tservermock.ResponseFromFixture(\"zones.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com.\")).\n\t\tBuild(t)\n\n\tzones, err := client.GetDNSZones(t.Context(), \"example.com.\")\n\trequire.NoError(t, err)\n\n\texpected := []Zone{{\n\t\tName: \"example.com.\",\n\t\tHandlerConfigs: []HandlerConfig{\n\t\t\t{Type: \"memory\"},\n\t\t\t{Type: \"etcd\"},\n\t\t},\n\t\tDefaultTTL:  86400,\n\t\tRecordCount: 1,\n\t}}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_CreateDNSRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /api/v1/dns/zones/records\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_record-request.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"zone\", \"example.com.\").\n\t\t\t\tWith(\"uid\", \"123\").\n\t\t\t\tWith(\"hostname\", \"_acme-challenge\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tData:     \"txtTXTtxt\",\n\t\tHostname: \"_acme-challenge\",\n\t\tType:     \"TXT\",\n\t\tUID:      \"123\",\n\t}\n\n\terr := client.CreateDNSRecord(t.Context(), \"example.com.\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteDNSRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /api/v1/dns/zones/records\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"zone\", \"example.com.\").\n\t\t\t\tWith(\"uid\", \"123\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"hostname\", \"_acme-challenge\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tData:     \"txtTXTtxt\",\n\t\tHostname: \"_acme-challenge\",\n\t\tType:     \"TXT\",\n\t\tUID:      \"123\",\n\t}\n\n\terr := client.DeleteDNSRecord(t.Context(), \"example.com.\", record)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/gravity/internal/fixtures/create_record-request.json",
    "content": "{\n  \"data\": \"txtTXTtxt\",\n  \"hostname\": \"_acme-challenge\",\n  \"type\": \"TXT\",\n  \"uid\": \"123\"\n}\n"
  },
  {
    "path": "providers/dns/gravity/internal/fixtures/error.json",
    "content": "{\n  \"code\": 0,\n  \"context\": {\n    \"additionalProp1\": \"string\"\n  },\n  \"error\": \"unauthenticated\",\n  \"status\": \"UNAUTHENTICATED\"\n}\n"
  },
  {
    "path": "providers/dns/gravity/internal/fixtures/login-request.json",
    "content": "{\n  \"username\": \"user\",\n  \"password\": \"secret\"\n}\n"
  },
  {
    "path": "providers/dns/gravity/internal/fixtures/login.json",
    "content": "{\n  \"successful\": true\n}\n"
  },
  {
    "path": "providers/dns/gravity/internal/fixtures/me.json",
    "content": "{\n  \"username\": \"admin\",\n  \"authenticated\": true,\n  \"permissions\": [\n    {\n      \"path\": \"/*\",\n      \"methods\": [\n        \"GET\",\n        \"POST\",\n        \"PUT\",\n        \"HEAD\",\n        \"DELETE\"\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/gravity/internal/fixtures/me_unauthenticated.json",
    "content": "{\n  \"username\": \"\",\n  \"authenticated\": false,\n  \"permissions\": null\n}\n"
  },
  {
    "path": "providers/dns/gravity/internal/fixtures/zones.json",
    "content": "{\n  \"zones\": [\n    {\n      \"name\": \"example.com.\",\n      \"handlerConfigs\": [\n        {\n          \"type\": \"memory\"\n        },\n        {\n          \"type\": \"etcd\"\n        }\n      ],\n      \"defaultTTL\": 86400,\n      \"authoritative\": false,\n      \"hook\": \"\",\n      \"recordCount\": 1\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/gravity/internal/fixtures/zones_empty.json",
    "content": "{\n  \"zones\": null\n}\n"
  },
  {
    "path": "providers/dns/gravity/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype APIError struct {\n\tStatus   string            `json:\"status\"`\n\tErrorMsg string            `json:\"error\"`\n\tCode     int               `json:\"code\"`\n\tContext  map[string]string `json:\"context\"`\n}\n\nfunc (a *APIError) Error() string {\n\tmsg := new(strings.Builder)\n\n\t_, _ = fmt.Fprintf(msg, \"status: %s, error: %s\", a.Status, a.ErrorMsg)\n\n\tif a.Code != 0 {\n\t\t_, _ = fmt.Fprintf(msg, \", code: %d\", a.Code)\n\t}\n\n\tif len(a.Context) != 0 {\n\t\tfor k, v := range a.Context {\n\t\t\t_, _ = fmt.Fprintf(msg, \", %s: %s\", k, v)\n\t\t}\n\t}\n\n\treturn msg.String()\n}\n\ntype Login struct {\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n}\n\ntype Auth struct {\n\tSuccessful bool `json:\"successful\"`\n}\n\ntype UserInfo struct {\n\tUsername      string       `json:\"username\"`\n\tAuthenticated bool         `json:\"authenticated\"`\n\tPermissions   []Permission `json:\"permissions\"`\n}\n\ntype Permission struct {\n\tMethods []string `json:\"methods\"`\n\tPath    string   `json:\"path\"`\n}\n\ntype Zones struct {\n\tZones []Zone `json:\"zones\"`\n}\n\ntype Zone struct {\n\tName           string          `json:\"name\"`\n\tHandlerConfigs []HandlerConfig `json:\"handlerConfigs\"`\n\tDefaultTTL     int             `json:\"defaultTTL\"`\n\tAuthoritative  bool            `json:\"authoritative\"`\n\tHook           string          `json:\"hook\"`\n\tRecordCount    int             `json:\"recordCount\"`\n}\n\ntype HandlerConfig struct {\n\tType     string   `json:\"type\"`\n\tCacheTTL int      `json:\"cache_ttl,omitempty\"`\n\tTo       []string `json:\"to,omitempty\"`\n}\n\ntype Record struct {\n\tData         string `json:\"data,omitempty\"`\n\tFqdn         string `json:\"fqdn,omitempty\"`\n\tHostname     string `json:\"hostname,omitempty\"`\n\tMxPreference int    `json:\"mxPreference,omitempty\"`\n\tSrvPort      int    `json:\"srvPort,omitempty\"`\n\tSrvPriority  int    `json:\"srvPriority,omitempty\"`\n\tSrvWeight    int    `json:\"srvWeight,omitempty\"`\n\tType         string `json:\"type,omitempty\"`\n\tUID          string `json:\"uid,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/hetzner/hetzner.go",
    "content": "// Package hetzner implements a DNS provider for solving the DNS-01 challenge using Hetzner DNS.\npackage hetzner\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hetzner/internal/hetznerv1\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hetzner/internal/legacy\"\n)\n\n// Environment variables names.\nconst (\n\t// Deprecated: use EnvAPIToken instead.\n\tEnvAPIKey   = legacy.EnvAPIKey\n\tEnvAPIToken = hetznerv1.EnvAPIToken\n\n\tEnvTTL                = hetznerv1.EnvTTL\n\tEnvPropagationTimeout = hetznerv1.EnvPropagationTimeout\n\tEnvPollingInterval    = hetznerv1.EnvPollingInterval\n\tEnvHTTPTimeout        = hetznerv1.EnvHTTPTimeout\n)\n\nconst minTTL = 60\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\t// Deprecated: use APIToken instead\n\tAPIKey string\n\n\tAPIToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprovider challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for hetzner.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tfoundAPIToken := env.GetOrFile(EnvAPIToken) != \"\"\n\tfoundAPIKey := env.GetOrFile(EnvAPIKey) != \"\"\n\n\tswitch {\n\tcase foundAPIToken:\n\t\tprovider, err := hetznerv1.NewDNSProvider()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &DNSProvider{provider: provider}, nil\n\n\tcase foundAPIKey:\n\t\tlog.Warnf(\"APIKey (legacy Hetzner DNS API) is deprecated, please use APIToken (Hetzner Cloud API) instead.\")\n\n\t\tprovider, err := legacy.NewDNSProvider()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &DNSProvider{provider: provider}, nil\n\n\tdefault:\n\t\tprovider, err := hetznerv1.NewDNSProvider()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &DNSProvider{provider: provider}, nil\n\t}\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for hetzner.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"hetzner: the configuration of the DNS provider is nil\")\n\t}\n\n\tswitch {\n\tcase config.APIToken != \"\":\n\t\tcfg := &hetznerv1.Config{\n\t\t\tAPIToken:           config.APIToken,\n\t\t\tPropagationTimeout: config.PropagationTimeout,\n\t\t\tPollingInterval:    config.PollingInterval,\n\t\t\tTTL:                config.TTL,\n\t\t\tHTTPClient:         config.HTTPClient,\n\t\t}\n\n\t\tprovider, err := hetznerv1.NewDNSProviderConfig(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &DNSProvider{provider: provider}, nil\n\n\tcase config.APIKey != \"\":\n\t\tlog.Warnf(\"%s (legacy Hetzner DNS API) is deprecated, please use %s (Hetzner Cloud API) instead.\", EnvAPIKey, EnvAPIToken)\n\n\t\tcfg := &legacy.Config{\n\t\t\tAPIKey:             config.APIKey,\n\t\t\tPropagationTimeout: config.PropagationTimeout,\n\t\t\tPollingInterval:    config.PollingInterval,\n\t\t\tTTL:                config.TTL,\n\t\t\tHTTPClient:         config.HTTPClient,\n\t\t}\n\n\t\tprovider, err := legacy.NewDNSProviderConfig(cfg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &DNSProvider{provider: provider}, nil\n\t}\n\n\treturn nil, errors.New(\"hetzner: credentials missing\")\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.provider.Timeout()\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\treturn d.provider.Present(domain, token, keyAuth)\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\treturn d.provider.CleanUp(domain, token, keyAuth)\n}\n"
  },
  {
    "path": "providers/dns/hetzner/hetzner.toml",
    "content": "Name = \"Hetzner\"\nDescription = ''''''\nURL = \"https://hetzner.com\"\nCode = \"hetzner\"\nSince = \"v3.7.0\"\n\nExample = '''\nHETZNER_API_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns hetzner -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    HETZNER_API_TOKEN = \"API token\"\n  [Configuration.Additional]\n    HETZNER_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    HETZNER_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    HETZNER_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    HETZNER_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://docs.hetzner.cloud/reference/cloud#dns\"\n"
  },
  {
    "path": "providers/dns/hetzner/hetzner_test.go",
    "content": "package hetzner\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hetzner/internal/hetznerv1\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hetzner/internal/legacy\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar envTest = tester.NewEnvTest(EnvAPIKey, EnvAPIToken)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc    string\n\t\tenvVars map[string]string\n\n\t\texpectedProvider challenge.ProviderTimeout\n\t\texpectedError    string\n\t}{\n\t\t{\n\t\t\tdesc: \"success (v1)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"123\",\n\t\t\t},\n\t\t\texpectedProvider: &hetznerv1.DNSProvider{},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success (legacy)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t\texpectedProvider: &legacy.DNSProvider{},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success (both)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:   \"123\",\n\t\t\t\tEnvAPIToken: \"123\",\n\t\t\t},\n\t\t\texpectedProvider: &hetznerv1.DNSProvider{},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:   \"\",\n\t\t\t\tEnvAPIToken: \"\",\n\t\t\t},\n\t\t\texpectedError: \"hetzner: some credentials information are missing: HETZNER_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.IsType(t, test.expectedProvider, p.provider)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tapiToken string\n\t\tttl      int\n\n\t\texpectedProvider challenge.ProviderTimeout\n\t\texpectedError    string\n\t}{\n\t\t{\n\t\t\tdesc:             \"success (v1)\",\n\t\t\tttl:              minTTL,\n\t\t\tapiToken:         \"123\",\n\t\t\texpectedProvider: &hetznerv1.DNSProvider{},\n\t\t},\n\t\t{\n\t\t\tdesc:             \"success (legacy)\",\n\t\t\tttl:              minTTL,\n\t\t\tapiKey:           \"456\",\n\t\t\texpectedProvider: &legacy.DNSProvider{},\n\t\t},\n\t\t{\n\t\t\tdesc:             \"success (both)\",\n\t\t\tttl:              minTTL,\n\t\t\tapiToken:         \"123\",\n\t\t\tapiKey:           \"456\",\n\t\t\texpectedProvider: &hetznerv1.DNSProvider{},\n\t\t},\n\t\t{\n\t\t\tdesc:          \"missing credentials\",\n\t\t\tttl:           minTTL,\n\t\t\texpectedError: \"hetzner: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = test.apiToken\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.TTL = test.ttl\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.IsType(t, test.expectedProvider, p.provider)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/fixtures/add_rrset_records-request.json",
    "content": "{\n  \"ttl\": 120,\n  \"records\": [\n    {\n      \"value\": \"\\\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\\\"\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/fixtures/add_rrset_records.json",
    "content": "{\n  \"action\": {\n    \"id\": 1,\n    \"command\": \"add_rrset_records\",\n    \"status\": \"running\",\n    \"progress\": 50,\n    \"started\": \"2016-01-30T23:55:00+00:00\",\n    \"finished\": null,\n    \"resources\": [\n      {\n        \"id\": 42,\n        \"type\": \"zone\"\n      }\n    ],\n    \"error\": null\n  }\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/fixtures/get_action_error.json",
    "content": "{\n  \"action\": {\n    \"id\": 1,\n    \"command\": \"remove_rrset_records\",\n    \"status\": \"error\",\n    \"started\": \"2016-01-30T23:55:00+00:00\",\n    \"finished\": \"2016-01-30T23:55:00+00:00\",\n    \"progress\": 50,\n    \"resources\": [\n      {\n        \"id\": 42,\n        \"type\": \"zone\"\n      }\n    ],\n    \"error\": {\n      \"code\": \"action_failed\",\n      \"message\": \"Action failed\"\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/fixtures/get_action_running.json",
    "content": "{\n  \"action\": {\n    \"id\": 1,\n    \"command\": \"remove_rrset_records\",\n    \"status\": \"running\",\n    \"started\": \"2016-01-30T23:55:00+00:00\",\n    \"finished\": \"2016-01-30T23:55:00+00:00\",\n    \"progress\": 50,\n    \"resources\": [\n      {\n        \"id\": 42,\n        \"type\": \"zone\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/fixtures/get_action_success.json",
    "content": "{\n  \"action\": {\n    \"id\": 1,\n    \"command\": \"remove_rrset_records\",\n    \"status\": \"success\",\n    \"started\": \"2016-01-30T23:55:00+00:00\",\n    \"finished\": \"2016-01-30T23:55:00+00:00\",\n    \"progress\": 100,\n    \"resources\": [\n      {\n        \"id\": 42,\n        \"type\": \"zone\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/fixtures/remove_rrset_records-request.json",
    "content": "{\n  \"records\": [\n    {\n      \"value\": \"\\\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\\\"\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/fixtures/remove_rrset_records.json",
    "content": "{\n  \"action\": {\n    \"id\": 1,\n    \"command\": \"remove_rrset_records\",\n    \"status\": \"running\",\n    \"progress\": 50,\n    \"started\": \"2016-01-30T23:55:00+00:00\",\n    \"finished\": null,\n    \"resources\": [\n      {\n        \"id\": 42,\n        \"type\": \"zone\"\n      }\n    ],\n    \"error\": null\n  }\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/hetznerv1.go",
    "content": "// Package hetznerv1 implements a DNS provider for solving the DNS-01 challenge using Hetzner.\npackage hetznerv1\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hetzner/internal/hetznerv1/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"golang.org/x/net/idna\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"HETZNER_\"\n\n\tEnvAPIToken = envNamespace + \"API_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Hetzner.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"hetzner: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIToken = values[EnvAPIToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Hetzner.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"hetzner: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIToken == \"\" {\n\t\treturn nil, errors.New(\"hetzner: credentials missing\")\n\t}\n\n\tclient, err := internal.NewClient(\n\t\tclientdebug.Wrap(\n\t\t\tinternal.OAuthStaticAccessToken(config.HTTPClient, config.APIToken),\n\t\t),\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"hetzner: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner: %w\", err)\n\t}\n\n\tsubDomainPunnycoded, err := idna.ToASCII(dns01.UnFqdn(subDomain))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner: %w\", err)\n\t}\n\n\tzone, err := idna.ToASCII(dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner: %w\", err)\n\t}\n\n\trecords := []internal.Record{{Value: strconv.Quote(info.Value)}}\n\n\taction, err := d.client.AddRRSetRecords(ctx, zone, \"TXT\", subDomainPunnycoded, d.config.TTL, records)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner: add RRSet records: %w\", err)\n\t}\n\n\terr = d.waitAction(ctx, action.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner: wait (add RRSet records): %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner: %w\", err)\n\t}\n\n\tsubDomainPunnycoded, err := idna.ToASCII(dns01.UnFqdn(subDomain))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner: %w\", err)\n\t}\n\n\tzone, err := idna.ToASCII(dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner: %w\", err)\n\t}\n\n\trecords := []internal.Record{{Value: strconv.Quote(info.Value)}}\n\n\taction, err := d.client.RemoveRRSetRecords(ctx, zone, \"TXT\", subDomainPunnycoded, records)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner: remove RRSet records: %w\", err)\n\t}\n\n\terr = d.waitAction(ctx, action.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner: wait (remove RRSet records): %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) waitAction(ctx context.Context, actionID int64) error {\n\treturn wait.Retry(ctx,\n\t\tfunc() error {\n\t\t\tresult, err := d.client.GetAction(ctx, actionID)\n\t\t\tif err != nil {\n\t\t\t\treturn backoff.Permanent(fmt.Errorf(\"get action %d: %w\", actionID, err))\n\t\t\t}\n\n\t\t\tswitch result.Status {\n\t\t\tcase internal.StatusRunning:\n\t\t\t\treturn fmt.Errorf(\"action %d is %s\", actionID, internal.StatusRunning)\n\n\t\t\tcase internal.StatusError:\n\t\t\t\treturn backoff.Permanent(fmt.Errorf(\"action %d: %s: %w\", actionID, internal.StatusError, result.ErrorInfo))\n\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t},\n\t\tbackoff.WithBackOff(backoff.NewConstantBackOff(d.config.PollingInterval)),\n\t\tbackoff.WithMaxElapsedTime(d.config.PropagationTimeout),\n\t)\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/hetznerv1_test.go",
    "content": "package hetznerv1\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"hetzner: some credentials information are missing: HETZNER_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiToken string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiToken: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"hetzner: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = test.apiToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /zones/example.com/rrsets/_acme-challenge/TXT/actions/add_records\",\n\t\t\tservermock.ResponseFromFixture(\"add_rrset_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"add_rrset_records-request.json\")).\n\t\tRoute(\"GET /actions/1\",\n\t\t\tservermock.ResponseFromFixture(\"get_action_success.json\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"\", \"foobar\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_Present_error(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /zones/example.com/rrsets/_acme-challenge/TXT/actions/add_records\",\n\t\t\tservermock.ResponseFromFixture(\"add_rrset_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"add_rrset_records-request.json\")).\n\t\tRoute(\"GET /actions/1\",\n\t\t\tservermock.ResponseFromFixture(\"get_action_error.json\")).\n\t\tBuild(t)\n\n\tprovider.config.PollingInterval = 20 * time.Millisecond\n\tprovider.config.PropagationTimeout = 1 * time.Second\n\n\terr := provider.Present(\"example.com\", \"\", \"foobar\")\n\trequire.EqualError(t, err, \"hetzner: wait (add RRSet records): action 1: error: action_failed: Action failed\")\n}\n\nfunc TestDNSProvider_Present_running(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /zones/example.com/rrsets/_acme-challenge/TXT/actions/add_records\",\n\t\t\tservermock.ResponseFromFixture(\"add_rrset_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"add_rrset_records-request.json\")).\n\t\tRoute(\"GET /actions/1\",\n\t\t\tservermock.ResponseFromFixture(\"get_action_running.json\")).\n\t\tBuild(t)\n\n\tprovider.config.PollingInterval = 20 * time.Millisecond\n\tprovider.config.PropagationTimeout = 1 * time.Second\n\n\terr := provider.Present(\"example.com\", \"\", \"foobar\")\n\trequire.EqualError(t, err, \"hetzner: wait (add RRSet records): action 1 is running\")\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /zones/example.com/rrsets/_acme-challenge/TXT/actions/remove_records\",\n\t\t\tservermock.ResponseFromFixture(\"remove_rrset_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"remove_rrset_records-request.json\")).\n\t\tRoute(\"GET /actions/1\",\n\t\t\tservermock.ResponseFromFixture(\"get_action_success.json\")).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"\", \"foobar\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp_error(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /zones/example.com/rrsets/_acme-challenge/TXT/actions/remove_records\",\n\t\t\tservermock.ResponseFromFixture(\"remove_rrset_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"remove_rrset_records-request.json\")).\n\t\tRoute(\"GET /actions/1\",\n\t\t\tservermock.ResponseFromFixture(\"get_action_error.json\")).\n\t\tBuild(t)\n\n\tprovider.config.PollingInterval = 20 * time.Millisecond\n\tprovider.config.PropagationTimeout = 1 * time.Second\n\n\terr := provider.CleanUp(\"example.com\", \"\", \"foobar\")\n\trequire.EqualError(t, err, \"hetzner: wait (remove RRSet records): action 1: error: action_failed: Action failed\")\n}\n\nfunc TestDNSProvider_CleanUp_running(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /zones/example.com/rrsets/_acme-challenge/TXT/actions/remove_records\",\n\t\t\tservermock.ResponseFromFixture(\"remove_rrset_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"remove_rrset_records-request.json\")).\n\t\tRoute(\"GET /actions/1\",\n\t\t\tservermock.ResponseFromFixture(\"get_action_running.json\")).\n\t\tBuild(t)\n\n\tprovider.config.PollingInterval = 20 * time.Millisecond\n\tprovider.config.PropagationTimeout = 1 * time.Second\n\n\terr := provider.CleanUp(\"example.com\", \"\", \"foobar\")\n\trequire.EqualError(t, err, \"hetzner: wait (remove RRSet records): action 1 is running\")\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"golang.org/x/oauth2\"\n)\n\nconst defaultBaseURL = \"https://api.hetzner.cloud/v1\"\n\nconst (\n\tStatusRunning = \"running\"\n\tStatusSuccess = \"success\"\n\tStatusError   = \"error\"\n)\n\n// Client the Hetzner API client.\ntype Client struct {\n\tBaseURL    *url.URL\n\thttpClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(hc *http.Client) (*Client, error) {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\tif hc == nil {\n\t\thc = &http.Client{Timeout: 10 * time.Second}\n\t}\n\n\treturn &Client{\n\t\tBaseURL:    baseURL,\n\t\thttpClient: hc,\n\t}, nil\n}\n\n// AddRRSetRecords adds records to an RRSet.\n// https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-add-records-to-an-rrset\nfunc (c *Client) AddRRSetRecords(ctx context.Context, zoneIDName, recordType, recordName string, ttl int, records []Record) (*Action, error) {\n\tendpoint := c.BaseURL.JoinPath(\"zones\", zoneIDName, \"rrsets\", recordName, recordType, \"actions\", \"add_records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, RRSet{TTL: ttl, Records: records})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result ActionResponse\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Action, nil\n}\n\n// RemoveRRSetRecords removes records from an RRSet.\n// https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-remove-records-from-an-rrset\nfunc (c *Client) RemoveRRSetRecords(ctx context.Context, zoneIDName, recordType, recordName string, records []Record) (*Action, error) {\n\tendpoint := c.BaseURL.JoinPath(\"zones\", zoneIDName, \"rrsets\", recordName, recordType, \"actions\", \"remove_records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, RRSet{Records: records})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result ActionResponse\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Action, nil\n}\n\n// GetAction gets an action.\n// https://docs.hetzner.cloud/reference/cloud#actions-get-an-action\nfunc (c *Client) GetAction(ctx context.Context, id int64) (*Action, error) {\n\tendpoint := c.BaseURL.JoinPath(\"actions\", strconv.FormatInt(id, 10))\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result ActionResponse\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Action, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n\nfunc OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {\n\tif client == nil {\n\t\tclient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tclient.Transport = &oauth2.Transport{\n\t\tSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),\n\t\tBase:   client.Transport,\n\t}\n\n\treturn client\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(OAuthStaticAccessToken(server.Client(), \"secret\"))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer secret\"),\n\t)\n}\n\nfunc TestClient_AddRRSetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zones/example.com/rrsets/www/TXT/actions/add_records\",\n\t\t\tservermock.ResponseFromFixture(\"add_rrset_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"add_rrset_records-request.json\")).\n\t\tBuild(t)\n\n\trecords := []Record{{\n\t\tValue:   \"198.51.100.1\",\n\t\tComment: \"My web server at Hetzner Cloud.\",\n\t}}\n\n\tresult, err := client.AddRRSetRecords(t.Context(), \"example.com\", \"TXT\", \"www\", 3600, records)\n\trequire.NoError(t, err)\n\n\texpected := &Action{\n\t\tID:        1,\n\t\tCommand:   \"add_rrset_records\",\n\t\tStatus:    \"running\",\n\t\tProgress:  50,\n\t\tResources: []Resources{{ID: 590000000000000, Type: \"zone\"}},\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_AddRRSetRecords_error_invalid_input(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zones/example.com/rrsets/www/TXT/actions/add_records\",\n\t\t\tservermock.ResponseFromFixture(\"error-invalid_input.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\trecords := []Record{{\n\t\tValue:   \"198.51.100.1\",\n\t\tComment: \"My web server at Hetzner Cloud.\",\n\t}}\n\n\t_, err := client.AddRRSetRecords(t.Context(), \"example.com\", \"TXT\", \"www\", 0, records)\n\trequire.EqualError(t, err, \"invalid_input: invalid input in field 'broken_field': is too longfield: broken_field: is too long\")\n}\n\nfunc TestClient_AddRRSetRecords_error_resource_limit_exceeded(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zones/example.com/rrsets/www/TXT/actions/add_records\",\n\t\t\tservermock.ResponseFromFixture(\"error-resource_limit_exceeded.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\trecords := []Record{{\n\t\tValue:   \"198.51.100.1\",\n\t\tComment: \"My web server at Hetzner Cloud.\",\n\t}}\n\n\t_, err := client.AddRRSetRecords(t.Context(), \"example.com\", \"TXT\", \"www\", 0, records)\n\trequire.EqualError(t, err, \"resource_limit_exceeded: project limit exceededlimit: project_limit\")\n}\n\nfunc TestClient_AddRRSetRecords_error_deprecated_api_endpoint(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zones/example.com/rrsets/www/TXT/actions/add_records\",\n\t\t\tservermock.ResponseFromFixture(\"error-deprecated_api_endpoint.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\trecords := []Record{{\n\t\tValue:   \"198.51.100.1\",\n\t\tComment: \"My web server at Hetzner Cloud.\",\n\t}}\n\n\t_, err := client.AddRRSetRecords(t.Context(), \"example.com\", \"TXT\", \"www\", 0, records)\n\trequire.EqualError(t, err, \"deprecated_api_endpoint: API functionality was removed: https://docs.hetzner.cloud/changelog#2023-07-20-foo-endpoint-is-deprecated\")\n}\n\nfunc TestClient_RemoveRRSetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zones/example.com/rrsets/www/TXT/actions/remove_records\",\n\t\t\tservermock.ResponseFromFixture(\"remove_rrset_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"remove_rrset_records-request.json\")).\n\t\tBuild(t)\n\n\trecords := []Record{{\n\t\tValue:   \"198.51.100.1\",\n\t\tComment: \"My web server at Hetzner Cloud.\",\n\t}}\n\n\tresult, err := client.RemoveRRSetRecords(t.Context(), \"example.com\", \"TXT\", \"www\", records)\n\trequire.NoError(t, err)\n\n\texpected := &Action{\n\t\tID:        1,\n\t\tCommand:   \"remove_rrset_records\",\n\t\tStatus:    \"running\",\n\t\tProgress:  50,\n\t\tResources: []Resources{{ID: 42, Type: \"zone\"}},\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_GetAction(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /actions/123\", servermock.ResponseFromFixture(\"get_action.json\")).\n\t\tRoute(\"/\", servermock.DumpRequest()).\n\t\tBuild(t)\n\n\tresult, err := client.GetAction(t.Context(), 123)\n\trequire.NoError(t, err)\n\n\texpected := &Action{\n\t\tID:        590000000000000,\n\t\tCommand:   \"start_resource\",\n\t\tStatus:    \"running\",\n\t\tProgress:  100,\n\t\tResources: []Resources{{ID: 590000000000000, Type: \"server\"}},\n\t\tErrorInfo: &ErrorInfo{\n\t\t\tCode:    \"action_failed\",\n\t\t\tMessage: \"Action failed\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/internal/fixtures/add_rrset_records-request.json",
    "content": "{\n  \"ttl\": 3600,\n  \"records\": [\n    {\n      \"value\": \"198.51.100.1\",\n      \"comment\": \"My web server at Hetzner Cloud.\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/internal/fixtures/add_rrset_records.json",
    "content": "{\n  \"action\": {\n    \"id\": 1,\n    \"command\": \"add_rrset_records\",\n    \"status\": \"running\",\n    \"progress\": 50,\n    \"started\": \"2016-01-30T23:55:00+00:00\",\n    \"finished\": null,\n    \"resources\": [\n      {\n        \"id\": 590000000000000,\n        \"type\": \"zone\"\n      }\n    ],\n    \"error\": null\n  }\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/internal/fixtures/error-deprecated_api_endpoint.json",
    "content": "{\n  \"error\": {\n    \"code\": \"deprecated_api_endpoint\",\n    \"message\": \"API functionality was removed\",\n    \"details\": {\n      \"announcement\": \"https://docs.hetzner.cloud/changelog#2023-07-20-foo-endpoint-is-deprecated\"\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/internal/fixtures/error-invalid_input.json",
    "content": "{\n  \"error\": {\n    \"code\": \"invalid_input\",\n    \"message\": \"invalid input in field 'broken_field': is too long\",\n    \"details\": {\n      \"fields\": [\n        {\n          \"name\": \"broken_field\",\n          \"messages\": [\n            \"is too long\"\n          ]\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/internal/fixtures/error-resource_limit_exceeded.json",
    "content": "{\n  \"error\": {\n    \"code\": \"resource_limit_exceeded\",\n    \"message\": \"project limit exceeded\",\n    \"details\": {\n      \"limits\": [\n        {\n          \"name\": \"project_limit\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/internal/fixtures/get_action.json",
    "content": "{\n  \"action\": {\n    \"id\": 590000000000000,\n    \"command\": \"start_resource\",\n    \"status\": \"running\",\n    \"started\": \"2016-01-30T23:55:00+00:00\",\n    \"finished\": \"2016-01-30T23:55:00+00:00\",\n    \"progress\": 100,\n    \"resources\": [\n      {\n        \"id\": 590000000000000,\n        \"type\": \"server\"\n      }\n    ],\n    \"error\": {\n      \"code\": \"action_failed\",\n      \"message\": \"Action failed\"\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/internal/fixtures/remove_rrset_records-request.json",
    "content": "{\n  \"records\": [\n    {\n      \"value\": \"198.51.100.1\",\n      \"comment\": \"My web server at Hetzner Cloud.\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/internal/fixtures/remove_rrset_records.json",
    "content": "{\n  \"action\": {\n    \"id\": 1,\n    \"command\": \"remove_rrset_records\",\n    \"status\": \"running\",\n    \"progress\": 50,\n    \"started\": \"2016-01-30T23:55:00+00:00\",\n    \"finished\": null,\n    \"resources\": [\n      {\n        \"id\": 42,\n        \"type\": \"zone\"\n      }\n    ],\n    \"error\": null\n  }\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/hetznerv1/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype APIError struct {\n\tErrorInfo ErrorInfo `json:\"error\"`\n}\n\ntype ErrorInfo struct {\n\tCode    string       `json:\"code,omitempty\"`\n\tMessage string       `json:\"message,omitempty\"`\n\tDetails ErrorDetails `json:\"details\"`\n}\n\nfunc (i *ErrorInfo) Error() string {\n\tmsg := new(strings.Builder)\n\n\t_, _ = fmt.Fprintf(msg, \"%s: %s\", i.Code, i.Message)\n\n\tif i.Details.Announcement != \"\" {\n\t\t_, _ = fmt.Fprintf(msg, \": %s\", i.Details.Announcement)\n\t}\n\n\tfor _, limit := range i.Details.Limits {\n\t\t_, _ = fmt.Fprintf(msg, \"limit: %s\", limit.Name)\n\t}\n\n\tfor _, field := range i.Details.Fields {\n\t\t_, _ = fmt.Fprintf(msg, \"field: %s: %s\", field.Name, strings.Join(field.Messages, \", \"))\n\t}\n\n\treturn msg.String()\n}\n\ntype ErrorDetails struct {\n\tAnnouncement string       `json:\"announcement,omitempty\"`\n\tLimits       []LimitError `json:\"limits,omitempty\"`\n\tFields       []FieldError `json:\"fields,omitempty\"`\n}\n\ntype FieldError struct {\n\tName     string   `json:\"name,omitempty\"`\n\tMessages []string `json:\"messages,omitempty\"`\n}\n\ntype LimitError struct {\n\tName string `json:\"name,omitempty\"`\n}\n\nfunc (a *APIError) Error() string {\n\treturn a.ErrorInfo.Error()\n}\n\ntype RRSet struct {\n\tID         string            `json:\"id,omitempty\"`\n\tName       string            `json:\"name,omitempty\"`\n\tType       string            `json:\"type,omitempty\"`\n\tTTL        int               `json:\"ttl,omitempty\"`\n\tLabels     map[string]string `json:\"labels,omitempty\"`\n\tProtection *Protection       `json:\"protection,omitempty\"`\n\tRecords    []Record          `json:\"records,omitempty\"`\n\tZoneID     int               `json:\"zone,omitempty\"`\n}\n\ntype Protection struct {\n\tChange bool `json:\"change,omitempty\"`\n}\n\ntype Record struct {\n\tValue   string `json:\"value,omitempty\"`\n\tComment string `json:\"comment,omitempty\"`\n}\n\ntype ActionResponse struct {\n\tAction *Action `json:\"action,omitempty\"`\n}\n\ntype Action struct {\n\tID      int64  `json:\"id,omitempty\"`\n\tCommand string `json:\"command,omitempty\"`\n\n\t// It can be: `running`, `success`, `error`.\n\t// https://docs.hetzner.cloud/reference/cloud#zone-actions-get-an-action\n\t// https://docs.hetzner.cloud/reference/cloud#zone-actions\n\tStatus   string `json:\"status,omitempty\"`\n\tProgress int    `json:\"progress,omitempty\"`\n\n\tResources []Resources `json:\"resources,omitempty\"`\n\tErrorInfo *ErrorInfo  `json:\"error,omitempty\"`\n}\n\ntype Resources struct {\n\tID   int64  `json:\"id,omitempty\"`\n\tType string `json:\"type,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/legacy/hetzner.go",
    "content": "// Package legacy implements a DNS provider for solving the DNS-01 challenge using Hetzner DNS.\npackage legacy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hetzner/internal/legacy/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"HETZNER_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst minTTL = 60\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for hetzner.\n// Credentials must be passed in the environment variable: HETZNER_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"hetzner (legacy): %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for hetzner.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"hetzner (legacy): the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"hetzner (legacy): credentials missing\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"hetzner (legacy): invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\tclient := internal.NewClient(config.APIKey)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner (legacy): could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzone := dns01.UnFqdn(authZone)\n\n\tctx := context.Background()\n\n\tzoneID, err := d.client.GetZoneID(ctx, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner (legacy): %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner (legacy): %w\", err)\n\t}\n\n\trecord := internal.DNSRecord{\n\t\tType:   \"TXT\",\n\t\tName:   subDomain,\n\t\tValue:  info.Value,\n\t\tTTL:    d.config.TTL,\n\t\tZoneID: zoneID,\n\t}\n\n\tif err := d.client.CreateRecord(ctx, record); err != nil {\n\t\treturn fmt.Errorf(\"hetzner (legacy): failed to add TXT record: fqdn=%s, zoneID=%s: %w\", info.EffectiveFQDN, zoneID, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner (legacy): could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzone := dns01.UnFqdn(authZone)\n\n\tctx := context.Background()\n\n\tzoneID, err := d.client.GetZoneID(ctx, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner (legacy): %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner (legacy): %w\", err)\n\t}\n\n\trecord, err := d.client.GetTxtRecord(ctx, subDomain, info.Value, zoneID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hetzner (legacy): %w\", err)\n\t}\n\n\tif err := d.client.DeleteRecord(ctx, record.ID); err != nil {\n\t\treturn fmt.Errorf(\"hetzner (legacy): failed to delete TXT record: id=%s, name=%s: %w\", record.ID, record.Name, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/legacy/hetzner_test.go",
    "content": "package legacy\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"hetzner (legacy): some credentials information are missing: HETZNER_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tttl      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tttl:    minTTL,\n\t\t\tapiKey: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tttl:      minTTL,\n\t\t\texpected: \"hetzner (legacy): credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"invalid TTL\",\n\t\t\tapiKey:   \"123\",\n\t\t\tttl:      10,\n\t\t\texpected: \"hetzner (legacy): invalid TTL, TTL (10) must be greater than 60\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.TTL = test.ttl\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/legacy/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// defaultBaseURL represents the API endpoint to call.\nconst defaultBaseURL = \"https://dns.hetzner.com\"\n\nconst authHeader = \"Auth-API-Token\"\n\n// Client the Hetzner client.\ntype Client struct {\n\tapiKey string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Hetzner client.\nfunc NewClient(apiKey string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// GetTxtRecord gets a TXT record.\nfunc (c *Client) GetTxtRecord(ctx context.Context, name, value, zoneID string) (*DNSRecord, error) {\n\trecords, err := c.getRecords(ctx, zoneID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, record := range records.Records {\n\t\tif record.Type == \"TXT\" && record.Name == name && record.Value == value {\n\t\t\treturn &record, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"could not find record: zone ID: %s; Record: %s\", zoneID, name)\n}\n\n// https://dns.hetzner.com/api-docs#operation/GetRecords\nfunc (c *Client) getRecords(ctx context.Context, zoneID string) (*DNSRecords, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v1\", \"records\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"zone_id\", zoneID)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := c.newRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\trecords := &DNSRecords{}\n\n\terr = json.Unmarshal(raw, records)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn records, nil\n}\n\n// CreateRecord creates a DNS record.\n// https://dns.hetzner.com/api-docs#operation/CreateRecord\nfunc (c *Client) CreateRecord(ctx context.Context, record DNSRecord) error {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v1\", \"records\")\n\n\treq, err := c.newRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\treturn nil\n}\n\n// DeleteRecord deletes a DNS record.\n// https://dns.hetzner.com/api-docs#operation/DeleteRecord\nfunc (c *Client) DeleteRecord(ctx context.Context, recordID string) error {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v1\", \"records\", recordID)\n\n\treq, err := c.newRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\treturn nil\n}\n\n// GetZoneID gets the zone ID for a domain.\nfunc (c *Client) GetZoneID(ctx context.Context, domain string) (string, error) {\n\tzones, err := c.getZones(ctx, domain)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, zone := range zones.Zones {\n\t\tif zone.Name == domain {\n\t\t\treturn zone.ID, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"could not get zone for domain %s not found\", domain)\n}\n\n// https://dns.hetzner.com/api-docs#operation/GetZones\nfunc (c *Client) getZones(ctx context.Context, name string) (*Zones, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v1\", \"zones\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"name\", name)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := c.newRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not get zones: %w\", err)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\t// EOF fallback\n\tif resp.StatusCode == http.StatusNotFound {\n\t\treturn &Zones{}, nil\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tzones := &Zones{}\n\n\terr = json.Unmarshal(raw, zones)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn zones, nil\n}\n\nfunc (c *Client) newRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treq.Header.Set(authHeader, c.apiKey)\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/legacy/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder(apiKey string) *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(apiKey)\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(authHeader, apiKey))\n}\n\nfunc TestClient_GetTxtRecord(t *testing.T) {\n\tconst zoneID = \"zoneA\"\n\n\tclient := mockBuilder(\"myKeyA\").\n\t\tRoute(\"GET /api/v1/records\", servermock.ResponseFromFixture(\"get_txt_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"zone_id\", zoneID)).\n\t\tBuild(t)\n\n\trecord, err := client.GetTxtRecord(t.Context(), \"test1\", \"txttxttxt\", zoneID)\n\trequire.NoError(t, err)\n\n\texpected := &DNSRecord{\n\t\tID:       \"1b\",\n\t\tName:     \"test1\",\n\t\tType:     \"TXT\",\n\t\tValue:    \"txttxttxt\",\n\t\tPriority: 0,\n\t\tTTL:      600,\n\t\tZoneID:   \"zoneA\",\n\t}\n\n\tassert.Equal(t, expected, record)\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tconst zoneID = \"zoneA\"\n\n\tclient := mockBuilder(\"myKeyB\").\n\t\tRoute(\"POST /api/v1/records\", servermock.ResponseFromFixture(\"create_txt_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_txt_record-request.json\")).\n\t\tBuild(t)\n\n\trecord := DNSRecord{\n\t\tName:   \"test\",\n\t\tType:   \"TXT\",\n\t\tValue:  \"txttxttxt\",\n\t\tTTL:    600,\n\t\tZoneID: zoneID,\n\t}\n\n\terr := client.CreateRecord(t.Context(), record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder(\"myKeyC\").\n\t\tRoute(\"DELETE /api/v1/records/recordID\", nil).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"recordID\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_GetZoneID(t *testing.T) {\n\tclient := mockBuilder(\"myKeyD\").\n\t\tRoute(\"GET /api/v1/zones\", servermock.ResponseFromFixture(\"get_zone_id.json\")).\n\t\tBuild(t)\n\n\tzoneID, err := client.GetZoneID(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"zoneA\", zoneID)\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/legacy/internal/fixtures/create_txt_record-request.json",
    "content": "{\n  \"name\": \"test\",\n  \"type\": \"TXT\",\n  \"value\": \"txttxttxt\",\n  \"ttl\": 600,\n  \"zone_id\": \"zoneA\"\n}\n"
  },
  {
    "path": "providers/dns/hetzner/internal/legacy/internal/fixtures/create_txt_record.json",
    "content": "{\n  \"record\": {\n    \"type\": \"A\",\n    \"id\": \"string\",\n    \"created\": \"2020-05-08T10:49:18Z\",\n    \"modified\": \"2020-05-08T10:49:18Z\",\n    \"zone_id\": \"string\",\n    \"name\": \"string\",\n    \"value\": \"string\",\n    \"ttl\": 0\n  }\n}"
  },
  {
    "path": "providers/dns/hetzner/internal/legacy/internal/fixtures/get_txt_record.json",
    "content": "{\n  \"records\": [\n    {\n      \"type\": \"A\",\n      \"id\": \"1a\",\n      \"created\": \"2020-05-08T10:49:18Z\",\n      \"modified\": \"2020-05-08T10:49:18Z\",\n      \"zone_id\": \"zoneA\",\n      \"name\": \"test\",\n      \"value\": \"10.10.10.10\",\n      \"ttl\": 600\n    },\n    {\n      \"type\": \"TXT\",\n      \"id\": \"1b\",\n      \"created\": \"2020-05-08T10:49:19Z\",\n      \"modified\": \"2020-05-08T10:49:19Z\",\n      \"zone_id\": \"zoneA\",\n      \"name\": \"test1\",\n      \"value\": \"txttxttxt\",\n      \"ttl\": 600\n    }\n  ],\n  \"meta\": {\n    \"pagination\": {\n      \"page\": 1,\n      \"per_page\": 20,\n      \"last_page\": 1,\n      \"total_entries\": 2\n    }\n  }\n}"
  },
  {
    "path": "providers/dns/hetzner/internal/legacy/internal/fixtures/get_zone_id.json",
    "content": "{\n  \"zones\": [\n    {\n      \"id\": \"zoneA\",\n      \"created\": \"2020-05-08T10:49:18Z\",\n      \"modified\": \"2020-05-08T10:49:18Z\",\n      \"legacy_dns_host\": \"string\",\n      \"legacy_ns\": [\n        \"string\"\n      ],\n      \"name\": \"example.com\",\n      \"ns\": [\n        \"string\"\n      ],\n      \"owner\": \"string\",\n      \"paused\": true,\n      \"permission\": \"string\",\n      \"project\": \"string\",\n      \"registrar\": \"string\",\n      \"status\": \"verified\",\n      \"ttl\": 0,\n      \"verified\": \"2020-05-08T10:49:18Z\",\n      \"records_count\": 0,\n      \"is_secondary_dns\": true,\n      \"txt_verification\": {\n        \"name\": \"string\",\n        \"token\": \"string\"\n      }\n    },\n    {\n      \"id\": \"zoneB\",\n      \"created\": \"2020-05-08T10:49:18Z\",\n      \"modified\": \"2020-05-08T10:49:18Z\",\n      \"legacy_dns_host\": \"string\",\n      \"legacy_ns\": [\n        \"string\"\n      ],\n      \"name\": \"example.org\",\n      \"ns\": [\n        \"string\"\n      ],\n      \"owner\": \"string\",\n      \"paused\": true,\n      \"permission\": \"string\",\n      \"project\": \"string\",\n      \"registrar\": \"string\",\n      \"status\": \"verified\",\n      \"ttl\": 0,\n      \"verified\": \"2020-05-08T10:49:18Z\",\n      \"records_count\": 0,\n      \"is_secondary_dns\": true,\n      \"txt_verification\": {\n        \"name\": \"string\",\n        \"token\": \"string\"\n      }\n    }\n  ],\n  \"meta\": {\n    \"pagination\": {\n      \"page\": 1,\n      \"per_page\": 1,\n      \"last_page\": 1,\n      \"total_entries\": 0\n    }\n  }\n}"
  },
  {
    "path": "providers/dns/hetzner/internal/legacy/internal/types.go",
    "content": "package internal\n\n// DNSRecord a DNS record.\ntype DNSRecord struct {\n\tID       string `json:\"id,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tType     string `json:\"type,omitempty\"`\n\tValue    string `json:\"value\"`\n\tPriority int    `json:\"priority,omitempty\"`\n\tTTL      int    `json:\"ttl,omitempty\"`\n\tZoneID   string `json:\"zone_id,omitempty\"`\n}\n\n// DNSRecords a set of DNS record.\ntype DNSRecords struct {\n\tRecords []DNSRecord `json:\"records\"`\n}\n\n// Zone a DNS zone.\ntype Zone struct {\n\tID   string `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\n// Zones a set of DNS zones.\ntype Zones struct {\n\tZones []Zone `json:\"zones\"`\n\tMeta  Meta   `json:\"meta\"`\n}\n\n// Meta response metadata.\ntype Meta struct {\n\tPagination Pagination `json:\"pagination\"`\n}\n\n// Pagination information about pagination.\ntype Pagination struct {\n\tPage         int `json:\"page,omitempty\" url:\"page\"`\n\tPerPage      int `json:\"per_page,omitempty\" url:\"per_page\"`\n\tLastPage     int `json:\"last_page,omitempty\" url:\"-\"`\n\tTotalEntries int `json:\"total_entries,omitempty\" url:\"-\"`\n}\n"
  },
  {
    "path": "providers/dns/hostingde/hostingde.go",
    "content": "// Package hostingde implements a DNS provider for solving the DNS-01 challenge using hosting.de.\npackage hostingde\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/hostingde\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"HOSTINGDE_\"\n\n\tEnvAPIKey   = envNamespace + \"API_KEY\"\n\tEnvZoneName = envNamespace + \"ZONE_NAME\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config = hostingde.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tZoneName:           env.GetOrFile(EnvZoneName),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for hosting.de.\n// Credentials must be passed in the environment variables:\n// HOSTINGDE_ZONE_NAME and HOSTINGDE_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"hostingde: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for hosting.de.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"hostingde: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := hostingde.NewDNSProviderConfig(config, \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"hostingde: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hostingde: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hostingde: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n"
  },
  {
    "path": "providers/dns/hostingde/hostingde.toml",
    "content": "Name = \"Hosting.de\"\nDescription = ''''''\nURL = \"https://www.hosting.de/\"\nCode = \"hostingde\"\nSince = \"v1.1.0\"\n\nExample = '''\nHOSTINGDE_API_KEY=xxxxxxxx \\\nlego --dns hostingde -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    HOSTINGDE_API_KEY = \"API key\"\n  [Configuration.Additional]\n    HOSTINGDE_ZONE_NAME = \"Zone name in ACE format\"\n    HOSTINGDE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    HOSTINGDE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    HOSTINGDE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    HOSTINGDE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.hosting.de/api/#dns\"\n\n\n"
  },
  {
    "path": "providers/dns/hostingde/hostingde_test.go",
    "content": "package hostingde\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIKey,\n\tEnvZoneName).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:   \"123\",\n\t\t\t\tEnvZoneName: \"example.org\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:   \"\",\n\t\t\t\tEnvZoneName: \"\",\n\t\t\t},\n\t\t\texpected: \"hostingde: some credentials information are missing: HOSTINGDE_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:   \"\",\n\t\t\t\tEnvZoneName: \"456\",\n\t\t\t},\n\t\t\texpected: \"hostingde: some credentials information are missing: HOSTINGDE_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tzoneName string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiKey:   \"123\",\n\t\t\tzoneName: \"example.org\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"hostingde: API key missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api key\",\n\t\t\tzoneName: \"456\",\n\t\t\texpected: \"hostingde: API key missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.ZoneName = test.zoneName\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/hostinger/hostinger.go",
    "content": "// Package hostinger implements a DNS provider for solving the DNS-01 challenge using Hostinger.\npackage hostinger\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hostinger/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"HOSTINGER_\"\n\n\tEnvAPIToken = envNamespace + \"API_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Hostinger.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"hostinger: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIToken = values[EnvAPIToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Hostinger.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"hostinger: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.APIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"hostinger: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hostinger: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hostinger: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\trequest := internal.ZoneRequest{\n\t\tOverwrite: false,\n\t\tZone: []internal.RecordSet{{\n\t\t\tName: subDomain,\n\t\t\tType: \"TXT\",\n\t\t\tTTL:  d.config.TTL,\n\t\t\tRecords: []internal.Record{\n\t\t\t\t{Content: info.Value},\n\t\t\t},\n\t\t}},\n\t}\n\n\terr = d.client.UpdateDNSRecords(ctx, dns01.UnFqdn(authZone), request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hostinger: update DNS records (add): %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hostinger: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hostinger: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\trecordSet, err := d.findRecordSet(ctx, authZone, subDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hostinger: %w\", err)\n\t}\n\n\tvar newRecords []internal.Record\n\n\tfor _, record := range recordSet.Records {\n\t\tif record.Content == info.Value || record.Content == strconv.Quote(info.Value) {\n\t\t\tcontinue\n\t\t}\n\n\t\tnewRecords = append(newRecords, record)\n\t}\n\n\trecordSet.Records = newRecords\n\n\tif len(recordSet.Records) > 0 {\n\t\trequest := internal.ZoneRequest{\n\t\t\tOverwrite: true,\n\t\t\tZone:      []internal.RecordSet{recordSet},\n\t\t}\n\n\t\terr = d.client.UpdateDNSRecords(ctx, dns01.UnFqdn(authZone), request)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"hostinger: update DNS records (delete): %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfilters := []internal.Filter{{\n\t\tName: subDomain,\n\t\tType: \"TXT\",\n\t}}\n\n\terr = d.client.DeleteDNSRecords(ctx, dns01.UnFqdn(authZone), filters)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hostinger: delete DNS records: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) findRecordSet(ctx context.Context, authZone, subDomain string) (internal.RecordSet, error) {\n\trecordSets, err := d.client.GetDNSRecords(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn internal.RecordSet{}, fmt.Errorf(\"get DNS records: %w\", err)\n\t}\n\n\tfor _, recordSet := range recordSets {\n\t\tif recordSet.Name != subDomain || recordSet.Type != \"TXT\" {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn recordSet, nil\n\t}\n\n\treturn internal.RecordSet{}, fmt.Errorf(\"no record found for domain %q and subdomain %q\", authZone, subDomain)\n}\n"
  },
  {
    "path": "providers/dns/hostinger/hostinger.toml",
    "content": "Name = \"Hostinger\"\nDescription = ''''''\nURL = \"https://www.hostinger.com/\"\nCode = \"hostinger\"\nSince = \"v4.27.0\"\n\nExample = '''\nHOSTINGER_API_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns hostinger -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    HOSTINGER_API_TOKEN = \"API Token\"\n  [Configuration.Additional]\n    HOSTINGER_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    HOSTINGER_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    HOSTINGER_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    HOSTINGER_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://developers.hostinger.com/#tag/dns-zone\"\n"
  },
  {
    "path": "providers/dns/hostinger/hostinger_test.go",
    "content": "package hostinger\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"\",\n\t\t\t},\n\t\t\texpected: \"hostinger: some credentials information are missing: HOSTINGER_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiToken string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiToken: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API token\",\n\t\t\texpected: \"hostinger: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = test.apiToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"PUT /api/dns/v1/zones/example.com\",\n\t\t\tservermock.ResponseFromInternal(\"update_dns_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"update_dns_records-request.json\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp_update(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /api/dns/v1/zones/example.com\",\n\t\t\tservermock.ResponseFromInternal(\"get_dns_records_acme.json\")).\n\t\tRoute(\"PUT /api/dns/v1/zones/example.com\",\n\t\t\tservermock.ResponseFromInternal(\"update_dns_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"update_dns_records_base-request.json\")).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp_delete(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /api/dns/v1/zones/example.com\",\n\t\t\tservermock.ResponseFromInternal(\"get_dns_records_empty.json\")).\n\t\tRoute(\"DELETE /api/dns/v1/zones/example.com\",\n\t\t\tservermock.ResponseFromInternal(\"delete_dns_records.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"filters\":[{\"name\":\"_acme-challenge\",\"type\":\"TXT\"}]}`)).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/hostinger/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://developers.hostinger.com\"\n\nconst authorizationHeader = \"Authorization\"\n\n// Client the Hostinger API client.\ntype Client struct {\n\ttoken string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(token string) (*Client, error) {\n\tif token == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\ttoken:      token,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// GetDNSRecords retrieves DNS zone records for a specific domain.\n// https://developers.hostinger.com/#tag/dns-zone/get/api/dns/v1/zones/{domain}\nfunc (c *Client) GetDNSRecords(ctx context.Context, domain string) ([]RecordSet, error) {\n\tendpoint := c.BaseURL.JoinPath(\"/api/dns/v1/zones/\", domain)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result []RecordSet\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// UpdateDNSRecords updates DNS records for the selected domain.\n// https://developers.hostinger.com/#tag/dns-zone/put/api/dns/v1/zones/{domain}\nfunc (c *Client) UpdateDNSRecords(ctx context.Context, domain string, zone ZoneRequest) error {\n\tendpoint := c.BaseURL.JoinPath(\"/api/dns/v1/zones/\", domain)\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, zone)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\n// DeleteDNSRecords deletes DNS records for the selected domain.\n// https://developers.hostinger.com/#tag/dns-zone/delete/api/dns/v1/zones/{domain}\nfunc (c *Client) DeleteDNSRecords(ctx context.Context, domain string, filters []Filter) error {\n\tendpoint := c.BaseURL.JoinPath(\"/api/dns/v1/zones/\", domain)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, Filters{Filters: filters})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(authorizationHeader, \"Bearer \"+c.token)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/hostinger/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(\"Authorization\", \"Bearer secret\"),\n\t)\n}\n\nfunc TestClient_GetDNSRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/dns/v1/zones/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"get_dns_records.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetDNSRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []RecordSet{\n\t\t{\n\t\t\tName: \"_acme-challenge\",\n\t\t\tRecords: []Record{{\n\t\t\t\tContent: \"aaa\",\n\t\t\t}},\n\t\t\tTTL:  14400,\n\t\t\tType: \"TXT\",\n\t\t},\n\t\t{\n\t\t\tName: \"_acme-challenge\",\n\t\t\tRecords: []Record{{\n\t\t\t\tContent: \"example.com.\",\n\t\t\t}},\n\t\t\tTTL:  14400,\n\t\t\tType: \"A\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetDNSRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/dns/v1/zones/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"error_401.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetDNSRecords(t.Context(), \"example.com\")\n\n\trequire.EqualError(t, err, \"26a91bd9-f8c8-4a83-9df9-83e23d696fe3: Unauthenticated\")\n}\n\nfunc TestClient_UpdateDNSRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /api/dns/v1/zones/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"update_dns_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"update_dns_records-request.json\")).\n\t\tBuild(t)\n\n\tzone := ZoneRequest{\n\t\tOverwrite: false,\n\t\tZone: []RecordSet{\n\t\t\t{\n\t\t\t\tName: \"_acme-challenge\",\n\t\t\t\tRecords: []Record{\n\t\t\t\t\t{Content: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"},\n\t\t\t\t},\n\t\t\t\tTTL:  120,\n\t\t\t\tType: \"TXT\",\n\t\t\t},\n\t\t},\n\t}\n\n\terr := client.UpdateDNSRecords(t.Context(), \"example.com\", zone)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_UpdateDNSRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /api/dns/v1/zones/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"error_422.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\tzone := ZoneRequest{\n\t\tZone: []RecordSet{{\n\t\t\tName: \"_acme-challenge\",\n\t\t\tRecords: []Record{{\n\t\t\t\tContent: \"aaa\",\n\t\t\t}},\n\t\t\tTTL:  14400,\n\t\t\tType: \"TXT\",\n\t\t}},\n\t}\n\n\terr := client.UpdateDNSRecords(t.Context(), \"example.com\", zone)\n\n\trequire.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.\")\n}\n\nfunc TestClient_DeleteDNSRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /api/dns/v1/zones/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"delete_dns_records.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"filters\":[{\"name\":\"_acme-challenge\",\"type\":\"TXT\"}]}`)).\n\t\tBuild(t)\n\n\tfilters := []Filter{{\n\t\tName: \"_acme-challenge\",\n\t\tType: \"TXT\",\n\t}}\n\n\terr := client.DeleteDNSRecords(t.Context(), \"example.com\", filters)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteDNSRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /api/dns/v1/zones/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"error_401.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\tfilters := []Filter{{\n\t\tName: \"_acme-challenge\",\n\t\tType: \"TXT\",\n\t}}\n\n\terr := client.DeleteDNSRecords(t.Context(), \"example.com\", filters)\n\n\trequire.EqualError(t, err, \"26a91bd9-f8c8-4a83-9df9-83e23d696fe3: Unauthenticated\")\n}\n"
  },
  {
    "path": "providers/dns/hostinger/internal/fixtures/delete_dns_records.json",
    "content": "{\n  \"message\": \"Request accepted\"\n}\n"
  },
  {
    "path": "providers/dns/hostinger/internal/fixtures/error_401.json",
    "content": "{\n  \"message\": \"Unauthenticated\",\n  \"correlation_id\": \"26a91bd9-f8c8-4a83-9df9-83e23d696fe3\"\n}\n"
  },
  {
    "path": "providers/dns/hostinger/internal/fixtures/error_422.json",
    "content": "{\n  \"message\": \"The name field is required. (and 1 more error)\",\n  \"errors\": {\n    \"field_1\": [\n      \"The field_1 field is required.\",\n      \"The field_1 must be a number.\"\n    ]\n  },\n  \"correlation_id\": \"26a91bd9-f8c8-4a83-9df9-83e23d696fe3\"\n}\n"
  },
  {
    "path": "providers/dns/hostinger/internal/fixtures/get_dns_records.json",
    "content": "[\n  {\n    \"name\": \"_acme-challenge\",\n    \"records\": [\n      {\n        \"content\": \"aaa\",\n        \"is_disabled\": false\n      }\n    ],\n    \"ttl\": 14400,\n    \"type\": \"TXT\"\n  },\n  {\n    \"name\": \"_acme-challenge\",\n    \"records\": [\n      {\n        \"content\": \"example.com.\",\n        \"is_disabled\": false\n      }\n    ],\n    \"ttl\": 14400,\n    \"type\": \"A\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/hostinger/internal/fixtures/get_dns_records_acme.json",
    "content": "[\n  {\n    \"name\": \"_acme-challenge\",\n    \"records\": [\n      {\n        \"content\": \"aaa\",\n        \"is_disabled\": false\n      },\n      {\n        \"content\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"\n      }\n    ],\n    \"ttl\": 14400,\n    \"type\": \"TXT\"\n  },\n  {\n    \"name\": \"_acme-challenge\",\n    \"records\": [\n      {\n        \"content\": \"example.com.\",\n        \"is_disabled\": false\n      }\n    ],\n    \"ttl\": 14400,\n    \"type\": \"A\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/hostinger/internal/fixtures/get_dns_records_empty.json",
    "content": "[\n  {\n    \"name\": \"_acme-challenge\",\n    \"records\": [\n      {\n        \"content\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"\n      }\n    ],\n    \"ttl\": 14400,\n    \"type\": \"TXT\"\n  },\n  {\n    \"name\": \"_acme-challenge\",\n    \"records\": [\n      {\n        \"content\": \"example.com.\",\n        \"is_disabled\": false\n      }\n    ],\n    \"ttl\": 14400,\n    \"type\": \"A\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/hostinger/internal/fixtures/update_dns_records-request.json",
    "content": "{\n  \"overwrite\": false,\n  \"zone\": [\n    {\n      \"name\": \"_acme-challenge\",\n      \"records\": [\n        {\n          \"content\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"\n        }\n      ],\n      \"ttl\": 120,\n      \"type\": \"TXT\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/hostinger/internal/fixtures/update_dns_records.json",
    "content": "{\n  \"message\": \"Request accepted\"\n}\n"
  },
  {
    "path": "providers/dns/hostinger/internal/fixtures/update_dns_records_base-request.json",
    "content": "{\n  \"overwrite\": true,\n  \"zone\": [\n    {\n      \"name\": \"_acme-challenge\",\n      \"records\": [\n        {\n          \"content\": \"aaa\"\n        }\n      ],\n      \"ttl\": 14400,\n      \"type\": \"TXT\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/hostinger/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype APIError struct {\n\tMessage       string              `json:\"message,omitempty\"`\n\tErrors        map[string][]string `json:\"errors,omitempty\"`\n\tCorrelationID string              `json:\"correlation_id,omitempty\"`\n}\n\nfunc (a *APIError) Error() string {\n\tmsg := new(strings.Builder)\n\n\t_, _ = fmt.Fprintf(msg, \"%s: %s\", a.CorrelationID, a.Message)\n\n\tfor field, values := range a.Errors {\n\t\t_, _ = fmt.Fprintf(msg, \": %s: %s\", field, strings.Join(values, \", \"))\n\t}\n\n\treturn msg.String()\n}\n\ntype ZoneRequest struct {\n\tOverwrite bool        `json:\"overwrite\"`\n\tZone      []RecordSet `json:\"zone,omitempty\"`\n}\n\ntype RecordSet struct {\n\tName    string   `json:\"name,omitempty\"`\n\tRecords []Record `json:\"records,omitempty\"`\n\tTTL     int      `json:\"ttl,omitempty\"`\n\tType    string   `json:\"type,omitempty\"`\n}\n\ntype Record struct {\n\tContent    string `json:\"content,omitempty\"`\n\tIsDisabled bool   `json:\"is_disabled,omitempty\"`\n}\n\ntype Filters struct {\n\tFilters []Filter `json:\"filters\"`\n}\n\ntype Filter struct {\n\tName string `json:\"name\"`\n\tType string `json:\"type\"`\n}\n"
  },
  {
    "path": "providers/dns/hostingnl/hostingnl.go",
    "content": "// Package hostingnl implements a DNS provider for solving the DNS-01 challenge using hosting.nl.\npackage hostingnl\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hostingnl/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"HOSTINGNL_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tHTTPClient         *http.Client\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for hosting.nl.\n// Credentials must be passed in the environment variables:\n// HOSTINGNL_APIKEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"hostingnl: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for hosting.nl.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"hostingnl: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"hostingnl: APIKey is missing\")\n\t}\n\n\tclient := internal.NewClient(config.APIKey)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hostingnl: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecord := internal.Record{\n\t\tName:     dns01.UnFqdn(info.EffectiveFQDN),\n\t\tType:     \"TXT\",\n\t\tContent:  strconv.Quote(info.Value),\n\t\tTTL:      d.config.TTL,\n\t\tPriority: 0,\n\t}\n\n\tnewRecord, err := d.client.AddRecord(context.Background(), dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hostingnl: failed to create TXT record, fqdn=%s: %w\", info.EffectiveFQDN, err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = newRecord.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT records matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hostingnl: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// gets the record's unique ID\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"hostingnl: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\terr = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hostingnl: failed to delete TXT record, id=%s: %w\", recordID, err)\n\t}\n\n\t// deletes record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/hostingnl/hostingnl.toml",
    "content": "Name = \"Hosting.nl\"\nDescription = ''''''\nURL = \"https://hosting.nl\"\nCode = \"hostingnl\"\nSince = \"v4.30.0\"\n\nExample = '''\nHOSTINGNL_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns hostingnl -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    HOSTINGNL_API_KEY = \"The API key\"\n  [Configuration.Additional]\n    HOSTINGNL_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    HOSTINGNL_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    HOSTINGNL_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    HOSTINGNL_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://api.hosting.nl/api/documentation\"\n"
  },
  {
    "path": "providers/dns/hostingnl/hostingnl_test.go",
    "content": "package hostingnl\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"key\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"hostingnl: some credentials information are missing: HOSTINGNL_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"key\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\texpected: \"hostingnl: APIKey is missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tprovider, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tprovider.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn provider, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(\"API-TOKEN\", \"secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /domains/example.com/dns\",\n\t\t\tservermock.ResponseFromInternal(\"add_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict(),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"add_record-request.json\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"DELETE /domains/example.com/dns\",\n\t\t\tservermock.ResponseFromInternal(\"delete_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict(),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"delete_record-request.json\")).\n\t\tBuild(t)\n\n\tprovider.recordIDs[\"abc\"] = \"12345\"\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/hostingnl/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\nconst defaultBaseURL = \"https://api.hosting.nl\"\n\ntype Client struct {\n\tapiKey string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(apiKey string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\nfunc (c Client) AddRecord(ctx context.Context, domain string, record Record) (*Record, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", domain, \"dns\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, []Record{record})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result APIResponse[Record]\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(result.Data) != 1 {\n\t\treturn nil, fmt.Errorf(\"unexpected response data: %v\", result.Data)\n\t}\n\n\treturn &result.Data[0], nil\n}\n\nfunc (c Client) DeleteRecord(ctx context.Context, domain, recordID string) error {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", domain, \"dns\")\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, []Record{{ID: recordID}})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar result APIResponse[Record]\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c Client) do(req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\treq.Header.Set(\"API-TOKEN\", c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar apiErr APIError\n\n\terr := json.Unmarshal(raw, &apiErr)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code: %d] %w\", resp.StatusCode, apiErr)\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/hostingnl/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(\"API-TOKEN\", \"secret\"),\n\t)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/example.com/dns\",\n\t\t\tservermock.ResponseFromFixture(\"add_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict(),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"add_record-request.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:    \"_acme-challenge.example.com\",\n\t\tType:    \"TXT\",\n\t\tContent: strconv.Quote(\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"),\n\t\tTTL:     120,\n\t}\n\n\tnewRecord, err := client.AddRecord(context.Background(), \"example.com\", record)\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tID:      \"12345\",\n\t\tName:    \"_acme-challenge.example.com\",\n\t\tType:    \"TXT\",\n\t\tContent: strconv.Quote(\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"),\n\t\tTTL:     120,\n\t}\n\n\tassert.Equal(t, expected, newRecord)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domains/example.com/dns\",\n\t\t\tservermock.ResponseFromFixture(\"delete_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict(),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"delete_record-request.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(context.Background(), \"example.com\", \"12345\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domains/example.com/dns\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(context.Background(), \"example.com\", \"12345\")\n\trequire.EqualError(t, err, \"[status code: 401] Something went wrong\")\n}\n\nfunc TestClient_DeleteRecord_error_other(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domains/example.com/dns\",\n\t\t\tservermock.ResponseFromFixture(\"error_other.json\").\n\t\t\t\tWithStatusCode(http.StatusNotFound)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(context.Background(), \"example.com\", \"12345\")\n\trequire.EqualError(t, err, \"[status code: 404] Resource not found\")\n}\n"
  },
  {
    "path": "providers/dns/hostingnl/internal/fixtures/add_record-request.json",
    "content": "[\n  {\n    \"name\": \"_acme-challenge.example.com\",\n    \"type\": \"TXT\",\n    \"content\": \"\\\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\\\"\",\n    \"ttl\": 120\n  }\n]\n"
  },
  {
    "path": "providers/dns/hostingnl/internal/fixtures/add_record.json",
    "content": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"12345\",\n      \"type\": \"TXT\",\n      \"content\": \"\\\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\\\"\",\n      \"name\": \"_acme-challenge.example.com\",\n      \"prio\": 0,\n      \"ttl\": 120\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/hostingnl/internal/fixtures/delete_record-request.json",
    "content": "[\n  {\n    \"id\": \"12345\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/hostingnl/internal/fixtures/delete_record.json",
    "content": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"12345\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/hostingnl/internal/fixtures/error.json",
    "content": "{\n  \"errors\": {\n    \"message\": \"Something went wrong\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/hostingnl/internal/fixtures/error_other.json",
    "content": "{\n  \"error\": \"Resource not found\"\n}\n"
  },
  {
    "path": "providers/dns/hostingnl/internal/types.go",
    "content": "package internal\n\ntype Record struct {\n\tID       string `json:\"id,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tType     string `json:\"type,omitempty\"`\n\tContent  string `json:\"content,omitempty\"`\n\tTTL      int    `json:\"ttl,omitempty\"`\n\tPriority int    `json:\"prio,omitempty\"`\n}\n\ntype APIResponse[T any] struct {\n\tSuccess bool `json:\"success\"`\n\tData    []T  `json:\"data\"`\n}\n\ntype APIError struct {\n\tErrorMsg string `json:\"error\"`\n\tErrors   Error  `json:\"errors\"`\n}\n\nfunc (e APIError) Error() string {\n\tif e.ErrorMsg != \"\" {\n\t\treturn e.ErrorMsg\n\t}\n\n\treturn e.Errors.Error()\n}\n\ntype Error struct {\n\tMessage string `json:\"message\"`\n}\n\nfunc (e Error) Error() string {\n\treturn e.Message\n}\n"
  },
  {
    "path": "providers/dns/hosttech/hosttech.go",
    "content": "// Package hosttech implements a DNS provider for solving the DNS-01 challenge using hosttech.\npackage hosttech\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hosttech/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"HOSTTECH_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 3600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]int\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for hosttech.\n// Credentials must be passed in the environment variable: HOSTTECH_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"hosttech: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for hosttech.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"hosttech: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"hosttech: missing credentials\")\n\t}\n\n\tclient := internal.NewClient(\n\t\tclientdebug.Wrap(\n\t\t\tinternal.OAuthStaticAccessToken(config.HTTPClient, config.APIKey),\n\t\t),\n\t)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: map[string]int{},\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hosttech: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tzone, err := d.client.GetZone(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hosttech: could not find zone for domain %q (%s): %w\", domain, authZone, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hosttech: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tType: \"TXT\",\n\t\tName: subDomain,\n\t\tText: info.Value,\n\t\tTTL:  d.config.TTL,\n\t}\n\n\tnewRecord, err := d.client.AddRecord(ctx, strconv.Itoa(zone.ID), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hosttech: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = newRecord.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hosttech: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tzone, err := d.client.GetZone(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hosttech: could not find zone for domain %q (%s): %w\", domain, authZone, err)\n\t}\n\n\t// gets the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"hosttech: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\terr = d.client.DeleteRecord(ctx, strconv.Itoa(zone.ID), strconv.Itoa(recordID))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hosttech: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/hosttech/hosttech.toml",
    "content": "Name = \"Hosttech\"\nDescription = ''''''\nURL = \"https://www.hosttech.eu/\"\nCode = \"hosttech\"\nSince = \"v4.5.0\"\n\nExample = '''\nHOSTTECH_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns hosttech -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    HOSTTECH_API_KEY = \"API login\"\n    HOSTTECH_PASSWORD = \"API password\"\n  [Configuration.Additional]\n    HOSTTECH_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    HOSTTECH_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    HOSTTECH_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)\"\n    HOSTTECH_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api.ns1.hosttech.eu/api/documentation\"\n"
  },
  {
    "path": "providers/dns/hosttech/hosttech_test.go",
    "content": "package hosttech\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\texpected: \"hosttech: some credentials information are missing: HOSTTECH_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\texpected: \"hosttech: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/hosttech/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"golang.org/x/oauth2\"\n)\n\nconst defaultBaseURL = \"https://api.ns1.hosttech.eu/api\"\n\n// Client a Hosttech client.\ntype Client struct {\n\tbaseURL    *url.URL\n\thttpClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(hc *http.Client) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\tif hc == nil {\n\t\thc = &http.Client{Timeout: 10 * time.Second}\n\t}\n\n\treturn &Client{baseURL: baseURL, httpClient: hc}\n}\n\n// GetZones Get a list of all zones.\n// https://api.ns1.hosttech.eu/api/documentation/#/Zones/get_api_user_v1_zones\nfunc (c *Client) GetZones(ctx context.Context, query string, limit, offset int) ([]Zone, error) {\n\tendpoint := c.baseURL.JoinPath(\"user\", \"v1\", \"zones\")\n\n\tvalues := endpoint.Query()\n\tvalues.Set(\"query\", query)\n\n\tif limit > 0 {\n\t\tvalues.Set(\"limit\", strconv.Itoa(limit))\n\t}\n\n\tif offset > 0 {\n\t\tvalues.Set(\"offset\", strconv.Itoa(offset))\n\t}\n\n\tendpoint.RawQuery = values.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tresult := apiResponse[[]Zone]{}\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Data, nil\n}\n\n// GetZone Get a single zone.\n// https://api.ns1.hosttech.eu/api/documentation/#/Zones/get_api_user_v1_zones__zoneId_\nfunc (c *Client) GetZone(ctx context.Context, zoneID string) (*Zone, error) {\n\tendpoint := c.baseURL.JoinPath(\"user\", \"v1\", \"zones\", zoneID)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tresult := apiResponse[*Zone]{}\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Data, nil\n}\n\n// GetRecords Returns a list of all records for the given zone.\n// https://api.ns1.hosttech.eu/api/documentation/#/Records/get_api_user_v1_zones__zoneId__records\nfunc (c *Client) GetRecords(ctx context.Context, zoneID, recordType string) ([]Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"user\", \"v1\", \"zones\", zoneID, \"records\")\n\n\tvalues := endpoint.Query()\n\n\tif recordType != \"\" {\n\t\tvalues.Set(\"type\", recordType)\n\t}\n\n\tendpoint.RawQuery = values.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tresult := apiResponse[[]Record]{}\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Data, nil\n}\n\n// AddRecord Adds a new record to the zone and returns the newly created record.\n// https://api.ns1.hosttech.eu/api/documentation/#/Records/post_api_user_v1_zones__zoneId__records\nfunc (c *Client) AddRecord(ctx context.Context, zoneID string, record Record) (*Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"user\", \"v1\", \"zones\", zoneID, \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tresult := apiResponse[*Record]{}\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Data, nil\n}\n\n// DeleteRecord Deletes a single record for the given id.\n// https://api.ns1.hosttech.eu/api/documentation/#/Records/delete_api_user_v1_zones__zoneId__records__recordId_\nfunc (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID string) error {\n\tendpoint := c.baseURL.JoinPath(\"user\", \"v1\", \"zones\", zoneID, \"records\", recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tresp, errD := c.httpClient.Do(req)\n\tif errD != nil {\n\t\treturn errutils.NewHTTPDoError(req, errD)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tswitch resp.StatusCode {\n\tcase http.StatusOK, http.StatusCreated:\n\t\traw, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t\t}\n\n\t\terr = json.Unmarshal(raw, result)\n\t\tif err != nil {\n\t\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t\t}\n\n\t\treturn nil\n\n\tcase http.StatusNoContent:\n\t\treturn nil\n\n\tdefault:\n\t\treturn parseError(req, resp)\n\t}\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\terrAPI := &APIError{StatusCode: resp.StatusCode}\n\n\terr := json.Unmarshal(raw, errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn errAPI\n}\n\nfunc OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {\n\tif client == nil {\n\t\tclient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tclient.Transport = &oauth2.Transport{\n\t\tSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),\n\t\tBase:   client.Transport,\n\t}\n\n\treturn client\n}\n"
  },
  {
    "path": "providers/dns/hosttech/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst testAPIKey = \"secret\"\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(OAuthStaticAccessToken(server.Client(), testAPIKey))\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer secret\"))\n}\n\nfunc TestClient_GetZones(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /user/v1/zones\",\n\t\t\tservermock.ResponseFromFixture(\"zones.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"limit\", \"100\").\n\t\t\t\tWith(\"query\", \"\")).\n\t\tBuild(t)\n\n\tzones, err := client.GetZones(t.Context(), \"\", 100, 0)\n\trequire.NoError(t, err)\n\n\texpected := []Zone{\n\t\t{\n\t\t\tID:          10,\n\t\t\tName:        \"user1.ch\",\n\t\t\tEmail:       \"test@hosttech.ch\",\n\t\t\tTTL:         10800,\n\t\t\tNameserver:  \"ns1.hosttech.ch\",\n\t\t\tDnssec:      false,\n\t\t\tDnssecEmail: \"test@hosttech.ch\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_GetZones_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /user/v1/zones\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetZones(t.Context(), \"\", 100, 0)\n\trequire.Error(t, err)\n}\n\nfunc TestClient_GetZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /user/v1/zones/123\",\n\t\t\tservermock.ResponseFromFixture(\"zone.json\")).\n\t\tBuild(t)\n\n\tzone, err := client.GetZone(t.Context(), \"123\")\n\trequire.NoError(t, err)\n\n\texpected := &Zone{\n\t\tID:          10,\n\t\tName:        \"user1.ch\",\n\t\tEmail:       \"test@hosttech.ch\",\n\t\tTTL:         10800,\n\t\tNameserver:  \"ns1.hosttech.ch\",\n\t\tDnssec:      false,\n\t\tDnssecEmail: \"test@hosttech.ch\",\n\t}\n\n\tassert.Equal(t, expected, zone)\n}\n\nfunc TestClient_GetZone_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /user/v1/zones/123\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetZone(t.Context(), \"123\")\n\trequire.EqualError(t, err, \"401: Unauthenticated.\")\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /user/v1/zones/123/records\",\n\t\t\tservermock.ResponseFromFixture(\"records.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"type\", \"TXT\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"123\", \"TXT\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tID:      10,\n\t\t\tType:    \"A\",\n\t\t\tName:    \"www\",\n\t\t\tTTL:     3600,\n\t\t\tComment: \"my first record\",\n\t\t},\n\t\t{\n\t\t\tID:      11,\n\t\t\tType:    \"AAAA\",\n\t\t\tName:    \"www\",\n\t\t\tTTL:     3600,\n\t\t\tComment: \"my first record\",\n\t\t},\n\t\t{\n\t\t\tID:      12,\n\t\t\tType:    \"CAA\",\n\t\t\tTTL:     3600,\n\t\t\tComment: \"my first record\",\n\t\t},\n\t\t{\n\t\t\tID:      13,\n\t\t\tType:    \"CNAME\",\n\t\t\tName:    \"www\",\n\t\t\tTTL:     3600,\n\t\t\tComment: \"my first record\",\n\t\t},\n\t\t{\n\t\t\tID:      14,\n\t\t\tType:    \"MX\",\n\t\t\tName:    \"mail.example.com\",\n\t\t\tTTL:     3600,\n\t\t\tComment: \"my first record\",\n\t\t},\n\t\t{\n\t\t\tID:      14,\n\t\t\tType:    \"NS\",\n\t\t\tName:    \"ns1.example.com\",\n\t\t\tTTL:     3600,\n\t\t\tComment: \"my first record\",\n\t\t},\n\t\t{\n\t\t\tID:      15,\n\t\t\tType:    \"PTR\",\n\t\t\tName:    \"smtp.example.com\",\n\t\t\tTTL:     3600,\n\t\t\tComment: \"my first record\",\n\t\t},\n\t\t{\n\t\t\tID:      16,\n\t\t\tType:    \"SRV\",\n\t\t\tTTL:     3600,\n\t\t\tComment: \"my first record\",\n\t\t},\n\t\t{\n\t\t\tID:      17,\n\t\t\tType:    \"TXT\",\n\t\t\tText:    \"v=spf1 ip4:1.2.3.4/32 -all\",\n\t\t\tTTL:     3600,\n\t\t\tComment: \"my first record\",\n\t\t},\n\t\t{\n\t\t\tID:      17,\n\t\t\tType:    \"TLSA\",\n\t\t\tText:    \"0 0 1 d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971\",\n\t\t\tTTL:     3600,\n\t\t\tComment: \"my first record\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /user/v1/zones/123/records\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetRecords(t.Context(), \"123\", \"TXT\")\n\trequire.EqualError(t, err, \"401: Unauthenticated.\")\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /user/v1/zones/123/records\",\n\t\t\tservermock.ResponseFromFixture(\"record.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:    \"TXT\",\n\t\tName:    \"lego\",\n\t\tText:    \"content\",\n\t\tTTL:     3600,\n\t\tComment: \"example\",\n\t}\n\n\tnewRecord, err := client.AddRecord(t.Context(), \"123\", record)\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tID:      10,\n\t\tType:    \"TXT\",\n\t\tName:    \"lego\",\n\t\tText:    \"content\",\n\t\tTTL:     3600,\n\t\tComment: \"example\",\n\t}\n\n\tassert.Equal(t, expected, newRecord)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /user/v1/zones/123/records\",\n\t\t\tservermock.ResponseFromFixture(\"error-details.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:    \"TXT\",\n\t\tName:    \"lego\",\n\t\tText:    \"content\",\n\t\tTTL:     3600,\n\t\tComment: \"example\",\n\t}\n\n\t_, err := client.AddRecord(t.Context(), \"123\", record)\n\trequire.EqualError(t, err, \"401: The given data was invalid. type: [Darf nicht leer sein.]\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /user/v1/zones/123/records/6\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusNoContent).\n\t\t\t\tWithStatusCode(http.StatusCreated)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"123\", \"6\")\n\trequire.Error(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /user/v1/zones/123/records/6\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"123\", \"6\")\n\trequire.EqualError(t, err, \"401: Unauthenticated.\")\n}\n"
  },
  {
    "path": "providers/dns/hosttech/internal/fixtures/error-details.json",
    "content": "{\n  \"message\": \"The given data was invalid.\",\n  \"errors\": {\n    \"type\": [\n      \"Darf nicht leer sein.\"\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/hosttech/internal/fixtures/error.json",
    "content": "{\n  \"message\": \"Unauthenticated.\"\n}\n"
  },
  {
    "path": "providers/dns/hosttech/internal/fixtures/record.json",
    "content": "{\n  \"data\": {\n    \"id\": 10,\n    \"type\": \"TXT\",\n    \"name\": \"lego\",\n    \"Text\": \"content\",\n    \"ttl\": 3600,\n    \"comment\": \"example\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/hosttech/internal/fixtures/records.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": 10,\n      \"type\": \"A\",\n      \"name\": \"www\",\n      \"ipv4\": \"1.2.3.4\",\n      \"ttl\": 3600,\n      \"comment\": \"my first record\"\n    },\n    {\n      \"id\": 11,\n      \"type\": \"AAAA\",\n      \"name\": \"www\",\n      \"ipv6\": \"2001:db8:1234::1\",\n      \"ttl\": 3600,\n      \"comment\": \"my first record\"\n    },\n    {\n      \"id\": 12,\n      \"type\": \"CAA\",\n      \"name\": \"\",\n      \"flag\": \"0\",\n      \"tag\": \"issue\",\n      \"value\": \"letsencrypt.org\",\n      \"ttl\": 3600,\n      \"comment\": \"my first record\"\n    },\n    {\n      \"id\": 13,\n      \"type\": \"CNAME\",\n      \"name\": \"www\",\n      \"cname\": \"site.example.com\",\n      \"ttl\": 3600,\n      \"comment\": \"my first record\"\n    },\n    {\n      \"id\": 14,\n      \"type\": \"MX\",\n      \"ownername\": \"\",\n      \"name\": \"mail.example.com\",\n      \"pref\": 10,\n      \"ttl\": 3600,\n      \"comment\": \"my first record\"\n    },\n    {\n      \"id\": 14,\n      \"type\": \"NS\",\n      \"ownername\": \"sub\",\n      \"name\": \"ns1.example.com\",\n      \"ttl\": 3600,\n      \"comment\": \"my first record\"\n    },\n    {\n      \"id\": 15,\n      \"type\": \"PTR\",\n      \"origin\": \"4.3.2.1\",\n      \"name\": \"smtp.example.com\",\n      \"ttl\": 3600,\n      \"comment\": \"my first record\"\n    },\n    {\n      \"id\": 16,\n      \"type\": \"SRV\",\n      \"service\": \"_autodiscover._tcp\",\n      \"priority\": 0,\n      \"weight\": 0,\n      \"port\": 443,\n      \"target\": \"exchange.example.com\",\n      \"ttl\": 3600,\n      \"comment\": \"my first record\"\n    },\n    {\n      \"id\": 17,\n      \"type\": \"TXT\",\n      \"name\": \"\",\n      \"text\": \"v=spf1 ip4:1.2.3.4/32 -all\",\n      \"ttl\": 3600,\n      \"comment\": \"my first record\"\n    },\n    {\n      \"id\": 17,\n      \"type\": \"TLSA\",\n      \"name\": \"\",\n      \"text\": \"0 0 1 d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971\",\n      \"ttl\": 3600,\n      \"comment\": \"my first record\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/hosttech/internal/fixtures/zone.json",
    "content": "{\n  \"data\": {\n    \"id\": 10,\n    \"name\": \"user1.ch\",\n    \"email\": \"test@hosttech.ch\",\n    \"ttl\": 10800,\n    \"nameserver\": \"ns1.hosttech.ch\",\n    \"dnssec\": false,\n    \"dnssec_email\": \"test@hosttech.ch\",\n    \"ds_records\": \"[]\",\n    \"records\": \"[{'id': 10, 'type': 'A', 'name': 'www', 'ipv4': '1.2.3.4', 'ttl': 3600, 'comment': 'my first record'}]\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/hosttech/internal/fixtures/zones.json",
    "content": "{\n  \"data\": [\n    {\n      \"id\": 10,\n      \"name\": \"user1.ch\",\n      \"email\": \"test@hosttech.ch\",\n      \"ttl\": 10800,\n      \"nameserver\": \"ns1.hosttech.ch\",\n      \"dnssec\": false,\n      \"dnssec_email\": \"test@hosttech.ch\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/hosttech/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype apiResponse[T any] struct {\n\tData T `json:\"data\"`\n}\n\ntype APIError struct {\n\tMessage    string         `json:\"message,omitempty\"`\n\tErrors     map[string]any `json:\"errors,omitempty\"`\n\tStatusCode int            `json:\"-\"`\n}\n\nfunc (a APIError) Error() string {\n\tmsg := new(strings.Builder)\n\n\t_, _ = fmt.Fprintf(msg, \"%d: %s\", a.StatusCode, a.Message)\n\n\tfor k, v := range a.Errors {\n\t\t_, _ = fmt.Fprintf(msg, \" %s: %v\", k, v)\n\t}\n\n\treturn msg.String()\n}\n\ntype Zone struct {\n\tID          int    `json:\"id\"`\n\tName        string `json:\"name,omitempty\"`\n\tEmail       string `json:\"email,omitempty\"`\n\tTTL         int    `json:\"ttl,omitempty\"`\n\tNameserver  string `json:\"nameserver,omitempty\"`\n\tDnssec      bool   `json:\"dnssec,omitempty\"`\n\tDnssecEmail string `json:\"dnssec_email,omitempty\"`\n}\n\ntype Record struct {\n\tID      int    `json:\"id,omitempty\"`\n\tType    string `json:\"type,omitempty\"`\n\tName    string `json:\"name,omitempty\"`\n\tZone    string `json:\"zone,omitempty\"`\n\tText    string `json:\"text,omitempty\"`\n\tTTL     int    `json:\"ttl,omitempty\"`\n\tComment string `json:\"comment,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/httpnet/httpnet.go",
    "content": "// Package httpnet implements a DNS provider for solving the DNS-01 challenge using http.net.\npackage httpnet\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/hostingde\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"HTTPNET_\"\n\n\tEnvAPIKey   = envNamespace + \"API_KEY\"\n\tEnvZoneName = envNamespace + \"ZONE_NAME\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultBaseURL = \"https://partner.http.net/api/dns/v1/json\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config = hostingde.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tZoneName:           env.GetOrFile(EnvZoneName),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for http.net.\n// Credentials must be passed in the environment variables:\n// HTTPNET_ZONE_NAME and HTTPNET_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"httpnet: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for http.net.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"httpnet: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := hostingde.NewDNSProviderConfig(config, defaultBaseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"httpnet: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"httpnet: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"httpnet: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n"
  },
  {
    "path": "providers/dns/httpnet/httpnet.toml",
    "content": "Name = \"http.net\"\nDescription = ''''''\nURL = \"https://www.http.net/\"\nCode = \"httpnet\"\nSince = \"v4.15.0\"\n\nExample = '''\nHTTPNET_API_KEY=xxxxxxxx \\\nlego --dns httpnet -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    HTTPNET_API_KEY = \"API key\"\n  [Configuration.Additional]\n    HTTPNET_ZONE_NAME = \"Zone name in ACE format\"\n    HTTPNET_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    HTTPNET_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    HTTPNET_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    HTTPNET_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.http.net/docs/api/#dns\"\n\n\n"
  },
  {
    "path": "providers/dns/httpnet/httpnet_test.go",
    "content": "package httpnet\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIKey,\n\tEnvZoneName).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:   \"123\",\n\t\t\t\tEnvZoneName: \"example.org\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:   \"\",\n\t\t\t\tEnvZoneName: \"\",\n\t\t\t},\n\t\t\texpected: \"httpnet: some credentials information are missing: HTTPNET_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:   \"\",\n\t\t\t\tEnvZoneName: \"456\",\n\t\t\t},\n\t\t\texpected: \"httpnet: some credentials information are missing: HTTPNET_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tzoneName string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiKey:   \"123\",\n\t\t\tzoneName: \"example.org\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"httpnet: API key missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api key\",\n\t\t\tzoneName: \"456\",\n\t\t\texpected: \"httpnet: API key missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.ZoneName = test.zoneName\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/httpreq/httpreq.go",
    "content": "// Package httpreq implements a DNS provider for solving the DNS-01 challenge through an HTTP server.\npackage httpreq\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"HTTPREQ_\"\n\n\tEnvEndpoint = envNamespace + \"ENDPOINT\"\n\tEnvMode     = envNamespace + \"MODE\"\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\ntype message struct {\n\tFQDN  string `json:\"fqdn\"`\n\tValue string `json:\"value\"`\n}\n\ntype messageRaw struct {\n\tDomain  string `json:\"domain\"`\n\tToken   string `json:\"token\"`\n\tKeyAuth string `json:\"keyAuth\"`\n}\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tEndpoint           *url.URL\n\tMode               string\n\tUsername           string\n\tPassword           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvEndpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"httpreq: %w\", err)\n\t}\n\n\tendpoint, err := url.Parse(values[EnvEndpoint])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"httpreq: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Mode = env.GetOrFile(EnvMode)\n\tconfig.Username = env.GetOrFile(EnvUsername)\n\tconfig.Password = env.GetOrFile(EnvPassword)\n\tconfig.Endpoint = endpoint\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"httpreq: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Endpoint == nil {\n\t\treturn nil, errors.New(\"httpreq: the endpoint is missing\")\n\t}\n\n\tconfig.HTTPClient = clientdebug.Wrap(config.HTTPClient)\n\n\treturn &DNSProvider{config: config}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tif d.config.Mode == \"RAW\" {\n\t\tmsg := &messageRaw{\n\t\t\tDomain:  domain,\n\t\t\tToken:   token,\n\t\t\tKeyAuth: keyAuth,\n\t\t}\n\n\t\terr := d.doPost(ctx, \"/present\", msg)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"httpreq: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\tmsg := &message{\n\t\tFQDN:  info.EffectiveFQDN,\n\t\tValue: info.Value,\n\t}\n\n\terr := d.doPost(ctx, \"/present\", msg)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"httpreq: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tif d.config.Mode == \"RAW\" {\n\t\tmsg := &messageRaw{\n\t\t\tDomain:  domain,\n\t\t\tToken:   token,\n\t\t\tKeyAuth: keyAuth,\n\t\t}\n\n\t\terr := d.doPost(ctx, \"/cleanup\", msg)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"httpreq: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\tmsg := &message{\n\t\tFQDN:  info.EffectiveFQDN,\n\t\tValue: info.Value,\n\t}\n\n\terr := d.doPost(ctx, \"/cleanup\", msg)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"httpreq: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) doPost(ctx context.Context, uri string, msg any) error {\n\treqBody := new(bytes.Buffer)\n\n\terr := json.NewEncoder(reqBody).Encode(msg)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t}\n\n\tendpoint := d.config.Endpoint.JoinPath(uri)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), reqBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tif d.config.Username != \"\" && d.config.Password != \"\" {\n\t\treq.SetBasicAuth(d.config.Username, d.config.Password)\n\t}\n\n\tresp, err := d.config.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/httpreq/httpreq.toml",
    "content": "Name = \"HTTP request\"\nDescription = ''''''\nURL = \"/lego/dns/httpreq/\"\nCode = \"httpreq\"\nSince = \"v2.0.0\"\n\nExample = '''\nHTTPREQ_ENDPOINT=http://my.server.com:9090 \\\nlego --dns httpreq -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Description\n\nThe server must provide:\n\n- `POST` `/present`\n- `POST` `/cleanup`\n\nThe URL of the server must be defined by `HTTPREQ_ENDPOINT`.\n\n### Mode\n\nThere are 2 modes (`HTTPREQ_MODE`):\n\n- default mode:\n```json\n{\n  \"fqdn\": \"_acme-challenge.domain.\",\n  \"value\": \"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM\"\n}\n```\n\n- `RAW`\n```json\n{\n  \"domain\": \"domain\",\n  \"token\": \"token\",\n  \"keyAuth\": \"key\"\n}\n```\n\n### Authentication\n\nBasic authentication (optional) can be set with some environment variables:\n\n- `HTTPREQ_USERNAME` and `HTTPREQ_PASSWORD`\n- both values must be set, otherwise basic authentication is not defined.\n\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    HTTPREQ_MODE = \"`RAW`, none\"\n    HTTPREQ_ENDPOINT = \"The URL of the server\"\n  [Configuration.Additional]\n    HTTPREQ_USERNAME = \"Basic authentication username\"\n    HTTPREQ_PASSWORD = \"Basic authentication password\"\n    HTTPREQ_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    HTTPREQ_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    HTTPREQ_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n"
  },
  {
    "path": "providers/dns/httpreq/httpreq_test.go",
    "content": "package httpreq\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar envTest = tester.NewEnvTest(EnvEndpoint, EnvMode, EnvUsername, EnvPassword)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint: \"http://localhost:8090\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid URL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint: \":\",\n\t\t\t},\n\t\t\texpected: `httpreq: parse \":\": missing protocol scheme`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing endpoint\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint: \"\",\n\t\t\t},\n\t\t\texpected: \"httpreq: some credentials information are missing: HTTPREQ_ENDPOINT\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tendpoint *url.URL\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tendpoint: mustParse(\"http://localhost:8090\"),\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing endpoint\",\n\t\t\texpected: \"httpreq: the endpoint is missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Endpoint = test.endpoint\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProvider_Present(t *testing.T) {\n\tenvTest.RestoreEnv()\n\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tbuilder       *servermock.Builder[*DNSProvider]\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tbuilder: mockBuilder(\"\").\n\t\t\t\tRoute(\"/present\",\n\t\t\t\t\tservermock.RawStringResponse(\"lego\"),\n\t\t\t\t\tservermock.CheckRequestJSONBody(`{\"fqdn\":\"_acme-challenge.domain.\",\"value\":\"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM\"}`)),\n\t\t},\n\t\t{\n\t\t\tdesc: \"success with path prefix\",\n\t\t\tbuilder: mockBuilderWithPathPrefix(\"\", \"/api/acme/\").\n\t\t\t\tRoute(\"/api/acme/present\",\n\t\t\t\t\tservermock.RawStringResponse(\"lego\"),\n\t\t\t\t\tservermock.CheckRequestJSONBody(`{\"fqdn\":\"_acme-challenge.domain.\",\"value\":\"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM\"}`)),\n\t\t},\n\t\t{\n\t\t\tdesc:          \"error\",\n\t\t\tbuilder:       mockBuilder(\"\"),\n\t\t\texpectedError: \"httpreq: unexpected status code: [status code: 404] body: 404 page not found\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"success raw mode\",\n\t\t\tbuilder: mockBuilder(\"RAW\").\n\t\t\t\tRoute(\"/present\",\n\t\t\t\t\tservermock.RawStringResponse(\"lego\"),\n\t\t\t\t\tservermock.CheckRequestBody(`{\"domain\":\"domain\",\"token\":\"token\",\"keyAuth\":\"key\"}`)),\n\t\t},\n\t\t{\n\t\t\tdesc:          \"error raw mode\",\n\t\t\tbuilder:       mockBuilder(\"RAW\"),\n\t\t\texpectedError: \"httpreq: unexpected status code: [status code: 404] body: 404 page not found\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"basic auth fail\",\n\t\t\tbuilder: mockBuilderWithBasicAuth(\"nope\", \"nope\").\n\t\t\t\tRoute(\"/present\", servermock.Noop()),\n\t\t\texpectedError: `httpreq: unexpected status code: [status code: 400] body: invalid credentials: got [username: \"nope\", password: \"nope\"], want [username: \"user\", password: \"secret\"]`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"basic auth success\",\n\t\t\tbuilder: mockBuilderWithBasicAuth(\"user\", \"secret\").\n\t\t\t\tRoute(\"/present\",\n\t\t\t\t\tservermock.RawStringResponse(\"lego\"),\n\t\t\t\t\tservermock.CheckRequestJSONBody(`{\"fqdn\":\"_acme-challenge.domain.\",\"value\":\"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM\"}`)),\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tp := test.builder.Build(t)\n\n\t\t\terr := p.Present(\"domain\", \"token\", \"key\")\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProvider_Cleanup(t *testing.T) {\n\tenvTest.RestoreEnv()\n\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tbuilder       *servermock.Builder[*DNSProvider]\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tbuilder: mockBuilder(\"\").\n\t\t\t\tRoute(\"/cleanup\",\n\t\t\t\t\tservermock.RawStringResponse(\"lego\"),\n\t\t\t\t\tservermock.CheckRequestJSONBody(`{\"fqdn\":\"_acme-challenge.domain.\",\"value\":\"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM\"}`)),\n\t\t},\n\t\t{\n\t\t\tdesc:          \"error\",\n\t\t\tbuilder:       mockBuilder(\"\"),\n\t\t\texpectedError: \"httpreq: unexpected status code: [status code: 404] body: 404 page not found\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"success raw mode\",\n\t\t\tbuilder: mockBuilder(\"RAW\").\n\t\t\t\tRoute(\"/cleanup\",\n\t\t\t\t\tservermock.RawStringResponse(\"lego\"),\n\t\t\t\t\tservermock.CheckRequestBody(`{\"domain\":\"domain\",\"token\":\"token\",\"keyAuth\":\"key\"}`)),\n\t\t},\n\t\t{\n\t\t\tdesc:          \"error raw mode\",\n\t\t\tbuilder:       mockBuilder(\"RAW\"),\n\t\t\texpectedError: \"httpreq: unexpected status code: [status code: 404] body: 404 page not found\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"basic auth fail\",\n\t\t\tbuilder: mockBuilderWithBasicAuth(\"test\", \"example\").\n\t\t\t\tRoute(\"/cleanup\", servermock.Noop()),\n\t\t\texpectedError: `httpreq: unexpected status code: [status code: 400] body: invalid credentials: got [username: \"test\", password: \"example\"], want [username: \"user\", password: \"secret\"]`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"basic auth success\",\n\t\t\tbuilder: mockBuilderWithBasicAuth(\"user\", \"secret\").\n\t\t\t\tRoute(\"/cleanup\",\n\t\t\t\t\tservermock.RawStringResponse(\"lego\"),\n\t\t\t\t\tservermock.CheckRequestJSONBody(`{\"fqdn\":\"_acme-challenge.domain.\",\"value\":\"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM\"}`)),\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tp := test.builder.Build(t)\n\n\t\t\terr := p.CleanUp(\"domain\", \"token\", \"key\")\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mockBuilder(mode string) *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.HTTPClient = server.Client()\n\t\t\tconfig.Endpoint, _ = url.Parse(server.URL)\n\t\t\tconfig.Mode = mode\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t})\n}\n\nfunc mockBuilderWithPathPrefix(mode, prefix string) *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.HTTPClient = server.Client()\n\t\t\tconfig.Endpoint, _ = url.Parse(server.URL + prefix)\n\t\t\tconfig.Mode = mode\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t})\n}\n\nfunc mockBuilderWithBasicAuth(username, password string) *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.HTTPClient = server.Client()\n\t\t\tconfig.Endpoint, _ = url.Parse(server.URL)\n\t\t\tconfig.Username = username\n\t\t\tconfig.Password = password\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t},\n\t\tservermock.CheckHeader().WithBasicAuth(\"user\", \"secret\"))\n}\n\nfunc mustParse(rawURL string) *url.URL {\n\turi, err := url.Parse(rawURL)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn uri\n}\n"
  },
  {
    "path": "providers/dns/huaweicloud/huaweicloud.go",
    "content": "// Package huaweicloud implements a DNS provider for solving the DNS-01 challenge using Huawei Cloud.\npackage huaweicloud\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n\t\"github.com/go-acme/lego/v4/providers/dns/huaweicloud/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n\thwauthbasic \"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic\"\n\thwconfig \"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/config\"\n\thwdns \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2\"\n\thwmodel \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model\"\n\thwregion \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/region\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"HUAWEICLOUD_\"\n\n\tEnvAccessKeyID     = envNamespace + \"ACCESS_KEY_ID\"\n\tEnvSecretAccessKey = envNamespace + \"SECRET_ACCESS_KEY\"\n\tEnvRegion          = envNamespace + \"REGION\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAccessKeyID     string\n\tSecretAccessKey string\n\tRegion          string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int32\n\tHTTPTimeout        time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                int32(env.GetOrDefaultInt(EnvTTL, 300)),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.DnsClient\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Huawei Cloud.\n// Credentials must be passed in the environment variables:\n// HUAWEICLOUD_ACCESS_KEY_ID, HUAWEICLOUD_SECRET_ACCESS_KEY, and HUAWEICLOUD_REGION.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey, EnvRegion)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"huaweicloud: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AccessKeyID = values[EnvAccessKeyID]\n\tconfig.SecretAccessKey = values[EnvSecretAccessKey]\n\tconfig.Region = values[EnvRegion]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Huawei Cloud.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"huaweicloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.AccessKeyID == \"\" || config.SecretAccessKey == \"\" || config.Region == \"\" {\n\t\treturn nil, errors.New(\"huaweicloud: credentials missing\")\n\t}\n\n\tauth, err := hwauthbasic.NewCredentialsBuilder().\n\t\tWithAk(config.AccessKeyID).\n\t\tWithSk(config.SecretAccessKey).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"huaweicloud: crendential build: %w\", err)\n\t}\n\n\tregion, err := hwregion.SafeValueOf(config.Region)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"huaweicloud: safe region: %w\", err)\n\t}\n\n\tclient, err := hwdns.DnsClientBuilder().\n\t\tWithHttpConfig(hwconfig.DefaultHttpConfig().WithTimeout(config.HTTPTimeout)).\n\t\tWithRegion(region).\n\t\tWithCredential(auth).\n\t\tSafeBuild()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"huaweicloud: client build: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    internal.NewDnsClient(client),\n\t\trecordIDs: map[string]string{},\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"huaweicloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzoneID, err := d.getZoneID(authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"huaweicloud: %w\", err)\n\t}\n\n\trecordSetID, err := d.getOrCreateRecordSetID(domain, zoneID, info)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"huaweicloud: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = recordSetID\n\td.recordIDsMu.Unlock()\n\n\terr = wait.Retry(context.Background(),\n\t\tfunc() error {\n\t\t\trs, errShow := d.client.ShowRecordSet(&hwmodel.ShowRecordSetRequest{\n\t\t\t\tZoneId:      zoneID,\n\t\t\t\tRecordsetId: recordSetID,\n\t\t\t})\n\t\t\tif errShow != nil {\n\t\t\t\treturn fmt.Errorf(\"show record set: %w\", errShow)\n\t\t\t}\n\n\t\t\tif !strings.HasSuffix(ptr.Deref(rs.Status), \"PENDING_\") {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"status: %s\", ptr.Deref(rs.Status))\n\t\t},\n\t\tbackoff.WithBackOff(backoff.NewConstantBackOff(d.config.PollingInterval)),\n\t\tbackoff.WithMaxElapsedTime(d.config.PropagationTimeout),\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"huaweicloud: record set sync on %s: %w\", domain, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// gets the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"huaweicloud: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"huaweicloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzoneID, err := d.getZoneID(authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"huaweicloud: %w\", err)\n\t}\n\n\trequest := &hwmodel.DeleteRecordSetRequest{\n\t\tZoneId:      zoneID,\n\t\tRecordsetId: recordID,\n\t}\n\n\t_, err = d.client.DeleteRecordSet(request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"huaweicloud: delete record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) getOrCreateRecordSetID(domain, zoneID string, info dns01.ChallengeInfo) (string, error) {\n\trecords, err := d.client.ListRecordSetsByZone(&hwmodel.ListRecordSetsByZoneRequest{\n\t\tZoneId: zoneID,\n\t\tName:   ptr.Pointer(info.EffectiveFQDN),\n\t})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"record list: unable to get record %s for zone %s: %w\", info.EffectiveFQDN, domain, err)\n\t}\n\n\tvar existingRecordSet *hwmodel.ListRecordSets\n\n\tfor _, record := range ptr.Deref(records.Recordsets) {\n\t\tif ptr.Deref(record.Type) == \"TXT\" && ptr.Deref(record.Name) == info.EffectiveFQDN {\n\t\t\texistingRecordSet = &record\n\t\t}\n\t}\n\n\tvalue := strconv.Quote(info.Value)\n\n\tif existingRecordSet == nil {\n\t\trequest := &hwmodel.CreateRecordSetRequest{\n\t\t\tZoneId: zoneID,\n\t\t\tBody: &hwmodel.CreateRecordSetRequestBody{\n\t\t\t\tName:        info.EffectiveFQDN,\n\t\t\t\tDescription: ptr.Pointer(\"Added TXT record for ACME dns-01 challenge using lego client\"),\n\t\t\t\tType:        \"TXT\",\n\t\t\t\tTtl:         ptr.Pointer(d.config.TTL),\n\t\t\t\tRecords:     []string{value},\n\t\t\t},\n\t\t}\n\n\t\tresp, errCreate := d.client.CreateRecordSet(request)\n\t\tif errCreate != nil {\n\t\t\treturn \"\", fmt.Errorf(\"create record set: %w\", errCreate)\n\t\t}\n\n\t\treturn ptr.Deref(resp.Id), nil\n\t}\n\n\tupdateRequest := &hwmodel.UpdateRecordSetRequest{\n\t\tZoneId:      zoneID,\n\t\tRecordsetId: ptr.Deref(existingRecordSet.Id),\n\t\tBody: &hwmodel.UpdateRecordSetReq{\n\t\t\tName:        existingRecordSet.Name,\n\t\t\tDescription: existingRecordSet.Description,\n\t\t\tType:        existingRecordSet.Type,\n\t\t\tTtl:         existingRecordSet.Ttl,\n\t\t\tRecords:     ptr.Pointer(append(ptr.Deref(existingRecordSet.Records), value)),\n\t\t},\n\t}\n\n\tresp, err := d.client.UpdateRecordSet(updateRequest)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"update record set: %w\", err)\n\t}\n\n\treturn ptr.Deref(resp.Id), nil\n}\n\nfunc (d *DNSProvider) getZoneID(authZone string) (string, error) {\n\tzones, err := d.client.ListPublicZones(&hwmodel.ListPublicZonesRequest{})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to get zone: %w\", err)\n\t}\n\n\tfor _, zone := range ptr.Deref(zones.Zones) {\n\t\tif ptr.Deref(zone.Name) == authZone {\n\t\t\treturn ptr.Deref(zone.Id), nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"zone %q not found\", authZone)\n}\n"
  },
  {
    "path": "providers/dns/huaweicloud/huaweicloud.toml",
    "content": "Name = \"Huawei Cloud\"\nDescription = ''''''\nURL = \"https://huaweicloud.com\"\nCode = \"huaweicloud\"\nSince = \"v4.19\"\n\nExample = '''\nHUAWEICLOUD_ACCESS_KEY_ID=your-access-key-id \\\nHUAWEICLOUD_SECRET_ACCESS_KEY=your-secret-access-key \\\nHUAWEICLOUD_REGION=cn-south-1 \\\nlego --dns huaweicloud -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    HUAWEICLOUD_ACCESS_KEY_ID = \"Access key ID\"\n    HUAWEICLOUD_SECRET_ACCESS_KEY = \"Access Key secret\"\n    HUAWEICLOUD_REGION = \"Region\"\n\n  [Configuration.Additional]\n    HUAWEICLOUD_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    HUAWEICLOUD_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    HUAWEICLOUD_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    HUAWEICLOUD_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://console-intl.huaweicloud.com/apiexplorer/#/openapi/DNS/doc?locale=en-us\"\n  CN_API = \"https://support.huaweicloud.com/api-dns/zh-cn_topic_0132421999.html\"\n  GoClient = \"https://github.com/huaweicloud/huaweicloud-sdk-go-v3\"\n"
  },
  {
    "path": "providers/dns/huaweicloud/huaweicloud_test.go",
    "content": "package huaweicloud\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\thwregion \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/region\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAccessKeyID, EnvSecretAccessKey, EnvRegion).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t// The \"success\" cannot be tested because there is an API call that require a valid authentication.\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKeyID:     \"\",\n\t\t\t\tEnvSecretAccessKey: \"\",\n\t\t\t\tEnvRegion:          \"\",\n\t\t\t},\n\t\t\texpected: \"huaweicloud: some credentials information are missing: HUAWEICLOUD_ACCESS_KEY_ID,HUAWEICLOUD_SECRET_ACCESS_KEY,HUAWEICLOUD_REGION\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKeyID:     \"\",\n\t\t\t\tEnvSecretAccessKey: \"456\",\n\t\t\t\tEnvRegion:          hwregion.CN_EAST_2.Id,\n\t\t\t},\n\t\t\texpected: \"huaweicloud: some credentials information are missing: HUAWEICLOUD_ACCESS_KEY_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKeyID:     \"123\",\n\t\t\t\tEnvSecretAccessKey: \"\",\n\t\t\t\tEnvRegion:          hwregion.CN_EAST_2.Id,\n\t\t\t},\n\t\t\texpected: \"huaweicloud: some credentials information are missing: HUAWEICLOUD_SECRET_ACCESS_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKeyID:     \"123\",\n\t\t\t\tEnvSecretAccessKey: \"456\",\n\t\t\t\tEnvRegion:          \"\",\n\t\t\t},\n\t\t\texpected: \"huaweicloud: some credentials information are missing: HUAWEICLOUD_REGION\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc            string\n\t\taccessKeyID     string\n\t\tsecretAccessKey string\n\t\tregion          string\n\t\texpected        string\n\t}{\n\t\t// The \"success\" cannot be tested because there is an API call that require a valid authentication.\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"huaweicloud: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:            \"missing secret id\",\n\t\t\tsecretAccessKey: \"456\",\n\t\t\tregion:          hwregion.CN_EAST_2.Id,\n\t\t\texpected:        \"huaweicloud: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:        \"missing secret key\",\n\t\t\taccessKeyID: \"123\",\n\t\t\tregion:      hwregion.CN_EAST_2.Id,\n\t\t\texpected:    \"huaweicloud: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:            \"missing region\",\n\t\t\taccessKeyID:     \"123\",\n\t\t\tsecretAccessKey: \"456\",\n\t\t\texpected:        \"huaweicloud: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AccessKeyID = test.accessKeyID\n\t\t\tconfig.SecretAccessKey = test.secretAccessKey\n\t\t\tconfig.Region = test.region\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/huaweicloud/internal/client.go",
    "content": "/*\nCopyright (c) Huawei Technologies Co., Ltd. 2020-present. All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n     http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// 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\npackage internal\n\nimport (\n\thttpclient \"github.com/huaweicloud/huaweicloud-sdk-go-v3/core\"\n\thwdns \"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2\"\n\t\"github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model\"\n)\n\ntype DnsClient struct {\n\tHcClient *httpclient.HcHttpClient\n}\n\nfunc NewDnsClient(hcClient *httpclient.HcHttpClient) *DnsClient {\n\treturn &DnsClient{HcClient: hcClient}\n}\n\nfunc (c *DnsClient) ShowRecordSet(request *model.ShowRecordSetRequest) (*model.ShowRecordSetResponse, error) {\n\trequestDef := hwdns.GenReqDefForShowRecordSet()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ShowRecordSetResponse), nil\n\t}\n}\n\nfunc (c *DnsClient) CreateRecordSet(request *model.CreateRecordSetRequest) (*model.CreateRecordSetResponse, error) {\n\trequestDef := hwdns.GenReqDefForCreateRecordSet()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.CreateRecordSetResponse), nil\n\t}\n}\n\nfunc (c *DnsClient) UpdateRecordSet(request *model.UpdateRecordSetRequest) (*model.UpdateRecordSetResponse, error) {\n\trequestDef := hwdns.GenReqDefForUpdateRecordSet()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.UpdateRecordSetResponse), nil\n\t}\n}\n\nfunc (c *DnsClient) DeleteRecordSet(request *model.DeleteRecordSetRequest) (*model.DeleteRecordSetResponse, error) {\n\trequestDef := hwdns.GenReqDefForDeleteRecordSet()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.DeleteRecordSetResponse), nil\n\t}\n}\n\nfunc (c *DnsClient) ListRecordSetsByZone(request *model.ListRecordSetsByZoneRequest) (*model.ListRecordSetsByZoneResponse, error) {\n\trequestDef := hwdns.GenReqDefForListRecordSetsByZone()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ListRecordSetsByZoneResponse), nil\n\t}\n}\n\nfunc (c *DnsClient) ListPublicZones(request *model.ListPublicZonesRequest) (*model.ListPublicZonesResponse, error) {\n\trequestDef := hwdns.GenReqDefForListPublicZones()\n\n\tif resp, err := c.HcClient.Sync(request, requestDef); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn resp.(*model.ListPublicZonesResponse), nil\n\t}\n}\n"
  },
  {
    "path": "providers/dns/hurricane/hurricane.go",
    "content": "package hurricane\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hurricane/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"HURRICANE_\"\n\n\tEnvTokens = envNamespace + \"TOKENS\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tCredentials        map[string]string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 300*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Hurricane Electric.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tconfig := NewDefaultConfig()\n\n\tvalues, err := env.Get(EnvTokens)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"hurricane: %w\", err)\n\t}\n\n\tcredentials, err := env.ParsePairs(values[EnvTokens])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"hurricane: credentials: %w\", err)\n\t}\n\n\tconfig.Credentials = credentials\n\n\treturn NewDNSProviderConfig(config)\n}\n\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"hurricane: the configuration of the DNS provider is nil\")\n\t}\n\n\tif len(config.Credentials) == 0 {\n\t\treturn nil, errors.New(\"hurricane: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(config.Credentials)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present updates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.client.UpdateTxtRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN), info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hurricane: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp updates the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.client.UpdateTxtRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN), \".\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hurricane: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n"
  },
  {
    "path": "providers/dns/hurricane/hurricane.toml",
    "content": "Name = \"Hurricane Electric DNS\"\nDescription = ''''''\nURL = \"https://dns.he.net/\"\nCode = \"hurricane\"\nSince = \"v4.3.0\"\n\nExample = '''\nHURRICANE_TOKENS=example.org:token \\\nlego --dns hurricane -d '*.example.com' -d example.com run\n\nHURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2 \\\nlego --dns hurricane -d my.example.org -d demo.example.org\n'''\n\nAdditional = \"\"\"\nBefore using lego to request a certificate for a given domain or wildcard (such as `my.example.org` or `*.my.example.org`),\ncreate a TXT record named `_acme-challenge.my.example.org`, and enable dynamic updates on it.\nGenerate a token for each URL with Hurricane Electric's UI, and copy it down.\nStick to alphanumeric tokens for greatest reliability.\n\nTo authenticate with the Hurricane Electric API,\nadd each record name/token pair you want to update to the `HURRICANE_TOKENS` environment variable, as shown in the examples.\nRecord names (without the `_acme-challenge.` component) and their tokens are separated with colons,\nwhile the credential pairs are concatenated into a comma-separated list, like so:\n\n```\nHURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2\n```\n\nIf you are issuing both a wildcard certificate and a standard certificate for a given subdomain,\nyou should not have repeat entries for that name, as both will use the same credential.\n\n```\nHURRICANE_TOKENS=example.org:token\n```\n\"\"\"\n\n[Configuration]\n  [Configuration.Credentials]\n    HURRICANE_TOKENS = \"TXT record names and tokens\"\n  [Configuration.Additional]\n    HURRICANE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    HURRICANE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation (Default: 300)\"\n    HURRICANE_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n    HURRICANE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://dns.he.net/\"\n"
  },
  {
    "path": "providers/dns/hurricane/hurricane_test.go",
    "content": "package hurricane\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvTokens).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTokens: \"example.org:123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success multiple domains\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTokens: \"example.org:123,example.com:456,example.net:789\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTokens: \",\",\n\t\t\t},\n\t\t\texpected: \"hurricane: credentials: incorrect pair: \",\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid credentials, partial\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTokens: \"example.org:123,example.net\",\n\t\t\t},\n\t\t\texpected: \"hurricane: credentials: incorrect pair: example.net\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvTokens: \"\",\n\t\t\t},\n\t\t\texpected: \"hurricane: some credentials information are missing: HURRICANE_TOKENS\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tcreds    map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\tcreds: map[string]string{\"example.org\": \"123\"},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success multiple domains\",\n\t\t\tcreds: map[string]string{\n\t\t\t\t\"example.org\": \"123\",\n\t\t\t\t\"example.com\": \"456\",\n\t\t\t\t\"example.net\": \"789\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"hurricane: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Credentials = test.creds\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/hurricane/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"golang.org/x/time/rate\"\n)\n\nconst defaultBaseURL = \"https://dyn.dns.he.net/nic/update\"\n\nconst (\n\tcodeGood     = \"good\"\n\tcodeNoChg    = \"nochg\"\n\tcodeAbuse    = \"abuse\"\n\tcodeBadAgent = \"badagent\"\n\tcodeBadAuth  = \"badauth\"\n\tcodeInterval = \"interval\"\n\tcodeNoHost   = \"nohost\"\n\tcodeNotFqdn  = \"notfqdn\"\n)\n\nconst defaultBurst = 5\n\n// Client the Hurricane Electric client.\ntype Client struct {\n\tHTTPClient   *http.Client\n\trateLimiters sync.Map\n\n\tbaseURL string\n\n\tcredentials map[string]string\n\tcredMu      sync.Mutex\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(credentials map[string]string) *Client {\n\treturn &Client{\n\t\tHTTPClient:  &http.Client{Timeout: 5 * time.Second},\n\t\tbaseURL:     defaultBaseURL,\n\t\tcredentials: credentials,\n\t}\n}\n\n// UpdateTxtRecord updates a TXT record.\nfunc (c *Client) UpdateTxtRecord(ctx context.Context, hostname, txt string) error {\n\tdomain := strings.TrimPrefix(hostname, \"_acme-challenge.\")\n\n\tc.credMu.Lock()\n\ttoken, ok := c.credentials[domain]\n\tc.credMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"domain %s not found in credentials, check your credentials map\", domain)\n\t}\n\n\tdata := url.Values{}\n\tdata.Set(\"password\", token)\n\tdata.Set(\"hostname\", hostname)\n\tdata.Set(\"txt\", txt)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, strings.NewReader(data.Encode()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\trl, _ := c.rateLimiters.LoadOrStore(hostname, rate.NewLimiter(limit(defaultBurst), defaultBurst))\n\n\terr = rl.(*rate.Limiter).Wait(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\treturn evaluateBody(string(bytes.TrimSpace(raw)), hostname)\n}\n\nfunc evaluateBody(body, hostname string) error {\n\tcode, _, _ := strings.Cut(body, \" \")\n\n\tswitch code {\n\tcase codeGood:\n\t\treturn nil\n\tcase codeNoChg:\n\t\tlog.Printf(\"%s: unchanged content written to TXT record %s\", body, hostname)\n\t\treturn nil\n\tcase codeAbuse:\n\t\treturn fmt.Errorf(\"%s: blocked hostname for abuse: %s\", body, hostname)\n\tcase codeBadAgent:\n\t\treturn fmt.Errorf(\"%s: user agent not sent or HTTP method not recognized; open an issue on go-acme/lego on GitHub\", body)\n\tcase codeBadAuth:\n\t\treturn fmt.Errorf(\"%s: wrong authentication token provided for TXT record %s\", body, hostname)\n\tcase codeInterval:\n\t\treturn fmt.Errorf(\"%s: TXT records update exceeded API rate limit\", body)\n\tcase codeNoHost:\n\t\treturn fmt.Errorf(\"%s: the record provided does not exist in this account: %s\", body, hostname)\n\tcase codeNotFqdn:\n\t\treturn fmt.Errorf(\"%s: the record provided isn't an FQDN: %s\", body, hostname)\n\tdefault:\n\t\t// This is basically only server errors.\n\t\treturn fmt.Errorf(\"attempt to change TXT record %s returned %s\", hostname, body)\n\t}\n}\n\n// limit computes the rate based on burst.\n// The API rate limit per-record is 10 reqs / 2 minutes.\n//\n//\t10 reqs / 2 minutes = freq 1/12 (burst = 1)\n//\t6 reqs / 2 minutes = freq 1/20 (burst = 5)\n//\n// https://github.com/go-acme/lego/issues/1415\nfunc limit(burst int) rate.Limit {\n\treturn 1 / rate.Limit(120/(10-burst+1))\n}\n"
  },
  {
    "path": "providers/dns/hurricane/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(map[string]string{\"example.com\": \"secret\"})\n\tclient.baseURL = server.URL\n\tclient.HTTPClient = server.Client()\n\n\treturn client, nil\n}\n\nfunc TestClient_UpdateTxtRecord(t *testing.T) {\n\ttestCases := []struct {\n\t\tcode     string\n\t\texpected assert.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tcode:     codeGood,\n\t\t\texpected: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tcode:     codeNoChg + ` \"0123456789abcdef\"`,\n\t\t\texpected: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tcode:     codeAbuse,\n\t\t\texpected: assert.Error,\n\t\t},\n\t\t{\n\t\t\tcode:     codeBadAgent,\n\t\t\texpected: assert.Error,\n\t\t},\n\t\t{\n\t\t\tcode:     codeBadAuth,\n\t\t\texpected: assert.Error,\n\t\t},\n\t\t{\n\t\t\tcode:     codeNoHost,\n\t\t\texpected: assert.Error,\n\t\t},\n\t\t{\n\t\t\tcode:     codeNotFqdn,\n\t\t\texpected: assert.Error,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.code, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithContentTypeFromURLEncoded()).\n\t\t\t\tRoute(\"POST /\",\n\t\t\t\t\tservermock.RawStringResponse(test.code),\n\t\t\t\t\tservermock.CheckForm().Strict().\n\t\t\t\t\t\tWith(\"hostname\", \"_acme-challenge.example.com\").\n\t\t\t\t\t\tWith(\"password\", \"secret\").\n\t\t\t\t\t\tWith(\"txt\", \"foo\")).\n\t\t\t\tBuild(t)\n\n\t\t\terr := client.UpdateTxtRecord(t.Context(), \"_acme-challenge.example.com\", \"foo\")\n\t\t\ttest.expected(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/hyperone/hyperone.go",
    "content": "// Package hyperone implements a DNS provider for solving the DNS-01 challenge using HyperOne.\npackage hyperone\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hyperone/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"HYPERONE_\"\n\n\tEnvPassportLocation = envNamespace + \"PASSPORT_LOCATION\"\n\tEnvAPIUrl           = envNamespace + \"API_URL\"\n\tEnvLocationID       = envNamespace + \"LOCATION_ID\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIEndpoint      string\n\tLocationID       string\n\tPassportLocation string\n\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *internal.Client\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for HyperOne.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tconfig := NewDefaultConfig()\n\n\tconfig.PassportLocation = env.GetOrFile(EnvPassportLocation)\n\tconfig.LocationID = env.GetOrFile(EnvLocationID)\n\tconfig.APIEndpoint = env.GetOrFile(EnvAPIUrl)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for HyperOne.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config.PassportLocation == \"\" {\n\t\tvar err error\n\n\t\tconfig.PassportLocation, err = GetDefaultPassportLocation()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"hyperone: %w\", err)\n\t\t}\n\t}\n\n\tpassport, err := internal.LoadPassportFile(config.PassportLocation)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"hyperone: %w\", err)\n\t}\n\n\tclient, err := internal.NewClient(config.APIEndpoint, config.LocationID, passport)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"hyperone: failed to create client: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzone, err := d.getHostedZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hyperone: failed to get zone for fqdn=%s: %w\", info.EffectiveFQDN, err)\n\t}\n\n\trecordset, err := d.client.FindRecordset(ctx, zone.ID, \"TXT\", info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hyperone: fqdn=%s, zone ID=%s: %w\", info.EffectiveFQDN, zone.ID, err)\n\t}\n\n\tif recordset == nil {\n\t\t_, err = d.client.CreateRecordset(ctx, zone.ID, \"TXT\", info.EffectiveFQDN, info.Value, d.config.TTL)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"hyperone: failed to create recordset: fqdn=%s, zone ID=%s, value=%s: %w\", info.EffectiveFQDN, zone.ID, info.Value, err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t_, err = d.client.CreateRecord(ctx, zone.ID, recordset.ID, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hyperone: failed to create record: fqdn=%s, zone ID=%s, recordset ID=%s: %w\", info.EffectiveFQDN, zone.ID, recordset.ID, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters and recordset if no other records are remaining.\n// There is a small possibility that race will cause to delete recordset with records for other DNS Challenges.\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzone, err := d.getHostedZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hyperone: failed to get zone for fqdn=%s: %w\", info.EffectiveFQDN, err)\n\t}\n\n\trecordset, err := d.client.FindRecordset(ctx, zone.ID, \"TXT\", info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hyperone: fqdn=%s, zone ID=%s: %w\", info.EffectiveFQDN, zone.ID, err)\n\t}\n\n\tif recordset == nil {\n\t\treturn fmt.Errorf(\"hyperone: recordset to remove not found: fqdn=%s\", info.EffectiveFQDN)\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, zone.ID, recordset.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"hyperone: %w\", err)\n\t}\n\n\tif len(records) == 1 {\n\t\tif records[0].Content != info.Value {\n\t\t\treturn fmt.Errorf(\"hyperone: record with content %s not found: fqdn=%s\", info.Value, info.EffectiveFQDN)\n\t\t}\n\n\t\terr = d.client.DeleteRecordset(ctx, zone.ID, recordset.ID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"hyperone: failed to delete record: fqdn=%s, zone ID=%s, recordset ID=%s: %w\", info.EffectiveFQDN, zone.ID, recordset.ID, err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfor _, record := range records {\n\t\tif record.Content == info.Value {\n\t\t\terr = d.client.DeleteRecord(ctx, zone.ID, recordset.ID, record.ID)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"hyperone: fqdn=%s, zone ID=%s, recordset ID=%s, record ID=%s: %w\", info.EffectiveFQDN, zone.ID, recordset.ID, record.ID, err)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"hyperone: fqdn=%s, failed to find record with given value\", info.EffectiveFQDN)\n}\n\n// getHostedZone gets the hosted zone.\nfunc (d *DNSProvider) getHostedZone(ctx context.Context, fqdn string) (*internal.Zone, error) {\n\tauthZone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\treturn d.client.FindZone(ctx, authZone)\n}\n\nfunc GetDefaultPassportLocation() (string, error) {\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get user home directory: %w\", err)\n\t}\n\n\treturn filepath.Join(homeDir, \".h1\", \"passport.json\"), nil\n}\n"
  },
  {
    "path": "providers/dns/hyperone/hyperone.toml",
    "content": "Name = \"HyperOne\"\nDescription = ''''''\nURL = \"https://www.hyperone.com\"\nCode = \"hyperone\"\nSince = \"v3.9.0\"\n\nExample = '''\nlego --dns hyperone -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Description\n\nDefault configuration does not require any additional environment variables,\njust a passport file in `~/.h1/passport.json` location.\n\n### Generating passport file using H1 CLI\n\nTo use this application you have to generate passport file for `sa`:\n\n```\nh1 iam project sa credential generate --name my-passport --project <project ID> --sa <sa ID> --passport-output-file ~/.h1/passport.json\n```\n\n### Required permissions\n\nThe application requires following permissions:\n-  `dns/zone/list`\n-  `dns/zone.recordset/list`\n-  `dns/zone.recordset/create`\n-  `dns/zone.recordset/delete`\n-  `dns/zone.record/create`\n-  `dns/zone.record/list`\n-  `dns/zone.record/delete`\n\nAll required permissions are available via platform role `tool.lego`.\n'''\n\n[Configuration]\n  [Configuration.Additional]\n    HYPERONE_PASSPORT_LOCATION = \"Allows to pass custom passport file location (default ~/.h1/passport.json)\"\n    HYPERONE_API_URL = \"Allows to pass custom API Endpoint to be used in the challenge (default https://api.hyperone.com/v2)\"\n    HYPERONE_LOCATION_ID = \"Specifies location (region) to be used in API calls. (default pl-waw-1)\"\n    HYPERONE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    HYPERONE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 2)\"\n    HYPERONE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 60)\"\n    HYPERONE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api.hyperone.com/v2/docs\"\n"
  },
  {
    "path": "providers/dns/hyperone/hyperone_test.go",
    "content": "package hyperone\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvPassportLocation, EnvAPIUrl, EnvLocationID).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPassportLocation: \"./internal/fixtures/validPassport.json\",\n\t\t\t\tEnvAPIUrl:           \"\",\n\t\t\t\tEnvLocationID:       \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid passport\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPassportLocation: \"./internal/fixtures/invalidPassport.json\",\n\t\t\t\tEnvAPIUrl:           \"\",\n\t\t\t\tEnvLocationID:       \"\",\n\t\t\t},\n\t\t\texpected: \"hyperone: passport file validation failed: private key is missing\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"non existing passport\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPassportLocation: \"./internal/fixtures/non-existing.json\",\n\t\t\t\tEnvAPIUrl:           \"\",\n\t\t\t\tEnvLocationID:       \"\",\n\t\t\t},\n\t\t\texpected: \"hyperone: failed to open passport file: open ./internal/fixtures/non-existing.json:\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc             string\n\t\tpassportLocation string\n\t\tapiEndpoint      string\n\t\tlocationID       string\n\t\texpected         string\n\t}{\n\t\t{\n\t\t\tdesc:             \"success\",\n\t\t\tpassportLocation: \"./internal/fixtures/validPassport.json\",\n\t\t\tapiEndpoint:      \"\",\n\t\t\tlocationID:       \"\",\n\t\t},\n\t\t{\n\t\t\tdesc:             \"invalid passport\",\n\t\t\tpassportLocation: \"./internal/fixtures/invalidPassport.json\",\n\t\t\tapiEndpoint:      \"\",\n\t\t\tlocationID:       \"\",\n\t\t\texpected:         \"hyperone: passport file validation failed: private key is missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:             \"non existing passport\",\n\t\t\tpassportLocation: \"./internal/fixtures/non-existing.json\",\n\t\t\tapiEndpoint:      \"\",\n\t\t\tlocationID:       \"\",\n\t\t\texpected:         \"hyperone: failed to open passport file: open ./internal/fixtures/non-existing.json:\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.PassportLocation = test.passportLocation\n\t\t\tconfig.APIEndpoint = test.apiEndpoint\n\t\t\tconfig.LocationID = test.locationID\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/hyperone/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api.hyperone.com/v2\"\n\nconst defaultLocationID = \"pl-waw-1\"\n\ntype signer interface {\n\tGetJWT() (string, error)\n}\n\n// Client the HyperOne client.\ntype Client struct {\n\tpassport *Passport\n\tsigner   signer\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new HyperOne client.\nfunc NewClient(apiEndpoint, locationID string, passport *Passport) (*Client, error) {\n\tif passport == nil {\n\t\treturn nil, errors.New(\"the passport is missing\")\n\t}\n\n\tprojectID, err := passport.ExtractProjectID()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif apiEndpoint == \"\" {\n\t\tapiEndpoint = defaultBaseURL\n\t}\n\n\tbaseURL, err := url.Parse(apiEndpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttokenSigner := &TokenSigner{\n\t\tPrivateKey: passport.PrivateKey,\n\t\tKeyID:      passport.CertificateID,\n\t\tAudience:   apiEndpoint,\n\t\tIssuer:     passport.Issuer,\n\t\tSubject:    passport.SubjectID,\n\t}\n\n\tif locationID == \"\" {\n\t\tlocationID = defaultLocationID\n\t}\n\n\tclient := &Client{\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t\tbaseURL:    baseURL.JoinPath(\"dns\", locationID, \"project\", projectID),\n\t\tpassport:   passport,\n\t\tsigner:     tokenSigner,\n\t}\n\n\treturn client, nil\n}\n\n// FindRecordset looks for recordset with given recordType and name and returns it.\n// In case if recordset is not found returns nil.\n// https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_list\nfunc (c *Client) FindRecordset(ctx context.Context, zoneID, recordType, name string) (*Recordset, error) {\n\t// https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset\n\tendpoint := c.baseURL.JoinPath(\"zone\", zoneID, \"recordset\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar recordSets []Recordset\n\n\terr = c.do(req, &recordSets)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get recordsets from server: %w\", err)\n\t}\n\n\tfor _, v := range recordSets {\n\t\tif v.RecordType == recordType && v.Name == name {\n\t\t\treturn &v, nil\n\t\t}\n\t}\n\n\t// when recordset is not present returns nil, but error is not thrown\n\treturn nil, nil\n}\n\n// CreateRecordset creates recordset and record with given value within one request.\n// https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_create\nfunc (c *Client) CreateRecordset(ctx context.Context, zoneID, recordType, name, recordValue string, ttl int) (*Recordset, error) {\n\t// https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset\n\tendpoint := c.baseURL.JoinPath(\"zone\", zoneID, \"recordset\")\n\n\trecordsetInput := Recordset{\n\t\tRecordType: recordType,\n\t\tName:       name,\n\t\tTTL:        ttl,\n\t\tRecord:     &Record{Content: recordValue},\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, recordsetInput)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar recordsetResponse Recordset\n\n\terr = c.do(req, &recordsetResponse)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create recordset: %w\", err)\n\t}\n\n\treturn &recordsetResponse, nil\n}\n\n// DeleteRecordset deletes a recordset.\n// https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_delete\nfunc (c *Client) DeleteRecordset(ctx context.Context, zoneID, recordsetID string) error {\n\t// https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset/{recordsetId}\n\tendpoint := c.baseURL.JoinPath(\"zone\", zoneID, \"recordset\", recordsetID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\n// GetRecords gets all records within specified recordset.\n// https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_record_list\nfunc (c *Client) GetRecords(ctx context.Context, zoneID, recordsetID string) ([]Record, error) {\n\t// https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset/{recordsetId}/record\n\tendpoint := c.baseURL.JoinPath(\"zone\", zoneID, \"recordset\", recordsetID, \"record\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar records []Record\n\n\terr = c.do(req, &records)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get records from server: %w\", err)\n\t}\n\n\treturn records, err\n}\n\n// CreateRecord creates a record.\n// https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_record_create\nfunc (c *Client) CreateRecord(ctx context.Context, zoneID, recordsetID, recordContent string) (*Record, error) {\n\t// https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset/{recordsetId}/record\n\tendpoint := c.baseURL.JoinPath(\"zone\", zoneID, \"recordset\", recordsetID, \"record\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, Record{Content: recordContent})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar recordResponse Record\n\n\terr = c.do(req, &recordResponse)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to set record: %w\", err)\n\t}\n\n\treturn &recordResponse, nil\n}\n\n// DeleteRecord deletes a record.\n// https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_record_delete\nfunc (c *Client) DeleteRecord(ctx context.Context, zoneID, recordsetID, recordID string) error {\n\t// https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset/{recordsetId}/record/{recordId}\n\tendpoint := c.baseURL.JoinPath(\"zone\", zoneID, \"recordset\", recordsetID, \"record\", recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\n// FindZone looks for DNS Zone and returns nil if it does not exist.\nfunc (c *Client) FindZone(ctx context.Context, name string) (*Zone, error) {\n\tzones, err := c.GetZones(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, zone := range zones {\n\t\tif zone.DNSName == name {\n\t\t\treturn &zone, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"failed to find zone for %s\", name)\n}\n\n// GetZones gets all user's zones.\n// https://api.hyperone.com/v2/docs#operation/dns_project_zone_list\nfunc (c *Client) GetZones(ctx context.Context) ([]Zone, error) {\n\t// https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone\n\tendpoint := c.baseURL.JoinPath(\"zone\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar zones []Zone\n\n\terr = c.do(req, &zones)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to fetch available zones: %w\", err)\n\t}\n\n\treturn zones, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tjwt, err := c.signer.GetJWT()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to sign the request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Authorization\", \"Bearer \"+jwt)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif err = json.Unmarshal(raw, result); err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\tvar msg string\n\tif resp.StatusCode == http.StatusForbidden {\n\t\tmsg = \"forbidden: check if service account you are trying to use has permissions required for managing DNS\"\n\t} else {\n\t\tmsg = \"unknown error\"\n\t}\n\n\treturn fmt.Errorf(\"%s: %w\", msg, errutils.NewUnexpectedResponseStatusCodeError(req, resp))\n}\n"
  },
  {
    "path": "providers/dns/hyperone/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype signerMock struct{}\n\nfunc (s signerMock) GetJWT() (string, error) {\n\treturn \"\", nil\n}\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tpassport := &Passport{\n\t\t\t\tSubjectID: \"/iam/project/proj123/sa/xxxxxxx\",\n\t\t\t}\n\n\t\t\tclient, err := NewClient(server.URL, \"loc123\", passport)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.signer = signerMock{}\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer\"))\n}\n\nfunc TestClient_FindRecordset(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/loc123/project/proj123/zone/zone321/recordset\",\n\t\t\tservermock.ResponseFromFixture(\"recordset.json\")).\n\t\tBuild(t)\n\n\trecordset, err := client.FindRecordset(t.Context(), \"zone321\", \"SOA\", \"example.com.\")\n\trequire.NoError(t, err)\n\n\texpected := &Recordset{\n\t\tID:         \"123456789abcd\",\n\t\tName:       \"example.com.\",\n\t\tRecordType: \"SOA\",\n\t\tTTL:        1800,\n\t}\n\n\tassert.Equal(t, expected, recordset)\n}\n\nfunc TestClient_CreateRecordset(t *testing.T) {\n\texpectedReqBody := Recordset{\n\t\tRecordType: \"TXT\",\n\t\tName:       \"test.example.com.\",\n\t\tTTL:        3600,\n\t\tRecord:     &Record{Content: \"value\"},\n\t}\n\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/loc123/project/proj123/zone/zone123/recordset\",\n\t\t\tservermock.ResponseFromFixture(\"createRecordset.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromStruct(expectedReqBody)).\n\t\tBuild(t)\n\n\trs, err := client.CreateRecordset(t.Context(), \"zone123\", \"TXT\", \"test.example.com.\", \"value\", 3600)\n\trequire.NoError(t, err)\n\n\texpected := &Recordset{RecordType: \"TXT\", Name: \"test.example.com.\", TTL: 3600, ID: \"1234567890qwertyuiop\"}\n\tassert.Equal(t, expected, rs)\n}\n\nfunc TestClient_DeleteRecordset(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns/loc123/project/proj123/zone/zone321/recordset/rs322\", nil).\n\t\tBuild(t)\n\n\terr := client.DeleteRecordset(t.Context(), \"zone321\", \"rs322\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/loc123/project/proj123/zone/321/recordset/322/record\",\n\t\t\tservermock.ResponseFromFixture(\"record.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"321\", \"322\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tID:      \"135128352183572dd\",\n\t\t\tContent: \"pns.hyperone.com. hostmaster.hyperone.com. 1 15 180 1209600 1800\",\n\t\t\tEnabled: true,\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\texpectedReqBody := Record{\n\t\tContent: \"value\",\n\t}\n\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/loc123/project/proj123/zone/z123/recordset/rs325/record\",\n\t\t\tservermock.ResponseFromFixture(\"createRecord.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromStruct(expectedReqBody)).\n\t\tBuild(t)\n\n\trs, err := client.CreateRecord(t.Context(), \"z123\", \"rs325\", \"value\")\n\trequire.NoError(t, err)\n\n\texpected := &Record{ID: \"123321qwerqwewqerq\", Content: \"value\", Enabled: true}\n\tassert.Equal(t, expected, rs)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns/loc123/project/proj123/zone/321/recordset/322/record/323\",\n\t\t\tservermock.ResponseFromFixture(\"createRecord.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"321\", \"322\", \"323\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_FindZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/loc123/project/proj123/zone\",\n\t\t\tservermock.ResponseFromFixture(\"zones.json\")).\n\t\tBuild(t)\n\n\tzone, err := client.FindZone(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := &Zone{\n\t\tID:      \"zoneB\",\n\t\tName:    \"example.com\",\n\t\tDNSName: \"example.com\",\n\t\tFQDN:    \"example.com.\",\n\t\tURI:     \"\",\n\t}\n\n\tassert.Equal(t, expected, zone)\n}\n\nfunc TestClient_GetZones(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/loc123/project/proj123/zone\",\n\t\t\tservermock.ResponseFromFixture(\"zones.json\")).\n\t\tBuild(t)\n\n\tzones, err := client.GetZones(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []Zone{\n\t\t{\n\t\t\tID:      \"zoneA\",\n\t\t\tName:    \"example.org\",\n\t\t\tDNSName: \"example.org\",\n\t\t\tFQDN:    \"example.org.\",\n\t\t\tURI:     \"\",\n\t\t},\n\t\t{\n\t\t\tID:      \"zoneB\",\n\t\t\tName:    \"example.com\",\n\t\t\tDNSName: \"example.com\",\n\t\t\tFQDN:    \"example.com.\",\n\t\t\tURI:     \"\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zones)\n}\n"
  },
  {
    "path": "providers/dns/hyperone/internal/fixtures/createRecord.json",
    "content": "{\n  \"id\": \"123321qwerqwewqerq\",\n  \"content\": \"value\",\n  \"enabled\": true\n}\n"
  },
  {
    "path": "providers/dns/hyperone/internal/fixtures/createRecordset.json",
    "content": "{\n  \"id\": \"1234567890qwertyuiop\",\n  \"name\": \"test.example.com.\",\n  \"type\": \"TXT\",\n  \"ttl\": 3600\n}\n"
  },
  {
    "path": "providers/dns/hyperone/internal/fixtures/invalidPassport.json",
    "content": "{\n  \"subject_id\": \"/iam/project/projectId/sa/serviceAccountId\",\n  \"certificate_id\": \"certificateID\",\n  \"issuer\": \"https://api.hyperone.com/v2/iam/project/projectId/sa/serviceAccountId\"\n}\n"
  },
  {
    "path": "providers/dns/hyperone/internal/fixtures/record.json",
    "content": "[\n  {\n    \"id\": \"135128352183572dd\",\n    \"content\": \"pns.hyperone.com. hostmaster.hyperone.com. 1 15 180 1209600 1800\",\n    \"enabled\": true\n  }\n]\n"
  },
  {
    "path": "providers/dns/hyperone/internal/fixtures/recordset.json",
    "content": "[\n  {\n    \"id\": \"123456789abcd\",\n    \"name\": \"example.com.\",\n    \"type\": \"SOA\",\n    \"ttl\": 1800\n  },\n  {\n    \"id\": \"123456789abcde\",\n    \"name\": \"example.com.\",\n    \"type\": \"NS\",\n    \"ttl\": 3600\n  },\n  {\n    \"id\": \"123456789abcdf\",\n    \"name\": \"example.com.\",\n    \"type\": \"CNAME\",\n    \"ttl\": 3600\n  }\n]\n"
  },
  {
    "path": "providers/dns/hyperone/internal/fixtures/validPassport.json",
    "content": "{\n  \"subject_id\": \"/iam/project/projectId/sa/serviceAccountId\",\n  \"certificate_id\": \"certificateID\",\n  \"issuer\": \"https://api.hyperone.com/v2/iam/project/projectId/sa/serviceAccountId\",\n  \"private_key\": \"-----BEGIN RSA PRIVATE KEY-----\\nlrMAsSjjkKiRxGdgR8p5kZJj0AFgdWYa3OT2snIXnN5+/p7j13PSkseUcrAFyokc\\nV9pgeDfitAhb9lpdjxjjuxRcuQjBfmNVLPF9MFyNOvhrprGNukUh/12oSKO9dFEt\\ns39F/2h6Ld5IQrGt3gZaBB1aGO+tw3ill1VBy2zGPIDeuSz6DS3GG/oQ2gLSSMP4\\nOVfQ32Oajo496iHRkdIh/7Hho7BNzMYr1GxrYTcE9/Znr6xgeSdNT37CCeCH8cmP\\naEAUgSMTeIMVSpILwkKeNvBURic1EWaqXRgPRIWK0vNyOCs/+jNoFISnV4pu1ROF\\n92vayHDNSVw9wHcdSQ75XSE4Msawqv5U1iI7e2lD64uo1qhmJdrPcXDJQCiDbh+F\\nhQhF+wAoLRvMNwwhg+LttL8vXqMDQl3olsWSvWPs6b/MZpB0qwd1bklzA6P+PeAU\\nsfOvTqi9edIOfKqvXqTXEhBP8qC7ZtOKLGnryZb7W04SSVrNtuJUFRcLiqu+w/F/\\nMSxGSGalYpzIZ1B5HLQqISgWMXdbt39uMeeooeZjkuI3VIllFjtybecjPR9ZYQPt\\nFFEP1XqNXjLFmGh84TXtvGLWretWM1OZmN8UKKUeATqrr7zuh5AYGAIbXd8BvweL\\nPigl9ei0hTculPqohvkoc5x1srPBvzHrirGlxOYjW3fc4kDgZpy+6ik5k5g7JWQD\\nlbXCRz3HGazgUPeiwUr06a52vhgT7QuNIUZqdHb4IfCYs2pQTLHzQjAqvVk1mm2D\\nkh4myIcTtf69BFcu/Wuptm3NaKd1nwk1squR6psvcTXOWII81pstnxNYkrokx4r2\\n7YVllNruOD+cMDNZbIG2CwT6V9ukIS8tl9EJp8eyb0a1uAEc22BNOjYHPF50beWF\\nukf3uc0SA+G3zhmXCM5sMf5OxVjKr5jgcir7kySY5KbmG71omYhczgr4H0qgxYo9\\nZyj2wMKrTHLfFOpd4OOEun9Gi3srqlKZep7Hj7gNyUwZu1qiBvElmBVmp0HJxT0N\\nmktuaVbaFgBsTS0/us1EqWvCA4REh1Ut/NoA9oG3JFt0lGDstTw1j+orDmIHOmSu\\n7FKYzr0uCz14AkLMSOixdPD1F0YyED1NMVnRVXw77HiAFGmb0CDi2KEg70pEKpn3\\nksa8oe0MQi6oEwlMsAxVTXOB1wblTBuSBeaECzTzWE+/DHF+QQfQi8kAjjSdmmMJ\\nyN+shdBWHYRGYnxRkTatONhcDBIY7sZV7wolYHz/rf7dpYUZf37vdQnYV8FpO1um\\nYa0GslyRJ5GqMBfDS1cQKne+FvVHxEE2YqEGBcOYhx/JI2soE8aA8W4XffN+DoEy\\nZkinJ/+BOwJ/zUI9GZtwB4JXqbNEE+j7r7/fJO9KxfPp4MPK4YWu0H0EUWONpVwe\\nTWtbRhQUCOe4PVSC/Vv1pstvMD/D+E/0L4GQNHxr+xyFxuvILty5lvFTxoAVYpqD\\nu8gNhk3NWefTrlSkhY4N+tPP6o7E4t3y40nOA/d9qaqiid+lYcIDB0cJTpZvgeeQ\\nijohxY3PHruU4vVZa37ITQnco9az6lsy18vbU0bOyK2fEZ2R9XVO8fH11jiV8oGH\\n-----END RSA PRIVATE KEY-----\\n\",\n  \"public_key\": \"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxzYuc22QSst/dS7geYYK\\n5l5kLxU0tayNdixkEQ17ix+CUcUbKIsnyftZxaCYT46rQtXgCaYRdJcbB3hmyrOa\\nvkhTpX79xJZnQmfuamMbZBqitvscxW9zRR9tBUL6vdi/0rpoUwPMEh8+Bw7CgYR0\\nFK0DhWYBNDfe9HKcyZEv3max8Cdq18htxjEsdYO0iwzhtKRXomBWTdhD5ykd/fAC\\nVTr4+KEY+IeLvubHVmLUhbE5NgWXxrRpGasDqzKhCTmsa2Ysf712rl57SlH0Wz/M\\nr3F7aM9YpErzeYLrl0GhQr9BVJxOvXcVd4kmY+XkiCcrkyS1cnghnllh+LCwQu1s\\nYwIDAQAB\\n-----END PUBLIC KEY-----\\n\"\n}\n"
  },
  {
    "path": "providers/dns/hyperone/internal/fixtures/zones.json",
    "content": "[\n  {\n    \"id\": \"zoneA\",\n    \"name\": \"example.org\",\n    \"dnsName\": \"example.org\",\n    \"fqdn\": \"example.org.\",\n    \"uri\": \"\"\n  },\n  {\n    \"id\": \"zoneB\",\n    \"name\": \"example.com\",\n    \"dnsName\": \"example.com\",\n    \"fqdn\": \"example.com.\",\n    \"uri\": \"\"\n  }\n]"
  },
  {
    "path": "providers/dns/hyperone/internal/passport.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n)\n\ntype Passport struct {\n\tSubjectID     string `json:\"subject_id\"`\n\tCertificateID string `json:\"certificate_id\"`\n\tIssuer        string `json:\"issuer\"`\n\tPrivateKey    string `json:\"private_key\"`\n\tPublicKey     string `json:\"public_key\"`\n}\n\nfunc LoadPassportFile(location string) (*Passport, error) {\n\tfile, err := os.Open(location)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open passport file: %w\", err)\n\t}\n\n\tdefer func() { _ = file.Close() }()\n\n\tvar passport Passport\n\n\terr = json.NewDecoder(file).Decode(&passport)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse passport file: %w\", err)\n\t}\n\n\terr = passport.validate()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"passport file validation failed: %w\", err)\n\t}\n\n\treturn &passport, nil\n}\n\nfunc (passport *Passport) validate() error {\n\tif passport.Issuer == \"\" {\n\t\treturn errors.New(\"issuer is empty\")\n\t}\n\n\tif passport.CertificateID == \"\" {\n\t\treturn errors.New(\"certificate ID is empty\")\n\t}\n\n\tif passport.PrivateKey == \"\" {\n\t\treturn errors.New(\"private key is missing\")\n\t}\n\n\tif passport.SubjectID == \"\" {\n\t\treturn errors.New(\"subject is empty\")\n\t}\n\n\treturn nil\n}\n\nfunc (passport *Passport) ExtractProjectID() (string, error) {\n\tre := regexp.MustCompile(\"iam/project/([a-zA-Z0-9]+)\")\n\n\tparts := re.FindStringSubmatch(passport.SubjectID)\n\tif len(parts) != 2 {\n\t\treturn \"\", fmt.Errorf(\"failed to extract project ID from subject ID: %s\", passport.SubjectID)\n\t}\n\n\treturn parts[1], nil\n}\n"
  },
  {
    "path": "providers/dns/hyperone/internal/passport_test.go",
    "content": "package internal\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLoadPassportFile(t *testing.T) {\n\tpassport, err := LoadPassportFile(\"fixtures/validPassport.json\")\n\trequire.NoError(t, err)\n\n\texpected := &Passport{\n\t\tSubjectID:     \"/iam/project/projectId/sa/serviceAccountId\",\n\t\tCertificateID: \"certificateID\",\n\t\tIssuer:        \"https://api.hyperone.com/v2/iam/project/projectId/sa/serviceAccountId\",\n\t\tPrivateKey: `-----BEGIN RSA PRIVATE KEY-----\nlrMAsSjjkKiRxGdgR8p5kZJj0AFgdWYa3OT2snIXnN5+/p7j13PSkseUcrAFyokc\nV9pgeDfitAhb9lpdjxjjuxRcuQjBfmNVLPF9MFyNOvhrprGNukUh/12oSKO9dFEt\ns39F/2h6Ld5IQrGt3gZaBB1aGO+tw3ill1VBy2zGPIDeuSz6DS3GG/oQ2gLSSMP4\nOVfQ32Oajo496iHRkdIh/7Hho7BNzMYr1GxrYTcE9/Znr6xgeSdNT37CCeCH8cmP\naEAUgSMTeIMVSpILwkKeNvBURic1EWaqXRgPRIWK0vNyOCs/+jNoFISnV4pu1ROF\n92vayHDNSVw9wHcdSQ75XSE4Msawqv5U1iI7e2lD64uo1qhmJdrPcXDJQCiDbh+F\nhQhF+wAoLRvMNwwhg+LttL8vXqMDQl3olsWSvWPs6b/MZpB0qwd1bklzA6P+PeAU\nsfOvTqi9edIOfKqvXqTXEhBP8qC7ZtOKLGnryZb7W04SSVrNtuJUFRcLiqu+w/F/\nMSxGSGalYpzIZ1B5HLQqISgWMXdbt39uMeeooeZjkuI3VIllFjtybecjPR9ZYQPt\nFFEP1XqNXjLFmGh84TXtvGLWretWM1OZmN8UKKUeATqrr7zuh5AYGAIbXd8BvweL\nPigl9ei0hTculPqohvkoc5x1srPBvzHrirGlxOYjW3fc4kDgZpy+6ik5k5g7JWQD\nlbXCRz3HGazgUPeiwUr06a52vhgT7QuNIUZqdHb4IfCYs2pQTLHzQjAqvVk1mm2D\nkh4myIcTtf69BFcu/Wuptm3NaKd1nwk1squR6psvcTXOWII81pstnxNYkrokx4r2\n7YVllNruOD+cMDNZbIG2CwT6V9ukIS8tl9EJp8eyb0a1uAEc22BNOjYHPF50beWF\nukf3uc0SA+G3zhmXCM5sMf5OxVjKr5jgcir7kySY5KbmG71omYhczgr4H0qgxYo9\nZyj2wMKrTHLfFOpd4OOEun9Gi3srqlKZep7Hj7gNyUwZu1qiBvElmBVmp0HJxT0N\nmktuaVbaFgBsTS0/us1EqWvCA4REh1Ut/NoA9oG3JFt0lGDstTw1j+orDmIHOmSu\n7FKYzr0uCz14AkLMSOixdPD1F0YyED1NMVnRVXw77HiAFGmb0CDi2KEg70pEKpn3\nksa8oe0MQi6oEwlMsAxVTXOB1wblTBuSBeaECzTzWE+/DHF+QQfQi8kAjjSdmmMJ\nyN+shdBWHYRGYnxRkTatONhcDBIY7sZV7wolYHz/rf7dpYUZf37vdQnYV8FpO1um\nYa0GslyRJ5GqMBfDS1cQKne+FvVHxEE2YqEGBcOYhx/JI2soE8aA8W4XffN+DoEy\nZkinJ/+BOwJ/zUI9GZtwB4JXqbNEE+j7r7/fJO9KxfPp4MPK4YWu0H0EUWONpVwe\nTWtbRhQUCOe4PVSC/Vv1pstvMD/D+E/0L4GQNHxr+xyFxuvILty5lvFTxoAVYpqD\nu8gNhk3NWefTrlSkhY4N+tPP6o7E4t3y40nOA/d9qaqiid+lYcIDB0cJTpZvgeeQ\nijohxY3PHruU4vVZa37ITQnco9az6lsy18vbU0bOyK2fEZ2R9XVO8fH11jiV8oGH\n-----END RSA PRIVATE KEY-----\n`,\n\t\tPublicKey: `-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxzYuc22QSst/dS7geYYK\n5l5kLxU0tayNdixkEQ17ix+CUcUbKIsnyftZxaCYT46rQtXgCaYRdJcbB3hmyrOa\nvkhTpX79xJZnQmfuamMbZBqitvscxW9zRR9tBUL6vdi/0rpoUwPMEh8+Bw7CgYR0\nFK0DhWYBNDfe9HKcyZEv3max8Cdq18htxjEsdYO0iwzhtKRXomBWTdhD5ykd/fAC\nVTr4+KEY+IeLvubHVmLUhbE5NgWXxrRpGasDqzKhCTmsa2Ysf712rl57SlH0Wz/M\nr3F7aM9YpErzeYLrl0GhQr9BVJxOvXcVd4kmY+XkiCcrkyS1cnghnllh+LCwQu1s\nYwIDAQAB\n-----END PUBLIC KEY-----\n`,\n\t}\n\n\tassert.Equal(t, expected, passport)\n}\n\nfunc TestLoadPassportFile_invalid(t *testing.T) {\n\tpassport, err := LoadPassportFile(\"fixtures/invalidPassport.json\")\n\trequire.EqualError(t, err, \"passport file validation failed: private key is missing\")\n\n\tassert.Nil(t, passport)\n}\n\nfunc TestExtractProjectID(t *testing.T) {\n\tpassport := Passport{SubjectID: \"/iam/project/ddd/sa/5ef759c0ab0acab07xxxxxxx\"}\n\textractedID, err := passport.ExtractProjectID()\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"ddd\", extractedID)\n}\n\nfunc TestExtractProjectID_invalid(t *testing.T) {\n\tpassport := Passport{SubjectID: \"ddddddd\"}\n\n\textractedID, err := passport.ExtractProjectID()\n\trequire.EqualError(t, err, \"failed to extract project ID from subject ID: ddddddd\")\n\n\tassert.Empty(t, extractedID)\n}\n"
  },
  {
    "path": "providers/dns/hyperone/internal/token.go",
    "content": "package internal\n\nimport (\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-jose/go-jose/v4\"\n\t\"github.com/go-jose/go-jose/v4/jwt\"\n)\n\ntype TokenSigner struct {\n\tPrivateKey string\n\tKeyID      string\n\tAudience   string\n\tIssuer     string\n\tSubject    string\n}\n\nfunc (input *TokenSigner) GetJWT() (string, error) {\n\tsigner, err := getRSASigner(input.PrivateKey, input.KeyID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tissuedAt := time.Now()\n\texpiresAt := issuedAt.Add(5 * time.Minute)\n\n\tpayload := Payload{IssuedAt: issuedAt.Unix(), Expiry: expiresAt.Unix(), Audience: input.Audience, Issuer: input.Issuer, Subject: input.Subject}\n\ttoken, err := payload.buildToken(&signer)\n\n\treturn token, err\n}\n\nfunc getRSASigner(privateKey, keyID string) (jose.Signer, error) {\n\tparsedKey, err := parseRSAKey(privateKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tkey := jose.SigningKey{Algorithm: jose.RS256, Key: parsedKey}\n\n\tsignerOpts := jose.SignerOptions{}\n\tsignerOpts.WithType(\"JWT\")\n\tsignerOpts.WithHeader(\"kid\", keyID)\n\n\trsaSigner, err := jose.NewSigner(key, &signerOpts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create JWS RSA256 signer: %w\", err)\n\t}\n\n\treturn rsaSigner, nil\n}\n\ntype Payload struct {\n\tIssuedAt int64  `json:\"iat\"`\n\tExpiry   int64  `json:\"exp\"`\n\tAudience string `json:\"aud\"`\n\tIssuer   string `json:\"iss\"`\n\tSubject  string `json:\"sub\"`\n}\n\nfunc (payload *Payload) buildToken(signer *jose.Signer) (string, error) {\n\tbuilder := jwt.Signed(*signer).Claims(payload)\n\n\ttoken, err := builder.Serialize()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to build JWT: %w\", err)\n\t}\n\n\treturn token, nil\n}\n\nfunc parseRSAKey(pemString string) (*rsa.PrivateKey, error) {\n\tblock, _ := pem.Decode([]byte(pemString))\n\n\tkey, err := x509.ParsePKCS1PrivateKey(block.Bytes)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse private key: %w\", err)\n\t}\n\n\treturn key, nil\n}\n"
  },
  {
    "path": "providers/dns/hyperone/internal/token_test.go",
    "content": "package internal\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/certcrypto\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype Header struct {\n\tAlgorithm string `json:\"alg\"`\n\tType      string `json:\"typ\"`\n\tKeyID     string `json:\"kid\"`\n}\n\nfunc TestPayload_buildToken(t *testing.T) {\n\tkey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err)\n\n\tsigner, err := getRSASigner(string(certcrypto.PEMEncode(key)), \"sampleKeyId\")\n\trequire.NoError(t, err)\n\n\tpayload := Payload{IssuedAt: 1234, Expiry: 4321, Audience: \"api.url\", Issuer: \"issuer\", Subject: \"subject\"}\n\n\ttoken, err := payload.buildToken(&signer)\n\trequire.NoError(t, err)\n\n\tsegments := strings.Split(token, \".\")\n\trequire.Len(t, segments, 3)\n\n\theaderString, err := base64.RawStdEncoding.DecodeString(segments[0])\n\trequire.NoError(t, err)\n\n\tvar headerStruct Header\n\n\terr = json.Unmarshal(headerString, &headerStruct)\n\trequire.NoError(t, err)\n\n\tpayloadString, err := base64.RawStdEncoding.DecodeString(segments[1])\n\trequire.NoError(t, err)\n\n\tvar payloadStruct Payload\n\n\terr = json.Unmarshal(payloadString, &payloadStruct)\n\trequire.NoError(t, err)\n\n\texpectedHeader := Header{Algorithm: \"RS256\", Type: \"JWT\", KeyID: \"sampleKeyId\"}\n\n\tassert.Equal(t, expectedHeader, headerStruct)\n\tassert.Equal(t, payload, payloadStruct)\n}\n"
  },
  {
    "path": "providers/dns/hyperone/internal/types.go",
    "content": "package internal\n\ntype Recordset struct {\n\tRecordType string  `json:\"type\"`\n\tName       string  `json:\"name\"`\n\tTTL        int     `json:\"ttl,omitempty\"`\n\tID         string  `json:\"id,omitempty\"`\n\tRecord     *Record `json:\"record,omitempty\"`\n}\n\ntype Record struct {\n\tID      string `json:\"id,omitempty\"`\n\tContent string `json:\"content\"`\n\tEnabled bool   `json:\"enabled,omitempty\"`\n}\n\ntype Zone struct {\n\tID      string `json:\"id\"`\n\tName    string `json:\"name\"`\n\tDNSName string `json:\"dnsName\"`\n\tFQDN    string `json:\"fqdn\"`\n\tURI     string `json:\"uri\"`\n}\n"
  },
  {
    "path": "providers/dns/ibmcloud/ibmcloud.go",
    "content": "// Package ibmcloud implements a DNS provider for solving the DNS-01 challenge using IBM Cloud (SoftLayer).\npackage ibmcloud\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ibmcloud/internal\"\n\t\"github.com/softlayer/softlayer-go/session\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"SOFTLAYER_\"\n\n\t// EnvUsername  the name must be the same as here:\n\t// https://github.com/softlayer/softlayer-go/blob/534185047ea683dd1e29fd23e445598295d94be4/session/session.go#L171\n\tEnvUsername = envNamespace + \"USERNAME\"\n\t// EnvAPIKey  the name must be the same as here:\n\t// https://github.com/softlayer/softlayer-go/blob/534185047ea683dd1e29fd23e445598295d94be4/session/session.go#L175\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\t// EnvHTTPTimeout the name must be the same as here:\n\t// https://github.com/softlayer/softlayer-go/blob/534185047ea683dd1e29fd23e445598295d94be4/session/session.go#L182\n\tEnvHTTPTimeout = envNamespace + \"TIMEOUT\"\n\tEnvDebug       = envNamespace + \"DEBUG\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername           string\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n\tDebug              bool\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, session.DefaultTimeout),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig  *Config\n\twrapper *internal.Wrapper\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for IBM Cloud (SoftLayer).\n// Credentials must be passed in the environment variables:\n// SOFTLAYER_USERNAME, SOFTLAYER_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ibmcloud: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.Debug = env.GetOrDefaultBool(EnvDebug, false)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for IBM Cloud (SoftLayer).\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"ibmcloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Username == \"\" {\n\t\treturn nil, errors.New(\"ibmcloud: username is missing\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"ibmcloud: API key is missing\")\n\t}\n\n\tsess := session.New(config.Username, config.APIKey)\n\n\tsess.Timeout = config.HTTPTimeout\n\tsess.Debug = config.Debug\n\n\treturn &DNSProvider{wrapper: internal.NewWrapper(sess), config: config}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\terr := d.wrapper.AddTXTRecord(info.EffectiveFQDN, domain, info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ibmcloud: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\terr := d.wrapper.CleanupTXTRecord(info.EffectiveFQDN, domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ibmcloud: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/ibmcloud/ibmcloud.toml",
    "content": "Name = \"IBM Cloud (SoftLayer)\"\nDescription = ''''''\nURL = \"https://www.ibm.com/cloud/\"\nCode = \"ibmcloud\"\nSince = \"v4.5.0\"\n\nExample = '''\nSOFTLAYER_USERNAME=xxxxx \\\nSOFTLAYER_API_KEY=yyyyy \\\nlego --dns ibmcloud -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    SOFTLAYER_USERNAME = \"Username (IBM Cloud is {accountID}_{emailAddress})\"\n    SOFTLAYER_API_KEY = \"Classic Infrastructure API key\"\n  [Configuration.Additional]\n    SOFTLAYER_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    SOFTLAYER_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    SOFTLAYER_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    SOFTLAYER_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://cloud.ibm.com/docs/dns?topic=dns-getting-started-with-the-dns-api\"\n  GoClient = \"https://github.com/softlayer/softlayer-go\"\n"
  },
  {
    "path": "providers/dns/ibmcloud/ibmcloud_test.go",
    "content": "package ibmcloud\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvUsername, EnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvAPIKey:   \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvAPIKey:   \"\",\n\t\t\t},\n\t\t\texpected: \"ibmcloud: some credentials information are missing: SOFTLAYER_USERNAME,SOFTLAYER_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvAPIKey:   \"456\",\n\t\t\t},\n\t\t\texpected: \"ibmcloud: some credentials information are missing: SOFTLAYER_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing token secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvAPIKey:   \"\",\n\t\t\t},\n\t\t\texpected: \"ibmcloud: some credentials information are missing: SOFTLAYER_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.wrapper)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"123\",\n\t\t\tapiKey:   \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"ibmcloud: username is missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing token\",\n\t\t\tapiKey:   \"456\",\n\t\t\texpected: \"ibmcloud: username is missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret\",\n\t\t\tusername: \"123\",\n\t\t\texpected: \"ibmcloud: API key is missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.wrapper)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/ibmcloud/internal/wrapper.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/softlayer/softlayer-go/datatypes\"\n\t\"github.com/softlayer/softlayer-go/services\"\n\t\"github.com/softlayer/softlayer-go/session\"\n\t\"github.com/softlayer/softlayer-go/sl\"\n)\n\ntype Wrapper struct {\n\tsession *session.Session\n}\n\nfunc NewWrapper(sess *session.Session) *Wrapper {\n\treturn &Wrapper{session: sess}\n}\n\nfunc (w Wrapper) AddTXTRecord(fqdn, domain, value string, ttl int) error {\n\tservice := services.GetDnsDomainService(w.session)\n\n\tdomainID, err := getDomainID(service, domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get domain ID: %w\", err)\n\t}\n\n\tservice.Options.Id = domainID\n\n\tif _, err := service.CreateTxtRecord(sl.String(fqdn), sl.String(value), sl.Int(ttl)); err != nil {\n\t\treturn fmt.Errorf(\"failed to create TXT record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (w Wrapper) CleanupTXTRecord(fqdn, domain string) error {\n\tservice := services.GetDnsDomainService(w.session)\n\n\tdomainID, err := getDomainID(service, domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get domain ID: %w\", err)\n\t}\n\n\tservice.Options.Id = domainID\n\n\trecords, err := findTxtRecords(service, fqdn)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to find TXT records: %w\", err)\n\t}\n\n\treturn deleteResourceRecords(service, records)\n}\n\nfunc getDomainID(service services.Dns_Domain, domain string) (*int, error) {\n\tres, err := service.GetByDomainName(sl.String(domain))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, r := range res {\n\t\tif r.Id == nil || toString(r.Name) != domain {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn r.Id, nil\n\t}\n\n\t// The domain was not found by name.\n\t// For subdomains this is not unusual in softlayer.\n\t// So in case a subdomain like `sub.toplevel.tld` was used try again using the parent domain\n\t// (strip the first part in the domain string -> `toplevel.tld`).\n\t_, parent, found := strings.Cut(domain, \".\")\n\tif !found || !strings.Contains(parent, \".\") {\n\t\treturn nil, fmt.Errorf(\"no data found for domain: %s\", domain)\n\t}\n\n\treturn getDomainID(service, parent)\n}\n\nfunc findTxtRecords(service services.Dns_Domain, fqdn string) ([]datatypes.Dns_Domain_ResourceRecord, error) {\n\tvar results []datatypes.Dns_Domain_ResourceRecord\n\n\trecords, err := service.GetResourceRecords()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, record := range records {\n\t\tif toString(record.Host) == fqdn && toString(record.Type) == \"txt\" {\n\t\t\tresults = append(results, record)\n\t\t}\n\t}\n\n\tif len(results) == 0 {\n\t\treturn nil, fmt.Errorf(\"no data found of fqdn: %s\", fqdn)\n\t}\n\n\treturn results, nil\n}\n\nfunc deleteResourceRecords(service services.Dns_Domain, records []datatypes.Dns_Domain_ResourceRecord) error {\n\tresourceRecord := services.GetDnsDomainResourceRecordService(service.Session)\n\n\t// TODO maybe a bug: only the last record will be deleted\n\tfor _, record := range records {\n\t\tresourceRecord.Options.Id = record.Id\n\t}\n\n\t_, err := resourceRecord.DeleteObject()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"no data found of fqdn: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc toString(v *string) string {\n\tif v == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *v\n}\n"
  },
  {
    "path": "providers/dns/iij/iij.go",
    "content": "// Package iij implements a DNS provider for solving the DNS-01 challenge using IIJ DNS.\npackage iij\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/iij/doapi\"\n\t\"github.com/iij/doapi/protocol\"\n\t\"github.com/miekg/dns\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"IIJ_\"\n\n\tEnvAPIAccessKey  = envNamespace + \"API_ACCESS_KEY\"\n\tEnvAPISecretKey  = envNamespace + \"API_SECRET_KEY\"\n\tEnvDoServiceCode = envNamespace + \"DO_SERVICE_CODE\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAccessKey          string\n\tSecretKey          string\n\tDoServiceCode      string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tapi    *doapi.API\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for IIJ DNS.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIAccessKey, EnvAPISecretKey, EnvDoServiceCode)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"iij: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AccessKey = values[EnvAPIAccessKey]\n\tconfig.SecretKey = values[EnvAPISecretKey]\n\tconfig.DoServiceCode = values[EnvDoServiceCode]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig takes a given config\n// and returns a custom configured DNSProvider instance.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config.SecretKey == \"\" || config.AccessKey == \"\" || config.DoServiceCode == \"\" {\n\t\treturn nil, errors.New(\"iij: credentials missing\")\n\t}\n\n\treturn &DNSProvider{\n\t\tapi:    doapi.NewAPI(config.AccessKey, config.SecretKey),\n\t\tconfig: config,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\terr := d.addTxtRecord(domain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"iij: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\terr := d.deleteTxtRecord(domain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"iij: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) addTxtRecord(domain, value string) error {\n\tzones, err := d.listZones()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\towner, zone, err := splitDomain(domain, zones)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trequest := protocol.RecordAdd{\n\t\tDoServiceCode: d.config.DoServiceCode,\n\t\tZoneName:      zone,\n\t\tOwner:         owner,\n\t\tTTL:           strconv.Itoa(d.config.TTL),\n\t\tRecordType:    \"TXT\",\n\t\tRData:         value,\n\t}\n\n\tresponse := &protocol.RecordAddResponse{}\n\n\tif err := doapi.Call(*d.api, request, response); err != nil {\n\t\treturn err\n\t}\n\n\treturn d.commit()\n}\n\nfunc (d *DNSProvider) deleteTxtRecord(domain, value string) error {\n\tzones, err := d.listZones()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\towner, zone, err := splitDomain(domain, zones)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tid, err := d.findTxtRecord(owner, zone, value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trequest := protocol.RecordDelete{\n\t\tDoServiceCode: d.config.DoServiceCode,\n\t\tZoneName:      zone,\n\t\tRecordID:      id,\n\t}\n\n\tresponse := &protocol.RecordDeleteResponse{}\n\n\tif err := doapi.Call(*d.api, request, response); err != nil {\n\t\treturn err\n\t}\n\n\treturn d.commit()\n}\n\nfunc (d *DNSProvider) commit() error {\n\trequest := protocol.Commit{\n\t\tDoServiceCode: d.config.DoServiceCode,\n\t}\n\n\tresponse := &protocol.CommitResponse{}\n\n\treturn doapi.Call(*d.api, request, response)\n}\n\nfunc (d *DNSProvider) findTxtRecord(owner, zone, value string) (string, error) {\n\trequest := protocol.RecordListGet{\n\t\tDoServiceCode: d.config.DoServiceCode,\n\t\tZoneName:      zone,\n\t}\n\n\tresponse := &protocol.RecordListGetResponse{}\n\n\tif err := doapi.Call(*d.api, request, response); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar id string\n\n\tfor _, record := range response.RecordList {\n\t\tif record.Owner == owner && record.RecordType == \"TXT\" && record.RData == \"\\\"\"+value+\"\\\"\" {\n\t\t\tid = record.Id\n\t\t}\n\t}\n\n\tif id == \"\" {\n\t\treturn \"\", fmt.Errorf(\"%s record in %s not found\", owner, zone)\n\t}\n\n\treturn id, nil\n}\n\nfunc (d *DNSProvider) listZones() ([]string, error) {\n\trequest := protocol.ZoneListGet{\n\t\tDoServiceCode: d.config.DoServiceCode,\n\t}\n\n\tresponse := &protocol.ZoneListGetResponse{}\n\n\tif err := doapi.Call(*d.api, request, response); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn response.ZoneList, nil\n}\n\nfunc splitDomain(domain string, zones []string) (string, string, error) {\n\tbase := dns01.UnFqdn(domain)\n\n\tfor _, index := range dns.Split(base) {\n\t\tzone := base[index:]\n\n\t\tif slices.Contains(zones, zone) {\n\t\t\tbaseOwner := base[:index]\n\t\t\tif baseOwner != \"\" {\n\t\t\t\tbaseOwner = \".\" + baseOwner\n\t\t\t}\n\n\t\t\treturn \"_acme-challenge\" + dns01.UnFqdn(baseOwner), zone, nil\n\t\t}\n\t}\n\n\treturn \"\", \"\", fmt.Errorf(\"%s not found\", domain)\n}\n"
  },
  {
    "path": "providers/dns/iij/iij.toml",
    "content": "Name = \"Internet Initiative Japan\"\nDescription = ''''''\nURL = \"https://www.iij.ad.jp/en/\"\nCode = \"iij\"\nSince = \"v1.1.0\"\n\nExample = '''\nIIJ_API_ACCESS_KEY=xxxxxxxx \\\nIIJ_API_SECRET_KEY=yyyyyy \\\nIIJ_DO_SERVICE_CODE=zzzzzz \\\nlego --dns iij -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    IIJ_API_ACCESS_KEY = \"API access key\"\n    IIJ_API_SECRET_KEY = \"API secret key\"\n    IIJ_DO_SERVICE_CODE = \"DO service code\"\n  [Configuration.Additional]\n    IIJ_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 4)\"\n    IIJ_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 240)\"\n    IIJ_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n\n[Links]\n  API = \"https://manual.iij.jp/p2/pubapi/\"\n  GoClient = \"https://github.com/iij/doapi\"\n"
  },
  {
    "path": "providers/dns/iij/iij_test.go",
    "content": "package iij\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"TESTDOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIAccessKey,\n\tEnvAPISecretKey,\n\tEnvDoServiceCode).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIAccessKey:  \"A\",\n\t\t\t\tEnvAPISecretKey:  \"B\",\n\t\t\t\tEnvDoServiceCode: \"C\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIAccessKey:  \"\",\n\t\t\t\tEnvAPISecretKey:  \"\",\n\t\t\t\tEnvDoServiceCode: \"\",\n\t\t\t},\n\t\t\texpected: \"iij: some credentials information are missing: IIJ_API_ACCESS_KEY,IIJ_API_SECRET_KEY,IIJ_DO_SERVICE_CODE\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api access key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIAccessKey:  \"\",\n\t\t\t\tEnvAPISecretKey:  \"B\",\n\t\t\t\tEnvDoServiceCode: \"C\",\n\t\t\t},\n\t\t\texpected: \"iij: some credentials information are missing: IIJ_API_ACCESS_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIAccessKey:  \"A\",\n\t\t\t\tEnvAPISecretKey:  \"\",\n\t\t\t\tEnvDoServiceCode: \"C\",\n\t\t\t},\n\t\t\texpected: \"iij: some credentials information are missing: IIJ_API_SECRET_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing do service code\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIAccessKey:  \"A\",\n\t\t\t\tEnvAPISecretKey:  \"B\",\n\t\t\t\tEnvDoServiceCode: \"\",\n\t\t\t},\n\t\t\texpected: \"iij: some credentials information are missing: IIJ_DO_SERVICE_CODE\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.api)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\taccessKey     string\n\t\tsecretKey     string\n\t\tdoServiceCode string\n\t\texpected      string\n\t}{\n\t\t{\n\t\t\tdesc:          \"success\",\n\t\t\taccessKey:     \"A\",\n\t\t\tsecretKey:     \"B\",\n\t\t\tdoServiceCode: \"C\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"iij: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:          \"missing access key\",\n\t\t\taccessKey:     \"\",\n\t\t\tsecretKey:     \"B\",\n\t\t\tdoServiceCode: \"C\",\n\t\t\texpected:      \"iij: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:          \"missing secret key\",\n\t\t\taccessKey:     \"A\",\n\t\t\tsecretKey:     \"\",\n\t\t\tdoServiceCode: \"C\",\n\t\t\texpected:      \"iij: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:          \"missing do service code\",\n\t\t\taccessKey:     \"A\",\n\t\t\tsecretKey:     \"B\",\n\t\t\tdoServiceCode: \"\",\n\t\t\texpected:      \"iij: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AccessKey = test.accessKey\n\t\t\tconfig.SecretKey = test.secretKey\n\t\t\tconfig.DoServiceCode = test.doServiceCode\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.api)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSplitDomain(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tdomain        string\n\t\tzones         []string\n\t\texpectedOwner string\n\t\texpectedZone  string\n\t}{\n\t\t{\n\t\t\tdesc:          \"domain equals zone\",\n\t\t\tdomain:        \"example.com\",\n\t\t\tzones:         []string{\"example.com\"},\n\t\t\texpectedOwner: \"_acme-challenge\",\n\t\t\texpectedZone:  \"example.com\",\n\t\t},\n\t\t{\n\t\t\tdesc:          \"with a subdomain\",\n\t\t\tdomain:        \"my.example.com\",\n\t\t\tzones:         []string{\"example.com\"},\n\t\t\texpectedOwner: \"_acme-challenge.my\",\n\t\t\texpectedZone:  \"example.com\",\n\t\t},\n\t\t{\n\t\t\tdesc:          \"with a subdomain in a zone\",\n\t\t\tdomain:        \"my.sub.example.com\",\n\t\t\tzones:         []string{\"sub.example.com\", \"example.com\"},\n\t\t\texpectedOwner: \"_acme-challenge.my\",\n\t\t\texpectedZone:  \"sub.example.com\",\n\t\t},\n\t\t{\n\t\t\tdesc:          \"with a sub-subdomain\",\n\t\t\tdomain:        \"my.sub.example.com\",\n\t\t\tzones:         []string{\"domain1.com\", \"example.com\"},\n\t\t\texpectedOwner: \"_acme-challenge.my.sub\",\n\t\t\texpectedZone:  \"example.com\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\towner, zone, err := splitDomain(test.domain, test.zones)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.expectedOwner, owner)\n\t\t\tassert.Equal(t, test.expectedZone, zone)\n\t\t})\n\t}\n}\n\nfunc TestSplitDomain_error(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tdomain        string\n\t\tzones         []string\n\t\texpectedOwner string\n\t\texpectedZone  string\n\t}{\n\t\t{\n\t\t\tdesc:   \"no zone\",\n\t\t\tdomain: \"example.com\",\n\t\t\tzones:  nil,\n\t\t},\n\t\t{\n\t\t\tdesc:   \"domain does not contain zone\",\n\t\t\tdomain: \"example.com\",\n\t\t\tzones:  []string{\"example.org\"},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t_, _, err := splitDomain(test.domain, test.zones)\n\t\t\trequire.Error(t, err)\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/iijdpf/iijdpf.go",
    "content": "// Package iijdpf implements a DNS provider for solving the DNS-01 challenge using IIJ DNS Platform Service.\npackage iijdpf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/miekg/dns\"\n\tdpfapi \"github.com/mimuret/golang-iij-dpf/pkg/api\"\n\tdpfapiutils \"github.com/mimuret/golang-iij-dpf/pkg/apiutils\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"IIJ_DPF_\"\n\n\tEnvAPIToken    = envNamespace + \"API_TOKEN\"\n\tEnvServiceCode = envNamespace + \"DPM_SERVICE_CODE\"\n\n\tEnvAPIEndpoint        = envNamespace + \"API_ENDPOINT\"\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken       string\n\tServiceCode string\n\n\tEndpoint           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tEndpoint:           env.GetOrDefaultString(EnvAPIEndpoint, dpfapi.DefaultEndpoint),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 660*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient dpfapi.ClientInterface\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for IIJ DNS.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIToken, EnvServiceCode)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"iijdpf: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvAPIToken]\n\tconfig.ServiceCode = values[EnvServiceCode]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig takes a given config\n// and returns a custom configured DNSProvider instance.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"iijdpf: API token missing\")\n\t}\n\n\tif config.ServiceCode == \"\" {\n\t\treturn nil, errors.New(\"iijdpf: Servicecode missing\")\n\t}\n\n\treturn &DNSProvider{\n\t\tclient: dpfapi.NewClient(config.Token, config.Endpoint, nil),\n\t\tconfig: config,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneID, err := dpfapiutils.GetZoneIdFromServiceCode(ctx, d.client, d.config.ServiceCode)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"iijdpf: failed to get zone id: %w\", err)\n\t}\n\n\terr = d.addTxtRecord(ctx, zoneID, dns.CanonicalName(info.EffectiveFQDN), `\"`+info.Value+`\"`)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"iijdpf: %w\", err)\n\t}\n\n\terr = d.commit(ctx, zoneID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"iijdpf: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneID, err := dpfapiutils.GetZoneIdFromServiceCode(ctx, d.client, d.config.ServiceCode)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"iijdpf: failed to get zone id: %w\", err)\n\t}\n\n\terr = d.deleteTxtRecord(ctx, zoneID, dns.CanonicalName(info.EffectiveFQDN), `\"`+info.Value+`\"`)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"iijdpf: %w\", err)\n\t}\n\n\terr = d.commit(ctx, zoneID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"iijdpf: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/iijdpf/iijdpf.toml",
    "content": "Name = \"IIJ DNS Platform Service\"\nDescription = ''''''\nURL = \"https://www.iij.ad.jp/en/biz/dns-pfm/\"\nCode = \"iijdpf\"\nSince = \"v4.7.0\"\n\nExample = '''\nIIJ_DPF_API_TOKEN=xxxxxxxx \\\nIIJ_DPF_DPM_SERVICE_CODE=yyyyyy \\\nlego --dns iijdpf -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    IIJ_DPF_API_TOKEN = \"API token\"\n    IIJ_DPF_DPM_SERVICE_CODE = \"IIJ Managed DNS Service's service code\"\n  [Configuration.Additional]\n    IIJ_DPF_API_ENDPOINT = \"API endpoint URL, defaults to https://api.dns-platform.jp/dpf/v1\"\n    IIJ_DPF_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 5)\"\n    IIJ_DPF_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 660)\"\n    IIJ_DPF_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n\n[Links]\n  API = \"https://manual.iij.jp/dpf/dpfapi/\"\n  GoClient = \"https://github.com/mimuret/golang-iij-dpf\"\n"
  },
  {
    "path": "providers/dns/iijdpf/iijdpf_test.go",
    "content": "package iijdpf\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"TESTDOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIToken, EnvServiceCode).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken:    \"A\",\n\t\t\t\tEnvServiceCode: \"dpmXXXXXX\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"A\",\n\t\t\t},\n\t\t\texpected: \"iijdpf: some credentials information are missing: IIJ_DPF_DPM_SERVICE_CODE\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServiceCode: \"dpmXXXXXX\",\n\t\t\t},\n\t\t\texpected: \"iijdpf: some credentials information are missing: IIJ_DPF_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc        string\n\t\ttoken       string\n\t\tservicecode string\n\t\texpected    string\n\t}{\n\t\t{\n\t\t\tdesc:        \"success\",\n\t\t\ttoken:       \"A\",\n\t\t\tservicecode: \"dpm00000\",\n\t\t},\n\t\t{\n\t\t\tdesc:        \"missing credentials\",\n\t\t\tservicecode: \"dpm00000\",\n\t\t\texpected:    \"iijdpf: API token missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\ttoken:    \"A\",\n\t\t\texpected: \"iijdpf: Servicecode missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"iijdpf: API token missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.token\n\t\t\tconfig.ServiceCode = test.servicecode\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/iijdpf/wrapper.go",
    "content": "package iijdpf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\tdpfzones \"github.com/mimuret/golang-iij-dpf/pkg/apis/dpf/v1/zones\"\n\tdpfapiutils \"github.com/mimuret/golang-iij-dpf/pkg/apiutils\"\n\tdpftypes \"github.com/mimuret/golang-iij-dpf/pkg/types\"\n)\n\nfunc (d *DNSProvider) addTxtRecord(ctx context.Context, zoneID, fqdn, rdata string) error {\n\tr, err := dpfapiutils.GetRecordFromZoneID(ctx, d.client, zoneID, fqdn, dpfzones.TypeTXT)\n\tif err != nil && !errors.Is(err, dpfapiutils.ErrRecordNotFound) {\n\t\treturn err\n\t}\n\n\tif r != nil {\n\t\tr.RData = append(r.RData, dpfzones.RecordRDATA{Value: rdata})\n\n\t\t_, _, err = dpfapiutils.SyncUpdate(ctx, d.client, r, nil)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to update record: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\trecord := &dpfzones.Record{\n\t\tAttributeMeta: dpfzones.AttributeMeta{ZoneID: zoneID},\n\t\tName:          fqdn,\n\t\tTTL:           dpftypes.NullablePositiveInt32(d.config.TTL),\n\t\tRRType:        dpfzones.TypeTXT,\n\t\tRData:         dpfzones.RecordRDATASlice{dpfzones.RecordRDATA{Value: rdata}},\n\t\tDescription:   \"ACME\",\n\t}\n\n\t_, _, err = dpfapiutils.SyncCreate(ctx, d.client, record, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) deleteTxtRecord(ctx context.Context, zoneID, fqdn, rdata string) error {\n\tr, err := dpfapiutils.GetRecordFromZoneID(ctx, d.client, zoneID, fqdn, dpfzones.TypeTXT)\n\tif err != nil {\n\t\tif errors.Is(err, dpfapiutils.ErrRecordNotFound) {\n\t\t\t// empty target rrset\n\t\t\treturn nil\n\t\t}\n\n\t\treturn err\n\t}\n\n\tif len(r.RData) == 1 {\n\t\t// delete rrset\n\t\t_, _, err = dpfapiutils.SyncDelete(ctx, d.client, r)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to delete record: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// delete rdata\n\trdataSlice := dpfzones.RecordRDATASlice{}\n\n\tfor _, v := range r.RData {\n\t\tif v.Value != rdata {\n\t\t\trdataSlice = append(rdataSlice, v)\n\t\t}\n\t}\n\n\tr.RData = rdataSlice\n\n\t_, _, err = dpfapiutils.SyncUpdate(ctx, d.client, r, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to update record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) commit(ctx context.Context, zoneID string) error {\n\tapply := &dpfzones.ZoneApply{\n\t\tAttributeMeta: dpfzones.AttributeMeta{ZoneID: zoneID},\n\t\tDescription:   \"ACME Processing\",\n\t}\n\n\t_, _, err := dpfapiutils.SyncApply(ctx, d.client, apply, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to apply zone: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/infoblox/infoblox.go",
    "content": "// Package infoblox implements a DNS provider for solving the DNS-01 challenge using on prem infoblox DNS.\npackage infoblox\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\tinfoblox \"github.com/infobloxopen/infoblox-go-client/v2\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"INFOBLOX_\"\n\n\tEnvHost          = envNamespace + \"HOST\"\n\tEnvPort          = envNamespace + \"PORT\"\n\tEnvUsername      = envNamespace + \"USERNAME\"\n\tEnvPassword      = envNamespace + \"PASSWORD\"\n\tEnvDNSView       = envNamespace + \"DNS_VIEW\"\n\tEnvWApiVersion   = envNamespace + \"WAPI_VERSION\"\n\tEnvSSLVerify     = envNamespace + \"SSL_VERIFY\"\n\tEnvCACertificate = envNamespace + \"CA_CERTIFICATE\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultPoolConnections = 10\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\t// Host is the URL of the grid manager.\n\tHost string\n\t// Port is the Port for the grid manager.\n\tPort string\n\n\t// Username the user for accessing API.\n\tUsername string\n\t// Password the password for accessing API.\n\tPassword string\n\n\t// DNSView is the dns view to put new records and search from.\n\tDNSView string\n\t// WapiVersion is the version of web api used.\n\tWapiVersion string\n\n\t// SSLVerify is whether or not to verify the ssl of the server being hit.\n\tSSLVerify bool\n\n\t// CACertificate is the path to the CA certificate (PEM encoded).\n\tCACertificate string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tDNSView:       env.GetOrDefaultString(EnvDNSView, \"External\"),\n\t\tWapiVersion:   env.GetOrDefaultString(EnvWApiVersion, \"2.11\"),\n\t\tPort:          env.GetOrDefaultString(EnvPort, \"443\"),\n\t\tSSLVerify:     env.GetOrDefaultBool(EnvSSLVerify, true),\n\t\tCACertificate: env.GetOrDefaultString(EnvCACertificate, \"\"),\n\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultInt(EnvHTTPTimeout, 30),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig          *Config\n\ttransportConfig infoblox.TransportConfig\n\tibConfig        infoblox.HostConfig\n\tibAuth          infoblox.AuthConfig\n\n\trecordRefs   map[string]string\n\trecordRefsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Infoblox.\n// Credentials must be passed in the environment variables:\n// INFOBLOX_USERNAME, INFOBLOX_PASSWORD\n// INFOBLOX_HOST, INFOBLOX_PORT\n// INFOBLOX_DNS_VIEW, INFOBLOX_WAPI_VERSION\n// INFOBLOX_SSL_VERIFY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvHost, EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"infoblox: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Host = values[EnvHost]\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for HyperOne.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"infoblox: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Host == \"\" {\n\t\treturn nil, errors.New(\"infoblox: missing host\")\n\t}\n\n\tif config.Username == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"infoblox: missing credentials\")\n\t}\n\n\tvar sslVerify string\n\tif config.CACertificate != \"\" {\n\t\tsslVerify = config.CACertificate\n\t} else {\n\t\tsslVerify = strconv.FormatBool(config.SSLVerify)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:          config,\n\t\ttransportConfig: infoblox.NewTransportConfig(sslVerify, config.HTTPTimeout, defaultPoolConnections),\n\t\tibConfig: infoblox.HostConfig{\n\t\t\tHost:    config.Host,\n\t\t\tVersion: config.WapiVersion,\n\t\t\tPort:    config.Port,\n\t\t},\n\t\tibAuth: infoblox.AuthConfig{\n\t\t\tUsername: config.Username,\n\t\t\tPassword: config.Password,\n\t\t},\n\t\trecordRefs: make(map[string]string),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tconnector, err := infoblox.NewConnector(d.ibConfig, d.ibAuth, d.transportConfig, &infoblox.WapiRequestBuilder{}, &infoblox.WapiHttpRequestor{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"infoblox: %w\", err)\n\t}\n\n\tdefer func() { _ = connector.Logout() }()\n\n\tobjectManager := infoblox.NewObjectManager(connector, useragent.Get(), \"\")\n\n\trecord, err := objectManager.CreateTXTRecord(d.config.DNSView, dns01.UnFqdn(info.EffectiveFQDN), info.Value, uint32(d.config.TTL), true, \"lego\", nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"infoblox: could not create TXT record for %s: %w\", domain, err)\n\t}\n\n\td.recordRefsMu.Lock()\n\td.recordRefs[token] = record.Ref\n\td.recordRefsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tconnector, err := infoblox.NewConnector(d.ibConfig, d.ibAuth, d.transportConfig, &infoblox.WapiRequestBuilder{}, &infoblox.WapiHttpRequestor{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"infoblox: %w\", err)\n\t}\n\n\tdefer func() { _ = connector.Logout() }()\n\n\tobjectManager := infoblox.NewObjectManager(connector, useragent.Get(), \"\")\n\n\t// gets the record's unique ref from when we created it\n\td.recordRefsMu.Lock()\n\trecordRef, ok := d.recordRefs[token]\n\td.recordRefsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"infoblox: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\t_, err = objectManager.DeleteTXTRecord(recordRef)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"infoblox: could not delete TXT record for %s: %w\", domain, err)\n\t}\n\n\t// Delete record ref from map\n\td.recordRefsMu.Lock()\n\tdelete(d.recordRefs, token)\n\td.recordRefsMu.Unlock()\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/infoblox/infoblox.toml",
    "content": "Name = \"Infoblox\"\nDescription = ''''''\nURL = \"https://www.infoblox.com/\"\nCode = \"infoblox\"\nSince = \"v4.4.0\"\n\nExample = '''\nINFOBLOX_USERNAME=api-user-529 \\\nINFOBLOX_PASSWORD=b9841238feb177a84330febba8a83208921177bffe733 \\\nINFOBLOX_HOST=infoblox.example.org\nlego --dns infoblox -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\nWhen creating an API's user ensure it has the proper permissions for the view you are working with.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    INFOBLOX_USERNAME = \"Account Username\"\n    INFOBLOX_PASSWORD = \"Account Password\"\n    INFOBLOX_HOST = \"Host URI\"\n  [Configuration.Additional]\n    INFOBLOX_DNS_VIEW = \"The view for the TXT records (Default: External)\"\n    INFOBLOX_WAPI_VERSION = \"The version of WAPI being used  (Default: 2.11)\"\n    INFOBLOX_PORT = \"The port for the infoblox grid manager  (Default: 443)\"\n    INFOBLOX_SSL_VERIFY = \"Whether or not to verify the TLS certificate  (Default: true)\"\n    INFOBLOX_CA_CERTIFICATE = \"The path to the CA certificate (PEM encoded)\"\n    INFOBLOX_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    INFOBLOX_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    INFOBLOX_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    INFOBLOX_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n\n[Links]\n  API = \"https://your.infoblox.server/wapidoc/\"\n  GoClient = \"https://github.com/infobloxopen/infoblox-go-client\"\n"
  },
  {
    "path": "providers/dns/infoblox/infoblox_test.go",
    "content": "package infoblox\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvHost,\n\tEnvPort,\n\tEnvUsername,\n\tEnvPassword,\n\tEnvSSLVerify,\n).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvHost:      \"example.com\",\n\t\t\t\tEnvUsername:  \"user\",\n\t\t\t\tEnvPassword:  \"secret\",\n\t\t\t\tEnvSSLVerify: \"false\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing host\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvHost:      \"\",\n\t\t\t\tEnvUsername:  \"user\",\n\t\t\t\tEnvPassword:  \"secret\",\n\t\t\t\tEnvSSLVerify: \"false\",\n\t\t\t},\n\t\t\texpected: \"infoblox: some credentials information are missing: INFOBLOX_HOST\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvHost:      \"example.com\",\n\t\t\t\tEnvUsername:  \"\",\n\t\t\t\tEnvPassword:  \"secret\",\n\t\t\t\tEnvSSLVerify: \"false\",\n\t\t\t},\n\t\t\texpected: \"infoblox: some credentials information are missing: INFOBLOX_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvHost:      \"example.com\",\n\t\t\t\tEnvUsername:  \"user\",\n\t\t\t\tEnvPassword:  \"\",\n\t\t\t\tEnvSSLVerify: \"false\",\n\t\t\t},\n\t\t\texpected: \"infoblox: some credentials information are missing: INFOBLOX_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\thost     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\thost:     \"example.com\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing host\",\n\t\t\thost:     \"\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"infoblox: missing host\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\thost:     \"example.com\",\n\t\t\tusername: \"\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"infoblox: missing credentials\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\thost:     \"example.com\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"\",\n\t\t\texpected: \"infoblox: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Host = test.host\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\t\t\tconfig.SSLVerify = false\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/infomaniak/infomaniak.go",
    "content": "// Package infomaniak implements a DNS provider for solving the DNS-01 challenge using Infomaniak DNS.\npackage infomaniak\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/infomaniak/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Infomaniak API reference: https://api.infomaniak.com/doc\n// Create a Token: https://manager.infomaniak.com/v3/infomaniak-api\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"INFOMANIAK_\"\n\n\tEnvEndpoint    = envNamespace + \"ENDPOINT\"\n\tEnvAccessToken = envNamespace + \"ACCESS_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIEndpoint        string\n\tAccessToken        string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tAPIEndpoint:        env.GetOrDefaultString(EnvEndpoint, internal.DefaultBaseURL),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n\n\tdomainIDs   map[string]uint64\n\tdomainIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Infomaniak.\n// Credentials must be passed in the environment variables: INFOMANIAK_ACCESS_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAccessToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"infomaniak: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AccessToken = values[EnvAccessToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Infomaniak.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"infomaniak: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIEndpoint == \"\" {\n\t\treturn nil, errors.New(\"infomaniak: missing API endpoint\")\n\t}\n\n\tif config.AccessToken == \"\" {\n\t\treturn nil, errors.New(\"infomaniak: missing access token\")\n\t}\n\n\tclient, err := internal.New(\n\t\tclientdebug.Wrap(\n\t\t\tinternal.OAuthStaticAccessToken(config.HTTPClient, config.AccessToken),\n\t\t),\n\t\tconfig.APIEndpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"infomaniak: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]string),\n\t\tdomainIDs: make(map[string]uint64),\n\t}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tikDomain, err := d.client.GetDomainByName(ctx, dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"infomaniak: could not get domain %q: %w\", info.EffectiveFQDN, err)\n\t}\n\n\td.domainIDsMu.Lock()\n\td.domainIDs[token] = ikDomain.ID\n\td.domainIDsMu.Unlock()\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, ikDomain.CustomerName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"infomaniak: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tSource: subDomain,\n\t\tTarget: info.Value,\n\t\tType:   \"TXT\",\n\t\tTTL:    d.config.TTL,\n\t}\n\n\trecordID, err := d.client.CreateDNSRecord(ctx, ikDomain, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"infomaniak: error when calling api to create DNS record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = recordID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"infomaniak: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\td.domainIDsMu.Lock()\n\tdomainID, ok := d.domainIDs[token]\n\td.domainIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"infomaniak: unknown domain ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\terr := d.client.DeleteDNSRecord(context.Background(), domainID, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"infomaniak: could not delete record %q: %w\", dns01.UnFqdn(info.EffectiveFQDN), err)\n\t}\n\n\t// Delete record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\t// Delete domain ID from map\n\td.domainIDsMu.Lock()\n\tdelete(d.domainIDs, token)\n\td.domainIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/infomaniak/infomaniak.toml",
    "content": "Name = \"Infomaniak\"\nDescription = ''''''\nURL = \"https://www.infomaniak.com/\"\nCode = \"infomaniak\"\nSince = \"v4.1.0\"\n\nExample = '''\nINFOMANIAK_ACCESS_TOKEN=1234567898765432 \\\nlego --dns infomaniak -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Access token\n\nAccess token can be created at the url https://manager.infomaniak.com/v3/infomaniak-api.\nYou will need domain scope.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    INFOMANIAK_ACCESS_TOKEN = \"Access token\"\n  [Configuration.Additional]\n    INFOMANIAK_ENDPOINT = \"https://api.infomaniak.com\"\n    INFOMANIAK_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    INFOMANIAK_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    INFOMANIAK_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    INFOMANIAK_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api.infomaniak.com/doc\"\n"
  },
  {
    "path": "providers/dns/infomaniak/infomaniak_test.go",
    "content": "package infomaniak\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvEndpoint,\n\tEnvAccessToken).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessToken: \"\",\n\t\t\t},\n\t\t\texpected: \"infomaniak: some credentials information are missing: INFOMANIAK_ACCESS_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc        string\n\t\taccessToken string\n\t\texpected    string\n\t}{\n\t\t{\n\t\t\tdesc:        \"success\",\n\t\t\taccessToken: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:        \"missing access token\",\n\t\t\taccessToken: \"\",\n\t\t\texpected:    \"infomaniak: missing access token\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AccessToken = test.accessToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/infomaniak/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"golang.org/x/oauth2\"\n)\n\n// DefaultBaseURL Default API endpoint.\nconst DefaultBaseURL = \"https://api.infomaniak.com\"\n\n// Client the Infomaniak client.\ntype Client struct {\n\tbaseURL    *url.URL\n\thttpClient *http.Client\n}\n\n// New Creates a new Infomaniak client.\nfunc New(hc *http.Client, apiEndpoint string) (*Client, error) {\n\tbaseURL, err := url.Parse(apiEndpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif hc == nil {\n\t\thc = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\treturn &Client{baseURL: baseURL, httpClient: hc}, nil\n}\n\nfunc (c *Client) CreateDNSRecord(ctx context.Context, domain *DNSDomain, record Record) (string, error) {\n\tendpoint := c.baseURL.JoinPath(\"1\", \"domain\", strconv.FormatUint(domain.ID, 10), \"dns\", \"record\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\tresult := APIResponse[string]{}\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn result.Data, err\n}\n\nfunc (c *Client) DeleteDNSRecord(ctx context.Context, domainID uint64, recordID string) error {\n\tendpoint := c.baseURL.JoinPath(\"1\", \"domain\", strconv.FormatUint(domainID, 10), \"dns\", \"record\", recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treturn c.do(req, &APIResponse[json.RawMessage]{})\n}\n\n// GetDomainByName gets a Domain object from its name.\nfunc (c *Client) GetDomainByName(ctx context.Context, name string) (*DNSDomain, error) {\n\tname = dns01.UnFqdn(name)\n\n\t// Try to find the most specific domain\n\t// starts with the FQDN, then remove each left label until we have a match\n\tfor {\n\t\ti := strings.Index(name, \".\")\n\t\tif i == -1 {\n\t\t\tbreak\n\t\t}\n\n\t\tdomain, err := c.getDomainByName(ctx, name)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif domain != nil {\n\t\t\treturn domain, nil\n\t\t}\n\n\t\tlog.Infof(\"domain %q not found, trying with %q\", name, name[i+1:])\n\n\t\tname = name[i+1:]\n\t}\n\n\treturn nil, fmt.Errorf(\"domain not found %s\", name)\n}\n\nfunc (c *Client) getDomainByName(ctx context.Context, name string) (*DNSDomain, error) {\n\tendpoint := c.baseURL.JoinPath(\"1\", \"product\")\n\n\tquery := endpoint.Query()\n\tquery.Add(\"service_name\", \"domain\")\n\tquery.Add(\"customer_name\", name)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := APIResponse[[]DNSDomain]{}\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, domain := range result.Data {\n\t\tif domain.CustomerName == name {\n\t\t\treturn &domain, nil\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n\nfunc (c *Client) do(req *http.Request, result Response) error {\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif err := json.Unmarshal(raw, result); err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tif result.GetResult() != \"success\" {\n\t\treturn fmt.Errorf(\"%d: unexpected API result (%s): %w\", resp.StatusCode, result.GetResult(), result.GetError())\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {\n\tif client == nil {\n\t\tclient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tclient.Transport = &oauth2.Transport{\n\t\tSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),\n\t\tBase:   client.Transport,\n\t}\n\n\treturn client\n}\n"
  },
  {
    "path": "providers/dns/infomaniak/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := New(OAuthStaticAccessToken(server.Client(), \"token\"), server.URL)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer token\"))\n}\n\nfunc TestClient_CreateDNSRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /1/domain/666/dns/record\",\n\t\t\tservermock.RawStringResponse(`{\"result\":\"success\",\"data\": \"123\"}`),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_dns_record-request.json\")).\n\t\tBuild(t)\n\n\tdomain := &DNSDomain{\n\t\tID:           666,\n\t\tCustomerName: \"test\",\n\t}\n\n\trecord := Record{\n\t\tSource: \"foo\",\n\t\tTarget: \"txtxtxttxt\",\n\t\tType:   \"TXT\",\n\t\tTTL:    60,\n\t}\n\n\trecordID, err := client.CreateDNSRecord(t.Context(), domain, record)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"123\", recordID)\n}\n\nfunc TestClient_GetDomainByName(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /1/product\",\n\t\t\tservermock.ResponseFromFixture(\"get_domain_name.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWithRegexp(\"customer_name\", `.+\\.example\\.com`).\n\t\t\t\tWith(\"service_name\", \"domain\")).\n\t\tBuild(t)\n\n\tdomain, err := client.GetDomainByName(t.Context(), \"one.two.three.example.com.\")\n\trequire.NoError(t, err)\n\n\texpected := &DNSDomain{ID: 123, CustomerName: \"two.three.example.com\"}\n\tassert.Equal(t, expected, domain)\n}\n\nfunc TestClient_DeleteDNSRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /1/domain/123/dns/record/456\",\n\t\t\tservermock.RawStringResponse(`{\"result\":\"success\"}`)).\n\t\tBuild(t)\n\n\terr := client.DeleteDNSRecord(t.Context(), 123, \"456\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/infomaniak/internal/fixtures/create_dns_record-request.json",
    "content": "{\n  \"source\": \"foo\",\n  \"type\": \"TXT\",\n  \"ttl\": 60,\n  \"target\": \"txtxtxttxt\"\n}\n"
  },
  {
    "path": "providers/dns/infomaniak/internal/fixtures/get_domain_name.json",
    "content": "{\n  \"result\": \"success\",\n  \"data\": [\n    {\n      \"id\": 123,\n      \"customer_name\": \"two.three.example.com\"\n    },\n    {\n      \"id\": 456,\n      \"customer_name\": \"three.example.com\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/infomaniak/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n)\n\n// Record a DNS record.\ntype Record struct {\n\tID     string `json:\"id,omitempty\"`\n\tSource string `json:\"source,omitempty\"`\n\tType   string `json:\"type,omitempty\"`\n\tTTL    int    `json:\"ttl,omitempty\"`\n\tTarget string `json:\"target,omitempty\"`\n}\n\ntype DNSDomain struct {\n\tID           uint64 `json:\"id,omitempty\"`\n\tCustomerName string `json:\"customer_name,omitempty\"`\n}\n\ntype Response interface {\n\tGetResult() string\n\tGetError() *APIErrorResponse\n}\n\ntype APIResponse[T any] struct {\n\tResult      string            `json:\"result\"`\n\tData        T                 `json:\"data,omitempty\"`\n\tErrResponse *APIErrorResponse `json:\"error,omitempty\"`\n}\n\nfunc (a APIResponse[T]) GetResult() string {\n\treturn a.Result\n}\n\nfunc (a APIResponse[T]) GetError() *APIErrorResponse {\n\treturn a.ErrResponse\n}\n\ntype APIErrorResponse struct {\n\tCode        string             `json:\"code\"`\n\tDescription string             `json:\"description,omitempty\"`\n\tContext     map[string]string  `json:\"context,omitempty\"`\n\tErrors      []APIErrorResponse `json:\"errors,omitempty\"`\n}\n\nfunc (a APIErrorResponse) Error() string {\n\treturn fmt.Sprintf(\"code: %s, description: %s\", a.Code, a.Description)\n}\n"
  },
  {
    "path": "providers/dns/internal/active24/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://rest.%s\"\n\n// Client the Active24 API client.\ntype Client struct {\n\tapiKey string\n\tsecret string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(baseAPIDomain, apiKey, secret string) (*Client, error) {\n\tif apiKey == \"\" || secret == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(fmt.Sprintf(defaultBaseURL, baseAPIDomain))\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tsecret:     secret,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// GetServices lists of all services.\n// https://rest.active24.cz/docs/v1.service#services\nfunc (c *Client) GetServices(ctx context.Context) ([]Service, error) {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"user\", \"self\", \"service\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result OldAPIResponse\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Items, err\n}\n\n// GetRecords lists of DNS records.\n// https://rest.active24.cz/v2/docs#/DNS/rest.v2.dns.record_f94908d4e0e48489468498fce87cb90b\nfunc (c *Client) GetRecords(ctx context.Context, service string, filter RecordFilter) ([]Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"v2\", \"service\", service, \"dns\", \"record\")\n\n\tencodedFilter, err := json.Marshal(filter)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"marshal records filter: %w\", err)\n\t}\n\n\tquery := endpoint.Query()\n\tquery.Add(\"filters\", string(encodedFilter))\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result APIResponse\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Data, err\n}\n\n// CreateRecord creates a new DNS record.\n// https://rest.active24.cz/v2/docs#/DNS/rest.v2.dns.create-record_6773d572235be9a72646bf6c54863573\nfunc (c *Client) CreateRecord(ctx context.Context, service string, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"v2\", \"service\", service, \"dns\", \"record\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\n// DeleteRecord deletes a DNS record.\n// https://rest.active24.cz/v2/docs#/DNS/rest.v2.dns.delete-record_fc6603c14848e547f8d0b967842f0a2c\nfunc (c *Client) DeleteRecord(ctx context.Context, service, recordID string) error {\n\tendpoint := c.baseURL.JoinPath(\"v2\", \"service\", service, \"dns\", \"record\", recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(\"Accept-Language\", \"en_us\")\n\n\terr := c.sign(req, time.Now())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"sign request: %w\", err)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n\n// sign creates and sets request signature and date.\n// https://rest.active24.cz/v2/docs/intro\nfunc (c *Client) sign(req *http.Request, now time.Time) error {\n\tif req.URL.Path == \"\" {\n\t\treq.URL.Path += \"/\"\n\t}\n\n\tcanonicalRequest := fmt.Sprintf(\"%s %s %d\", req.Method, req.URL.Path, now.Unix())\n\n\tmac := hmac.New(sha1.New, []byte(c.secret))\n\n\t_, err := mac.Write([]byte(canonicalRequest))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thashed := mac.Sum(nil)\n\tsignature := hex.EncodeToString(hashed)\n\n\treq.SetBasicAuth(c.apiKey, signature)\n\n\treq.Header.Set(\"Date\", now.Format(time.RFC3339))\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/internal/active24/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"example.com\", \"user\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithRegexp(\"Authorization\", `Basic .+`).\n\t\t\tWithRegexp(\"Date\", `\\d+-\\d+-\\d+T\\d{2}:\\d{2}:\\d{2}.*`).\n\t\t\tWith(\"Accept-Language\", \"en_us\"))\n}\n\nfunc TestClient_GetServices(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/user/self/service\",\n\t\t\tservermock.ResponseFromFixture(\"services.json\")).\n\t\tBuild(t)\n\n\tservices, err := client.GetServices(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []Service{\n\t\t{\n\t\t\tID:          1111,\n\t\t\tServiceName: \".sk doména\",\n\t\t\tStatus:      \"active\",\n\t\t\tName:        \"mydomain.sk\",\n\t\t\tCreateTime:  1374357600,\n\t\t\tExpireTime:  1405914526,\n\t\t\tPrice:       12.3,\n\t\t},\n\t\t{\n\t\t\tID:          2222,\n\t\t\tServiceName: \"The Hosting\",\n\t\t\tStatus:      \"active\",\n\t\t\tName:        \"myname_1\",\n\t\t\tCreateTime:  1400145443,\n\t\t\tExpireTime:  1431702371,\n\t\t\tPrice:       55.2,\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, services)\n}\n\nfunc TestClient_GetServices_errors(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/user/self/service\",\n\t\t\tservermock.ResponseFromFixture(\"error_v1.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetServices(t.Context())\n\trequire.EqualError(t, err, \"401: No username or password.\")\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v2/service/aaa/dns/record\",\n\t\t\tservermock.ResponseFromFixture(\"records.json\")).\n\t\tBuild(t)\n\n\tfilter := RecordFilter{\n\t\tName:    \"example.com\",\n\t\tType:    []string{\"TXT\"},\n\t\tContent: \"txt\",\n\t}\n\n\trecords, err := client.GetRecords(t.Context(), \"aaa\", filter)\n\trequire.NoError(t, err)\n\n\texpected := []Record{{\n\t\tID:       13,\n\t\tName:     \"string\",\n\t\tContent:  \"string\",\n\t\tTTL:      120,\n\t\tPriority: 1,\n\t\tPort:     443,\n\t\tWeight:   50,\n\t}}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetRecords_errors(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v2/service/aaa/dns/record\",\n\t\t\tservermock.ResponseFromFixture(\"error_403.json\").\n\t\t\t\tWithStatusCode(http.StatusForbidden)).\n\t\tBuild(t)\n\n\tfilter := RecordFilter{\n\t\tName:    \"example.com\",\n\t\tType:    []string{\"TXT\"},\n\t\tContent: \"txt\",\n\t}\n\n\t_, err := client.GetRecords(t.Context(), \"aaa\", filter)\n\trequire.EqualError(t, err, \"403: /errors/httpException: This action is unauthorized.\")\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /v2/service/aaa/dns/record\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\terr := client.CreateRecord(t.Context(), \"aaa\", Record{})\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_CreateRecord_errors(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /v2/service/aaa/dns/record\",\n\t\t\tservermock.ResponseFromFixture(\"error_403.json\").\n\t\t\t\tWithStatusCode(http.StatusForbidden)).\n\t\tBuild(t)\n\n\terr := client.CreateRecord(t.Context(), \"aaa\", Record{})\n\trequire.EqualError(t, err, \"403: /errors/httpException: This action is unauthorized.\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v2/service/aaa/dns/record/123\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"aaa\", \"123\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v2/service/aaa/dns/record/123\",\n\t\t\tservermock.ResponseFromFixture(\"error_403.json\").\n\t\t\t\tWithStatusCode(http.StatusForbidden)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"aaa\", \"123\")\n\trequire.EqualError(t, err, \"403: /errors/httpException: This action is unauthorized.\")\n}\n\nfunc TestClient_sign(t *testing.T) {\n\tclient, err := NewClient(\"example.com\", \"user\", \"secret\")\n\trequire.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodGet, \"/v1/user/self/service\", nil)\n\trequire.NoError(t, err)\n\n\terr = client.sign(req, time.Date(2025, 6, 28, 1, 2, 3, 4, time.UTC))\n\trequire.NoError(t, err)\n\n\tusername, password, ok := req.BasicAuth()\n\trequire.True(t, ok)\n\n\tassert.Equal(t, \"user\", username)\n\tassert.Equal(t, \"743e2257421b260ed561f3e7af4b035414636393\", password)\n}\n"
  },
  {
    "path": "providers/dns/internal/active24/internal/fixtures/error_403.json",
    "content": "{\n  \"type\": \"/errors/httpException\",\n  \"status\": 403,\n  \"title\": \"This action is unauthorized.\"\n}\n"
  },
  {
    "path": "providers/dns/internal/active24/internal/fixtures/error_422.json",
    "content": "{\n  \"type\": \"/errors/validation\",\n  \"status\": 422,\n  \"title\": \"The given data was invalid.\",\n  \"violations\": [\n    {\n      \"propertyPath\": \"string\",\n      \"errors\": [\n        {}\n      ]\n    }\n  ],\n  \"data\": {\n    \"name\": \"Merlin\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/internal/active24/internal/fixtures/error_v1.json",
    "content": "{\n  \"message\": \"No username or password.\",\n  \"code\": 401\n}\n"
  },
  {
    "path": "providers/dns/internal/active24/internal/fixtures/records.json",
    "content": "{\n  \"currentPage\": 0,\n  \"rowsPerPage\": 0,\n  \"totalPages\": 0,\n  \"totalRecords\": 0,\n  \"actions\": {\n    \"additionalProp1\": {\n      \"additionalProp1\": {}\n    },\n    \"additionalProp2\": {\n      \"additionalProp1\": {}\n    },\n    \"additionalProp3\": {\n      \"additionalProp1\": {}\n    }\n  },\n  \"data\": [\n    {\n      \"id\": 13,\n      \"name\": \"string\",\n      \"content\": \"string\",\n      \"ttl\": 120,\n      \"priority\": 1,\n      \"port\": 443,\n      \"weight\": 50\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/internal/active24/internal/fixtures/services.json",
    "content": "{\n  \"items\":\n  [\n    {\n      \"id\": 1111,\n      \"serviceName\": \".sk doména\",\n      \"status\": \"active\",\n      \"name\": \"mydomain.sk\",\n      \"createTime\": 1374357600,\n      \"expireTime\": 1405914526,\n      \"price\": 12.3,\n      \"autoExtend\": false\n    },\n    {\n      \"id\": 2222,\n      \"serviceName\": \"The Hosting\",\n      \"status\": \"active\",\n      \"name\": \"myname_1\",\n      \"createTime\": 1400145443,\n      \"expireTime\": 1431702371,\n      \"price\": 55.2,\n      \"autoExtend\": false\n    }\n  ],\n  \"pager\":\n  {\n    \"page\": 1,\n    \"pagesize\": null,\n    \"items\": 2\n  }\n}\n"
  },
  {
    "path": "providers/dns/internal/active24/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype APIError struct {\n\t// v2 error\n\tType   string `json:\"type,omitempty\"`\n\tStatus int    `json:\"status,omitempty\"`\n\tTitle  string `json:\"title,omitempty\"`\n\n\t// v1 error\n\tMessage string `json:\"message,omitempty\"`\n\tCode    int    `json:\"code,omitempty\"`\n}\n\nfunc (a *APIError) Error() string {\n\tif a.Message != \"\" {\n\t\treturn fmt.Sprintf(\"%d: %s\", a.Code, a.Message)\n\t}\n\n\treturn fmt.Sprintf(\"%d: %s: %s\", a.Status, a.Type, a.Title)\n}\n\ntype APIResponse struct {\n\tData []Record `json:\"data\"`\n}\n\ntype Record struct {\n\tID       int    `json:\"id,omitempty\"`\n\tType     string `json:\"type,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tContent  string `json:\"content,omitempty\"`\n\tTTL      int    `json:\"ttl,omitempty\"`\n\tPriority int    `json:\"priority,omitempty\"`\n\tPort     int    `json:\"port,omitempty\"`\n\tWeight   int    `json:\"weight,omitempty\"`\n}\n\ntype OldAPIResponse struct {\n\tItems []Service `json:\"items\"`\n}\n\ntype Service struct {\n\tID          int     `json:\"id,omitempty\"`\n\tServiceName string  `json:\"serviceName,omitempty\"`\n\tStatus      string  `json:\"status,omitempty\"`\n\tName        string  `json:\"name,omitempty\"`\n\tCreateTime  int     `json:\"createTime,omitempty\"`\n\tExpireTime  int     `json:\"expireTime,omitempty\"`\n\tPrice       float64 `json:\"price,omitempty\"`\n\tAutoExtend  bool    `json:\"autoExtend,omitempty\"`\n}\n\ntype RecordFilter struct {\n\tName     string   `json:\"name,omitempty\"`\n\tType     []string `json:\"type,omitempty\"`\n\tContent  string   `json:\"content,omitempty\"`\n\tTTL      int      `json:\"ttl,omitempty\"`\n\tNote     string   `json:\"note,omitempty\"`\n\tPriority int      `json:\"priority,omitempty\"`\n\tPort     int      `json:\"port,omitempty\"`\n\tWeight   int      `json:\"weight,omitempty\"`\n\tFlags    int      `json:\"flags,omitempty\"`\n\tTag      []string `json:\"tag,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/internal/active24/provider.go",
    "content": "// Package active24 implements a DNS provider for solving the DNS-01 challenge using Active24.\npackage active24\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/active24/internal\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey string\n\tSecret string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Active24.\nfunc NewDNSProviderConfig(config *Config, baseAPIDomain string) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(baseAPIDomain, config.APIKey, config.Secret)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tserviceID, err := d.findServiceID(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"find service ID: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tType:    \"TXT\",\n\t\tName:    subDomain,\n\t\tContent: info.Value,\n\t\tTTL:     d.config.TTL,\n\t}\n\n\terr = d.client.CreateRecord(ctx, strconv.Itoa(serviceID), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tserviceID, err := d.findServiceID(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"find service ID: %w\", err)\n\t}\n\n\trecordID, err := d.findRecordID(ctx, strconv.Itoa(serviceID), info)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"find record ID: %w\", err)\n\t}\n\n\terr = d.client.DeleteRecord(ctx, strconv.Itoa(serviceID), strconv.Itoa(recordID))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"delete record %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) findServiceID(ctx context.Context, domain string) (int, error) {\n\tservices, err := d.client.GetServices(ctx)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"get services: %w\", err)\n\t}\n\n\tfor _, service := range services {\n\t\tif service.ServiceName != \"domain\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif service.Name != domain {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn service.ID, nil\n\t}\n\n\treturn 0, fmt.Errorf(\"service not found for domain: %s\", domain)\n}\n\nfunc (d *DNSProvider) findRecordID(ctx context.Context, serviceID string, info dns01.ChallengeInfo) (int, error) {\n\t// NOTE(ldez): Despite the API documentation, the filter doesn't seem to work.\n\tfilter := internal.RecordFilter{\n\t\tName:    dns01.UnFqdn(info.EffectiveFQDN),\n\t\tType:    []string{\"TXT\"},\n\t\tContent: info.Value,\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, serviceID, filter)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"get records: %w\", err)\n\t}\n\n\tfor _, record := range records {\n\t\tif record.Type != \"TXT\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif record.Name != dns01.UnFqdn(info.EffectiveFQDN) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif record.Content != info.Value {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn record.ID, nil\n\t}\n\n\treturn 0, errors.New(\"no record found\")\n}\n"
  },
  {
    "path": "providers/dns/internal/active24/provider_test.go",
    "content": "package active24\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tsecret   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"user\",\n\t\t\tsecret: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\tapiKey:   \"\",\n\t\t\tsecret:   \"secret\",\n\t\t\texpected: \"credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret\",\n\t\t\tapiKey:   \"user\",\n\t\t\tsecret:   \"\",\n\t\t\texpected: \"credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := &Config{}\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.Secret = test.secret\n\n\t\t\tp, err := NewDNSProviderConfig(config, \"example.com\")\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/internal/clientdebug/.gitattributes",
    "content": "/testdata/** text eol=lf\n"
  },
  {
    "path": "providers/dns/internal/clientdebug/client.go",
    "content": "package clientdebug\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n)\n\nconst replacement = \"***\"\n\ntype Option func(*DumpTransport)\n\nfunc WithEnvKeys(keys ...string) Option {\n\treturn func(d *DumpTransport) {\n\t\tfor _, key := range keys {\n\t\t\tv := strings.TrimSpace(env.GetOrFile(key))\n\t\t\tif v == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\td.replacements = append(d.replacements, v, replacement)\n\t\t}\n\t}\n}\n\nfunc WithValues(values ...string) Option {\n\treturn func(d *DumpTransport) {\n\t\tfor _, value := range values {\n\t\t\td.replacements = append(d.replacements, value, replacement)\n\t\t}\n\t}\n}\n\nfunc WithHeaders(keys ...string) Option {\n\treturn func(d *DumpTransport) {\n\t\td.regexps = append(d.regexps,\n\t\t\tregexp.MustCompile(fmt.Sprintf(`(?im)^(%s):.+$`, strings.Join(keys, \"|\"))))\n\t}\n}\n\ntype DumpTransport struct {\n\trt http.RoundTripper\n\n\treplacements []string\n\treplacer     *strings.Replacer\n\n\tregexps []*regexp.Regexp\n\n\twriter io.Writer\n}\n\nfunc NewDumpTransport(rt http.RoundTripper, opts ...Option) *DumpTransport {\n\tif rt == nil {\n\t\trt = http.DefaultTransport\n\t}\n\n\td := &DumpTransport{\n\t\trt:     rt,\n\t\twriter: os.Stdout,\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(d)\n\t}\n\n\td.regexps = append(d.regexps,\n\t\tregexp.MustCompile(`(?im)^(Authorization):.+$`),\n\t\tregexp.MustCompile(`(?im)^(Token|X-Token):.+$`),\n\t\tregexp.MustCompile(`(?im)^(Auth-Token|X-Auth-Token):.+$`),\n\t\tregexp.MustCompile(`(?im)^(Api-Key|X-Api-Key|X-Api-Secret):.+$`),\n\t)\n\n\tif len(d.replacements) > 0 {\n\t\td.replacer = strings.NewReplacer(d.replacements...)\n\t}\n\n\treturn d\n}\n\nfunc (d *DumpTransport) RoundTrip(h *http.Request) (*http.Response, error) {\n\tdata, _ := httputil.DumpRequestOut(h, true)\n\n\t_, _ = fmt.Fprintln(d.writer, \"[HTTP Request]\")\n\t_, _ = fmt.Fprintln(d.writer, d.redact(data))\n\n\tresp, err := d.rt.RoundTrip(h)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata, _ = httputil.DumpResponse(resp, true)\n\n\t_, _ = fmt.Fprintln(d.writer, \"[HTTP Response]\")\n\t_, _ = fmt.Fprintln(d.writer, d.redact(data))\n\n\treturn resp, err\n}\n\nfunc (d *DumpTransport) redact(content []byte) string {\n\tdata := string(content)\n\n\tfor _, r := range d.regexps {\n\t\tdata = r.ReplaceAllString(data, \"$1: \"+replacement)\n\t}\n\n\tif d.replacer == nil {\n\t\treturn data\n\t}\n\n\treturn d.replacer.Replace(data)\n}\n\n// Wrap wraps an HTTP client Transport with the [DumpTransport].\nfunc Wrap(client *http.Client, opts ...Option) *http.Client {\n\tval, found := os.LookupEnv(\"LEGO_DEBUG_DNS_API_HTTP_CLIENT\")\n\tif !found {\n\t\treturn client\n\t}\n\n\tif ok, _ := strconv.ParseBool(val); !ok {\n\t\treturn client\n\t}\n\n\tclient.Transport = NewDumpTransport(client.Transport, opts...)\n\n\treturn client\n}\n"
  },
  {
    "path": "providers/dns/internal/clientdebug/client_test.go",
    "content": "package clientdebug\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestWrap_redact_env_vars(t *testing.T) {\n\tt.Setenv(\"LEGO_DEBUG_DNS_API_HTTP_CLIENT\", \"true\")\n\n\tt.Setenv(\"MY_VAR_01\", \"env-aaaa-aaaa\")\n\tt.Setenv(\"MY_VAR_02\", \"query-aaaa-aaaa\")\n\tt.Setenv(\"MY_VAR_03\", \"path-aaaa-aaaa\")\n\tt.Setenv(\"MY_VAR_04\", \"request-body-aaaa-aaaa\")\n\tt.Setenv(\"MY_VAR_05\", \"request-header-aaaa-aaaa\")\n\tt.Setenv(\"MY_VAR_06\", \"response-body-aaaa-aaaa\")\n\n\tbuf := bytes.NewBufferString(\"\")\n\n\tserver, client, req := setupTest(t, buf,\n\t\tWithEnvKeys(\"MY_VAR_01\", \"MY_VAR_02\", \"MY_VAR_03\", \"MY_VAR_04\", \"MY_VAR_05\", \"MY_VAR_06\"),\n\t)\n\n\tnow := time.Now()\n\n\tresp, err := client.Transport.RoundTrip(req)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\n\tassertDump(t, now, server, buf, \"env_vars.txt\")\n}\n\nfunc TestWrap_redact_headers(t *testing.T) {\n\tt.Setenv(\"LEGO_DEBUG_DNS_API_HTTP_CLIENT\", \"true\")\n\n\tbuf := bytes.NewBufferString(\"\")\n\n\tserver, client, req := setupTest(t, buf,\n\t\tWithHeaders(\"Secret-Request-Header\", \"Super-Secret-Request-Header\", \"Secret-Response-Header\"),\n\t)\n\n\tnow := time.Now()\n\n\tresp, err := client.Transport.RoundTrip(req)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\n\tassertDump(t, now, server, buf, \"headers.txt\")\n}\n\nfunc TestWrap_redact_values(t *testing.T) {\n\tt.Setenv(\"LEGO_DEBUG_DNS_API_HTTP_CLIENT\", \"true\")\n\n\tbuf := bytes.NewBufferString(\"\")\n\n\tserver, client, req := setupTest(t, buf,\n\t\tWithValues(\"query-aaaa-aaaa\", \"path-aaaa-aaaa\", \"request-body-aaaa-aaaa\"),\n\t)\n\n\tnow := time.Now()\n\n\tresp, err := client.Transport.RoundTrip(req)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\n\tassertDump(t, now, server, buf, \"values.txt\")\n}\n\nfunc fakeRequest(t *testing.T, baseURL string) *http.Request {\n\tt.Helper()\n\n\tendpoint, err := url.Parse(baseURL)\n\trequire.NoError(t, err)\n\n\tquery := endpoint.Query()\n\tquery.Set(\"foo\", \"query-aaaa-aaaa\")\n\tendpoint.RawQuery = query.Encode()\n\n\tendpoint = endpoint.JoinPath(\"path-aaaa-aaaa\")\n\n\tbody := `{\n\t\"foo\": \"request-body-aaaa-aaaa\"\n}\n`\n\n\treq := httptest.NewRequest(http.MethodGet, endpoint.String(), bytes.NewBufferString(body))\n\n\treq.Header.Set(\"X-Authorization\", \"not-redacted\")\n\n\treq.Header.Set(\"Secret-Request-Header\", \"request-header-aaaa-aaaa\")\n\treq.Header.Set(\"Super-Secret-Request-Header\", \"env-aaaa-aaaa\")\n\n\treq.Header.Set(\"Authorization\", \"header-aaaa-0000\")\n\treq.Header.Set(\"Token\", \"header-aaaa-0001\")\n\treq.Header.Set(\"X-Token\", \"header-aaaa-0002\")\n\treq.Header.Set(\"Auth-Token\", \"header-aaaa-0003\")\n\treq.Header.Set(\"X-Auth-Token\", \"header-aaaa-0004\")\n\treq.Header.Set(\"Api-Key\", \"header-aaaa-0006\")\n\treq.Header.Set(\"X-Api-Key\", \"header-aaaa-0007\")\n\treq.Header.Set(\"X-Api-Secret\", \"header-aaaa-0008\")\n\n\treq.SetBasicAuth(\"user\", \"secret\")\n\n\treturn req\n}\n\nfunc fakeResponse() http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Secret-Response-Header\", \"response-header-aaaa-aaaa\")\n\t\t_, _ = w.Write([]byte(`{\n\t\"bar\": \"response-body-aaaa-aaaa\"\n}`,\n\t\t))\n\t}\n}\n\nfunc withWriter(w io.Writer) Option {\n\treturn func(d *DumpTransport) {\n\t\tif w != nil {\n\t\t\td.writer = w\n\t\t}\n\t}\n}\n\nfunc setupTest(t *testing.T, buf io.Writer, opts ...Option) (*httptest.Server, *http.Client, *http.Request) {\n\tt.Helper()\n\n\tserver := httptest.NewServer(fakeResponse())\n\n\topts = append(opts, withWriter(buf))\n\n\tclient := Wrap(server.Client(), opts...)\n\n\treq := fakeRequest(t, server.URL)\n\n\treturn server, client, req\n}\n\nfunc assertDump(t *testing.T, now time.Time, server *httptest.Server, actual *bytes.Buffer, filename string) {\n\tt.Helper()\n\n\ttmpl, err := template.New(filename).ParseFiles(filepath.Join(\"testdata\", filename))\n\trequire.NoError(t, err)\n\n\texpected := bytes.NewBufferString(\"\")\n\n\tlocation, err := time.LoadLocation(\"GMT\")\n\trequire.NoError(t, err)\n\n\tbaseURL, err := url.Parse(server.URL)\n\trequire.NoError(t, err)\n\n\terr = tmpl.Execute(expected, map[string]string{\n\t\t\"Host\": baseURL.Host,\n\t\t\"Date\": now.In(location).Format(time.RFC1123),\n\t})\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, expected.String(), strings.ReplaceAll(actual.String(), \"\\r\", \"\"))\n}\n"
  },
  {
    "path": "providers/dns/internal/clientdebug/testdata/env_vars.txt",
    "content": "[HTTP Request]\nGET /***?foo=*** HTTP/1.1\nHost: {{ .Host }}\nUser-Agent: Go-http-client/1.1\nContent-Length: 37\nApi-Key: ***\nAuth-Token: ***\nAuthorization: ***\nSecret-Request-Header: ***\nSuper-Secret-Request-Header: ***\nToken: ***\nX-Api-Key: ***\nX-Api-Secret: ***\nX-Auth-Token: ***\nX-Authorization: not-redacted\nX-Token: ***\nAccept-Encoding: gzip\n\n{\n\t\"foo\": \"***\"\n}\n\n[HTTP Response]\nHTTP/1.1 200 OK\nContent-Length: 37\nContent-Type: text/plain; charset=utf-8\nDate: {{ .Date }}\nSecret-Response-Header: response-header-aaaa-aaaa\n\n{\n\t\"bar\": \"***\"\n}\n"
  },
  {
    "path": "providers/dns/internal/clientdebug/testdata/headers.txt",
    "content": "[HTTP Request]\nGET /path-aaaa-aaaa?foo=query-aaaa-aaaa HTTP/1.1\nHost: {{ .Host }}\nUser-Agent: Go-http-client/1.1\nContent-Length: 37\nApi-Key: ***\nAuth-Token: ***\nAuthorization: ***\nSecret-Request-Header: ***\nSuper-Secret-Request-Header: ***\nToken: ***\nX-Api-Key: ***\nX-Api-Secret: ***\nX-Auth-Token: ***\nX-Authorization: not-redacted\nX-Token: ***\nAccept-Encoding: gzip\n\n{\n\t\"foo\": \"request-body-aaaa-aaaa\"\n}\n\n[HTTP Response]\nHTTP/1.1 200 OK\nContent-Length: 37\nContent-Type: text/plain; charset=utf-8\nDate: {{ .Date }}\nSecret-Response-Header: ***\n\n{\n\t\"bar\": \"response-body-aaaa-aaaa\"\n}\n"
  },
  {
    "path": "providers/dns/internal/clientdebug/testdata/values.txt",
    "content": "[HTTP Request]\nGET /***?foo=*** HTTP/1.1\nHost: {{ .Host }}\nUser-Agent: Go-http-client/1.1\nContent-Length: 37\nApi-Key: ***\nAuth-Token: ***\nAuthorization: ***\nSecret-Request-Header: request-header-aaaa-aaaa\nSuper-Secret-Request-Header: env-aaaa-aaaa\nToken: ***\nX-Api-Key: ***\nX-Api-Secret: ***\nX-Auth-Token: ***\nX-Authorization: not-redacted\nX-Token: ***\nAccept-Encoding: gzip\n\n{\n\t\"foo\": \"***\"\n}\n\n[HTTP Response]\nHTTP/1.1 200 OK\nContent-Length: 37\nContent-Type: text/plain; charset=utf-8\nDate: {{ .Date }}\nSecret-Response-Header: response-header-aaaa-aaaa\n\n{\n\t\"bar\": \"response-body-aaaa-aaaa\"\n}\n"
  },
  {
    "path": "providers/dns/internal/errutils/client.go",
    "content": "package errutils\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n)\n\nconst legoDebugClientVerboseError = \"LEGO_DEBUG_CLIENT_VERBOSE_ERROR\"\n\n// HTTPDoError uses with `(http.Client).Do` error.\ntype HTTPDoError struct {\n\treq *http.Request\n\terr error\n}\n\n// NewHTTPDoError creates a new HTTPDoError.\nfunc NewHTTPDoError(req *http.Request, err error) *HTTPDoError {\n\treturn &HTTPDoError{req: req, err: err}\n}\n\nfunc (h HTTPDoError) Error() string {\n\tmsg := \"unable to communicate with the API server:\"\n\n\tif ok, _ := strconv.ParseBool(os.Getenv(legoDebugClientVerboseError)); ok {\n\t\tmsg += fmt.Sprintf(\" [request: %s %s]\", h.req.Method, h.req.URL)\n\t}\n\n\tif h.err == nil {\n\t\treturn msg\n\t}\n\n\treturn msg + fmt.Sprintf(\" error: %v\", h.err)\n}\n\nfunc (h HTTPDoError) Unwrap() error {\n\treturn h.err\n}\n\n// ReadResponseError use with `io.ReadAll` when reading response body.\ntype ReadResponseError struct {\n\treq        *http.Request\n\tStatusCode int\n\terr        error\n}\n\n// NewReadResponseError creates a new ReadResponseError.\nfunc NewReadResponseError(req *http.Request, statusCode int, err error) *ReadResponseError {\n\treturn &ReadResponseError{req: req, StatusCode: statusCode, err: err}\n}\n\nfunc (r ReadResponseError) Error() string {\n\tmsg := \"unable to read response body:\"\n\n\tif ok, _ := strconv.ParseBool(os.Getenv(legoDebugClientVerboseError)); ok {\n\t\tmsg += fmt.Sprintf(\" [request: %s %s]\", r.req.Method, r.req.URL)\n\t}\n\n\tmsg += fmt.Sprintf(\" [status code: %d]\", r.StatusCode)\n\n\tif r.err == nil {\n\t\treturn msg\n\t}\n\n\treturn msg + fmt.Sprintf(\" error: %v\", r.err)\n}\n\nfunc (r ReadResponseError) Unwrap() error {\n\treturn r.err\n}\n\n// UnmarshalError uses with `json.Unmarshal` or `xml.Unmarshal` when reading response body.\ntype UnmarshalError struct {\n\treq        *http.Request\n\tStatusCode int\n\tBody       []byte\n\terr        error\n}\n\n// NewUnmarshalError creates a new UnmarshalError.\nfunc NewUnmarshalError(req *http.Request, statusCode int, body []byte, err error) *UnmarshalError {\n\treturn &UnmarshalError{req: req, StatusCode: statusCode, Body: bytes.TrimSpace(body), err: err}\n}\n\nfunc (u UnmarshalError) Error() string {\n\tmsg := \"unable to unmarshal response:\"\n\n\tif ok, _ := strconv.ParseBool(os.Getenv(legoDebugClientVerboseError)); ok {\n\t\tmsg += fmt.Sprintf(\" [request: %s %s]\", u.req.Method, u.req.URL)\n\t}\n\n\tmsg += fmt.Sprintf(\" [status code: %d] body: %s\", u.StatusCode, string(u.Body))\n\n\tif u.err == nil {\n\t\treturn msg\n\t}\n\n\treturn msg + fmt.Sprintf(\" error: %v\", u.err)\n}\n\nfunc (u UnmarshalError) Unwrap() error {\n\treturn u.err\n}\n\n// UnexpectedStatusCodeError use when the status of the response is unexpected but there is no API error type.\ntype UnexpectedStatusCodeError struct {\n\treq        *http.Request\n\tStatusCode int\n\tBody       []byte\n}\n\n// NewUnexpectedStatusCodeError creates a new UnexpectedStatusCodeError.\nfunc NewUnexpectedStatusCodeError(req *http.Request, statusCode int, body []byte) *UnexpectedStatusCodeError {\n\treturn &UnexpectedStatusCodeError{req: req, StatusCode: statusCode, Body: bytes.TrimSpace(body)}\n}\n\nfunc NewUnexpectedResponseStatusCodeError(req *http.Request, resp *http.Response) *UnexpectedStatusCodeError {\n\traw, _ := io.ReadAll(resp.Body)\n\treturn &UnexpectedStatusCodeError{req: req, StatusCode: resp.StatusCode, Body: bytes.TrimSpace(raw)}\n}\n\nfunc (u UnexpectedStatusCodeError) Error() string {\n\tmsg := \"unexpected status code:\"\n\n\tif ok, _ := strconv.ParseBool(os.Getenv(legoDebugClientVerboseError)); ok {\n\t\tmsg += fmt.Sprintf(\" [request: %s %s]\", u.req.Method, u.req.URL)\n\t}\n\n\treturn msg + fmt.Sprintf(\" [status code: %d] body: %s\", u.StatusCode, string(u.Body))\n}\n"
  },
  {
    "path": "providers/dns/internal/gcore/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api.gcore.com/dns\"\n\nconst (\n\tauthorizationHeader = \"Authorization\"\n\ttokenTypeHeader     = \"APIKey\"\n)\n\nconst txtRecordType = \"TXT\"\n\n// Client for DNS API.\ntype Client struct {\n\ttoken string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient constructor of Client.\nfunc NewClient(token string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\ttoken:      token,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}\n}\n\n// GetZone gets zone information.\n// https://api.gcore.com/docs/dns#tag/zones/operation/Zone\nfunc (c *Client) GetZone(ctx context.Context, name string) (Zone, error) {\n\tendpoint := c.BaseURL.JoinPath(\"v2\", \"zones\", name)\n\n\tzone := Zone{}\n\n\terr := c.doRequest(ctx, http.MethodGet, endpoint, nil, &zone)\n\tif err != nil {\n\t\treturn Zone{}, fmt.Errorf(\"get zone %s: %w\", name, err)\n\t}\n\n\treturn zone, nil\n}\n\n// GetRRSet gets RRSet item.\n// https://api.gcore.com/docs/dns#tag/rrsets/operation/RRSet\nfunc (c *Client) GetRRSet(ctx context.Context, zone, name string) (RRSet, error) {\n\tendpoint := c.BaseURL.JoinPath(\"v2\", \"zones\", zone, name, txtRecordType)\n\n\tvar result RRSet\n\n\terr := c.doRequest(ctx, http.MethodGet, endpoint, nil, &result)\n\tif err != nil {\n\t\treturn RRSet{}, fmt.Errorf(\"get txt records %s -> %s: %w\", zone, name, err)\n\t}\n\n\treturn result, nil\n}\n\n// DeleteRRSet removes RRSet record.\n// https://api.gcore.com/docs/dns#tag/rrsets/operation/DeleteRRSet\nfunc (c *Client) DeleteRRSet(ctx context.Context, zone, name string) error {\n\tendpoint := c.BaseURL.JoinPath(\"v2\", \"zones\", zone, name, txtRecordType)\n\n\terr := c.doRequest(ctx, http.MethodDelete, endpoint, nil, nil)\n\tif err != nil {\n\t\t// Support DELETE idempotence https://developer.mozilla.org/en-US/docs/Glossary/Idempotent\n\t\tstatusErr := new(APIError)\n\t\tif errors.As(err, statusErr) && statusErr.StatusCode == http.StatusNotFound {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"delete record request: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// AddRRSet adds TXT record (create or update).\nfunc (c *Client) AddRRSet(ctx context.Context, zone, recordName, value string, ttl int) error {\n\trecord := RRSet{TTL: ttl, Records: []Records{{Content: []string{value}}}}\n\n\ttxt, err := c.GetRRSet(ctx, zone, recordName)\n\tif err == nil && len(txt.Records) > 0 {\n\t\trecord.Records = append(record.Records, txt.Records...)\n\t\treturn c.updateRRSet(ctx, zone, recordName, record)\n\t}\n\n\treturn c.createRRSet(ctx, zone, recordName, record)\n}\n\n// https://api.gcore.com/docs/dns#tag/rrsets/operation/CreateRRSet\nfunc (c *Client) createRRSet(ctx context.Context, zone, name string, record RRSet) error {\n\tendpoint := c.BaseURL.JoinPath(\"v2\", \"zones\", zone, name, txtRecordType)\n\n\treturn c.doRequest(ctx, http.MethodPost, endpoint, record, nil)\n}\n\n// https://api.gcore.com/docs/dns#tag/rrsets/operation/UpdateRRSet\nfunc (c *Client) updateRRSet(ctx context.Context, zone, name string, record RRSet) error {\n\tendpoint := c.BaseURL.JoinPath(\"v2\", \"zones\", zone, name, txtRecordType)\n\n\treturn c.doRequest(ctx, http.MethodPut, endpoint, record, nil)\n}\n\nfunc (c *Client) doRequest(ctx context.Context, method string, endpoint *url.URL, bodyParams, result any) error {\n\treq, err := newJSONRequest(ctx, method, endpoint, bodyParams)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"new request: %w\", err)\n\t}\n\n\treq.Header.Set(authorizationHeader, fmt.Sprintf(\"%s %s\", tokenTypeHeader, c.token))\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\terrAPI := APIError{StatusCode: resp.StatusCode}\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\terrAPI.Message = string(raw)\n\t}\n\n\treturn errAPI\n}\n"
  },
  {
    "path": "providers/dns/internal/gcore/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestToken         = \"test\"\n\ttestRecordContent = \"acme\"\n\ttestTTL           = 10\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(testToken)\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders())\n}\n\nfunc TestClient_GetZone(t *testing.T) {\n\texpected := Zone{Name: \"example.com\"}\n\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v2/zones/example.com\",\n\t\t\tservermock.JSONEncode(expected)).\n\t\tBuild(t)\n\n\tzone, err := client.GetZone(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, expected, zone)\n}\n\nfunc TestClient_GetZone_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v2/zones/example.com\",\n\t\t\tservermock.JSONEncode(APIError{Message: \"oops\"}).WithStatusCode(http.StatusInternalServerError)).\n\t\tBuild(t)\n\n\t_, err := client.GetZone(t.Context(), \"example.com\")\n\trequire.EqualError(t, err, \"get zone example.com: 500: oops\")\n}\n\nfunc TestClient_GetRRSet(t *testing.T) {\n\texpected := RRSet{\n\t\tTTL: testTTL,\n\t\tRecords: []Records{\n\t\t\t{Content: []string{testRecordContent}},\n\t\t},\n\t}\n\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v2/zones/example.com/foo.example.com/TXT\",\n\t\t\tservermock.JSONEncode(expected)).\n\t\tBuild(t)\n\n\trrSet, err := client.GetRRSet(t.Context(), \"example.com\", \"foo.example.com\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, expected, rrSet)\n}\n\nfunc TestClient_GetRRSet_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v2/zones/example.com/foo.example.com/TXT\",\n\t\t\tservermock.JSONEncode(APIError{Message: \"oops\"}).WithStatusCode(http.StatusInternalServerError)).\n\t\tBuild(t)\n\n\t_, err := client.GetRRSet(t.Context(), \"example.com\", \"foo.example.com\")\n\trequire.EqualError(t, err, \"get txt records example.com -> foo.example.com: 500: oops\")\n}\n\nfunc TestClient_DeleteRRSet(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v2/zones/test.example.com/my.test.example.com/TXT\", nil).\n\t\tBuild(t)\n\n\terr := client.DeleteRRSet(t.Context(), \"test.example.com\", \"my.test.example.com.\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRRSet_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v2/zones/test.example.com/my.test.example.com/TXT\",\n\t\t\tservermock.JSONEncode(APIError{Message: \"oops\"}).WithStatusCode(http.StatusInternalServerError)).\n\t\tBuild(t)\n\n\terr := client.DeleteRRSet(t.Context(), \"test.example.com\", \"my.test.example.com.\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddRRSet_add(t *testing.T) {\n\tclient := mockBuilder().\n\t\t// GetRRSet\n\t\tRoute(\"GET /v2/zones/test.example.com/my.test.example.com/TXT\",\n\t\t\tservermock.JSONEncode(APIError{Message: \"not found\"}).WithStatusCode(http.StatusBadRequest)).\n\t\t// createRRSet\n\t\tRoute(\"POST /v2/zones/test.example.com/my.test.example.com/TXT\",\n\t\t\tservermock.JSONEncode([]Records{{Content: []string{testRecordContent}}}),\n\t\t\tservermock.CheckRequestJSONBody(`{\"ttl\":10,\"resource_records\":[{\"content\":[\"acme\"]}]}`)).\n\t\tBuild(t)\n\n\terr := client.AddRRSet(t.Context(), \"test.example.com\", \"my.test.example.com\", testRecordContent, testTTL)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddRRSet_add_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\t// GetRRSet\n\t\tRoute(\"GET /v2/zones/test.example.com/my.test.example.com/TXT\",\n\t\t\tservermock.JSONEncode(APIError{Message: \"not found\"}).WithStatusCode(http.StatusBadRequest)).\n\t\t// createRRSet\n\t\tRoute(\"POST /v2/zones/test.example.com/my.test.example.com/TXT\",\n\t\t\tservermock.JSONEncode(APIError{Message: \"oops\"}).WithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\terr := client.AddRRSet(t.Context(), \"test.example.com\", \"my.test.example.com\", testRecordContent, testTTL)\n\trequire.EqualError(t, err, \"400: oops\")\n}\n\nfunc TestClient_AddRRSet_update(t *testing.T) {\n\tclient := mockBuilder().\n\t\t// GetRRSet\n\t\tRoute(\"GET /v2/zones/test.example.com/my.test.example.com/TXT\",\n\t\t\tservermock.JSONEncode(RRSet{\n\t\t\t\tTTL:     testTTL,\n\t\t\t\tRecords: []Records{{Content: []string{\"foo\"}}},\n\t\t\t})).\n\t\t// updateRRSet\n\t\tRoute(\"PUT /v2/zones/test.example.com/my.test.example.com/TXT\", nil,\n\t\t\tservermock.CheckRequestJSONBody(`{\"ttl\":10,\"resource_records\":[{\"content\":[\"acme\"]},{\"content\":[\"foo\"]}]}`)).\n\t\tBuild(t)\n\n\terr := client.AddRRSet(t.Context(), \"test.example.com\", \"my.test.example.com\", testRecordContent, testTTL)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddRRSet_update_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\t// GetRRSet\n\t\tRoute(\"GET /v2/zones/test.example.com/my.test.example.com/TXT\",\n\t\t\tservermock.JSONEncode(RRSet{\n\t\t\t\tTTL:     testTTL,\n\t\t\t\tRecords: []Records{{Content: []string{\"foo\"}}},\n\t\t\t})).\n\t\t// updateRRSet\n\t\tRoute(\"PUT /v2/zones/test.example.com/my.test.example.com/TXT\",\n\t\t\tservermock.JSONEncode(APIError{Message: \"oops\"}).WithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\terr := client.AddRRSet(t.Context(), \"test.example.com\", \"my.test.example.com\", testRecordContent, testTTL)\n\trequire.EqualError(t, err, \"400: oops\")\n}\n"
  },
  {
    "path": "providers/dns/internal/gcore/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype Zone struct {\n\tName string `json:\"name\"`\n}\n\ntype RRSet struct {\n\tTTL     int       `json:\"ttl\"`\n\tRecords []Records `json:\"resource_records\"`\n}\n\ntype Records struct {\n\tContent []string `json:\"content\"`\n}\n\ntype APIError struct {\n\tStatusCode int    `json:\"-\"`\n\tMessage    string `json:\"error,omitempty\"`\n}\n\nfunc (a APIError) Error() string {\n\treturn fmt.Sprintf(\"%d: %s\", a.StatusCode, a.Message)\n}\n"
  },
  {
    "path": "providers/dns/internal/gcore/provider.go",
    "content": "// Package gcore implements a DNS provider for solving the DNS-01 challenge using G-Core.\npackage gcore\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/gcore/internal\"\n)\n\nconst (\n\tDefaultPropagationTimeout = 360 * time.Second\n\tDefaultPollingInterval    = 20 * time.Second\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config for DNSProvider.\ntype Config struct {\n\tAPIToken           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// DNSProvider an implementation of challenge.Provider contract.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for G-Core DNS API.\nfunc NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIToken == \"\" {\n\t\treturn nil, errors.New(\"incomplete credentials provided\")\n\t}\n\n\tclient := internal.NewClient(config.APIToken)\n\n\tif baseURL != \"\" {\n\t\tclient.BaseURL, _ = url.Parse(baseURL)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzone, err := d.guessZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = d.client.AddRRSet(ctx, zone, dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"add txt record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzone, err := d.guessZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = d.client.DeleteRRSet(ctx, zone, dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"remove txt record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) guessZone(ctx context.Context, fqdn string) (string, error) {\n\tvar lastErr error\n\n\tfor zone := range dns01.UnFqdnDomainsSeq(fqdn) {\n\t\tdnsZone, err := d.client.GetZone(ctx, zone)\n\t\tif err != nil {\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\n\t\treturn dnsZone.Name, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"zone %q not found: %w\", fqdn, lastErr)\n}\n"
  },
  {
    "path": "providers/dns/internal/gcore/provider_test.go",
    "content": "package gcore\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiToken string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiToken: \"A\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"incomplete credentials provided\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := &Config{}\n\t\t\tconfig.APIToken = test.apiToken\n\n\t\t\tp, err := NewDNSProviderConfig(config, \"\")\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/internal/hostingde/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://secure.hosting.de/api/dns/v1/json\"\n\n// Client the API client for Hosting.de.\ntype Client struct {\n\tapiKey string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates new Client.\nfunc NewClient(apiKey string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// GetZone gets a zone.\nfunc (c *Client) GetZone(ctx context.Context, req ZoneConfigsFindRequest) (*ZoneConfig, error) {\n\toperation := func() (*ZoneConfig, error) {\n\t\tresponse, err := c.ListZoneConfigs(ctx, req)\n\t\tif err != nil {\n\t\t\treturn nil, backoff.Permanent(err)\n\t\t}\n\n\t\tif response.Data[0].Status != \"active\" {\n\t\t\treturn nil, fmt.Errorf(\"unexpected status: %q\", response.Data[0].Status)\n\t\t}\n\n\t\treturn &response.Data[0], nil\n\t}\n\n\tbo := backoff.NewExponentialBackOff()\n\tbo.InitialInterval = 3 * time.Second\n\tbo.MaxInterval = 10 * bo.InitialInterval\n\n\t// retry in case the zone was edited recently and is not yet active\n\treturn backoff.Retry(ctx, operation, backoff.WithBackOff(bo), backoff.WithMaxElapsedTime(100*bo.InitialInterval))\n}\n\n// ListZoneConfigs lists zone configuration.\n// https://www.hosting.de/api/?json#list-zoneconfigs\nfunc (c *Client) ListZoneConfigs(ctx context.Context, req ZoneConfigsFindRequest) (*ZoneResponse, error) {\n\tendpoint := c.BaseURL.JoinPath(\"zoneConfigsFind\")\n\n\treq.AuthToken = c.apiKey\n\n\tresponse := &BaseResponse[*ZoneResponse]{}\n\n\trawResp, err := c.post(ctx, endpoint, req, response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif response.Status != \"success\" && response.Status != \"pending\" {\n\t\treturn nil, fmt.Errorf(\"unexpected status: %q, %s\", response.Status, string(rawResp))\n\t}\n\n\tif response.Response == nil || len(response.Response.Data) == 0 {\n\t\treturn nil, fmt.Errorf(\"no data, status: %q, %s\", response.Status, string(rawResp))\n\t}\n\n\treturn response.Response, nil\n}\n\n// UpdateZone updates a zone.\n// https://www.hosting.de/api/?json#updating-zones\nfunc (c *Client) UpdateZone(ctx context.Context, req ZoneUpdateRequest) (*Zone, error) {\n\tendpoint := c.BaseURL.JoinPath(\"zoneUpdate\")\n\n\treq.AuthToken = c.apiKey\n\n\t// but we'll need the ID later to delete the record\n\tresponse := &BaseResponse[*Zone]{}\n\n\trawResp, err := c.post(ctx, endpoint, req, response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif response.Status != \"success\" && response.Status != \"pending\" {\n\t\treturn nil, fmt.Errorf(\"unexpected status: %q, %s\", response.Status, string(rawResp))\n\t}\n\n\treturn response.Response, nil\n}\n\nfunc (c *Client) post(ctx context.Context, endpoint *url.URL, request, result any) ([]byte, error) {\n\tbody, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn raw, nil\n}\n"
  },
  {
    "path": "providers/dns/internal/hostingde/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"secret\")\n\tclient.HTTPClient = server.Client()\n\tclient.BaseURL, _ = url.Parse(server.URL)\n\n\treturn client, nil\n}\n\nfunc TestClient_ListZoneConfigs(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /zoneConfigsFind\",\n\t\t\tservermock.ResponseFromFixture(\"zoneConfigsFind.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"zoneConfigsFind-request.json\")).\n\t\tBuild(t)\n\n\tzonesFind := ZoneConfigsFindRequest{\n\t\tFilter: Filter{Field: \"zoneName\", Value: \"example.com\"},\n\t\tLimit:  1,\n\t\tPage:   1,\n\t}\n\n\tzoneResponse, err := client.ListZoneConfigs(t.Context(), zonesFind)\n\trequire.NoError(t, err)\n\n\texpected := &ZoneResponse{\n\t\tLimit:        10,\n\t\tPage:         1,\n\t\tTotalEntries: 15,\n\t\tTotalPages:   2,\n\t\tType:         \"FindZoneConfigsResult\",\n\t\tData: []ZoneConfig{{\n\t\t\tID:                    \"123\",\n\t\t\tAccountID:             \"456\",\n\t\t\tStatus:                \"s\",\n\t\t\tName:                  \"n\",\n\t\t\tNameUnicode:           \"u\",\n\t\t\tMasterIP:              \"m\",\n\t\t\tType:                  \"t\",\n\t\t\tEMailAddress:          \"e\",\n\t\t\tZoneTransferWhitelist: []string{\"a\", \"b\"},\n\t\t\tLastChangeDate:        \"l\",\n\t\t\tDNSServerGroupID:      \"g\",\n\t\t\tDNSSecMode:            \"m\",\n\t\t\tSOAValues: &SOAValues{\n\t\t\t\tRefresh:     1,\n\t\t\t\tRetry:       2,\n\t\t\t\tExpire:      3,\n\t\t\t\tTTL:         4,\n\t\t\t\tNegativeTTL: 5,\n\t\t\t},\n\t\t\tTemplateValues: json.RawMessage(nil),\n\t\t}},\n\t}\n\n\tassert.Equal(t, expected, zoneResponse)\n}\n\nfunc TestClient_ListZoneConfigs_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /zoneConfigsFind\",\n\t\t\tservermock.ResponseFromFixture(\"zoneConfigsFind_error.json\")).\n\t\tBuild(t)\n\n\tzonesFind := ZoneConfigsFindRequest{\n\t\tFilter: Filter{Field: \"zoneName\", Value: \"example.com\"},\n\t\tLimit:  1,\n\t\tPage:   1,\n\t}\n\n\t_, err := client.ListZoneConfigs(t.Context(), zonesFind)\n\trequire.Error(t, err)\n}\n\nfunc TestClient_UpdateZone(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /zoneUpdate\",\n\t\t\tservermock.ResponseFromFixture(\"zoneUpdate.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"zoneUpdate-request.json\")).\n\t\tBuild(t)\n\n\trequest := ZoneUpdateRequest{\n\t\tZoneConfig: ZoneConfig{\n\t\t\tID:                    \"123\",\n\t\t\tAccountID:             \"456\",\n\t\t\tStatus:                \"s\",\n\t\t\tName:                  \"n\",\n\t\t\tNameUnicode:           \"u\",\n\t\t\tMasterIP:              \"m\",\n\t\t\tType:                  \"t\",\n\t\t\tEMailAddress:          \"e\",\n\t\t\tZoneTransferWhitelist: []string{\"a\", \"b\"},\n\t\t\tLastChangeDate:        \"l\",\n\t\t\tDNSServerGroupID:      \"g\",\n\t\t\tDNSSecMode:            \"m\",\n\t\t\tSOAValues: &SOAValues{\n\t\t\t\tRefresh:     1,\n\t\t\t\tRetry:       2,\n\t\t\t\tExpire:      3,\n\t\t\t\tTTL:         4,\n\t\t\t\tNegativeTTL: 5,\n\t\t\t},\n\t\t},\n\t\tRecordsToDelete: []DNSRecord{{\n\t\t\tType:    \"TXT\",\n\t\t\tName:    \"_acme-challenge.example.com\",\n\t\t\tContent: `\"txt\"`,\n\t\t}},\n\t}\n\n\tresponse, err := client.UpdateZone(t.Context(), request)\n\trequire.NoError(t, err)\n\n\texpected := &Zone{\n\t\tRecords: []DNSRecord{{\n\t\t\tID:               \"123\",\n\t\t\tZoneID:           \"456\",\n\t\t\tRecordTemplateID: \"789\",\n\t\t\tName:             \"n\",\n\t\t\tType:             \"TXT\",\n\t\t\tContent:          \"txt\",\n\t\t\tTTL:              120,\n\t\t\tPriority:         5,\n\t\t\tLastChangeDate:   \"d\",\n\t\t}},\n\t\tZoneConfig: ZoneConfig{\n\t\t\tID:                    \"123\",\n\t\t\tAccountID:             \"456\",\n\t\t\tStatus:                \"s\",\n\t\t\tName:                  \"n\",\n\t\t\tNameUnicode:           \"u\",\n\t\t\tMasterIP:              \"m\",\n\t\t\tType:                  \"t\",\n\t\t\tEMailAddress:          \"e\",\n\t\t\tZoneTransferWhitelist: []string{\"a\", \"b\"},\n\t\t\tLastChangeDate:        \"l\",\n\t\t\tDNSServerGroupID:      \"g\",\n\t\t\tDNSSecMode:            \"m\",\n\t\t\tSOAValues: &SOAValues{\n\t\t\t\tRefresh:     1,\n\t\t\t\tRetry:       2,\n\t\t\t\tExpire:      3,\n\t\t\t\tTTL:         4,\n\t\t\t\tNegativeTTL: 5,\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, response)\n}\n\nfunc TestClient_UpdateZone_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /zoneUpdate\",\n\t\t\tservermock.ResponseFromFixture(\"zoneUpdate_error.json\")).\n\t\tBuild(t)\n\n\trequest := ZoneUpdateRequest{\n\t\tZoneConfig: ZoneConfig{\n\t\t\tID:                    \"123\",\n\t\t\tAccountID:             \"456\",\n\t\t\tStatus:                \"s\",\n\t\t\tName:                  \"n\",\n\t\t\tNameUnicode:           \"u\",\n\t\t\tMasterIP:              \"m\",\n\t\t\tType:                  \"t\",\n\t\t\tEMailAddress:          \"e\",\n\t\t\tZoneTransferWhitelist: []string{\"a\", \"b\"},\n\t\t\tLastChangeDate:        \"l\",\n\t\t\tDNSServerGroupID:      \"g\",\n\t\t\tDNSSecMode:            \"m\",\n\t\t\tSOAValues: &SOAValues{\n\t\t\t\tRefresh:     1,\n\t\t\t\tRetry:       2,\n\t\t\t\tExpire:      3,\n\t\t\t\tTTL:         4,\n\t\t\t\tNegativeTTL: 5,\n\t\t\t},\n\t\t},\n\t\tRecordsToDelete: []DNSRecord{{\n\t\t\tType:    \"TXT\",\n\t\t\tName:    \"_acme-challenge.example.com\",\n\t\t\tContent: `\"txt\"`,\n\t\t}},\n\t}\n\n\t_, err := client.UpdateZone(t.Context(), request)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind-request.json",
    "content": "{\n  \"authToken\": \"secret\",\n  \"filter\": {\n    \"field\": \"zoneName\",\n    \"value\": \"example.com\"\n  },\n  \"limit\": 1,\n  \"page\": 1\n}\n"
  },
  {
    "path": "providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind.json",
    "content": "{\n  \"metadata\": {\n    \"clientTransactionId\": \"1\",\n    \"serverTransactionId\": \"2\"\n  },\n  \"warnings\": [\n    \"aaa\",\n    \"bbb\"\n  ],\n  \"status\": \"success\",\n  \"response\": {\n    \"limit\": 10,\n    \"page\": 1,\n    \"totalEntries\": 15,\n    \"totalPages\": 2,\n    \"type\": \"FindZoneConfigsResult\",\n    \"data\": [\n      {\n        \"id\": \"123\",\n        \"accountId\": \"456\",\n        \"status\": \"s\",\n        \"name\": \"n\",\n        \"nameUnicode\": \"u\",\n        \"masterIp\": \"m\",\n        \"type\": \"t\",\n        \"emailAddress\": \"e\",\n        \"zoneTransferWhitelist\": [\n          \"a\",\n          \"b\"\n        ],\n        \"lastChangeDate\": \"l\",\n        \"dnsServerGroupId\": \"g\",\n        \"dnsSecMode\": \"m\",\n        \"soaValues\": {\n          \"refresh\": 1,\n          \"retry\": 2,\n          \"expire\": 3,\n          \"ttl\": 4,\n          \"negativeTtl\": 5\n        }\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/internal/hostingde/internal/fixtures/zoneConfigsFind_error.json",
    "content": "{\n  \"errors\": [\n    {\n      \"code\": 123,\n      \"contextObject\": \"o\",\n      \"contextPath\": \"p\",\n      \"details\": [\n        \"a\",\n        \"b\"\n      ],\n      \"text\": \"t\",\n      \"value\": \"v\"\n    }\n  ],\n  \"metadata\": {\n    \"clientTransactionId\": \"1\",\n    \"serverTransactionId\": \"2\"\n  },\n  \"warnings\": [\n    \"aaa\",\n    \"bbb\"\n  ],\n  \"status\": \"error\",\n  \"response\": {\n    \"limit\": 10,\n    \"page\": 1,\n    \"totalEntries\": 15,\n    \"totalPages\": 2,\n    \"type\": \"FindZoneConfigsResult\",\n    \"data\": [\n      {\n        \"id\": \"123\",\n        \"accountId\": \"456\",\n        \"status\": \"s\",\n        \"name\": \"n\",\n        \"nameUnicode\": \"u\",\n        \"masterIp\": \"m\",\n        \"type\": \"t\",\n        \"emailAddress\": \"e\",\n        \"zoneTransferWhitelist\": [\n          \"a\",\n          \"b\"\n        ],\n        \"lastChangeDate\": \"l\",\n        \"dnsServerGroupId\": \"g\",\n        \"dnsSecMode\": \"m\",\n        \"soaValues\": {\n          \"refresh\": 1,\n          \"retry\": 2,\n          \"expire\": 3,\n          \"ttl\": 4,\n          \"negativeTtl\": 5\n        }\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/internal/hostingde/internal/fixtures/zoneUpdate-request.json",
    "content": "{\n  \"authToken\": \"secret\",\n  \"zoneConfig\": {\n    \"id\": \"123\",\n    \"accountId\": \"456\",\n    \"status\": \"s\",\n    \"name\": \"n\",\n    \"nameUnicode\": \"u\",\n    \"masterIp\": \"m\",\n    \"type\": \"t\",\n    \"emailAddress\": \"e\",\n    \"zoneTransferWhitelist\": [\n      \"a\",\n      \"b\"\n    ],\n    \"lastChangeDate\": \"l\",\n    \"dnsServerGroupId\": \"g\",\n    \"dnsSecMode\": \"m\",\n    \"soaValues\": {\n      \"refresh\": 1,\n      \"retry\": 2,\n      \"expire\": 3,\n      \"ttl\": 4,\n      \"negativeTtl\": 5\n    }\n  },\n  \"recordsToAdd\": null,\n  \"recordsToDelete\": [\n    {\n      \"name\": \"_acme-challenge.example.com\",\n      \"type\": \"TXT\",\n      \"content\": \"\\\"txt\\\"\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/internal/hostingde/internal/fixtures/zoneUpdate.json",
    "content": "{\n  \"metadata\": {\n    \"clientTransactionId\": \"\",\n    \"serverTransactionId\": \"\"\n  },\n  \"warnings\": [\n    \"aaa\",\n    \"bbb\"\n  ],\n  \"status\": \"success\",\n  \"response\": {\n    \"records\": [\n      {\n        \"id\": \"123\",\n        \"zoneId\": \"456\",\n        \"recordTemplateId\": \"789\",\n        \"name\": \"n\",\n        \"type\": \"TXT\",\n        \"content\": \"txt\",\n        \"ttl\": 120,\n        \"priority\": 5,\n        \"lastChangeDate\": \"d\"\n      }\n    ],\n    \"zoneConfig\": {\n      \"id\": \"123\",\n      \"accountId\": \"456\",\n      \"status\": \"s\",\n      \"name\": \"n\",\n      \"nameUnicode\": \"u\",\n      \"masterIp\": \"m\",\n      \"type\": \"t\",\n      \"emailAddress\": \"e\",\n      \"zoneTransferWhitelist\": [\n        \"a\",\n        \"b\"\n      ],\n      \"lastChangeDate\": \"l\",\n      \"dnsServerGroupId\": \"g\",\n      \"dnsSecMode\": \"m\",\n      \"soaValues\": {\n        \"refresh\": 1,\n        \"retry\": 2,\n        \"expire\": 3,\n        \"ttl\": 4,\n        \"negativeTtl\": 5\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/internal/hostingde/internal/fixtures/zoneUpdate_error.json",
    "content": "{\n  \"errors\": [\n    {\n      \"code\": 123,\n      \"contextObject\": \"o\",\n      \"contextPath\": \"p\",\n      \"details\": [\n        \"a\",\n        \"b\"\n      ],\n      \"text\": \"t\",\n      \"value\": \"v\"\n    }\n  ],\n  \"metadata\": {\n    \"clientTransactionId\": \"\",\n    \"serverTransactionId\": \"\"\n  },\n  \"warnings\": [\n    \"aaa\",\n    \"bbb\"\n  ],\n  \"status\": \"error\",\n  \"response\": {\n    \"records\": [\n      {\n        \"id\": \"123\",\n        \"zoneId\": \"456\",\n        \"recordTemplateId\": \"789\",\n        \"name\": \"n\",\n        \"type\": \"TXT\",\n        \"content\": \"txt\",\n        \"ttl\": 120,\n        \"priority\": 5,\n        \"lastChangeDate\": \"d\"\n      }\n    ],\n    \"zoneConfig\": {\n      \"id\": \"123\",\n      \"accountId\": \"456\",\n      \"status\": \"s\",\n      \"name\": \"n\",\n      \"nameUnicode\": \"u\",\n      \"masterIp\": \"m\",\n      \"type\": \"t\",\n      \"emailAddress\": \"e\",\n      \"zoneTransferWhitelist\": [\n        \"a\",\n        \"b\"\n      ],\n      \"lastChangeDate\": \"l\",\n      \"dnsServerGroupId\": \"g\",\n      \"dnsSecMode\": \"m\",\n      \"soaValues\": {\n        \"refresh\": 1,\n        \"retry\": 2,\n        \"expire\": 3,\n        \"ttl\": 4,\n        \"negativeTtl\": 5\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/internal/hostingde/internal/types.go",
    "content": "package internal\n\nimport \"encoding/json\"\n\n// APIError represents an error in an API response.\n// https://www.hosting.de/api/?json#warnings-and-errors\ntype APIError struct {\n\tCode          int      `json:\"code\"`\n\tContextObject string   `json:\"contextObject\"`\n\tContextPath   string   `json:\"contextPath\"`\n\tDetails       []string `json:\"details\"`\n\tText          string   `json:\"text\"`\n\tValue         string   `json:\"value\"`\n}\n\n// Filter is used to filter FindRequests to the API.\n// https://www.hosting.de/api/?json#filter-object\ntype Filter struct {\n\tField string `json:\"field\"`\n\tValue string `json:\"value\"`\n}\n\n// Sort is used to sort FindRequests from the API.\n// https://www.hosting.de/api/?json#filtering-and-sorting\ntype Sort struct {\n\tField string `json:\"zoneName\"`\n\tOrder string `json:\"order\"`\n}\n\n// Metadata represents the metadata in an API response.\n// https://www.hosting.de/api/?json#metadata-object\ntype Metadata struct {\n\tClientTransactionID string `json:\"clientTransactionId\"`\n\tServerTransactionID string `json:\"serverTransactionId\"`\n}\n\n// ZoneConfig The ZoneConfig object defines a zone.\n// https://www.hosting.de/api/?json#the-zoneconfig-object\ntype ZoneConfig struct {\n\tID                    string          `json:\"id\"`\n\tAccountID             string          `json:\"accountId\"`\n\tStatus                string          `json:\"status\"`\n\tName                  string          `json:\"name\"`\n\tNameUnicode           string          `json:\"nameUnicode\"`\n\tMasterIP              string          `json:\"masterIp\"`\n\tType                  string          `json:\"type\"`\n\tEMailAddress          string          `json:\"emailAddress\"`\n\tZoneTransferWhitelist []string        `json:\"zoneTransferWhitelist\"`\n\tLastChangeDate        string          `json:\"lastChangeDate\"`\n\tDNSServerGroupID      string          `json:\"dnsServerGroupId\"`\n\tDNSSecMode            string          `json:\"dnsSecMode\"`\n\tSOAValues             *SOAValues      `json:\"soaValues,omitempty\"`\n\tTemplateValues        json.RawMessage `json:\"templateValues,omitempty\"`\n}\n\n// SOAValues The SOA values object contains the time (seconds) used in a zone’s SOA record.\n// https://www.hosting.de/api/?json#the-soa-values-object\ntype SOAValues struct {\n\tRefresh     int `json:\"refresh\"`\n\tRetry       int `json:\"retry\"`\n\tExpire      int `json:\"expire\"`\n\tTTL         int `json:\"ttl\"`\n\tNegativeTTL int `json:\"negativeTtl\"`\n}\n\n// DNSRecord The DNS Record object is part of a zone. It is used to manage DNS resource records.\n// https://www.hosting.de/api/?json#the-record-object\ntype DNSRecord struct {\n\tID               string `json:\"id,omitempty\"`\n\tZoneID           string `json:\"zoneId,omitempty\"`\n\tRecordTemplateID string `json:\"recordTemplateId,omitempty\"`\n\tName             string `json:\"name,omitempty\"`\n\tType             string `json:\"type,omitempty\"`\n\tContent          string `json:\"content,omitempty\"`\n\tTTL              int    `json:\"ttl,omitempty\"`\n\tPriority         int    `json:\"priority,omitempty\"`\n\tLastChangeDate   string `json:\"lastChangeDate,omitempty\"`\n}\n\n// Zone The Zone Object.\n// https://www.hosting.de/api/?json#the-zone-object\ntype Zone struct {\n\tRecords    []DNSRecord `json:\"records\"`\n\tZoneConfig ZoneConfig  `json:\"zoneConfig\"`\n}\n\n// ZoneUpdateRequest represents a API ZoneUpdate request.\n// https://www.hosting.de/api/?json#updating-zones\ntype ZoneUpdateRequest struct {\n\tBaseRequest\n\tZoneConfig `json:\"zoneConfig\"`\n\n\tRecordsToAdd    []DNSRecord `json:\"recordsToAdd\"`\n\tRecordsToDelete []DNSRecord `json:\"recordsToDelete\"`\n}\n\n// ZoneConfigsFindRequest represents a API ZonesFind request.\n// https://www.hosting.de/api/?json#list-zoneconfigs\ntype ZoneConfigsFindRequest struct {\n\tBaseRequest\n\n\tFilter Filter `json:\"filter\"`\n\tLimit  int    `json:\"limit\"`\n\tPage   int    `json:\"page\"`\n\tSort   *Sort  `json:\"sort,omitempty\"`\n}\n\ntype ZoneResponse struct {\n\tLimit        int          `json:\"limit\"`\n\tPage         int          `json:\"page\"`\n\tTotalEntries int          `json:\"totalEntries\"`\n\tTotalPages   int          `json:\"totalPages\"`\n\tType         string       `json:\"type\"`\n\tData         []ZoneConfig `json:\"data\"`\n}\n\n// BaseResponse Common response struct.\n// base: https://www.hosting.de/api/?json#responses\n// ZoneConfigsFind: https://www.hosting.de/api/?json#list-zoneconfigs\n// ZoneUpdate: https://www.hosting.de/api/?json#updating-zones\ntype BaseResponse[T any] struct {\n\tErrors   []APIError `json:\"errors\"`\n\tMetadata Metadata   `json:\"metadata\"`\n\tWarnings []string   `json:\"warnings\"`\n\tStatus   string     `json:\"status\"`\n\tResponse T          `json:\"response\"`\n}\n\n// BaseRequest Common request struct.\ntype BaseRequest struct {\n\tAuthToken string `json:\"authToken\"`\n}\n"
  },
  {
    "path": "providers/dns/internal/hostingde/provider.go",
    "content": "// Package hostingde implements a DNS provider for solving the DNS-01 challenge using hosting.de.\npackage hostingde\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/hostingde/internal\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tZoneName           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for hosting.de.\nfunc NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"API key missing\")\n\t}\n\n\tclient := internal.NewClient(config.APIKey)\n\n\tif baseURL != \"\" {\n\t\tclient.BaseURL, _ = url.Parse(baseURL)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneName, err := d.getZoneName(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\t// get the ZoneConfig for that domain\n\tzonesFind := internal.ZoneConfigsFindRequest{\n\t\tFilter: internal.Filter{Field: \"zoneName\", Value: zoneName},\n\t\tLimit:  1,\n\t\tPage:   1,\n\t}\n\n\tzoneConfig, err := d.client.GetZone(ctx, zonesFind)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tzoneConfig.Name = zoneName\n\n\trec := []internal.DNSRecord{{\n\t\tType:    \"TXT\",\n\t\tName:    dns01.UnFqdn(info.EffectiveFQDN),\n\t\tContent: info.Value,\n\t\tTTL:     d.config.TTL,\n\t}}\n\n\treq := internal.ZoneUpdateRequest{\n\t\tZoneConfig:   *zoneConfig,\n\t\tRecordsToAdd: rec,\n\t}\n\n\tresponse, err := d.client.UpdateZone(ctx, req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, record := range response.Records {\n\t\tif record.Name == dns01.UnFqdn(info.EffectiveFQDN) && record.Content == fmt.Sprintf(`%q`, info.Value) {\n\t\t\td.recordIDsMu.Lock()\n\t\t\td.recordIDs[info.EffectiveFQDN] = record.ID\n\t\t\td.recordIDsMu.Unlock()\n\t\t}\n\t}\n\n\tif d.recordIDs[info.EffectiveFQDN] == \"\" {\n\t\treturn fmt.Errorf(\"error getting ID of just created record, for domain %s\", domain)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneName, err := d.getZoneName(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\t// get the ZoneConfig for that domain\n\tzonesFind := internal.ZoneConfigsFindRequest{\n\t\tFilter: internal.Filter{Field: \"zoneName\", Value: zoneName},\n\t\tLimit:  1,\n\t\tPage:   1,\n\t}\n\n\tzoneConfig, err := d.client.GetZone(ctx, zonesFind)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tzoneConfig.Name = zoneName\n\n\trec := []internal.DNSRecord{{\n\t\tType:    \"TXT\",\n\t\tName:    dns01.UnFqdn(info.EffectiveFQDN),\n\t\tContent: `\"` + info.Value + `\"`,\n\t}}\n\n\treq := internal.ZoneUpdateRequest{\n\t\tZoneConfig:      *zoneConfig,\n\t\tRecordsToDelete: rec,\n\t}\n\n\t_, err = d.client.UpdateZone(ctx, req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Delete record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, info.EffectiveFQDN)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) getZoneName(fqdn string) (string, error) {\n\tif d.config.ZoneName != \"\" {\n\t\treturn d.config.ZoneName, nil\n\t}\n\n\tzoneName, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not find zone for %s: %w\", fqdn, err)\n\t}\n\n\tif zoneName == \"\" {\n\t\treturn \"\", errors.New(\"empty zone name\")\n\t}\n\n\treturn dns01.UnFqdn(zoneName), nil\n}\n"
  },
  {
    "path": "providers/dns/internal/hostingde/provider_test.go",
    "content": "package hostingde\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tzoneName string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiKey:   \"123\",\n\t\t\tzoneName: \"example.org\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"API key missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api key\",\n\t\t\tzoneName: \"456\",\n\t\t\texpected: \"API key missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := &Config{}\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.ZoneName = test.zoneName\n\n\t\t\tp, err := NewDNSProviderConfig(config, \"\")\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/internal/ionos/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\nconst defaultBaseURL = \"https://api.hosting.ionos.com/dns\"\n\n// APIKeyHeader API key header.\nconst APIKeyHeader = \"X-Api-Key\"\n\n// Client Ionos API client.\ntype Client struct {\n\tapiKey string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiKey string) (*Client, error) {\n\tbaseURL, err := url.Parse(defaultBaseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}, nil\n}\n\n// ListZones gets all zones.\nfunc (c *Client) ListZones(ctx context.Context) ([]Zone, error) {\n\tendpoint := c.BaseURL.JoinPath(\"v1\", \"zones\")\n\n\treq, err := makeJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\tvar zones []Zone\n\n\terr = c.do(req, &zones)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to call API: %w\", err)\n\t}\n\n\treturn zones, nil\n}\n\n// ReplaceRecords replaces some records of a zones.\nfunc (c *Client) ReplaceRecords(ctx context.Context, zoneID string, records []Record) error {\n\tendpoint := c.BaseURL.JoinPath(\"v1\", \"zones\", zoneID)\n\n\treq, err := makeJSONRequest(ctx, http.MethodPatch, endpoint, records)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\terr = c.do(req, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to call API: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// GetRecords gets the records of a zones.\nfunc (c *Client) GetRecords(ctx context.Context, zoneID string, filter *RecordsFilter) ([]Record, error) {\n\tendpoint := c.BaseURL.JoinPath(\"v1\", \"zones\", zoneID)\n\n\treq, err := makeJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\tif filter != nil {\n\t\tv, errQ := querystring.Values(filter)\n\t\tif errQ != nil {\n\t\t\treturn nil, errQ\n\t\t}\n\n\t\treq.URL.RawQuery = v.Encode()\n\t}\n\n\tvar zone CustomerZone\n\n\terr = c.do(req, &zone)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to call API: %w\", err)\n\t}\n\n\treturn zone.Records, nil\n}\n\n// RemoveRecord removes a record.\nfunc (c *Client) RemoveRecord(ctx context.Context, zoneID, recordID string) error {\n\tendpoint := c.BaseURL.JoinPath(\"v1\", \"zones\", zoneID, \"records\", recordID)\n\n\treq, err := makeJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\terr = c.do(req, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to call API: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(APIKeyHeader, c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc makeJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\terrClient := &ClientError{StatusCode: resp.StatusCode}\n\n\terr := json.Unmarshal(raw, &errClient.errors)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn errClient\n}\n"
  },
  {
    "path": "providers/dns/internal/ionos/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t\tservermock.CheckHeader().With(APIKeyHeader, \"secret\"))\n}\n\nfunc TestClient_ListZones(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/zones\",\n\t\t\tservermock.ResponseFromFixture(\"list_zones.json\")).\n\t\tBuild(t)\n\n\tzones, err := client.ListZones(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []Zone{{\n\t\tID:   \"11af3414-ebba-11e9-8df5-66fbe8a334b4\",\n\t\tName: \"test.com\",\n\t\tType: \"NATIVE\",\n\t}}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_ListZones_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/zones\",\n\t\t\tservermock.ResponseFromFixture(\"list_zones_error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\tzones, err := client.ListZones(t.Context())\n\trequire.Error(t, err)\n\n\tassert.Nil(t, zones)\n\n\tvar cErr *ClientError\n\tassert.ErrorAs(t, err, &cErr)\n\tassert.Equal(t, http.StatusUnauthorized, cErr.StatusCode)\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/zones/azone01\",\n\t\t\tservermock.ResponseFromFixture(\"get_records.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"azone01\", nil)\n\trequire.NoError(t, err)\n\n\texpected := []Record{{\n\t\tID:      \"22af3414-abbe-9e11-5df5-66fbe8e334b4\",\n\t\tName:    \"string\",\n\t\tContent: \"string\",\n\t\tType:    \"A\",\n\t}}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /v1/zones/azone01\",\n\t\t\tservermock.ResponseFromFixture(\"get_records_error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"azone01\", nil)\n\trequire.Error(t, err)\n\n\tassert.Nil(t, records)\n\n\tvar cErr *ClientError\n\tassert.ErrorAs(t, err, &cErr)\n\tassert.Equal(t, http.StatusUnauthorized, cErr.StatusCode)\n}\n\nfunc TestClient_RemoveRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v1/zones/azone01/records/arecord01\", nil).\n\t\tBuild(t)\n\n\terr := client.RemoveRecord(t.Context(), \"azone01\", \"arecord01\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_RemoveRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v1/zones/azone01/records/arecord01\",\n\t\t\tservermock.ResponseFromFixture(\"remove_record_error.json\").\n\t\t\t\tWithStatusCode(http.StatusInternalServerError)).\n\t\tBuild(t)\n\n\terr := client.RemoveRecord(t.Context(), \"azone01\", \"arecord01\")\n\trequire.Error(t, err)\n\n\tvar cErr *ClientError\n\tassert.ErrorAs(t, err, &cErr)\n\tassert.Equal(t, http.StatusInternalServerError, cErr.StatusCode)\n}\n\nfunc TestClient_ReplaceRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PATCH /v1/zones/azone01\", nil).\n\t\tBuild(t)\n\n\trecords := []Record{{\n\t\tID:      \"22af3414-abbe-9e11-5df5-66fbe8e334b4\",\n\t\tName:    \"string\",\n\t\tContent: \"string\",\n\t\tType:    \"A\",\n\t}}\n\n\terr := client.ReplaceRecords(t.Context(), \"azone01\", records)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_ReplaceRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PATCH /v1/zones/azone01\",\n\t\t\tservermock.ResponseFromFixture(\"replace_records_error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\trecords := []Record{{\n\t\tID:      \"22af3414-abbe-9e11-5df5-66fbe8e334b4\",\n\t\tName:    \"string\",\n\t\tContent: \"string\",\n\t\tType:    \"A\",\n\t}}\n\n\terr := client.ReplaceRecords(t.Context(), \"azone01\", records)\n\trequire.Error(t, err)\n\n\tvar cErr *ClientError\n\tassert.ErrorAs(t, err, &cErr)\n\tassert.Equal(t, http.StatusBadRequest, cErr.StatusCode)\n}\n"
  },
  {
    "path": "providers/dns/internal/ionos/internal/fixtures/get_records.json",
    "content": "{\n  \"id\": \"11af3414-ebba-11e9-8df5-66fbe8a334b4\",\n  \"name\": \"example-zone.de\",\n  \"type\": \"NATIVE\",\n  \"records\": [\n    {\n      \"id\": \"22af3414-abbe-9e11-5df5-66fbe8e334b4\",\n      \"name\": \"string\",\n      \"rootName\": \"string\",\n      \"type\": \"A\",\n      \"content\": \"string\",\n      \"changeDate\": \"string\",\n      \"ttl\": 0,\n      \"prio\": 0,\n      \"disabled\": false\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/internal/ionos/internal/fixtures/get_records_error.json",
    "content": "[\n  {\n    \"code\": \"UNAUTHORIZED\",\n    \"message\": \"The customer is not authorized to do this operation.\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/internal/ionos/internal/fixtures/list_zones.json",
    "content": "[\n  {\n    \"id\": \"11af3414-ebba-11e9-8df5-66fbe8a334b4\",\n    \"name\": \"test.com\",\n    \"type\": \"NATIVE\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/internal/ionos/internal/fixtures/list_zones_error.json",
    "content": "[\n  {\n    \"code\": \"UNAUTHORIZED\",\n    \"message\": \"The customer is not authorized to do this operation.\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/internal/ionos/internal/fixtures/remove_record_error.json",
    "content": "[\n  {\n    \"code\": \"UNAUTHORIZED\",\n    \"message\": \"The customer is not authorized to do this operation.\"\n  },\n  {\n    \"code\": \"RECORD_NOT_FOUND\",\n    \"message\": \"Record does not exist.\"\n  },\n  {\n    \"code\": \"INTERNAL_SERVER_ERROR\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/internal/ionos/internal/fixtures/replace_records_error.json",
    "content": "[\n  {\n    \"code\": \"INVALID_RECORD\",\n    \"message\": \"string\",\n    \"parameters\": {\n      \"errorRecord\": {\n        \"id\": \"string\",\n        \"name\": \"string\",\n        \"disabled\": false,\n        \"rootName\": \"string\",\n        \"changeDate\": \"string\",\n        \"type\": \"A\",\n        \"content\": \"string\",\n        \"ttl\": 0,\n        \"prio\": 0\n      },\n      \"requiredFields\": [\n        \"string\"\n      ],\n      \"invalid\": [\n        \"string\"\n      ],\n      \"invalidFields\": [\n        \"string\"\n      ]\n    }\n  },\n  {\n    \"code\": \"UNAUTHORIZED\",\n    \"message\": \"The customer is not authorized to do this operation.\"\n  },\n  {\n    \"code\": \"INTERNAL_SERVER_ERROR\"\n  }\n]"
  },
  {
    "path": "providers/dns/internal/ionos/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// ClientError a detailed error.\ntype ClientError struct {\n\terrors     []Error\n\tStatusCode int\n\tmessage    string\n}\n\nfunc (f ClientError) Error() string {\n\tvar msg strings.Builder\n\n\tmsg.WriteString(strconv.Itoa(f.StatusCode) + \": \")\n\n\tif f.message != \"\" {\n\t\tmsg.WriteString(f.message + \": \")\n\t}\n\n\tfor i, e := range f.errors {\n\t\tif i != 0 {\n\t\t\tmsg.WriteString(\", \")\n\t\t}\n\n\t\tmsg.WriteString(e.Error())\n\t}\n\n\treturn msg.String()\n}\n\nfunc (f ClientError) Unwrap() error {\n\tif len(f.errors) == 0 {\n\t\treturn nil\n\t}\n\n\treturn &f.errors[0]\n}\n\n// Error defines model for error.\ntype Error struct {\n\t// The error code.\n\tCode string `json:\"code,omitempty\"`\n\n\t// The error message.\n\tMessage string `json:\"message,omitempty\"`\n}\n\nfunc (e Error) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", e.Code, e.Message)\n}\n\n// Zone defines model for zone.\ntype Zone struct {\n\t// The zone id.\n\tID string `json:\"id,omitempty\"`\n\n\t// The zone name.\n\tName string `json:\"name,omitempty\"`\n\n\t// Represents the possible zone types.\n\tType string `json:\"type,omitempty\"`\n}\n\n// CustomerZone defines model for customer-zone.\ntype CustomerZone struct {\n\t// The zone id.\n\tID string `json:\"id,omitempty\"`\n\n\t// The zone name\n\tName    string   `json:\"name,omitempty\"`\n\tRecords []Record `json:\"records,omitempty\"`\n\n\t// Represents the possible zone types.\n\tType string `json:\"type,omitempty\"`\n}\n\n// Record defines model for record.\ntype Record struct {\n\tID string `json:\"id,omitempty\"`\n\n\tName    string `json:\"name,omitempty\"`\n\tContent string `json:\"content,omitempty\"`\n\n\t// Time to live for the record, recommended 3600.\n\tTTL int `json:\"ttl,omitempty\"`\n\n\t// Holds supported dns record types.\n\tType string `json:\"type,omitempty\"`\n\n\tPriority int `json:\"prio,omitempty\"`\n\n\t// When is true, the record is not visible for lookup.\n\tDisabled bool `json:\"disabled,omitempty\"`\n}\n\ntype RecordsFilter struct {\n\t// The FQDN used to filter all the record names that end with it.\n\tSuffix string `url:\"suffix,omitempty\"`\n\n\t// The record names that should be included (same as name field of Record)\n\tRecordName string `url:\"recordName,omitempty\"`\n\n\t// A comma-separated list of record types that should be included\n\tRecordType string `url:\"recordType,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/internal/ionos/provider.go",
    "content": "package ionos\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\tionos \"github.com/go-acme/lego/v4/providers/dns/internal/ionos/internal\"\n)\n\nconst MinTTL = 300\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *ionos.Client\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Ionos.\nfunc NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tif config.TTL < MinTTL {\n\t\treturn nil, fmt.Errorf(\"invalid TTL, TTL (%d) must be greater than %d\", config.TTL, MinTTL)\n\t}\n\n\tclient, err := ionos.NewClient(config.APIKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif baseURL != \"\" {\n\t\tclient.BaseURL, _ = url.Parse(baseURL)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzones, err := d.client.ListZones(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get zones: %w\", err)\n\t}\n\n\tname := dns01.UnFqdn(info.EffectiveFQDN)\n\n\tzone := findZone(zones, name)\n\tif zone == nil {\n\t\treturn errors.New(\"no matching zone found for domain\")\n\t}\n\n\tfilter := &ionos.RecordsFilter{\n\t\tSuffix:     name,\n\t\tRecordType: \"TXT\",\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, zone.ID, filter)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get records (zone=%s): %w\", zone.ID, err)\n\t}\n\n\trecords = append(records, ionos.Record{\n\t\tName:    name,\n\t\tContent: info.Value,\n\t\tTTL:     d.config.TTL,\n\t\tType:    \"TXT\",\n\t})\n\n\terr = d.client.ReplaceRecords(ctx, zone.ID, records)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create/update records (zone=%s): %w\", zone.ID, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzones, err := d.client.ListZones(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get zones: %w\", err)\n\t}\n\n\tname := dns01.UnFqdn(info.EffectiveFQDN)\n\n\tzone := findZone(zones, name)\n\tif zone == nil {\n\t\treturn errors.New(\"no matching zone found for domain\")\n\t}\n\n\tfilter := &ionos.RecordsFilter{\n\t\tSuffix:     name,\n\t\tRecordType: \"TXT\",\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, zone.ID, filter)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get records (zone=%s): %w\", zone.ID, err)\n\t}\n\n\tfor _, record := range records {\n\t\tif record.Name == name && record.Content == strconv.Quote(info.Value) {\n\t\t\terr = d.client.RemoveRecord(ctx, zone.ID, record.ID)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to remove record (zone=%s, record=%s): %w\", zone.ID, record.ID, err)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"failed to remove record, record not found (zone=%s, domain=%s, fqdn=%s, value=%s)\", zone.ID, domain, info.EffectiveFQDN, info.Value)\n}\n\nfunc findZone(zones []ionos.Zone, domain string) *ionos.Zone {\n\tvar result *ionos.Zone\n\n\tfor _, zone := range zones {\n\t\tif zone.Name != \"\" && strings.HasSuffix(domain, zone.Name) {\n\t\t\tif result == nil || len(zone.Name) > len(result.Name) {\n\t\t\t\tresult = &zone\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "providers/dns/internal/ionos/provider_test.go",
    "content": "package ionos\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\ttll      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"123\",\n\t\t\ttll:    MinTTL,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\ttll:      MinTTL,\n\t\t\texpected: \"credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"invalid TTL\",\n\t\t\tapiKey:   \"123\",\n\t\t\ttll:      30,\n\t\t\texpected: \"invalid TTL, TTL (30) must be greater than 300\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := &Config{}\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.TTL = test.tll\n\n\t\t\tp, err := NewDNSProviderConfig(config, \"\")\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/internal/ptr/types.go",
    "content": "package ptr\n\nfunc Deref[T any](v *T) T {\n\tif v == nil {\n\t\tvar zero T\n\t\treturn zero\n\t}\n\n\treturn *v\n}\n\nfunc Pointer[T any](v T) *T { return &v }\n"
  },
  {
    "path": "providers/dns/internal/rimuhosting/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\nconst defaultBaseURL = \"https://rimuhosting.com/dns/dyndns.jsp\"\n\n// Action names.\nconst (\n\tSetAction    = \"SET\"\n\tQueryAction  = \"QUERY\"\n\tDeleteAction = \"DELETE\"\n)\n\n// Client the RimuHosting/Zonomi client.\ntype Client struct {\n\tapiKey string\n\n\tHTTPClient *http.Client\n\tBaseURL    string\n}\n\n// NewClient Creates a RimuHosting/Zonomi client.\nfunc NewClient(apiKey string) *Client {\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tBaseURL:    defaultBaseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// FindTXTRecords Finds TXT records.\n// ex:\n// - https://zonomi.com/app/dns/dyndns.jsp?action=QUERY&name=example.com&api_key=apikeyvaluehere\n// - https://zonomi.com/app/dns/dyndns.jsp?action=QUERY&name=**.example.com&api_key=apikeyvaluehere\nfunc (c *Client) FindTXTRecords(ctx context.Context, domain string) ([]Record, error) {\n\taction := ActionParameter{\n\t\tAction: QueryAction,\n\t\tName:   domain,\n\t\tType:   \"TXT\",\n\t}\n\n\tresp, err := c.DoActions(ctx, action)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn resp.Actions.Action.Records, nil\n}\n\n// DoActions performs actions.\nfunc (c *Client) DoActions(ctx context.Context, actions ...ActionParameter) (*DNSAPIResult, error) {\n\tif len(actions) == 0 {\n\t\treturn nil, errors.New(\"no action\")\n\t}\n\n\tresp := &DNSAPIResult{}\n\n\tif len(actions) == 1 {\n\t\taction := actionParameter{\n\t\t\tActionParameter: actions[0],\n\t\t\tAPIKey:          c.apiKey,\n\t\t}\n\n\t\terr := c.do(ctx, action, resp)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn resp, nil\n\t}\n\n\tmulti := c.toMultiParameters(actions)\n\n\terr := c.do(ctx, multi, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) toMultiParameters(params []ActionParameter) multiActionParameter {\n\tmulti := multiActionParameter{\n\t\tAPIKey: c.apiKey,\n\t}\n\n\tfor _, parameters := range params {\n\t\tmulti.Action = append(multi.Action, parameters.Action)\n\t\tmulti.Name = append(multi.Name, parameters.Name)\n\t\tmulti.Type = append(multi.Type, parameters.Type)\n\t\tmulti.Value = append(multi.Value, parameters.Value)\n\t\tmulti.TTL = append(multi.TTL, parameters.TTL)\n\t}\n\n\treturn multi\n}\n\nfunc (c *Client) do(ctx context.Context, params, result any) error {\n\tbaseURL, err := url.Parse(c.BaseURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tv, err := querystring.Values(params)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\texp := regexp.MustCompile(`(%5B)(%5D)(\\d+)=`)\n\tbaseURL.RawQuery = exp.ReplaceAllString(v.Encode(), \"${1}${3}${2}=\")\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL.String(), http.NoBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = xml.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unmarshaling %T error: %w: %s\", result, err, string(raw))\n\t}\n\n\treturn nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\terrAPI := APIError{}\n\n\terr := xml.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn errAPI\n}\n\n// NewAddRecordAction helper to create an action to add a TXT record.\nfunc NewAddRecordAction(domain, content string, ttl int) ActionParameter {\n\treturn ActionParameter{\n\t\tAction: SetAction,\n\t\tName:   domain,\n\t\tType:   \"TXT\",\n\t\tValue:  content,\n\t\tTTL:    ttl,\n\t}\n}\n\n// NewDeleteRecordAction helper to create an action to delete a TXT record.\nfunc NewDeleteRecordAction(domain, content string) ActionParameter {\n\treturn ActionParameter{\n\t\tAction: DeleteAction,\n\t\tName:   domain,\n\t\tType:   \"TXT\",\n\t\tValue:  content,\n\t}\n}\n"
  },
  {
    "path": "providers/dns/internal/rimuhosting/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"encoding/xml\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"apikeyvaluehere\")\n\tclient.BaseURL = server.URL\n\tclient.HTTPClient = server.Client()\n\n\treturn client, nil\n}\n\nfunc TestClient_FindTXTRecords(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tdomain   string\n\t\tresponse string\n\t\tquery    url.Values\n\t\texpected []Record\n\t}{\n\t\t{\n\t\t\tdesc:     \"simple\",\n\t\t\tdomain:   \"example.com\",\n\t\t\tresponse: \"find_records.xml\",\n\t\t\tquery: url.Values{\n\t\t\t\t\"name\":    []string{\"example.com\"},\n\t\t\t\t\"type\":    []string{\"TXT\"},\n\t\t\t\t\"action\":  []string{\"QUERY\"},\n\t\t\t\t\"api_key\": []string{\"apikeyvaluehere\"},\n\t\t\t},\n\t\t\texpected: []Record{\n\t\t\t\t{\n\t\t\t\t\tName:     \"example.org\",\n\t\t\t\t\tType:     \"TXT\",\n\t\t\t\t\tContent:  \"txttxtx\",\n\t\t\t\t\tTTL:      \"3600 seconds\",\n\t\t\t\t\tPriority: \"0\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"pattern\",\n\t\t\tdomain:   \"**.example.com\",\n\t\t\tresponse: \"find_records_pattern.xml\",\n\t\t\tquery: url.Values{\n\t\t\t\t\"name\":    []string{\"**.example.com\"},\n\t\t\t\t\"type\":    []string{\"TXT\"},\n\t\t\t\t\"action\":  []string{\"QUERY\"},\n\t\t\t\t\"api_key\": []string{\"apikeyvaluehere\"},\n\t\t\t},\n\t\t\texpected: []Record{\n\t\t\t\t{\n\t\t\t\t\tName:     \"_test.example.org\",\n\t\t\t\t\tType:     \"TXT\",\n\t\t\t\t\tContent:  \"txttxtx\",\n\t\t\t\t\tTTL:      \"3600 seconds\",\n\t\t\t\t\tPriority: \"0\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:     \"example.org\",\n\t\t\t\t\tType:     \"TXT\",\n\t\t\t\t\tContent:  \"txttxtx\",\n\t\t\t\t\tTTL:      \"3600 seconds\",\n\t\t\t\t\tPriority: \"0\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"empty\",\n\t\t\tdomain:   \"empty.com\",\n\t\t\tresponse: \"find_records_empty.xml\",\n\t\t\tquery: url.Values{\n\t\t\t\t\"name\":    []string{\"empty.com\"},\n\t\t\t\t\"type\":    []string{\"TXT\"},\n\t\t\t\t\"action\":  []string{\"QUERY\"},\n\t\t\t\t\"api_key\": []string{\"apikeyvaluehere\"},\n\t\t\t},\n\t\t\texpected: nil,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\t\t\tRoute(\"GET /\",\n\t\t\t\t\tservermock.ResponseFromFixture(test.response),\n\t\t\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\t\t\tWithValues(test.query)).\n\t\t\t\tBuild(t)\n\n\t\t\trecords, err := client.FindTXTRecords(t.Context(), test.domain)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.expected, records)\n\t\t})\n\t}\n}\n\nfunc TestClient_DoActions(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tactions  []ActionParameter\n\t\tquery    url.Values\n\t\tresponse string\n\t\texpected *DNSAPIResult\n\t}{\n\t\t{\n\t\t\tdesc: \"SET simple\",\n\t\t\tactions: []ActionParameter{\n\t\t\t\tNewAddRecordAction(\"example.org\", \"txttxtx\", 0),\n\t\t\t},\n\t\t\tresponse: \"add_record.xml\",\n\t\t\tquery: url.Values{\n\t\t\t\t\"action\":  []string{\"SET\"},\n\t\t\t\t\"name\":    []string{\"example.org\"},\n\t\t\t\t\"type\":    []string{\"TXT\"},\n\t\t\t\t\"value\":   []string{\"txttxtx\"},\n\t\t\t\t\"api_key\": []string{\"apikeyvaluehere\"},\n\t\t\t},\n\t\t\texpected: &DNSAPIResult{\n\t\t\t\tXMLName:      xml.Name{Space: \"\", Local: \"dnsapi_result\"},\n\t\t\t\tIsOk:         \"OK:\",\n\t\t\t\tResultCounts: ResultCounts{Added: \"1\", Changed: \"0\", Unchanged: \"0\", Deleted: \"0\"},\n\t\t\t\tActions: Actions{\n\t\t\t\t\tAction: Action{\n\t\t\t\t\t\tAction: \"SET\",\n\t\t\t\t\t\tHost:   \"example.org\",\n\t\t\t\t\t\tType:   \"TXT\",\n\t\t\t\t\t\tRecords: []Record{{\n\t\t\t\t\t\t\tName:     \"example.org\",\n\t\t\t\t\t\t\tType:     \"TXT\",\n\t\t\t\t\t\t\tContent:  \"txttxtx\",\n\t\t\t\t\t\t\tTTL:      \"3600 seconds\",\n\t\t\t\t\t\t\tPriority: \"0\",\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"SET multiple values\",\n\t\t\tactions: []ActionParameter{\n\t\t\t\tNewAddRecordAction(\"example.org\", \"txttxtx\", 0),\n\t\t\t\tNewAddRecordAction(\"example.org\", \"sample\", 0),\n\t\t\t},\n\t\t\tresponse: \"add_record_same_domain.xml\",\n\t\t\tquery: url.Values{\n\t\t\t\t\"api_key\":   []string{\"apikeyvaluehere\"},\n\t\t\t\t\"action[0]\": []string{\"SET\"},\n\t\t\t\t\"name[0]\":   []string{\"example.org\"},\n\t\t\t\t\"ttl[0]\":    []string{\"0\"},\n\t\t\t\t\"type[0]\":   []string{\"TXT\"},\n\t\t\t\t\"value[0]\":  []string{\"txttxtx\"},\n\t\t\t\t\"action[1]\": []string{\"SET\"},\n\t\t\t\t\"name[1]\":   []string{\"example.org\"},\n\t\t\t\t\"ttl[1]\":    []string{\"0\"},\n\t\t\t\t\"type[1]\":   []string{\"TXT\"},\n\t\t\t\t\"value[1]\":  []string{\"sample\"},\n\t\t\t},\n\t\t\texpected: &DNSAPIResult{\n\t\t\t\tXMLName:      xml.Name{Space: \"\", Local: \"dnsapi_result\"},\n\t\t\t\tIsOk:         \"OK:\",\n\t\t\t\tResultCounts: ResultCounts{Added: \"2\", Changed: \"0\", Unchanged: \"0\", Deleted: \"0\"},\n\t\t\t\tActions: Actions{\n\t\t\t\t\tAction: Action{\n\t\t\t\t\t\tAction: \"SET\",\n\t\t\t\t\t\tHost:   \"example.org\",\n\t\t\t\t\t\tType:   \"TXT\",\n\t\t\t\t\t\tRecords: []Record{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tName:     \"example.org\",\n\t\t\t\t\t\t\t\tType:     \"TXT\",\n\t\t\t\t\t\t\t\tContent:  \"txttxtx\",\n\t\t\t\t\t\t\t\tTTL:      \"0 seconds\",\n\t\t\t\t\t\t\t\tPriority: \"0\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tName:     \"example.org\",\n\t\t\t\t\t\t\t\tType:     \"TXT\",\n\t\t\t\t\t\t\t\tContent:  \"sample\",\n\t\t\t\t\t\t\t\tTTL:      \"0 seconds\",\n\t\t\t\t\t\t\t\tPriority: \"0\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"DELETE nothing\",\n\t\t\tactions: []ActionParameter{\n\t\t\t\tNewDeleteRecordAction(\"example.org\", \"nothing\"),\n\t\t\t},\n\t\t\tresponse: \"delete_record_nothing.xml\",\n\t\t\tquery: url.Values{\n\t\t\t\t\"action\":  []string{\"DELETE\"},\n\t\t\t\t\"name\":    []string{\"example.org\"},\n\t\t\t\t\"type\":    []string{\"TXT\"},\n\t\t\t\t\"value\":   []string{\"nothing\"},\n\t\t\t\t\"api_key\": []string{\"apikeyvaluehere\"},\n\t\t\t},\n\t\t\texpected: &DNSAPIResult{\n\t\t\t\tXMLName:      xml.Name{Space: \"\", Local: \"dnsapi_result\"},\n\t\t\t\tIsOk:         \"OK:\",\n\t\t\t\tResultCounts: ResultCounts{Added: \"0\", Changed: \"0\", Unchanged: \"0\", Deleted: \"0\"},\n\t\t\t\tActions: Actions{\n\t\t\t\t\tAction: Action{\n\t\t\t\t\t\tAction:  \"DELETE\",\n\t\t\t\t\t\tHost:    \"example.org\",\n\t\t\t\t\t\tType:    \"TXT\",\n\t\t\t\t\t\tRecords: nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"DELETE simple\",\n\t\t\tactions: []ActionParameter{\n\t\t\t\tNewDeleteRecordAction(\"example.org\", \"txttxtx\"),\n\t\t\t},\n\t\t\tresponse: \"delete_record.xml\",\n\t\t\tquery: url.Values{\n\t\t\t\t\"action\":  []string{\"DELETE\"},\n\t\t\t\t\"name\":    []string{\"example.org\"},\n\t\t\t\t\"type\":    []string{\"TXT\"},\n\t\t\t\t\"value\":   []string{\"txttxtx\"},\n\t\t\t\t\"api_key\": []string{\"apikeyvaluehere\"},\n\t\t\t},\n\t\t\texpected: &DNSAPIResult{\n\t\t\t\tXMLName:      xml.Name{Space: \"\", Local: \"dnsapi_result\"},\n\t\t\t\tIsOk:         \"OK:\",\n\t\t\t\tResultCounts: ResultCounts{Added: \"0\", Changed: \"0\", Unchanged: \"0\", Deleted: \"1\"},\n\t\t\t\tActions: Actions{\n\t\t\t\t\tAction: Action{\n\t\t\t\t\t\tAction: \"DELETE\",\n\t\t\t\t\t\tHost:   \"example.org\",\n\t\t\t\t\t\tType:   \"TXT\",\n\t\t\t\t\t\tRecords: []Record{{\n\t\t\t\t\t\t\tName:     \"example.org\",\n\t\t\t\t\t\t\tType:     \"TXT\",\n\t\t\t\t\t\t\tContent:  \"txttxtx\",\n\t\t\t\t\t\t\tTTL:      \"3600 seconds\",\n\t\t\t\t\t\t\tPriority: \"0\",\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\t\t\tRoute(\"GET /\",\n\t\t\t\t\tservermock.ResponseFromFixture(test.response),\n\t\t\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\t\t\tWithValues(test.query)).\n\t\t\t\tBuild(t)\n\n\t\t\tresp, err := client.DoActions(t.Context(), test.actions...)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.expected, resp)\n\t\t})\n\t}\n}\n\nfunc TestClient_DoActions_error(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tactions  []ActionParameter\n\t\tquery    url.Values\n\t\tresponse string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"SET error\",\n\t\t\tactions: []ActionParameter{\n\t\t\t\tNewAddRecordAction(\"example.com\", \"txttxtx\", 0),\n\t\t\t},\n\t\t\tresponse: \"add_record_error.xml\",\n\t\t\tquery: url.Values{\n\t\t\t\t\"action\":  []string{\"SET\"},\n\t\t\t\t\"name\":    []string{\"example.com\"},\n\t\t\t\t\"type\":    []string{\"TXT\"},\n\t\t\t\t\"value\":   []string{\"txttxtx\"},\n\t\t\t\t\"api_key\": []string{\"apikeyvaluehere\"},\n\t\t\t},\n\t\t\texpected: \"ERROR: No zone found for example.com\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"DELETE error\",\n\t\t\tactions: []ActionParameter{\n\t\t\t\tNewDeleteRecordAction(\"example.com\", \"txttxtx\"),\n\t\t\t},\n\t\t\tresponse: \"delete_record_error.xml\",\n\t\t\tquery: url.Values{\n\t\t\t\t\"action\":  []string{\"DELETE\"},\n\t\t\t\t\"name\":    []string{\"example.com\"},\n\t\t\t\t\"type\":    []string{\"TXT\"},\n\t\t\t\t\"value\":   []string{\"txttxtx\"},\n\t\t\t\t\"api_key\": []string{\"apikeyvaluehere\"},\n\t\t\t},\n\t\t\texpected: \"ERROR: No zone found for example.com\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\t\t\tRoute(\"GET /\",\n\t\t\t\t\tservermock.ResponseFromFixture(test.response).\n\t\t\t\t\t\tWithStatusCode(http.StatusInternalServerError),\n\t\t\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\t\t\tWithValues(test.query)).\n\t\t\t\tBuild(t)\n\n\t\t\t_, err := client.DoActions(t.Context(), test.actions...)\n\t\t\trequire.EqualError(t, err, test.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/internal/rimuhosting/internal/fixtures/add_record.xml",
    "content": "<?xml version =\"1.0\"  ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]><dnsapi_result><is_ok>OK:</is_ok>\n    <result_counts\n            added=\"1\"\n            changed=\"0\"\n            unchanged=\"0\"\n            deleted=\"0\"/>\n    <actions>\n\n        <action\n                action=\"SET\"\n                host=\"example.org\"\n                type=\"TXT\"\n                value=\"txttxtx\">\n            <record\n                    name=\"example.org\"\n                    type=\"TXT\"\n                    content=\"txttxtx\"\n                    ttl=\"3600 seconds\"\n                    prio=\"0\"\n                    added=\"\"/>\t</action></actions></dnsapi_result>"
  },
  {
    "path": "providers/dns/internal/rimuhosting/internal/fixtures/add_record_error.xml",
    "content": "<?xml version =\"1.0\"  ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]>\n<error>ERROR: No zone found for example.com</error>"
  },
  {
    "path": "providers/dns/internal/rimuhosting/internal/fixtures/add_record_same_domain.xml",
    "content": "<?xml version =\"1.0\"  ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]><dnsapi_result><is_ok>OK:</is_ok>\n    <result_counts\n            added=\"2\"\n            changed=\"0\"\n            unchanged=\"0\"\n            deleted=\"0\"/>\n    <actions>\n\n        <action\n                action=\"SET\"\n                host=\"example.org\"\n                type=\"TXT\"\n                value=\"txttxtx\"\n                ttl=\"0\">\n            <record\n                    name=\"example.org\"\n                    type=\"TXT\"\n                    content=\"txttxtx\"\n                    ttl=\"0 seconds\"\n                    prio=\"0\"\n                    added=\"\"/>\t</action>\n        <action\n                action=\"SET\"\n                host=\"example.org\"\n                type=\"TXT\"\n                value=\"sample\"\n                ttl=\"0\">\n            <record\n                    name=\"example.org\"\n                    type=\"TXT\"\n                    content=\"sample\"\n                    ttl=\"0 seconds\"\n                    prio=\"0\"\n                    added=\"\"/>\t</action></actions></dnsapi_result>"
  },
  {
    "path": "providers/dns/internal/rimuhosting/internal/fixtures/delete_record.xml",
    "content": "<?xml version =\"1.0\"  ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]><dnsapi_result><is_ok>OK:</is_ok>\n    <result_counts\n            added=\"0\"\n            changed=\"0\"\n            unchanged=\"0\"\n            deleted=\"1\"/>\n    <actions>\n\n        <action\n                action=\"DELETE\"\n                host=\"example.org\"\n                type=\"TXT\"\n                value=\"txttxtx\">\n            <record\n                    name=\"example.org\"\n                    type=\"TXT\"\n                    content=\"txttxtx\"\n                    ttl=\"3600 seconds\"\n                    prio=\"0\"\n                    deleted=\"\"/>\t</action></actions></dnsapi_result>"
  },
  {
    "path": "providers/dns/internal/rimuhosting/internal/fixtures/delete_record_error.xml",
    "content": "<?xml version =\"1.0\"  ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]>\n<error>ERROR: No zone found for example.com</error>"
  },
  {
    "path": "providers/dns/internal/rimuhosting/internal/fixtures/delete_record_nothing.xml",
    "content": "<?xml version =\"1.0\"  ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]><dnsapi_result><is_ok>OK:</is_ok>\n    <result_counts\n            added=\"0\"\n            changed=\"0\"\n            unchanged=\"0\"\n            deleted=\"0\"/>\n    <actions>\n\n        <action\n                action=\"DELETE\"\n                host=\"example.org\"\n                type=\"TXT\"\n                value=\"aaaa\"></action></actions></dnsapi_result>"
  },
  {
    "path": "providers/dns/internal/rimuhosting/internal/fixtures/find_records.xml",
    "content": "<?xml version =\"1.0\"  ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]><dnsapi_result><is_ok>OK:</is_ok>\n    <result_counts\n            added=\"0\"\n            changed=\"0\"\n            unchanged=\"0\"\n            deleted=\"0\"/>\n    <actions>\n\n        <action\n                action=\"QUERY\"\n                host=\"example.org\"\n                type=\"TXT\">\n            <record\n                    name=\"example.org\"\n                    type=\"TXT\"\n                    content=\"txttxtx\"\n                    ttl=\"3600 seconds\"\n                    prio=\"0\"/>\t</action></actions></dnsapi_result>"
  },
  {
    "path": "providers/dns/internal/rimuhosting/internal/fixtures/find_records_empty.xml",
    "content": "<?xml version =\"1.0\"  ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]><dnsapi_result><is_ok>OK:</is_ok>\n    <result_counts\n            added=\"0\"\n            changed=\"0\"\n            unchanged=\"0\"\n            deleted=\"0\"/>\n    <actions>\n\n        <action\n                action=\"QUERY\"\n                host=\"example.org\"\n                type=\"TXT\"></action></actions></dnsapi_result>\n"
  },
  {
    "path": "providers/dns/internal/rimuhosting/internal/fixtures/find_records_pattern.xml",
    "content": "<?xml version =\"1.0\"  ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]><dnsapi_result><is_ok>OK:</is_ok>\n    <result_counts\n            added=\"0\"\n            changed=\"0\"\n            unchanged=\"0\"\n            deleted=\"0\"/>\n    <actions>\n\n        <action\n                action=\"QUERY\"\n                host=\"_test.example.org, example.org\"\n                type=\"TXT\">\n            <record\n                    name=\"_test.example.org\"\n                    type=\"TXT\"\n                    content=\"txttxtx\"\n                    ttl=\"3600 seconds\"\n                    prio=\"0\"/>\n            <record\n                    name=\"example.org\"\n                    type=\"TXT\"\n                    content=\"txttxtx\"\n                    ttl=\"3600 seconds\"\n                    prio=\"0\"/>\t</action></actions></dnsapi_result>"
  },
  {
    "path": "providers/dns/internal/rimuhosting/internal/types.go",
    "content": "package internal\n\nimport \"encoding/xml\"\n\ntype ActionParameter struct {\n\tAction   string `url:\"action,omitempty\"`\n\tName     string `url:\"name,omitempty\"`\n\tType     string `url:\"type,omitempty\"`\n\tValue    string `url:\"value,omitempty\"`\n\tTTL      int    `url:\"ttl,omitempty\"`\n\tPriority int    `url:\"prio,omitempty\"`\n}\n\ntype actionParameter struct {\n\tActionParameter\n\n\tAPIKey string `url:\"api_key,omitempty\"`\n}\n\ntype multiActionParameter struct {\n\tAPIKey string `url:\"api_key,omitempty\"`\n\n\tAction   []string `url:\"action,brackets,numbered,omitempty\"`\n\tName     []string `url:\"name,brackets,numbered,omitempty\"`\n\tType     []string `url:\"type,brackets,numbered,omitempty\"`\n\tValue    []string `url:\"value,brackets,numbered,omitempty\"`\n\tTTL      []int    `url:\"ttl,brackets,numbered,omitempty\"`\n\tPriority []int    `url:\"prio,brackets,numbered,omitempty\"`\n}\n\ntype APIError struct {\n\tXMLName xml.Name `xml:\"error\"`\n\tText    string   `xml:\",chardata\"`\n}\n\nfunc (a APIError) Error() string {\n\treturn a.Text\n}\n\ntype DNSAPIResult struct {\n\tXMLName      xml.Name     `xml:\"dnsapi_result\"`\n\tIsOk         string       `xml:\"is_ok\"`\n\tResultCounts ResultCounts `xml:\"result_counts\"`\n\tActions      Actions      `xml:\"actions\"`\n}\n\ntype ResultCounts struct {\n\tAdded     string `xml:\"added,attr\"`\n\tChanged   string `xml:\"changed,attr\"`\n\tUnchanged string `xml:\"unchanged,attr\"`\n\tDeleted   string `xml:\"deleted,attr\"`\n}\n\ntype Actions struct {\n\tAction Action `xml:\"action\"`\n}\n\ntype Action struct {\n\tAction  string   `xml:\"action,attr\"`\n\tHost    string   `xml:\"host,attr\"`\n\tType    string   `xml:\"type,attr\"`\n\tRecords []Record `xml:\"record\"`\n}\n\ntype Record struct {\n\tName     string `xml:\"name,attr\"`\n\tType     string `xml:\"type,attr\"`\n\tContent  string `xml:\"content,attr\"`\n\tTTL      string `xml:\"ttl,attr\"`\n\tPriority string `xml:\"prio,attr\"`\n}\n"
  },
  {
    "path": "providers/dns/internal/rimuhosting/provider.go",
    "content": "// Package rimuhosting implements a DNS provider for solving the DNS-01 challenge using RimuHosting DNS.\npackage rimuhosting\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/rimuhosting/internal\"\n)\n\nconst DefaultTTL = 3600\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for RimuHosting.\nfunc NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"incomplete credentials, missing API key\")\n\t}\n\n\tclient := internal.NewClient(config.APIKey)\n\n\tif baseURL != \"\" {\n\t\tclient.BaseURL = baseURL\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\trecords, err := d.client.FindTXTRecords(ctx, dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to find record(s) for %s: %w\", domain, err)\n\t}\n\n\tactions := []internal.ActionParameter{\n\t\tinternal.NewAddRecordAction(dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL),\n\t}\n\n\tfor _, record := range records {\n\t\tactions = append(actions, internal.NewAddRecordAction(record.Name, record.Content, d.config.TTL))\n\t}\n\n\t_, err = d.client.DoActions(ctx, actions...)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to add record(s) for %s: %w\", domain, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\taction := internal.NewDeleteRecordAction(dns01.UnFqdn(info.EffectiveFQDN), info.Value)\n\n\t_, err := d.client.DoActions(context.Background(), action)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete record for %s: %w\", domain, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/internal/rimuhosting/provider_test.go",
    "content": "package rimuhosting\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\texpected  string\n\t\tapiKey    string\n\t\tsecretKey string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tapiKey:    \"api_key\",\n\t\t\tsecretKey: \"api_secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing api key\",\n\t\t\tapiKey:    \"\",\n\t\t\tsecretKey: \"api_secret\",\n\t\t\texpected:  \"incomplete credentials, missing API key\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := &Config{}\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config, \"\")\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/internal/selectel/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api.selectel.ru/domains/v1\"\n\nconst tokenHeader = \"X-Token\"\n\n// Client represents the DNS client.\ntype Client struct {\n\ttoken string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient returns a client instance.\nfunc NewClient(token string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\ttoken:      token,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// GetDomainByName gets Domain object by its name. If `domainName` level > 2 and there is\n// no such domain on the account - it'll recursively search for the first\n// which is exists in Selectel Domain API.\nfunc (c *Client) GetDomainByName(ctx context.Context, domainName string) (*Domain, error) {\n\treq, err := newJSONRequest(ctx, http.MethodGet, c.BaseURL.JoinPath(domainName), nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdomain := &Domain{}\n\n\tstatusCode, err := c.do(req, domain)\n\tif err != nil {\n\t\tif statusCode == http.StatusNotFound && strings.Count(domainName, \".\") > 1 {\n\t\t\t// Look up for the next subdomain\n\t\t\t_, after, _ := strings.Cut(domainName, \".\")\n\t\t\treturn c.GetDomainByName(ctx, after)\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\treturn domain, nil\n}\n\n// AddRecord adds Record for given domain.\nfunc (c *Client) AddRecord(ctx context.Context, domainID int, body Record) (*Record, error) {\n\treq, err := newJSONRequest(ctx, http.MethodPost, c.BaseURL.JoinPath(strconv.Itoa(domainID), \"records\", \"/\"), body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trecord := &Record{}\n\n\t_, err = c.do(req, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn record, nil\n}\n\n// ListRecords returns list records for specific domain.\nfunc (c *Client) ListRecords(ctx context.Context, domainID int) ([]Record, error) {\n\treq, err := newJSONRequest(ctx, http.MethodGet, c.BaseURL.JoinPath(strconv.Itoa(domainID), \"records\", \"/\"), nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar records []Record\n\n\t_, err = c.do(req, &records)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn records, nil\n}\n\n// DeleteRecord deletes specific record.\nfunc (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int) error {\n\tendpoint := c.BaseURL.JoinPath(strconv.Itoa(domainID), \"records\", strconv.Itoa(recordID))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = c.do(req, nil)\n\n\treturn err\n}\n\nfunc (c *Client) do(req *http.Request, result any) (int, error) {\n\treq.Header.Set(tokenHeader, c.token)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn 0, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn resp.StatusCode, parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn resp.StatusCode, nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn resp.StatusCode, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn resp.StatusCode, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn resp.StatusCode, nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\terrAPI := &APIError{}\n\n\terr := json.Unmarshal(raw, errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"request failed with status code %d: %w\", resp.StatusCode, errAPI)\n}\n"
  },
  {
    "path": "providers/dns/internal/selectel/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"token\")\n\tclient.BaseURL, _ = url.Parse(server.URL)\n\tclient.HTTPClient = server.Client()\n\n\treturn client, nil\n}\n\nfunc TestClient_ListRecords(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithJSONHeaders()).\n\t\tRoute(\"GET /123/records/\", servermock.ResponseFromFixture(\"list_records.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.ListRecords(t.Context(), 123)\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{ID: 123, Name: \"example.com\", Type: \"TXT\", TTL: 60, Email: \"email@example.com\", Content: \"txttxttxtA\"},\n\t\t{ID: 1234, Name: \"example.org\", Type: \"TXT\", TTL: 60, Email: \"email@example.org\", Content: \"txttxttxtB\"},\n\t\t{ID: 12345, Name: \"example.net\", Type: \"TXT\", TTL: 60, Email: \"email@example.net\", Content: \"txttxttxtC\"},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_ListRecords_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(tokenHeader, \"token\")).\n\t\tRoute(\"GET /123/records/\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").WithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\trecords, err := client.ListRecords(t.Context(), 123)\n\n\trequire.EqualError(t, err, \"request failed with status code 401: API error: 400 - error description - field that the error occurred in\")\n\tassert.Nil(t, records)\n}\n\nfunc TestClient_GetDomainByName(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(tokenHeader, \"token\")).\n\t\tRoute(\"GET /sub.sub.example.org\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusNotFound)).\n\t\tRoute(\"GET /sub.example.org\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusNotFound)).\n\t\tRoute(\"GET /example.org\",\n\t\t\tservermock.ResponseFromFixture(\"domains.json\")).\n\t\tBuild(t)\n\n\tdomain, err := client.GetDomainByName(t.Context(), \"sub.sub.example.org\")\n\trequire.NoError(t, err)\n\n\texpected := &Domain{\n\t\tID:   123,\n\t\tName: \"example.org\",\n\t}\n\n\tassert.Equal(t, expected, domain)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(tokenHeader, \"token\")).\n\t\tRoute(\"POST /123/records/\",\n\t\t\tservermock.ResponseFromFixture(\"add_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"add_record-request.json\")).\n\t\tBuild(t)\n\n\trecord, err := client.AddRecord(t.Context(), 123, Record{\n\t\tName:    \"example.org\",\n\t\tType:    \"TXT\",\n\t\tTTL:     60,\n\t\tEmail:   \"email@example.org\",\n\t\tContent: \"txttxttxttxt\",\n\t})\n\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tID:      456,\n\t\tName:    \"example.org\",\n\t\tType:    \"TXT\",\n\t\tTTL:     60,\n\t\tEmail:   \"email@example.org\",\n\t\tContent: \"txttxttxttxt\",\n\t}\n\n\tassert.Equal(t, expected, record)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(tokenHeader, \"token\")).\n\t\tRoute(\"DELETE /123/records/456\", nil).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), 123, 456)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/internal/selectel/internal/fixtures/add_record-request.json",
    "content": "{\n  \"name\": \"example.org\",\n  \"type\": \"TXT\",\n  \"ttl\": 60,\n  \"email\": \"email@example.org\",\n  \"content\": \"txttxttxttxt\"\n}\n"
  },
  {
    "path": "providers/dns/internal/selectel/internal/fixtures/add_record.json",
    "content": "{\n  \"id\": 456,\n  \"name\": \"example.org\",\n  \"type\": \"TXT\",\n  \"ttl\": 60,\n  \"email\": \"email@example.org\",\n  \"content\": \"txttxttxttxt\"\n}\n"
  },
  {
    "path": "providers/dns/internal/selectel/internal/fixtures/domains.json",
    "content": "{\n  \"id\": 123,\n  \"name\": \"example.org\"\n}"
  },
  {
    "path": "providers/dns/internal/selectel/internal/fixtures/error.json",
    "content": "{\n  \"error\": \"error description\",\n  \"code\": 400,\n  \"field\": \"field that the error occurred in\"\n}"
  },
  {
    "path": "providers/dns/internal/selectel/internal/fixtures/list_records.json",
    "content": "[\n  {\n    \"id\": 123,\n    \"name\": \"example.com\",\n    \"type\": \"TXT\",\n    \"ttl\": 60,\n    \"email\": \"email@example.com\",\n    \"content\": \"txttxttxtA\"\n  },\n  {\n    \"id\": 1234,\n    \"name\": \"example.org\",\n    \"type\": \"TXT\",\n    \"ttl\": 60,\n    \"email\": \"email@example.org\",\n    \"content\": \"txttxttxtB\"\n  },\n  {\n    \"id\": 12345,\n    \"name\": \"example.net\",\n    \"type\": \"TXT\",\n    \"ttl\": 60,\n    \"email\": \"email@example.net\",\n    \"content\": \"txttxttxtC\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/internal/selectel/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\n// Domain represents domain name.\ntype Domain struct {\n\tID   int    `json:\"id,omitempty\"`\n\tName string `json:\"name,omitempty\"`\n}\n\n// Record represents DNS record.\ntype Record struct {\n\tID      int    `json:\"id,omitempty\"`\n\tName    string `json:\"name,omitempty\"`\n\tType    string `json:\"type,omitempty\"` // Record type (SOA, NS, A/AAAA, CNAME, SRV, MX, TXT, SPF)\n\tTTL     int    `json:\"ttl,omitempty\"`\n\tEmail   string `json:\"email,omitempty\"`   // Email of domain's admin (only for SOA records)\n\tContent string `json:\"content,omitempty\"` // Record content (not for SRV)\n}\n\n// APIError API error message.\ntype APIError struct {\n\tDescription string `json:\"error\"`\n\tCode        int    `json:\"code\"`\n\tField       string `json:\"field\"`\n}\n\nfunc (a APIError) Error() string {\n\treturn fmt.Sprintf(\"API error: %d - %s - %s\", a.Code, a.Description, a.Field)\n}\n"
  },
  {
    "path": "providers/dns/internal/selectel/provider.go",
    "content": "// Package selectel implements a DNS provider for solving the DNS-01 challenge using Selectel Domains API.\npackage selectel\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/selectel/internal\"\n)\n\nconst MinTTL = 60\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken              string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n\n\t// TODO(ldez): remove in v5?\n\tBaseURL string\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for selectel.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tif config.TTL < MinTTL {\n\t\treturn nil, fmt.Errorf(\"invalid TTL, TTL (%d) must be greater than %d\", config.TTL, MinTTL)\n\t}\n\n\tclient := internal.NewClient(config.Token)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\tvar err error\n\n\tclient.BaseURL, err = url.Parse(config.BaseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%w\", err)\n\t}\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the Timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill DNS-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\tdomainObj, err := d.client.GetDomainByName(ctx, domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get domain by name: %w\", err)\n\t}\n\n\ttxtRecord := internal.Record{\n\t\tType:    \"TXT\",\n\t\tTTL:     d.config.TTL,\n\t\tName:    info.EffectiveFQDN,\n\t\tContent: info.Value,\n\t}\n\n\t_, err = d.client.AddRecord(ctx, domainObj.ID, txtRecord)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes a TXT record used for DNS-01 challenge.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecordName := dns01.UnFqdn(info.EffectiveFQDN)\n\n\tctx := context.Background()\n\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\tdomainObj, err := d.client.GetDomainByName(ctx, domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%w\", err)\n\t}\n\n\trecords, err := d.client.ListRecords(ctx, domainObj.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"list records: %w\", err)\n\t}\n\n\t// Delete records with specific FQDN\n\tvar lastErr error\n\n\tfor _, record := range records {\n\t\tif record.Name == recordName {\n\t\t\terr = d.client.DeleteRecord(ctx, domainObj.ID, record.ID)\n\t\t\tif err != nil {\n\t\t\t\tlastErr = fmt.Errorf(\"delete record: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn lastErr\n}\n"
  },
  {
    "path": "providers/dns/internal/selectel/provider_test.go",
    "content": "package selectel\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\tttl      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"123\",\n\t\t\tttl:   60,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api key\",\n\t\t\ttoken:    \"\",\n\t\t\tttl:      60,\n\t\t\texpected: \"credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"bad TTL value\",\n\t\t\ttoken:    \"123\",\n\t\t\tttl:      59,\n\t\t\texpected: fmt.Sprintf(\"invalid TTL, TTL (59) must be greater than %d\", MinTTL),\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := &Config{}\n\t\t\tconfig.TTL = test.ttl\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/internal/tecnocratica/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\n// defaultBaseURL is the default API endpoint.\nconst defaultBaseURL = \"https://api.neodigit.net/v1\"\n\n// Client is a Tecnocrática API client.\ntype Client struct {\n\ttoken string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(token string) (*Client, error) {\n\tif token == \"\" {\n\t\treturn nil, errors.New(\"credentials missing: token\")\n\t}\n\n\tbaseURL, err := url.Parse(defaultBaseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\ttoken:      token,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 30 * time.Second},\n\t}, nil\n}\n\n// GetZones lists all DNS zones.\nfunc (c *Client) GetZones(ctx context.Context) ([]Zone, error) {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", \"zones\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar zones []Zone\n\n\terr = c.do(req, &zones)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn zones, nil\n}\n\n// GetRecords lists all records in a zone.\nfunc (c *Client) GetRecords(ctx context.Context, zoneID int, recordType string) ([]Record, error) {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", \"zones\", strconv.Itoa(zoneID), \"records\")\n\n\tif recordType != \"\" {\n\t\tquery := endpoint.Query()\n\t\tquery.Set(\"type\", recordType)\n\t\tendpoint.RawQuery = query.Encode()\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar records []Record\n\n\terr = c.do(req, &records)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn records, nil\n}\n\n// CreateRecord creates a new DNS record.\nfunc (c *Client) CreateRecord(ctx context.Context, zoneID int, record Record) (*Record, error) {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", \"zones\", strconv.Itoa(zoneID), \"records\")\n\n\tpayload := RecordRequest{Record: record}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result Record\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &result, nil\n}\n\n// DeleteRecord deletes a DNS record.\nfunc (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID int) error {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", \"zones\", strconv.Itoa(zoneID), \"records\", strconv.Itoa(recordID))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\treq.Header.Set(\"X-TCpanel-Token\", c.token)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\traw, _ := io.ReadAll(resp.Body)\n\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/internal/tecnocratica/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(\"X-TCpanel-Token\", \"secret\"))\n}\n\nfunc TestClient_GetZones(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/zones\",\n\t\t\tservermock.ResponseFromFixture(\"get_zones.json\")).\n\t\tBuild(t)\n\n\tzones, err := client.GetZones(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []Zone{\n\t\t{\n\t\t\tID:        6,\n\t\t\tName:      \"example.com\",\n\t\t\tHumanName: \"example.com\",\n\t\t},\n\t\t{\n\t\t\tID:        7,\n\t\t\tName:      \"example.org\",\n\t\t\tHumanName: \"example.org\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_GetZones_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/zones\",\n\t\t\tservermock.RawStringResponse(`{\"error\": \"unauthorized\"}`).\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\tzones, err := client.GetZones(t.Context())\n\trequire.Error(t, err)\n\n\tassert.Nil(t, zones)\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/zones/6/records\",\n\t\t\tservermock.ResponseFromFixture(\"get_records.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), 6, \"\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tID:      98,\n\t\t\tName:    \"\",\n\t\t\tType:    \"SOA\",\n\t\t\tContent: \"ns1.example.org dns.example.org 2015092102 7200 7200 1209600 1800\",\n\t\t\tTTL:     7200,\n\t\t},\n\t\t{\n\t\t\tID:      99,\n\t\t\tName:    \"\",\n\t\t\tType:    \"NS\",\n\t\t\tContent: \"ns1.example.org\",\n\t\t\tTTL:     7200,\n\t\t},\n\t\t{\n\t\t\tID:      100,\n\t\t\tName:    \"_acme-challenge\",\n\t\t\tType:    \"TXT\",\n\t\t\tContent: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\t\tTTL:     120,\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/zones/6/records\",\n\t\t\tservermock.ResponseFromFixture(\"create_record.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:    \"_acme-challenge\",\n\t\tType:    \"TXT\",\n\t\tContent: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:     120,\n\t}\n\n\tresult, err := client.CreateRecord(t.Context(), 6, record)\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tID:      101,\n\t\tName:    \"_acme-challenge\",\n\t\tType:    \"TXT\",\n\t\tContent: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:     120,\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_CreateRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/zones/6/records\",\n\t\t\tservermock.RawStringResponse(`{\"error\": \"bad request\"}`).\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:    \"_acme-challenge\",\n\t\tType:    \"TXT\",\n\t\tContent: \"test-value\",\n\t\tTTL:     120,\n\t}\n\n\tresult, err := client.CreateRecord(t.Context(), 6, record)\n\trequire.Error(t, err)\n\n\tassert.Nil(t, result)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns/zones/6/records/101\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), 6, 101)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns/zones/6/records/999\",\n\t\t\tservermock.RawStringResponse(`{\"error\": \"not found\"}`).\n\t\t\t\tWithStatusCode(http.StatusNotFound)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), 6, 999)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "providers/dns/internal/tecnocratica/internal/fixtures/create_record-request.json",
    "content": "{\n  \"record\": {\n    \"name\": \"_acme-challenge\",\n    \"type\": \"TXT\",\n    \"content\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n    \"ttl\": 120\n  }\n}\n"
  },
  {
    "path": "providers/dns/internal/tecnocratica/internal/fixtures/create_record.json",
    "content": "{\n  \"id\": 101,\n  \"name\": \"_acme-challenge\",\n  \"type\": \"TXT\",\n  \"content\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n  \"ttl\": 120,\n  \"prio\": null,\n  \"created_at\": \"2015-09-21T14:40:27.127+02:00\",\n  \"updated_at\": \"2015-09-21T14:40:27.127+02:00\"\n}\n"
  },
  {
    "path": "providers/dns/internal/tecnocratica/internal/fixtures/get_records.json",
    "content": "[\n  {\n    \"id\": 98,\n    \"name\": \"\",\n    \"type\": \"SOA\",\n    \"content\": \"ns1.example.org dns.example.org 2015092102 7200 7200 1209600 1800\",\n    \"ttl\": 7200,\n    \"prio\": null\n  },\n  {\n    \"id\": 99,\n    \"name\": \"\",\n    \"type\": \"NS\",\n    \"content\": \"ns1.example.org\",\n    \"ttl\": 7200,\n    \"prio\": null\n  },\n  {\n    \"id\": 100,\n    \"name\": \"_acme-challenge\",\n    \"type\": \"TXT\",\n    \"content\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n    \"ttl\": 120,\n    \"prio\": null\n  }\n]\n"
  },
  {
    "path": "providers/dns/internal/tecnocratica/internal/fixtures/get_zones.json",
    "content": "[\n  {\n    \"id\": 6,\n    \"name\": \"example.com\",\n    \"created_at\": \"2015-09-21T12:19:04.000+02:00\",\n    \"updated_at\": \"2015-09-21T12:19:04.000+02:00\",\n    \"human_name\": \"example.com\"\n  },\n  {\n    \"id\": 7,\n    \"name\": \"example.org\",\n    \"created_at\": \"2015-09-22T10:00:00.000+02:00\",\n    \"updated_at\": \"2015-09-22T10:00:00.000+02:00\",\n    \"human_name\": \"example.org\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/internal/tecnocratica/internal/types.go",
    "content": "package internal\n\n// Zone represents a DNS zone.\ntype Zone struct {\n\tID        int    `json:\"id\"`\n\tName      string `json:\"name\"`\n\tHumanName string `json:\"human_name\"`\n}\n\n// Record represents a DNS record.\ntype Record struct {\n\tID       int    `json:\"id,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tType     string `json:\"type,omitempty\"`\n\tContent  string `json:\"content,omitempty\"`\n\tTTL      int    `json:\"ttl,omitempty\"`\n\tPriority int    `json:\"prio,omitempty\"`\n}\n\n// RecordRequest is the request body for creating/updating a record.\ntype RecordRequest struct {\n\tRecord Record `json:\"record\"`\n}\n"
  },
  {
    "path": "providers/dns/internal/tecnocratica/provider.go",
    "content": "// Package tecnocratica implements a DNS provider for solving the DNS-01 challenge using Tecnocrática.\npackage tecnocratica\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/tecnocratica/internal\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\tzoneIDs     map[string]int\n\trecordIDs   map[string]int\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Tecnocrática.\nfunc NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"missing credentials\")\n\t}\n\n\tclient, err := internal.NewClient(config.Token)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create client: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tif baseURL != \"\" {\n\t\tclient.BaseURL, err = url.Parse(baseURL)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\tzoneIDs:   make(map[string]int),\n\t\trecordIDs: make(map[string]int),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tzone, err := d.findZone(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName:    subDomain,\n\t\tType:    \"TXT\",\n\t\tContent: info.Value,\n\t\tTTL:     d.config.TTL,\n\t}\n\n\tnewRecord, err := d.client.CreateRecord(ctx, zone.ID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.zoneIDs[token] = zone.ID\n\td.recordIDs[token] = newRecord.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordIDsMu.Lock()\n\tzoneID, zoneOK := d.zoneIDs[token]\n\trecordID, recordOK := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !zoneOK || !recordOK {\n\t\treturn fmt.Errorf(\"unknown record ID or zone ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\terr := d.client.DeleteRecord(context.Background(), zoneID, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"delete record: fqdn=%s, zoneID=%d, recordID=%d: %w\",\n\t\t\tinfo.EffectiveFQDN, zoneID, recordID, err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.zoneIDs, token)\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) findZone(ctx context.Context, zoneName string) (*internal.Zone, error) {\n\tzones, err := d.client.GetZones(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get zones: %w\", err)\n\t}\n\n\tfor _, zone := range zones {\n\t\tif zone.Name == zoneName || zone.HumanName == zoneName {\n\t\t\treturn &zone, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"zone not found: %s\", zoneName)\n}\n"
  },
  {
    "path": "providers/dns/internal/tecnocratica/provider_test.go",
    "content": "package tecnocratica\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing token\",\n\t\t\texpected: \"missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := &Config{}\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config, \"\")\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := &Config{\n\t\t\t\tToken:              \"secret\",\n\t\t\t\tPropagationTimeout: 10 * time.Second,\n\t\t\t\tPollingInterval:    1 * time.Second,\n\t\t\t\tTTL:                120,\n\t\t\t\tHTTPClient:         server.Client(),\n\t\t\t}\n\n\t\t\tp, err := NewDNSProviderConfig(config, server.URL)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(\"X-TCpanel-Token\", \"secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /dns/zones\",\n\t\t\tservermock.ResponseFromInternal(\"get_zones.json\")).\n\t\tRoute(\"POST /dns/zones/6/records\",\n\t\t\tservermock.ResponseFromInternal(\"create_record.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"DELETE /dns/zones/456/records/123\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\ttoken := \"abc\"\n\n\tprovider.recordIDs[token] = 123\n\tprovider.zoneIDs[token] = 456\n\n\terr := provider.CleanUp(\"example.com\", token, \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/internal/useragent/useragent.go",
    "content": "// Code generated by 'internal/releaser'; DO NOT EDIT.\n\npackage useragent\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"runtime\"\n)\n\nconst (\n\t// ourUserAgent is the User-Agent of this underlying library package.\n\tourUserAgent = \"goacme-lego/4.33.0\"\n\n\t// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.\n\t// values: detach|release\n\t// NOTE: Update this with each tagged release.\n\tourUserAgentComment = \"detach\"\n)\n\n// Get builds and returns the User-Agent string.\nfunc Get() string {\n\treturn fmt.Sprintf(\"%s (%s; %s; %s)\", ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH)\n}\n\n// SetHeader sets the User-Agent header.\nfunc SetHeader(h http.Header) {\n\th.Set(\"User-Agent\", Get())\n}\n"
  },
  {
    "path": "providers/dns/internal/westcn/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n\t\"golang.org/x/text/encoding\"\n\t\"golang.org/x/text/encoding/simplifiedchinese\"\n\t\"golang.org/x/text/transform\"\n)\n\nconst defaultBaseURL = \"https://api.west.cn/api/v2\"\n\n// Client the West.cn API client.\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tencoder *encoding.Encoder\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(username, password string) (*Client, error) {\n\tif username == \"\" || password == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\tencoder:    simplifiedchinese.GBK.NewEncoder(),\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// AddRecord adds a record.\n// https://www.west.cn/CustomerCenter/doc/domain_v2.html#37u3001u6dfbu52a0u57dfu540du89e3u67900a3ca20id3d37u3001u6dfbu52a0u57dfu540du89e3u67903e203ca3e\nfunc (c *Client) AddRecord(ctx context.Context, record Record) (int, error) {\n\tvalues, err := querystring.Values(record)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treq, err := c.newRequest(ctx, \"domain\", \"adddnsrecord\", values)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tresults := &APIResponse[RecordID]{}\n\n\terr = c.do(req, results)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif results.Result != http.StatusOK {\n\t\treturn 0, results\n\t}\n\n\treturn results.Data.ID, nil\n}\n\n// DeleteRecord deleted a record.\n// https://www.west.cn/CustomerCenter/doc/domain_v2.html#39u3001u5220u9664u57dfu540du89e3u67900a3ca20id3d39u3001u5220u9664u57dfu540du89e3u67903e203ca3e\nfunc (c *Client) DeleteRecord(ctx context.Context, domain string, recordID int) error {\n\tvalues := url.Values{}\n\tvalues.Set(\"domain\", domain)\n\tvalues.Set(\"id\", strconv.Itoa(recordID))\n\n\treq, err := c.newRequest(ctx, \"domain\", \"deldnsrecord\", values)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresults := &APIResponse[any]{}\n\n\terr = c.do(req, results)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif results.Result != http.StatusOK {\n\t\treturn results\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) newRequest(ctx context.Context, p, act string, form url.Values) (*http.Request, error) {\n\tif form == nil {\n\t\tform = url.Values{}\n\t}\n\n\tc.sign(form, time.Now())\n\n\tvalues, err := c.convertURLValues(form)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tendpoint := c.BaseURL.JoinPath(p, \"/\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"act\", act)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(values.Encode()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\treturn req, nil\n}\n\nfunc (c *Client) sign(form url.Values, now time.Time) {\n\ttimestamp := strconv.FormatInt(now.UnixMilli(), 10)\n\n\tsum := md5.Sum([]byte(c.username + c.password + timestamp))\n\n\tform.Set(\"token\", hex.EncodeToString(sum[:]))\n\tform.Set(\"username\", c.username)\n\tform.Set(\"time\", timestamp)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = gbkDecoder(raw).Decode(result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) convertURLValues(values url.Values) (url.Values, error) {\n\tresults := make(url.Values)\n\n\tfor key, vs := range values {\n\t\tencKey, err := c.encoder.String(key)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor _, value := range vs {\n\t\t\tencValue, err := c.encoder.String(value)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tresults.Add(encKey, encValue)\n\t\t}\n\t}\n\n\treturn results, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tresult := &APIResponse[any]{}\n\n\terr := gbkDecoder(raw).Decode(result)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn result\n}\n\nfunc gbkDecoder(raw []byte) *json.Decoder {\n\treturn json.NewDecoder(transform.NewReader(bytes.NewBuffer(raw), simplifiedchinese.GBK.NewDecoder()))\n}\n"
  },
  {
    "path": "providers/dns/internal/westcn/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/text/encoding/simplifiedchinese\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"user\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded())\n}\n\nfunc TestClientAddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domain/\",\n\t\t\tservermock.ResponseFromFixture(\"adddnsrecord.json\").\n\t\t\t\tWithHeader(\"Content-Type\", \"application/json\", \"Charset=gb2312\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"act\", \"adddnsrecord\"),\n\t\t\tservermock.CheckForm().UsePostForm().Strict().\n\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\tWith(\"host\", \"@\").\n\t\t\t\tWith(\"ttl\", \"60\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"value\", \"txtTXTtxt\").\n\t\t\t\t// With(\"act\", \"adddnsrecord\").\n\t\t\t\tWith(\"username\", \"user\").\n\t\t\t\tWithRegexp(\"time\", `\\d+`).\n\t\t\t\tWithRegexp(\"token\", `[a-z0-9]{32}`),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tDomain: \"example.com\",\n\t\tHost:   \"@\",\n\t\tType:   \"TXT\",\n\t\tValue:  \"txtTXTtxt\",\n\t\tTTL:    60,\n\t}\n\n\tid, err := client.AddRecord(t.Context(), record)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 123456, id)\n}\n\nfunc TestClientAddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domain/\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithHeader(\"Content-Type\", \"application/json\", \"Charset=gb2312\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"act\", \"adddnsrecord\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tDomain: \"example.com\",\n\t\tHost:   \"@\",\n\t\tType:   \"TXT\",\n\t\tValue:  \"txtTXTtxt\",\n\t\tTTL:    60,\n\t}\n\n\t_, err := client.AddRecord(t.Context(), record)\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"10000: username,time,token必传 (500)\")\n}\n\nfunc TestClientDeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domain/\",\n\t\t\tservermock.ResponseFromFixture(\"deldnsrecord.json\").\n\t\t\t\tWithHeader(\"Content-Type\", \"application/json\", \"Charset=gb2312\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"act\", \"deldnsrecord\"),\n\t\t\tservermock.CheckForm().UsePostForm().Strict().\n\t\t\t\tWith(\"id\", \"123\").\n\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\tWith(\"username\", \"user\").\n\t\t\t\tWithRegexp(\"time\", `\\d+`).\n\t\t\t\tWithRegexp(\"token\", `[a-z0-9]{32}`),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", 123)\n\trequire.NoError(t, err)\n}\n\nfunc TestClientDeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domain/\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithHeader(\"Content-Type\", \"application/json\", \"Charset=gb2312\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"act\", \"deldnsrecord\"),\n\t\t).\n\t\tBuild(t)\n\terr := client.DeleteRecord(t.Context(), \"example.com\", 123)\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"10000: username,time,token必传 (500)\")\n}\n\nfunc Test_convertURLValues(t *testing.T) {\n\tclient, err := NewClient(\"user\", \"secret\")\n\trequire.NoError(t, err)\n\n\tkey := \"你好abc\"\n\tvalue := \"世界def\"\n\n\tform := url.Values{}\n\tform.Set(key, value)\n\n\tvalues, err := client.convertURLValues(form)\n\trequire.NoError(t, err)\n\n\tencoder := simplifiedchinese.GBK.NewEncoder()\n\n\tk, err := encoder.String(key)\n\trequire.NoError(t, err)\n\n\tv, err := encoder.String(value)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, v, values.Get(k))\n\n\tdecoder := simplifiedchinese.GBK.NewDecoder()\n\n\tdecValue, err := decoder.String(values.Get(k))\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, value, decValue)\n}\n\nfunc TestClient_sign(t *testing.T) {\n\tclient, err := NewClient(\"zhangsan\", \"5dh232kfg!*\")\n\trequire.NoError(t, err)\n\n\tform := url.Values{}\n\n\tclient.sign(form, time.UnixMilli(1554691950854))\n\n\tassert.Equal(t, \"zhangsan\", form.Get(\"username\"))\n\tassert.Equal(t, \"1554691950854\", form.Get(\"time\"))\n\tassert.Equal(t, \"f17581fb2535b2a7ee4468eb3f96a2a9\", form.Get(\"token\"))\n}\n"
  },
  {
    "path": "providers/dns/internal/westcn/internal/fixtures/adddnsrecord.json",
    "content": "{\n  \"result\": 200,\n  \"clientid\": \"54880064508339547956\",\n  \"data\": {\n    \"id\": 123456\n  }\n}\n"
  },
  {
    "path": "providers/dns/internal/westcn/internal/fixtures/deldnsrecord.json",
    "content": "{\n  \"result\": 200,\n  \"clientid\": \"54880064508339547956\"\n}\n"
  },
  {
    "path": "providers/dns/internal/westcn/internal/fixtures/error.json",
    "content": "{\n  \"result\": 500,\n  \"clientid\": \"54880064508339547956\",\n  \"msg\": \"username,time,tokenش\",\n  \"errcode\": 10000\n}\n"
  },
  {
    "path": "providers/dns/internal/westcn/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype APIResponse[T any] struct {\n\tResult    int    `json:\"result,omitempty\"`\n\tClientID  string `json:\"clientid,omitempty\"`\n\tMessage   string `json:\"msg,omitempty\"`\n\tErrorCode int    `json:\"errcode,omitempty\"`\n\tData      T      `json:\"data,omitempty\"`\n}\n\nfunc (a APIResponse[T]) Error() string {\n\treturn fmt.Sprintf(\"%d: %s (%d)\", a.ErrorCode, a.Message, a.Result)\n}\n\ntype Record struct {\n\tDomain   string `url:\"domain,omitempty\"`\n\tHost     string `url:\"host,omitempty\"`\n\tType     string `url:\"type,omitempty\"`\n\tValue    string `url:\"value,omitempty\"`\n\tTTL      int    `url:\"ttl,omitempty\"` // 60~86400 seconds\n\tPriority int    `url:\"level,omitempty\"`\n}\n\ntype RecordID struct {\n\tID int `json:\"id,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/internal/westcn/provider.go",
    "content": "package westcn\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/westcn/internal\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername string\n\tPassword string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]int\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for West.cn/西部数码.\nfunc NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.Username, config.Password)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%w\", err)\n\t}\n\n\tif baseURL != \"\" {\n\t\tclient.BaseURL, err = url.Parse(baseURL)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]int),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tDomain: dns01.UnFqdn(authZone),\n\t\tHost:   subDomain,\n\t\tType:   \"TXT\",\n\t\tValue:  info.Value,\n\t\tTTL:    d.config.TTL,\n\t}\n\n\trecordID, err := d.client.AddRecord(context.Background(), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"add record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = recordID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// gets the record's unique ID\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\terr = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"delete record: %w\", err)\n\t}\n\n\t// deletes record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/internal/westcn/provider_test.go",
    "content": "package westcn\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tusername: \"user\",\n\t\t\texpected: \"credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := &Config{}\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config, \"\")\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := &Config{\n\t\t\t\tUsername:           \"user\",\n\t\t\t\tPassword:           \"secret\",\n\t\t\t\tPropagationTimeout: 10 * time.Second,\n\t\t\t\tPollingInterval:    1 * time.Second,\n\t\t\t\tTTL:                120,\n\t\t\t\tHTTPClient:         server.Client(),\n\t\t\t}\n\n\t\t\tp, err := NewDNSProviderConfig(config, server.URL)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded())\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /domain/\",\n\t\t\tservermock.ResponseFromInternal(\"adddnsrecord.json\").\n\t\t\t\tWithHeader(\"Content-Type\", \"application/json\", \"Charset=gb2312\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"act\", \"adddnsrecord\"),\n\t\t\tservermock.CheckForm().UsePostForm().Strict().\n\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\tWith(\"host\", \"_acme-challenge\").\n\t\t\t\tWith(\"ttl\", \"120\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"value\", \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\").\n\t\t\t\t// With(\"act\", \"adddnsrecord\").\n\t\t\t\tWith(\"username\", \"user\").\n\t\t\t\tWithRegexp(\"time\", `\\d+`).\n\t\t\t\tWithRegexp(\"token\", `[a-z0-9]{32}`),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /domain/\",\n\t\t\tservermock.ResponseFromInternal(\"deldnsrecord.json\").\n\t\t\t\tWithHeader(\"Content-Type\", \"application/json\", \"Charset=gb2312\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"act\", \"deldnsrecord\"),\n\t\t\tservermock.CheckForm().UsePostForm().Strict().\n\t\t\t\tWith(\"id\", \"123\").\n\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\tWith(\"username\", \"user\").\n\t\t\t\tWithRegexp(\"time\", `\\d+`).\n\t\t\t\tWithRegexp(\"token\", `[a-z0-9]{32}`),\n\t\t).\n\t\tBuild(t)\n\n\tprovider.recordIDs[\"abc\"] = 123\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/internetbs/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\nconst baseURL = \"https://api.internet.bs\"\n\n// status SUCCESS, PENDING, FAILURE.\nconst statusSuccess = \"SUCCESS\"\n\n// Client is the API client.\ntype Client struct {\n\tapiKey   string\n\tpassword string\n\n\tdebug bool\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiKey, password string) *Client {\n\tbaseURL, _ := url.Parse(baseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tpassword:   password,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}\n}\n\n// AddRecord The command is intended to add a new DNS record to a specific zone (domain).\nfunc (c *Client) AddRecord(ctx context.Context, query RecordQuery) error {\n\tvar r APIResponse\n\n\terr := c.doRequest(ctx, \"Add\", query, &r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif r.Status != statusSuccess {\n\t\treturn r\n\t}\n\n\treturn nil\n}\n\n// RemoveRecord The command is intended to remove a DNS record from a specific zone.\nfunc (c *Client) RemoveRecord(ctx context.Context, query RecordQuery) error {\n\tvar r APIResponse\n\n\terr := c.doRequest(ctx, \"Remove\", query, &r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif r.Status != statusSuccess {\n\t\treturn r\n\t}\n\n\treturn nil\n}\n\n// ListRecords The command is intended to retrieve the list of DNS records for a specific domain.\nfunc (c *Client) ListRecords(ctx context.Context, query ListRecordQuery) ([]Record, error) {\n\tvar l ListResponse\n\n\terr := c.doRequest(ctx, \"List\", query, &l)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif l.Status != statusSuccess {\n\t\treturn nil, l.APIResponse\n\t}\n\n\treturn l.Records, nil\n}\n\nfunc (c *Client) doRequest(ctx context.Context, action string, params, result any) error {\n\tendpoint := c.baseURL.JoinPath(\"Domain\", \"DnsRecord\", action)\n\n\tvalues, err := querystring.Values(params)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parse query parameters: %w\", err)\n\t}\n\n\tvalues.Set(\"apiKey\", c.apiKey)\n\tvalues.Set(\"password\", c.password)\n\tvalues.Set(\"ResponseFormat\", \"JSON\")\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(values.Encode()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif c.debug {\n\t\treturn dump(endpoint, resp, result)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc dump(endpoint *url.URL, resp *http.Response, response any) error {\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfields := strings.FieldsFunc(endpoint.Path, func(r rune) bool {\n\t\treturn !unicode.IsLetter(r) && !unicode.IsNumber(r)\n\t})\n\n\terr = os.WriteFile(filepath.Join(\"fixtures\", strings.Join(fields, \"_\")+\".json\"), raw, 0o666)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn json.Unmarshal(raw, response)\n}\n"
  },
  {
    "path": "providers/dns/internetbs/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst testBaseURL = \"https://testapi.internet.bs\"\n\nconst (\n\ttestAPIKey   = \"testapi\"\n\ttestPassword = \"testpass\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(testAPIKey, testPassword)\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded(),\n\t)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /Domain/DnsRecord/Add\",\n\t\t\tservermock.ResponseFromFixture(\"Domain_DnsRecord_Add_SUCCESS.json\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"fullrecordname\", \"www.example.com\").\n\t\t\t\tWith(\"ttl\", \"36000\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"value\", \"xxx\").\n\t\t\t\tWith(\"password\", testPassword).\n\t\t\t\tWith(\"apiKey\", testAPIKey).\n\t\t\t\tWith(\"ResponseFormat\", \"JSON\")).\n\t\tBuild(t)\n\n\tquery := RecordQuery{\n\t\tFullRecordName: \"www.example.com\",\n\t\tType:           \"TXT\",\n\t\tValue:          \"xxx\",\n\t\tTTL:            36000,\n\t}\n\n\terr := client.AddRecord(t.Context(), query)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /Domain/DnsRecord/Add\",\n\t\t\tservermock.ResponseFromFixture(\"Domain_DnsRecord_Add_FAILURE.json\")).\n\t\tBuild(t)\n\n\tquery := RecordQuery{\n\t\tFullRecordName: \"www.example.com.\",\n\t\tType:           \"TXT\",\n\t\tValue:          \"xxx\",\n\t\tTTL:            36000,\n\t}\n\n\terr := client.AddRecord(t.Context(), query)\n\trequire.Error(t, err)\n}\n\nfunc TestClient_AddRecord_integration(t *testing.T) {\n\tenv, ok := os.LookupEnv(\"INTERNET_BS_DEBUG\")\n\tif !ok {\n\t\tt.Skip(\"skip integration test\")\n\t}\n\n\tclient := NewClient(testAPIKey, testPassword)\n\tclient.baseURL, _ = url.Parse(testBaseURL)\n\tclient.debug, _ = strconv.ParseBool(env)\n\n\tquery := RecordQuery{\n\t\tFullRecordName: \"www.example.com\",\n\t\tType:           \"TXT\",\n\t\tValue:          \"xxx\",\n\t\tTTL:            36000,\n\t}\n\n\terr := client.AddRecord(t.Context(), query)\n\trequire.NoError(t, err)\n\n\tquery = RecordQuery{\n\t\tFullRecordName: \"www.example.com\",\n\t\tType:           \"TXT\",\n\t\tValue:          \"yyy\",\n\t\tTTL:            36000,\n\t}\n\n\terr = client.AddRecord(t.Context(), query)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_RemoveRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /Domain/DnsRecord/Remove\",\n\t\t\tservermock.ResponseFromFixture(\"Domain_DnsRecord_Remove_SUCCESS.json\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"fullrecordname\", \"www.example.com\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"password\", testPassword).\n\t\t\t\tWith(\"apiKey\", testAPIKey).\n\t\t\t\tWith(\"ResponseFormat\", \"JSON\")).\n\t\tBuild(t)\n\n\tquery := RecordQuery{\n\t\tFullRecordName: \"www.example.com\",\n\t\tType:           \"TXT\",\n\t\tValue:          \"\",\n\t}\n\terr := client.RemoveRecord(t.Context(), query)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_RemoveRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /Domain/DnsRecord/Remove\",\n\t\t\tservermock.ResponseFromFixture(\"Domain_DnsRecord_Remove_FAILURE.json\")).\n\t\tBuild(t)\n\n\tquery := RecordQuery{\n\t\tFullRecordName: \"www.example.com.\",\n\t\tType:           \"TXT\",\n\t\tValue:          \"\",\n\t}\n\terr := client.RemoveRecord(t.Context(), query)\n\trequire.Error(t, err)\n}\n\nfunc TestClient_RemoveRecord_integration(t *testing.T) {\n\tenv, ok := os.LookupEnv(\"INTERNET_BS_DEBUG\")\n\tif !ok {\n\t\tt.Skip(\"skip integration test\")\n\t}\n\n\tclient := NewClient(testAPIKey, testPassword)\n\tclient.baseURL, _ = url.Parse(testBaseURL)\n\tclient.debug, _ = strconv.ParseBool(env)\n\n\tquery := RecordQuery{\n\t\tFullRecordName: \"www.example.com\",\n\t\tType:           \"TXT\",\n\t\tValue:          \"\",\n\t}\n\n\terr := client.RemoveRecord(t.Context(), query)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_ListRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /Domain/DnsRecord/List\",\n\t\t\tservermock.ResponseFromFixture(\"Domain_DnsRecord_List_SUCCESS.json\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"Domain\", \"example.com\").\n\t\t\t\tWith(\"password\", testPassword).\n\t\t\t\tWith(\"apiKey\", testAPIKey).\n\t\t\t\tWith(\"ResponseFormat\", \"JSON\")).\n\t\tBuild(t)\n\n\tquery := ListRecordQuery{\n\t\tDomain: \"example.com\",\n\t}\n\n\trecords, err := client.ListRecords(t.Context(), query)\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tName:  \"example.com\",\n\t\t\tValue: \"ns-hongkong.internet.bs\",\n\t\t\tTTL:   3600,\n\t\t\tType:  \"NS\",\n\t\t},\n\t\t{\n\t\t\tName:  \"example.com\",\n\t\t\tValue: \"ns-toronto.internet.bs\",\n\t\t\tTTL:   3600,\n\t\t\tType:  \"NS\",\n\t\t},\n\t\t{\n\t\t\tName:  \"example.com\",\n\t\t\tValue: \"ns-london.internet.bs\",\n\t\t\tTTL:   3600,\n\t\t\tType:  \"NS\",\n\t\t},\n\t\t{\n\t\t\tName:  \"test.example.com\",\n\t\t\tValue: \"example1.com\",\n\t\t\tTTL:   3600,\n\t\t\tType:  \"CNAME\",\n\t\t},\n\t\t{\n\t\t\tName:  \"www.example.com\",\n\t\t\tValue: \"xxx\",\n\t\t\tTTL:   36000,\n\t\t\tType:  \"TXT\",\n\t\t},\n\t\t{\n\t\t\tName:  \"www.example.com\",\n\t\t\tValue: \"yyy\",\n\t\t\tTTL:   36000,\n\t\t\tType:  \"TXT\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_ListRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /Domain/DnsRecord/List\",\n\t\t\tservermock.ResponseFromFixture(\"Domain_DnsRecord_List_FAILURE.json\")).\n\t\tBuild(t)\n\n\tquery := ListRecordQuery{\n\t\tDomain: \"www.example.com\",\n\t}\n\n\t_, err := client.ListRecords(t.Context(), query)\n\trequire.Error(t, err)\n}\n\nfunc TestClient_ListRecords_integration(t *testing.T) {\n\tenv, ok := os.LookupEnv(\"INTERNET_BS_DEBUG\")\n\tif !ok {\n\t\tt.Skip(\"skip integration test\")\n\t}\n\n\tclient := NewClient(testAPIKey, testPassword)\n\tclient.baseURL, _ = url.Parse(testBaseURL)\n\tclient.debug, _ = strconv.ParseBool(env)\n\n\tquery := ListRecordQuery{\n\t\tDomain: \"example.com\",\n\t}\n\n\trecords, err := client.ListRecords(t.Context(), query)\n\trequire.NoError(t, err)\n\n\tfor _, record := range records {\n\t\tfmt.Println(record)\n\t}\n}\n"
  },
  {
    "path": "providers/dns/internetbs/internal/fixtures/Domain_DnsRecord_Add_FAILURE.json",
    "content": "{\n  \"transactid\": \"67e4689073df2f153e7184aeb47a98f9\",\n  \"status\": \"FAILURE\",\n  \"message\": \"Invalid value \\\"www.example.com.\\\" for parameter \\\"fullrecordname\\\"!\",\n  \"code\": 100002\n}\n"
  },
  {
    "path": "providers/dns/internetbs/internal/fixtures/Domain_DnsRecord_Add_SUCCESS.json",
    "content": "{\n  \"transactid\": \"548e3298130b492de23258634fd74481\",\n  \"status\": \"SUCCESS\"\n}\n"
  },
  {
    "path": "providers/dns/internetbs/internal/fixtures/Domain_DnsRecord_List_FAILURE.json",
    "content": "{\n  \"transactid\": \"5d554e0a5d145feb316b1805aae50706\",\n  \"status\": \"FAILURE\",\n  \"message\": \"The domain www.example.com does not have a supported extension!\",\n  \"code\": 100004\n}\n"
  },
  {
    "path": "providers/dns/internetbs/internal/fixtures/Domain_DnsRecord_List_SUCCESS.json",
    "content": "{\n  \"transactid\": \"3d161c37da7c824c8b3463b25f461df0\",\n  \"status\": \"SUCCESS\",\n  \"total_records\": 6,\n  \"records\": [\n    {\n      \"name\": \"example.com\",\n      \"value\": \"ns-hongkong.internet.bs\",\n      \"ttl\": 3600,\n      \"type\": \"NS\"\n    },\n    {\n      \"name\": \"example.com\",\n      \"value\": \"ns-toronto.internet.bs\",\n      \"ttl\": 3600,\n      \"type\": \"NS\"\n    },\n    {\n      \"name\": \"example.com\",\n      \"value\": \"ns-london.internet.bs\",\n      \"ttl\": 3600,\n      \"type\": \"NS\"\n    },\n    {\n      \"name\": \"test.example.com\",\n      \"value\": \"example1.com\",\n      \"ttl\": 3600,\n      \"type\": \"CNAME\"\n    },\n    {\n      \"name\": \"www.example.com\",\n      \"value\": \"xxx\",\n      \"ttl\": 36000,\n      \"type\": \"TXT\"\n    },\n    {\n      \"name\": \"www.example.com\",\n      \"value\": \"yyy\",\n      \"ttl\": 36000,\n      \"type\": \"TXT\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/internetbs/internal/fixtures/Domain_DnsRecord_Remove_SUCCESS.json",
    "content": "{\n  \"transactid\": \"221a0fe572f0505194214405f395a847\",\n  \"status\": \"SUCCESS\"\n}\n"
  },
  {
    "path": "providers/dns/internetbs/internal/fixtures/auth_error.json",
    "content": "{\n  \"transactid\": \"d46d812569acdb8b39c3933ec4351e79\",\n  \"status\": \"FAILURE\",\n  \"message\": \"Invalid API key and\\/or Password\",\n  \"code\": 107002\n}\n"
  },
  {
    "path": "providers/dns/internetbs/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype APIResponse struct {\n\tTransactID string `json:\"transactid\"`\n\tStatus     string `json:\"status\"`\n\tMessage    string `json:\"message,omitempty\"`\n\tCode       int    `json:\"code,omitempty\"`\n}\n\nfunc (a APIResponse) Error() string {\n\treturn fmt.Sprintf(\"%s(%d): %s (%s)\", a.Status, a.Code, a.Message, a.TransactID)\n}\n\ntype ListResponse struct {\n\tAPIResponse\n\n\tTotalRecords int      `json:\"total_records,omitempty\"`\n\tRecords      []Record `json:\"records,omitempty\"`\n}\n\ntype Record struct {\n\tName  string `json:\"name,omitempty\"`\n\tValue string `json:\"value,omitempty\"`\n\tTTL   int    `json:\"ttl,omitempty\"`\n\tType  string `json:\"type,omitempty\"`\n}\n\ntype RecordQuery struct {\n\tFullRecordName string `url:\"fullrecordname\"`\n\tType           string `url:\"type\"`\n\tValue          string `url:\"value,omitempty\"`\n\tTTL            int    `url:\"ttl,omitempty\"`\n}\n\ntype ListRecordQuery struct {\n\tDomain     string `url:\"Domain\"`\n\tFilterType string `url:\"FilterType,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/internetbs/internetbs.go",
    "content": "// Package internetbs implements a DNS provider for solving the DNS-01 challenge using internet.bs.\npackage internetbs\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internetbs/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"INTERNET_BS_\"\n\n\tEnvAPIKey   = envNamespace + \"API_KEY\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tPassword           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 3600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for internet.bs.\n// Credentials must be passed in the environment variables: INTERNET_BS_API_KEY, INTERNET_BS_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"internetbs: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for internet.bs.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"internetbs: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"internetbs: missing credentials\")\n\t}\n\n\tclient := internal.NewClient(config.APIKey, config.Password)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tquery := internal.RecordQuery{\n\t\tFullRecordName: dns01.UnFqdn(info.EffectiveFQDN),\n\t\tType:           \"TXT\",\n\t\tValue:          info.Value,\n\t\tTTL:            d.config.TTL,\n\t}\n\n\terr := d.client.AddRecord(context.Background(), query)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"internetbs: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tquery := internal.RecordQuery{\n\t\tFullRecordName: dns01.UnFqdn(info.EffectiveFQDN),\n\t\tType:           \"TXT\",\n\t\tValue:          info.Value,\n\t\tTTL:            d.config.TTL,\n\t}\n\n\terr := d.client.RemoveRecord(context.Background(), query)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"internetbs: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/internetbs/internetbs.toml",
    "content": "Name = \"Internet.bs\"\nDescription = ''''''\nURL = \"https://internetbs.net\"\nCode = \"internetbs\"\nSince = \"v4.5.0\"\n\nExample = '''\nINTERNET_BS_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \\\nINTERNET_BS_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \\\nlego --dns internetbs -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    INTERNET_BS_API_KEY = \"API key\"\n    INTERNET_BS_PASSWORD = \"API password\"\n  [Configuration.Additional]\n    INTERNET_BS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    INTERNET_BS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    INTERNET_BS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)\"\n    INTERNET_BS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://internetbs.net/internet-bs-api.pdf\"\n"
  },
  {
    "path": "providers/dns/internetbs/internetbs_test.go",
    "content": "package internetbs\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey, EnvPassword).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:   \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"internetbs: some credentials information are missing: INTERNET_BS_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"user\",\n\t\t\t},\n\t\t\texpected: \"internetbs: some credentials information are missing: INTERNET_BS_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"internetbs: some credentials information are missing: INTERNET_BS_API_KEY,INTERNET_BS_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiKey:   \"user\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\texpected: \"internetbs: missing credentials\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\texpected: \"internetbs: missing credentials\",\n\t\t\tapiKey:   \"user\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"internetbs: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/inwx/inwx.go",
    "content": "// Package inwx implements a DNS provider for solving the DNS-01 challenge using inwx dom robot\npackage inwx\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/nrdcg/goinwx\"\n\t\"github.com/pquerna/otp/totp\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"INWX_\"\n\n\tEnvUsername     = envNamespace + \"USERNAME\"\n\tEnvPassword     = envNamespace + \"PASSWORD\"\n\tEnvSharedSecret = envNamespace + \"SHARED_SECRET\"\n\tEnvSandbox      = envNamespace + \"SANDBOX\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername           string\n\tPassword           string\n\tSharedSecret       string\n\tSandbox            bool\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL: env.GetOrDefaultInt(EnvTTL, 300),\n\t\t// INWX has rather unstable propagation delays, thus using a larger default value\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 6*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSandbox:            env.GetOrDefaultBool(EnvSandbox, false),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig         *Config\n\tclient         *goinwx.Client\n\tpreviousUnlock time.Time\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Dyn DNS.\n// Credentials must be passed in the environment variables:\n// INWX_USERNAME, INWX_PASSWORD, and INWX_SHARED_SECRET.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"inwx: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\tconfig.SharedSecret = env.GetOrFile(EnvSharedSecret)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Dyn DNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"inwx: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Username == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"inwx: credentials missing\")\n\t}\n\n\tif config.Sandbox {\n\t\tlog.Infof(\"inwx: sandbox mode is enabled\")\n\t}\n\n\tclient := goinwx.NewClient(config.Username, config.Password, &goinwx.ClientOptions{Sandbox: config.Sandbox})\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"inwx: could not find zone for domain %q (%s): %w\", domain, info.EffectiveFQDN, err)\n\t}\n\n\tlogin, err := d.client.Account.Login()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"inwx: %w\", err)\n\t}\n\n\tdefer func() {\n\t\terrL := d.client.Account.Logout()\n\t\tif errL != nil {\n\t\t\tlog.Infof(\"inwx: failed to log out: %v\", errL)\n\t\t}\n\t}()\n\n\terr = d.twoFactorAuth(login)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"inwx: %w\", err)\n\t}\n\n\trequest := &goinwx.NameserverRecordRequest{\n\t\tDomain:  dns01.UnFqdn(authZone),\n\t\tName:    dns01.UnFqdn(info.EffectiveFQDN),\n\t\tType:    \"TXT\",\n\t\tContent: info.Value,\n\t\tTTL:     d.config.TTL,\n\t}\n\n\t_, err = d.client.Nameservers.CreateRecord(request)\n\tif err != nil {\n\t\tvar er *goinwx.ErrorResponse\n\t\tif errors.As(err, &er) && er.Message == \"Object exists\" {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"inwx: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"inwx: could not find zone for domain %q (%s): %w\", domain, info.EffectiveFQDN, err)\n\t}\n\n\tlogin, err := d.client.Account.Login()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"inwx: %w\", err)\n\t}\n\n\tdefer func() {\n\t\terrL := d.client.Account.Logout()\n\t\tif errL != nil {\n\t\t\tlog.Infof(\"inwx: failed to log out: %v\", errL)\n\t\t}\n\t}()\n\n\terr = d.twoFactorAuth(login)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"inwx: %w\", err)\n\t}\n\n\tresponse, err := d.client.Nameservers.Info(&goinwx.NameserverInfoRequest{\n\t\tDomain: dns01.UnFqdn(authZone),\n\t\tName:   dns01.UnFqdn(info.EffectiveFQDN),\n\t\tType:   \"TXT\",\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"inwx: %w\", err)\n\t}\n\n\tvar recordID string\n\n\tfor _, record := range response.Records {\n\t\tif record.Content != info.Value {\n\t\t\tcontinue\n\t\t}\n\n\t\trecordID = record.ID\n\n\t\tbreak\n\t}\n\n\tif recordID == \"\" {\n\t\treturn errors.New(\"inwx: TXT record not found\")\n\t}\n\n\terr = d.client.Nameservers.DeleteRecord(recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"inwx: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) twoFactorAuth(info *goinwx.LoginResponse) error {\n\tif info.TFA != \"GOOGLE-AUTH\" {\n\t\treturn nil\n\t}\n\n\tif d.config.SharedSecret == \"\" {\n\t\treturn errors.New(\"two-factor authentication but no shared secret is given\")\n\t}\n\n\t// INWX forbids re-authentication with a previously used TAN.\n\t// To avoid using the same TAN twice, we wait until the next TOTP period.\n\tsleep := d.computeSleep(time.Now())\n\tif sleep != 0 {\n\t\tlog.Infof(\"inwx: waiting %s for next TOTP token\", sleep)\n\t\ttime.Sleep(sleep)\n\t}\n\n\tnow := time.Now()\n\n\ttan, err := totp.GenerateCode(d.config.SharedSecret, now)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\td.previousUnlock = now.Truncate(30 * time.Second)\n\n\treturn d.client.Account.Unlock(tan)\n}\n\nfunc (d *DNSProvider) computeSleep(now time.Time) time.Duration {\n\tif d.previousUnlock.IsZero() {\n\t\treturn 0\n\t}\n\n\tendPeriod := d.previousUnlock.Add(30 * time.Second)\n\tif endPeriod.After(now) {\n\t\treturn endPeriod.Sub(now)\n\t}\n\n\treturn 0\n}\n"
  },
  {
    "path": "providers/dns/inwx/inwx.toml",
    "content": "Name = \"INWX\"\nDescription = ''''''\nURL = \"https://www.inwx.de/en\"\nCode = \"inwx\"\nSince = \"v2.0.0\"\n\nExample = '''\nINWX_USERNAME=xxxxxxxxxx \\\nINWX_PASSWORD=yyyyyyyyyy \\\nlego --dns inwx -d '*.example.com' -d example.com run\n\n# 2FA\nINWX_USERNAME=xxxxxxxxxx \\\nINWX_PASSWORD=yyyyyyyyyy \\\nINWX_SHARED_SECRET=zzzzzzzzzz \\\nlego --dns inwx -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    INWX_USERNAME = \"Username\"\n    INWX_PASSWORD = \"Password\"\n  [Configuration.Additional]\n    INWX_SHARED_SECRET = \"shared secret related to 2FA\"\n    INWX_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    INWX_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 360)\"\n    INWX_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    INWX_SANDBOX = \"Activate the sandbox (boolean)\"\n\n[Links]\n  API = \"https://www.inwx.de/en/help/apidoc\"\n  GoClient = \"https://github.com/nrdcg/goinwx\"\n"
  },
  {
    "path": "providers/dns/inwx/inwx_test.go",
    "content": "package inwx\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvUsername,\n\tEnvPassword,\n\tEnvSharedSecret,\n\tEnvSandbox,\n\tEnvTTL).\n\tWithDomain(envDomain).\n\tWithLiveTestRequirements(EnvUsername, EnvPassword, envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvPassword: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"inwx: some credentials information are missing: INWX_USERNAME,INWX_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"456\",\n\t\t\t},\n\t\t\texpected: \"inwx: some credentials information are missing: INWX_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"inwx: some credentials information are missing: INWX_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"123\",\n\t\t\tpassword: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"inwx: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresentAndCleanup(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tenvTest.Apply(map[string]string{\n\t\tEnvSandbox: \"true\",\n\t\tEnvTTL:     \"3600\", // In sandbox mode, the minimum allowed TTL is 3600\n\t})\n\tdefer envTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n\n\t// Verify that no error is thrown if record already exists\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc Test_computeSleep(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tnow      string\n\t\texpected time.Duration\n\t}{\n\t\t{\n\t\t\tdesc:     \"after 30s\",\n\t\t\tnow:      \"2024-01-01T06:30:30Z\",\n\t\t\texpected: 0 * time.Second,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"0s\",\n\t\t\tnow:      \"2024-01-01T06:30:00Z\",\n\t\t\texpected: 0 * time.Second,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"before 30s\",\n\t\t\tnow:      \"2024-01-01T06:29:40Z\", // 10 s\n\t\t\texpected: 20 * time.Second,\n\t\t},\n\t}\n\n\tprevious, err := time.Parse(time.RFC3339, \"2024-01-01T06:29:30Z\")\n\trequire.NoError(t, err)\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tnow, err := time.Parse(time.RFC3339, test.now)\n\t\t\trequire.NoError(t, err)\n\n\t\t\td := &DNSProvider{previousUnlock: previous}\n\n\t\t\tsleep := d.computeSleep(now)\n\t\t\tassert.Equal(t, test.expected, sleep)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/ionos/ionos.go",
    "content": "// Package ionos implements a DNS provider for solving the DNS-01 challenge using Ionos/1&1.\npackage ionos\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ionos\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"IONOS_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst minTTL = 300\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config = ionos.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, ionos.MinTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 15*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Ionos.\n// Credentials must be passed in the environment variables: IONOS_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ionos: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Ionos.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"ionos: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := ionos.NewDNSProviderConfig(config, \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ionos: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ionos: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ionos: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/ionos/ionos.toml",
    "content": "Name = \"Ionos\"\nDescription = ''''''\nURL = \"https://ionos.com\"\nCode = \"ionos\"\nSince = \"v4.2.0\"\n\nExample = '''\nIONOS_API_KEY=xxxxxxxx \\\nlego --dns ionos -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    IONOS_API_KEY = \"API key `<prefix>.<secret>` https://developer.hosting.ionos.com/docs/getstarted\"\n  [Configuration.Additional]\n    IONOS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    IONOS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 900)\"\n    IONOS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    IONOS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://developer.hosting.ionos.com/docs/dns\"\n"
  },
  {
    "path": "providers/dns/ionos/ionos_test.go",
    "content": "package ionos\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"ionos: some credentials information are missing: IONOS_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\ttll      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"123\",\n\t\t\ttll:    minTTL,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\ttll:      minTTL,\n\t\t\texpected: \"ionos: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"invalid TTL\",\n\t\t\tapiKey:   \"123\",\n\t\t\ttll:      30,\n\t\t\texpected: \"ionos: invalid TTL, TTL (30) must be greater than 300\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.TTL = test.tll\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/ionoscloud/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\nconst defaultBaseURL = \"https://dns.de-fra.ionos.com\"\n\nconst authorizationHeader = \"Authorization\"\n\n// Client the Ionos Cloud API client.\ntype Client struct {\n\tapiKey string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiKey string) (*Client, error) {\n\tif apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// RetrieveZones returns a list of the DNS zones.\n// https://api.ionos.com/docs/dns/v1/#tag/Zones/operation/zonesGet\nfunc (c *Client) RetrieveZones(ctx context.Context, zoneName string) ([]Zone, error) {\n\tendpoint := c.BaseURL.JoinPath(\"zones\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := req.URL.Query()\n\tquery.Add(\"filter.zoneName\", zoneName)\n\treq.URL.RawQuery = query.Encode()\n\n\tresult := ZonesResponse{}\n\n\tif err := c.do(req, &result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Items, nil\n}\n\n// CreateRecord creates a new record for the DNS zone.\n// https://api.ionos.com/docs/dns/v1/#tag/Records/operation/zonesRecordsPost\nfunc (c *Client) CreateRecord(ctx context.Context, zoneID string, record RecordProperties) (*RecordResponse, error) {\n\tendpoint := c.BaseURL.JoinPath(\"zones\", zoneID, \"records\")\n\n\tpayload := map[string]RecordProperties{\n\t\t\"properties\": record,\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &RecordResponse{}\n\n\tif err := c.do(req, result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// DeleteRecord deletes a specified record from the DNS zone.\n// https://api.ionos.com/docs/dns/v1/#tag/Records/operation/zonesRecordsDelete\nfunc (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID string) error {\n\tendpoint := c.BaseURL.JoinPath(\"zones\", zoneID, \"records\", recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\treq.Header.Set(authorizationHeader, \"Bearer \"+c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/ionoscloud/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer secret\"),\n\t)\n}\n\nfunc TestClient_RetrieveZones(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones\",\n\t\t\tservermock.ResponseFromFixture(\"zones.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"filter.zoneName\", \"example.com\")).\n\t\tBuild(t)\n\n\tzones, err := client.RetrieveZones(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []Zone{{\n\t\tID:   \"e74d0d15-f567-4b7b-9069-26ee1f93bae3\",\n\t\tType: \"zone\",\n\t\tMetadata: ZoneMetadata{\n\t\t\tCreatedDate:          time.Date(2022, time.August, 21, 15, 52, 53, 0, time.UTC),\n\t\t\tCreatedBy:            \"ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3\",\n\t\t\tCreatedByUserID:      \"87f9a82e-b28d-49ed-9d04-fba2c0459cd3\",\n\t\t\tLastModifiedDate:     time.Date(2022, time.August, 21, 15, 52, 53, 0, time.UTC),\n\t\t\tLastModifiedBy:       \"ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3\",\n\t\t\tLastModifiedByUserID: \"63cef532-26fe-4a64-a4e0-de7c8a506c90\",\n\t\t\tResourceURN:          \"ionos:<product>:<location>:<contract>:<resource-path>\",\n\t\t\tState:                \"PROVISIONING\",\n\t\t\tNameservers:          []string{\"ns-ic.ui-dns.com\", \"ns-ic.ui-dns.de\", \"ns-ic.ui-dns.org\", \"ns-ic.ui-dns.biz\"},\n\t\t},\n\t\tProperties: ZoneProperties{\n\t\t\tZoneName:    \"example.com\",\n\t\t\tDescription: \"The hosted zone is used for example.com\",\n\t\t\tEnabled:     true,\n\t\t},\n\t}}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_RetrieveZones_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.RetrieveZones(t.Context(), \"example.com\")\n\trequire.EqualError(t, err, \"401: paas-auth-1: Unauthorized, wrong or no api key provided to process this request\")\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zones/abc/records\",\n\t\t\tservermock.ResponseFromFixture(\"create_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\trecord := RecordProperties{\n\t\tName:    \"_acme-challenge\",\n\t\tType:    \"TXT\",\n\t\tContent: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:     120,\n\t}\n\n\tresult, err := client.CreateRecord(t.Context(), \"abc\", record)\n\trequire.NoError(t, err)\n\n\texpected := &RecordResponse{\n\t\tID:   \"90d81ac0-3a30-44d4-95a5-12959effa6ee\",\n\t\tType: \"record\",\n\t\tMetadata: RecordMetadata{\n\t\t\tCreatedDate:          time.Date(2022, time.August, 21, 15, 52, 53, 0, time.UTC),\n\t\t\tCreatedBy:            \"ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3\",\n\t\t\tCreatedByUserID:      \"87f9a82e-b28d-49ed-9d04-fba2c0459cd3\",\n\t\t\tLastModifiedDate:     time.Date(2022, time.August, 21, 15, 52, 53, 0, time.UTC),\n\t\t\tLastModifiedBy:       \"ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3\",\n\t\t\tLastModifiedByUserID: \"63cef532-26fe-4a64-a4e0-de7c8a506c90\",\n\t\t\tResourceURN:          \"ionos:<product>:<location>:<contract>:<resource-path>\",\n\t\t\tState:                \"PROVISIONING\",\n\t\t\tFqdn:                 \"app.example.com\",\n\t\t\tZoneID:               \"a363f30c-4c0c-4552-9a07-298d87f219bf\",\n\t\t},\n\t\tProperties: RecordProperties{\n\t\t\tName:     \"app\",\n\t\t\tType:     \"A\",\n\t\t\tContent:  \"1.2.3.4\",\n\t\t\tTTL:      3600,\n\t\t\tPriority: 3600,\n\t\t\tEnabled:  true,\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /zones/abc/records/def\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusAccepted)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"abc\", \"def\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/ionoscloud/internal/fixtures/create_record-request.json",
    "content": "{\n  \"properties\": {\n    \"name\": \"_acme-challenge\",\n    \"type\": \"TXT\",\n    \"content\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n    \"ttl\": 120\n  }\n}\n"
  },
  {
    "path": "providers/dns/ionoscloud/internal/fixtures/create_record.json",
    "content": "{\n  \"id\": \"90d81ac0-3a30-44d4-95a5-12959effa6ee\",\n  \"type\": \"record\",\n  \"href\": \"<RESOURCE-URI>\",\n  \"metadata\": {\n    \"createdDate\": \"2022-08-21T15:52:53Z\",\n    \"createdBy\": \"ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3\",\n    \"createdByUserId\": \"87f9a82e-b28d-49ed-9d04-fba2c0459cd3\",\n    \"lastModifiedDate\": \"2022-08-21T15:52:53Z\",\n    \"lastModifiedBy\": \"ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3\",\n    \"lastModifiedByUserId\": \"63cef532-26fe-4a64-a4e0-de7c8a506c90\",\n    \"resourceURN\": \"ionos:<product>:<location>:<contract>:<resource-path>\",\n    \"state\": \"PROVISIONING\",\n    \"fqdn\": \"app.example.com\",\n    \"zoneId\": \"a363f30c-4c0c-4552-9a07-298d87f219bf\"\n  },\n  \"properties\": {\n    \"name\": \"app\",\n    \"type\": \"A\",\n    \"content\": \"1.2.3.4\",\n    \"ttl\": 3600,\n    \"priority\": 3600,\n    \"enabled\": true\n  }\n}\n"
  },
  {
    "path": "providers/dns/ionoscloud/internal/fixtures/error.json",
    "content": "{\n  \"httpStatus\": 401,\n  \"messages\": [\n    {\n      \"errorCode\": \"paas-auth-1\",\n      \"message\": \"Unauthorized, wrong or no api key provided to process this request\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/ionoscloud/internal/fixtures/zones.json",
    "content": "{\n  \"id\": \"e74d0d15-f567-4b7b-9069-26ee1f93bae3\",\n  \"type\": \"collection\",\n  \"href\": \"<RESOURCE-URI>\",\n  \"offset\": 0,\n  \"limit\": 1000,\n  \"_links\": {\n    \"prev\": \"http://PREVIOUS-PAGE-URI\",\n    \"self\": \"http://THIS-PAGE-URI\",\n    \"next\": \"http://NEXT-PAGE-URI\"\n  },\n  \"items\": [\n    {\n      \"id\": \"e74d0d15-f567-4b7b-9069-26ee1f93bae3\",\n      \"type\": \"zone\",\n      \"href\": \"<RESOURCE-URI>\",\n      \"metadata\": {\n        \"createdDate\": \"2022-08-21T15:52:53Z\",\n        \"createdBy\": \"ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3\",\n        \"createdByUserId\": \"87f9a82e-b28d-49ed-9d04-fba2c0459cd3\",\n        \"lastModifiedDate\": \"2022-08-21T15:52:53Z\",\n        \"lastModifiedBy\": \"ionos:iam:cloud:31960002:users/87f9a82e-b28d-49ed-9d04-fba2c0459cd3\",\n        \"lastModifiedByUserId\": \"63cef532-26fe-4a64-a4e0-de7c8a506c90\",\n        \"resourceURN\": \"ionos:<product>:<location>:<contract>:<resource-path>\",\n        \"state\": \"PROVISIONING\",\n        \"nameservers\": [\n          \"ns-ic.ui-dns.com\",\n          \"ns-ic.ui-dns.de\",\n          \"ns-ic.ui-dns.org\",\n          \"ns-ic.ui-dns.biz\"\n        ]\n      },\n      \"properties\": {\n        \"zoneName\": \"example.com\",\n        \"description\": \"The hosted zone is used for example.com\",\n        \"enabled\": true\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/ionoscloud/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype APIError struct {\n\tHTTPStatus int            `json:\"httpStatus\"`\n\tMessages   []ErrorMessage `json:\"messages\"`\n}\n\nfunc (a *APIError) Error() string {\n\tvar msg strings.Builder\n\n\tmsg.WriteString(strconv.Itoa(a.HTTPStatus))\n\n\tfor _, m := range a.Messages {\n\t\tmsg.WriteString(\": \")\n\t\tmsg.WriteString(m.String())\n\t}\n\n\treturn msg.String()\n}\n\ntype ErrorMessage struct {\n\tErrorCode string `json:\"errorCode\"`\n\tMessage   string `json:\"message\"`\n}\n\nfunc (e ErrorMessage) String() string {\n\treturn fmt.Sprintf(\"%s: %s\", e.ErrorCode, e.Message)\n}\n\ntype ZonesResponse struct {\n\tID     string `json:\"id\"`\n\tType   string `json:\"type\"`\n\tOffset int    `json:\"offset\"`\n\tLimit  int    `json:\"limit\"`\n\tItems  []Zone `json:\"items\"`\n}\n\ntype Zone struct {\n\tID         string         `json:\"id\"`\n\tType       string         `json:\"type\"`\n\tMetadata   ZoneMetadata   `json:\"metadata\"`\n\tProperties ZoneProperties `json:\"properties\"`\n}\n\ntype ZoneMetadata struct {\n\tCreatedDate          time.Time `json:\"createdDate\"`\n\tCreatedBy            string    `json:\"createdBy\"`\n\tCreatedByUserID      string    `json:\"createdByUserId\"`\n\tLastModifiedDate     time.Time `json:\"lastModifiedDate\"`\n\tLastModifiedBy       string    `json:\"lastModifiedBy\"`\n\tLastModifiedByUserID string    `json:\"lastModifiedByUserId\"`\n\tResourceURN          string    `json:\"resourceURN\"`\n\tState                string    `json:\"state\"`\n\tNameservers          []string  `json:\"nameservers\"`\n}\n\ntype ZoneProperties struct {\n\tZoneName    string `json:\"zoneName\"`\n\tDescription string `json:\"description\"`\n\tEnabled     bool   `json:\"enabled\"`\n}\n\ntype RecordResponse struct {\n\tID         string           `json:\"id\"`\n\tType       string           `json:\"type\"`\n\tMetadata   RecordMetadata   `json:\"metadata\"`\n\tProperties RecordProperties `json:\"properties\"`\n}\n\ntype RecordMetadata struct {\n\tCreatedDate          time.Time `json:\"createdDate\"`\n\tCreatedBy            string    `json:\"createdBy\"`\n\tCreatedByUserID      string    `json:\"createdByUserId\"`\n\tLastModifiedDate     time.Time `json:\"lastModifiedDate\"`\n\tLastModifiedBy       string    `json:\"lastModifiedBy\"`\n\tLastModifiedByUserID string    `json:\"lastModifiedByUserId\"`\n\tResourceURN          string    `json:\"resourceURN\"`\n\tState                string    `json:\"state\"`\n\tFqdn                 string    `json:\"fqdn\"`\n\tZoneID               string    `json:\"zoneId\"`\n}\n\ntype RecordProperties struct {\n\tName     string `json:\"name\"`\n\tType     string `json:\"type,omitempty\"`\n\tContent  string `json:\"content,omitempty\"`\n\tTTL      int    `json:\"ttl,omitempty\"`\n\tPriority int    `json:\"priority,omitempty\"`\n\tEnabled  bool   `json:\"enabled,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/ionoscloud/ionoscloud.go",
    "content": "// Package ionoscloud implements a DNS provider for solving the DNS-01 challenge using Ionos Cloud.\npackage ionoscloud\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ionoscloud/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"IONOSCLOUD_\"\n\n\tEnvAPIToken = envNamespace + \"API_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\tzoneIDs     map[string]string\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Ionos Cloud.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ionoscloud: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIToken = values[EnvAPIToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Ionos Cloud.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"ionoscloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.APIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ionoscloud: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\tzoneIDs:   make(map[string]string),\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ionoscloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzones, err := d.client.RetrieveZones(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ionoscloud: retrieve zones: %w\", err)\n\t}\n\n\tif len(zones) != 1 {\n\t\treturn fmt.Errorf(\"ionoscloud: zone ID not found for domain %q\", domain)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ionoscloud: %w\", err)\n\t}\n\n\tzoneID := zones[0].ID\n\n\trequest := internal.RecordProperties{\n\t\tName:    subDomain,\n\t\tType:    \"TXT\",\n\t\tContent: info.Value,\n\t\tTTL:     d.config.TTL,\n\t}\n\n\trecord, err := d.client.CreateRecord(ctx, zoneID, request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ionoscloud: create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.zoneIDs[token] = zoneID\n\td.recordIDs[token] = record.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordIDsMu.Lock()\n\tzoneID, ok := d.zoneIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"ionoscloud: unknown zone ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"ionoscloud: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\terr := d.client.DeleteRecord(context.Background(), zoneID, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ionoscloud: delete record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.zoneIDs, token)\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/ionoscloud/ionoscloud.toml",
    "content": "Name = \"Ionos Cloud\"\nDescription = ''''''\nURL = \"https://cloud.ionos.de/network/cloud-dns\"\nCode = \"ionoscloud\"\nSince = \"v4.30.0\"\n\nExample = '''\nIONOSCLOUD_API_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns ionoscloud -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    IONOSCLOUD_API_TOKEN = \"API token\"\n  [Configuration.Additional]\n    IONOSCLOUD_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    IONOSCLOUD_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    IONOSCLOUD_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    IONOSCLOUD_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api.ionos.com/docs/dns/v1/\"\n"
  },
  {
    "path": "providers/dns/ionoscloud/ionoscloud_test.go",
    "content": "package ionoscloud\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"ionoscloud: some credentials information are missing: IONOSCLOUD_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiToken string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiToken: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"ionoscloud: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = test.apiToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /zones\",\n\t\t\tservermock.ResponseFromInternal(\"zones.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"filter.zoneName\", \"example.com\")).\n\t\tRoute(\"POST /zones/e74d0d15-f567-4b7b-9069-26ee1f93bae3/records\",\n\t\t\tservermock.ResponseFromInternal(\"create_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"DELETE /zones/e74d0d15-f567-4b7b-9069-26ee1f93bae3/records/90d81ac0-3a30-44d4-95a5-12959effa6ee\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusAccepted)).\n\t\tBuild(t)\n\n\ttoken := \"abc\"\n\n\tprovider.zoneIDs[token] = \"e74d0d15-f567-4b7b-9069-26ee1f93bae3\"\n\tprovider.recordIDs[token] = \"90d81ac0-3a30-44d4-95a5-12959effa6ee\"\n\n\terr := provider.CleanUp(\"example.com\", token, \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/ipv64/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"golang.org/x/oauth2\"\n)\n\nconst defaultBaseURL = \"https://ipv64.net\"\n\ntype Client struct {\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(hc *http.Client) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\tif hc == nil {\n\t\thc = &http.Client{Timeout: 15 * time.Second}\n\t}\n\n\treturn &Client{\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: hc,\n\t}\n}\n\nfunc (c *Client) GetDomains(ctx context.Context) (*Domains, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"get_domains\", \"\")\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tresults := &Domains{}\n\n\terr = c.do(req, results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn results, nil\n}\n\nfunc (c *Client) AddRecord(ctx context.Context, domain, prefix, recordType, content string) error {\n\tendpoint := c.baseURL.JoinPath(\"api\")\n\n\tdata := make(url.Values)\n\tdata.Set(\"add_record\", domain)\n\tdata.Set(\"praefix\", prefix)\n\tdata.Set(\"type\", recordType)\n\tdata.Set(\"content\", content)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(data.Encode()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, domain, prefix, recordType, content string) error {\n\tendpoint := c.baseURL.JoinPath(\"api\")\n\n\tdata := make(url.Values)\n\tdata.Set(\"del_record\", domain)\n\tdata.Set(\"praefix\", prefix)\n\tdata.Set(\"type\", recordType)\n\tdata.Set(\"content\", content)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint.String(), strings.NewReader(data.Encode()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tif req.Method != http.MethodGet {\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif string(raw) == \"null\" {\n\t\treturn fmt.Errorf(\"unexpected response: %s\", string(raw))\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\terrAPI := &APIError{}\n\n\terr := json.Unmarshal(raw, errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn errAPI\n}\n\nfunc OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {\n\tif client == nil {\n\t\tclient = &http.Client{Timeout: 15 * time.Second}\n\t}\n\n\tclient.Transport = &oauth2.Transport{\n\t\tSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),\n\t\tBase:   client.Transport,\n\t}\n\n\treturn client\n}\n"
  },
  {
    "path": "providers/dns/ipv64/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst testAPIKey = \"secret\"\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(OAuthStaticAccessToken(server.Client(), testAPIKey))\n\tclient.HTTPClient = server.Client()\n\tclient.baseURL, _ = url.Parse(server.URL)\n\n\treturn client, nil\n}\n\nfunc TestClient_GetDomains(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /api\",\n\t\t\tservermock.ResponseFromFixture(\"get_domains.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"get_domains\", \"\")).\n\t\tBuild(t)\n\n\tdomains, err := client.GetDomains(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := &Domains{\n\t\tAPIResponse: APIResponse{\n\t\t\tStatus: \"200 OK\",\n\t\t\tInfo:   \"success\",\n\t\t},\n\t\tAPICall: \"get_domains\",\n\t\tSubdomains: map[string]Subdomain{\n\t\t\t\"lego.home64.net\": {\n\t\t\t\tUpdates:          0,\n\t\t\t\tWildcard:         1,\n\t\t\t\tDomainUpdateHash: \"Dr4l6jFVgkXITqZPEyMHLNsGAfwoSu9v\",\n\t\t\t\tRecords: []Record{\n\t\t\t\t\t{\n\t\t\t\t\t\tRecordID:   50665,\n\t\t\t\t\t\tContent:    \"2606:2800:220:1:248:1893:25c8:1946\",\n\t\t\t\t\t\tTTL:        60,\n\t\t\t\t\t\tType:       \"AAAA\",\n\t\t\t\t\t\tPrefix:     \"\",\n\t\t\t\t\t\tLastUpdate: \"2023-07-19 13:18:59\",\n\t\t\t\t\t\tRecordKey:  \"MTA0YzdmMWVjYTFiNDBmZjYwMTU0OGUy\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"lego.ipv64.net\": {\n\t\t\t\tUpdates:          0,\n\t\t\t\tWildcard:         1,\n\t\t\t\tDomainUpdateHash: \"Dr4l6jFVgkXITqZPEyMHLNsGAfwoSu9v\",\n\t\t\t\tRecords: []Record{\n\t\t\t\t\t{\n\t\t\t\t\t\tRecordID:   50664,\n\t\t\t\t\t\tContent:    \"2606:2800:220:1:248:1893:25c8:1946\",\n\t\t\t\t\t\tTTL:        60,\n\t\t\t\t\t\tType:       \"AAAA\",\n\t\t\t\t\t\tPrefix:     \"\",\n\t\t\t\t\t\tLastUpdate: \"2023-07-19 13:18:59\",\n\t\t\t\t\t\tRecordKey:  \"ZDMxOWUxMjZjOTk5MmQ3N2M3ODc4NjJj\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, domains)\n}\n\nfunc TestClient_GetDomains_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /api\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\tdomains, err := client.GetDomains(t.Context())\n\trequire.Error(t, err)\n\n\trequire.Nil(t, domains)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithContentTypeFromURLEncoded()).\n\t\tRoute(\"POST /api\",\n\t\t\tservermock.ResponseFromFixture(\"add_record.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"add_record\", \"lego.ipv64.net\").\n\t\t\t\tWith(\"content\", \"value\").\n\t\t\t\tWith(\"praefix\", \"_acme-challenge\").\n\t\t\t\tWith(\"type\", \"TXT\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.AddRecord(t.Context(), \"lego.ipv64.net\", \"_acme-challenge\", \"TXT\", \"value\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /api\",\n\t\t\tservermock.ResponseFromFixture(\"add_record-error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\terr := client.AddRecord(t.Context(), \"lego.ipv64.net\", \"_acme-challenge\", \"TXT\", \"value\")\n\trequire.Error(t, err)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithContentTypeFromURLEncoded()).\n\t\tRoute(\"DELETE /api\",\n\t\t\t// the query parameters can be checked because the Go server ignores the body of a DELETE request.\n\t\t\tservermock.ResponseFromFixture(\"del_record.json\").\n\t\t\t\tWithStatusCode(http.StatusAccepted)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"lego.ipv64.net\", \"_acme-challenge\", \"TXT\", \"value\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"DELETE /api\",\n\t\t\tservermock.ResponseFromFixture(\"del_record-error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"lego.ipv64.net\", \"_acme-challenge\", \"TXT\", \"value\")\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "providers/dns/ipv64/internal/fixtures/add_record-error.json",
    "content": "{\n  \"info\": \"error\",\n  \"status\": \"400 Bad Request\",\n  \"add_record\": \"dns record already there\"\n}\n"
  },
  {
    "path": "providers/dns/ipv64/internal/fixtures/add_record.json",
    "content": "{\n  \"info\": \"success\",\n  \"status\": \"201 Created\",\n  \"add_record\": \"lego.ipv64.net\"\n}\n"
  },
  {
    "path": "providers/dns/ipv64/internal/fixtures/del_record-error.json",
    "content": "{\n  \"info\": \"error\",\n  \"status\": \"403 Forbidden\",\n  \"del_record\": \"del_record\"\n}\n"
  },
  {
    "path": "providers/dns/ipv64/internal/fixtures/del_record.json",
    "content": "{\n  \"info\": \"success\",\n  \"status\": \"202 Accepted\",\n  \"del_record\": \"del_record\"\n}\n"
  },
  {
    "path": "providers/dns/ipv64/internal/fixtures/error.json",
    "content": "{\n  \"status\": \"401 Unauthorized\",\n  \"info\": \"Unauthorized\"\n}\n"
  },
  {
    "path": "providers/dns/ipv64/internal/fixtures/get_domains.json",
    "content": "{\n  \"subdomains\": {\n    \"lego.ipv64.net\": {\n      \"updates\": 0,\n      \"wildcard\": 1,\n      \"domain_update_hash\": \"Dr4l6jFVgkXITqZPEyMHLNsGAfwoSu9v\",\n      \"records\": [\n        {\n          \"record_id\": 50664,\n          \"content\": \"2606:2800:220:1:248:1893:25c8:1946\",\n          \"ttl\": 60,\n          \"type\": \"AAAA\",\n          \"praefix\": \"\",\n          \"last_update\": \"2023-07-19 13:18:59\",\n          \"record_key\": \"ZDMxOWUxMjZjOTk5MmQ3N2M3ODc4NjJj\"\n        }\n      ]\n    },\n    \"lego.home64.net\": {\n      \"updates\": 0,\n      \"wildcard\": 1,\n      \"domain_update_hash\": \"Dr4l6jFVgkXITqZPEyMHLNsGAfwoSu9v\",\n      \"records\": [\n        {\n          \"record_id\": 50665,\n          \"content\": \"2606:2800:220:1:248:1893:25c8:1946\",\n          \"ttl\": 60,\n          \"type\": \"AAAA\",\n          \"praefix\": \"\",\n          \"last_update\": \"2023-07-19 13:18:59\",\n          \"record_key\": \"MTA0YzdmMWVjYTFiNDBmZjYwMTU0OGUy\"\n        }\n      ]\n    }\n  },\n  \"info\": \"success\",\n  \"status\": \"200 OK\",\n  \"add_domain\": \"get_domains\"\n}\n"
  },
  {
    "path": "providers/dns/ipv64/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype APIResponse struct {\n\tStatus string `json:\"status\"`\n\tInfo   string `json:\"info\"`\n}\n\n// error\n\ntype APIError struct {\n\tAPIResponse\n\n\tAddRecordMessage string `json:\"add_record\"`\n\tDelRecordMessage string `json:\"del_record\"`\n\tAddDomainMessage string `json:\"add_domain\"`\n\tDelDomainMessage string `json:\"del_domain\"`\n}\n\nfunc (a APIError) Error() string {\n\tmsg := a.Info\n\tswitch {\n\tcase a.AddRecordMessage != \"\":\n\t\tmsg = a.AddRecordMessage\n\tcase a.DelRecordMessage != \"\":\n\t\tmsg = a.DelRecordMessage\n\tcase a.AddDomainMessage != \"\":\n\t\tmsg = a.AddDomainMessage\n\tcase a.DelDomainMessage != \"\":\n\t\tmsg = a.DelDomainMessage\n\t}\n\n\tif msg == \"\" {\n\t\treturn fmt.Sprintf(\"%s: %s\", a.Status, a.Info)\n\t}\n\n\treturn fmt.Sprintf(\"%s (%s): %s\", a.Info, a.Status, msg)\n}\n\n// get_domains\n\ntype Domains struct {\n\tAPIResponse\n\n\tAPICall    string               `json:\"add_domain\"`\n\tSubdomains map[string]Subdomain `json:\"subdomains\"`\n}\n\ntype Subdomain struct {\n\tUpdates          int      `json:\"updates\"`\n\tWildcard         int      `json:\"wildcard\"`\n\tDomainUpdateHash string   `json:\"domain_update_hash\"`\n\tRecords          []Record `json:\"records\"`\n}\n\ntype Record struct {\n\tRecordID   int    `json:\"record_id\"`\n\tContent    string `json:\"content\"`\n\tTTL        int    `json:\"ttl\"`\n\tType       string `json:\"type\"`\n\tPrefix     string `json:\"praefix\"`\n\tLastUpdate string `json:\"last_update\"`\n\tRecordKey  string `json:\"record_key\"`\n}\n"
  },
  {
    "path": "providers/dns/ipv64/ipv64.go",
    "content": "// Package ipv64 implements a DNS provider for solving the DNS-01 challenge using IPv64.\n// See https://ipv64.net/healthcheck_updater_api for more info on updating TXT records.\npackage ipv64\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ipv64/internal\"\n\t\"github.com/miekg/dns\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"IPV64_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n\tSequenceInterval   time.Duration // Deprecated: unused, will be removed in v5.\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a new DNS provider using\n// environment variable IPV64_TOKEN for adding and removing the DNS record.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ipv64: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for IPv64.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"ipv64: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"ipv64: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(internal.OAuthStaticAccessToken(config.HTTPClient, config.APIKey))\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tsub, root, err := splitDomain(dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ipv64: %w\", err)\n\t}\n\n\terr = d.client.AddRecord(context.Background(), root, sub, \"TXT\", info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ipv64: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp clears IPv64 TXT record.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tsub, root, err := splitDomain(dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ipv64: %w\", err)\n\t}\n\n\terr = d.client.DeleteRecord(context.Background(), root, sub, \"TXT\", info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ipv64: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc splitDomain(full string) (string, string, error) {\n\tsplit := dns.Split(full)\n\tif len(split) < 3 {\n\t\treturn \"\", \"\", fmt.Errorf(\"unsupported domain: %s\", full)\n\t}\n\n\tif len(split) == 3 {\n\t\treturn \"\", full, nil\n\t}\n\n\tdomain := full[split[len(split)-3]:]\n\tsubDomain := full[:split[len(split)-3]-1]\n\n\treturn subDomain, domain, nil\n}\n"
  },
  {
    "path": "providers/dns/ipv64/ipv64.toml",
    "content": "Name = \"IPv64\"\nDescription = ''''''\nURL = \"https://ipv64.net/\"\nCode = \"ipv64\"\nSince = \"v4.13.0\"\n\nExample = '''\nIPV64_API_KEY=xxxxxx \\\nlego --dns ipv64 -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    IPV64_API_KEY = \"Account API Key\"\n  [Configuration.Additional]\n    IPV64_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    IPV64_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    IPV64_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://ipv64.net/dyndns_updater_api\"\n"
  },
  {
    "path": "providers/dns/ipv64/ipv64_test.go",
    "content": "package ipv64\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc Test_splitDomain(t *testing.T) {\n\ttype expected struct {\n\t\troot       string\n\t\tsub        string\n\t\trequireErr require.ErrorAssertionFunc\n\t}\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tdomain   string\n\t\texpected expected\n\t}{\n\t\t{\n\t\t\tdesc:   \"empty\",\n\t\t\tdomain: \"\",\n\t\t\texpected: expected{\n\t\t\t\trequireErr: require.Error,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"2 levels\",\n\t\t\tdomain: \"example.com\",\n\t\t\texpected: expected{\n\t\t\t\trequireErr: require.Error,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"3 levels\",\n\t\t\tdomain: \"_acme-challenge.example.com\",\n\t\t\texpected: expected{\n\t\t\t\troot:       \"_acme-challenge.example.com\",\n\t\t\t\tsub:        \"\",\n\t\t\t\trequireErr: require.NoError,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"4 levels\",\n\t\t\tdomain: \"_acme-challenge.sub.example.com\",\n\t\t\texpected: expected{\n\t\t\t\troot:       \"sub.example.com\",\n\t\t\t\tsub:        \"_acme-challenge\",\n\t\t\t\trequireErr: require.NoError,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"5 levels\",\n\t\t\tdomain: \"_acme-challenge.my.sub.example.com\",\n\t\t\texpected: expected{\n\t\t\t\troot:       \"sub.example.com\",\n\t\t\t\tsub:        \"_acme-challenge.my\",\n\t\t\t\trequireErr: require.NoError,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"6 levels\",\n\t\t\tdomain: \"_acme-challenge.my.sub.sub.example.com\",\n\t\t\texpected: expected{\n\t\t\t\troot:       \"sub.example.com\",\n\t\t\t\tsub:        \"_acme-challenge.my.sub\",\n\t\t\t\trequireErr: require.NoError,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tsub, root, err := splitDomain(test.domain)\n\t\t\ttest.expected.requireErr(t, err)\n\n\t\t\tassert.Equal(t, test.expected.root, root)\n\t\t\tassert.Equal(t, test.expected.sub, sub)\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"ipv64: some credentials information are missing: IPV64_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"ipv64: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\ntype Client struct {\n\tserverURL  string\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(serverURL string) (*Client, error) {\n\t_, err := url.Parse(serverURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"server URL: %w\", err)\n\t}\n\n\treturn &Client{\n\t\tserverURL:  serverURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) Login(ctx context.Context, username, password string) (string, error) {\n\tpayload := LoginRequest{\n\t\tUsername:    username,\n\t\tPassword:    password,\n\t\tClientLogin: false,\n\t}\n\n\tendpoint, err := url.Parse(c.serverURL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tendpoint.RawQuery = \"login\"\n\n\treq, err := newJSONRequest(ctx, endpoint, payload)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar response APIResponse\n\n\terr = c.do(req, &response)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn extractResponse[string](response)\n}\n\nfunc (c *Client) GetClientID(ctx context.Context, sessionID, sysUserID string) (int, error) {\n\tpayload := ClientIDRequest{\n\t\tSessionID: sessionID,\n\t\tSysUserID: sysUserID,\n\t}\n\n\tendpoint, err := url.Parse(c.serverURL)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tendpoint.RawQuery = \"client_get_id\"\n\n\treq, err := newJSONRequest(ctx, endpoint, payload)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvar response APIResponse\n\n\terr = c.do(req, &response)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn extractResponse[int](response)\n}\n\n// GetZoneID returns the zone ID for the given name.\nfunc (c *Client) GetZoneID(ctx context.Context, sessionID, name string) (int, error) {\n\tpayload := map[string]any{\n\t\t\"session_id\": sessionID,\n\t\t\"origin\":     name,\n\t}\n\n\tendpoint, err := url.Parse(c.serverURL)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tendpoint.RawQuery = \"dns_zone_get_id\"\n\n\treq, err := newJSONRequest(ctx, endpoint, payload)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvar response APIResponse\n\n\terr = c.do(req, &response)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn extractResponse[int](response)\n}\n\n// GetZone returns the zone information for the zone ID.\nfunc (c *Client) GetZone(ctx context.Context, sessionID, zoneID string) (*Zone, error) {\n\tpayload := map[string]any{\n\t\t\"session_id\": sessionID,\n\t\t\"primary_id\": zoneID,\n\t}\n\n\tendpoint, err := url.Parse(c.serverURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tendpoint.RawQuery = \"dns_zone_get\"\n\n\treq, err := newJSONRequest(ctx, endpoint, payload)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar response APIResponse\n\n\terr = c.do(req, &response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn extractResponse[*Zone](response)\n}\n\n// GetTXT returns the TXT record for the given name.\n// `name` must be a fully qualified domain name, e.g. \"example.com.\".\nfunc (c *Client) GetTXT(ctx context.Context, sessionID, name string) (*Record, error) {\n\tpayload := GetTXTRequest{\n\t\tSessionID: sessionID,\n\t\tPrimaryID: struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tType string `json:\"type\"`\n\t\t}{\n\t\t\tName: name,\n\t\t\tType: \"txt\",\n\t\t},\n\t}\n\n\tendpoint, err := url.Parse(c.serverURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tendpoint.RawQuery = \"dns_txt_get\"\n\n\treq, err := newJSONRequest(ctx, endpoint, payload)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar response APIResponse\n\n\terr = c.do(req, &response)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn extractResponse[*Record](response)\n}\n\n// AddTXT adds a TXT record.\n// It returns the ID of the newly created record.\nfunc (c *Client) AddTXT(ctx context.Context, sessionID, clientID string, params RecordParams) (string, error) {\n\tpayload := AddTXTRequest{\n\t\tSessionID:    sessionID,\n\t\tClientID:     clientID,\n\t\tParams:       &params,\n\t\tUpdateSerial: true,\n\t}\n\n\tendpoint, err := url.Parse(c.serverURL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tendpoint.RawQuery = \"dns_txt_add\"\n\n\treq, err := newJSONRequest(ctx, endpoint, payload)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar response APIResponse\n\n\terr = c.do(req, &response)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn extractResponse[string](response)\n}\n\n// DeleteTXT deletes a TXT record.\n// It returns the number of deleted records.\nfunc (c *Client) DeleteTXT(ctx context.Context, sessionID, recordID string) (int, error) {\n\tpayload := DeleteTXTRequest{\n\t\tSessionID:    sessionID,\n\t\tPrimaryID:    recordID,\n\t\tUpdateSerial: true,\n\t}\n\n\tendpoint, err := url.Parse(c.serverURL)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tendpoint.RawQuery = \"dns_txt_delete\"\n\n\treq, err := newJSONRequest(ctx, endpoint, payload)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvar response APIResponse\n\n\terr = c.do(req, &response)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn extractResponse[int](response)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\traw, _ := io.ReadAll(resp.Body)\n\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc extractResponse[T any](response APIResponse) (T, error) {\n\tif response.Code != \"ok\" {\n\t\tvar zero T\n\n\t\treturn zero, &APIError{APIResponse: response}\n\t}\n\n\tvar result T\n\n\terr := json.Unmarshal(response.Response, &result)\n\tif err != nil {\n\t\tvar zero T\n\t\treturn zero, fmt.Errorf(\"unable to unmarshal response: %s, %w\", string(response.Response), err)\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(server.URL)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t})\n}\n\nfunc TestClient_Login(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"login.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"login-request.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"login\", \"\"),\n\t\t).\n\t\tBuild(t)\n\n\tsessionID, err := client.Login(t.Context(), \"user\", \"secret\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"abc\", sessionID)\n}\n\nfunc TestClient_Login_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\"),\n\t\t).\n\t\tBuild(t)\n\n\t_, err := client.Login(t.Context(), \"user\", \"secret\")\n\trequire.EqualError(t, err, `code: remote_fault, message: The login failed. Username or password wrong., response: false`)\n}\n\nfunc TestClient_GetClientID(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"client_get_id.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"client_get_id-request.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"client_get_id\", \"\"),\n\t\t).\n\t\tBuild(t)\n\n\tid, err := client.GetClientID(t.Context(), \"sessionA\", \"sysA\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 123, id)\n}\n\nfunc TestClient_GetZoneID(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"dns_zone_get_id.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"dns_zone_get_id-request.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"dns_zone_get_id\", \"\"),\n\t\t).\n\t\tBuild(t)\n\n\tzoneID, err := client.GetZoneID(t.Context(), \"sessionA\", \"example.com\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 123, zoneID)\n}\n\nfunc TestClient_GetZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"dns_zone_get.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"dns_zone_get-request.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"dns_zone_get\", \"\"),\n\t\t).\n\t\tBuild(t)\n\n\tzone, err := client.GetZone(t.Context(), \"sessionA\", \"example.com.\")\n\trequire.NoError(t, err)\n\n\texpected := &Zone{\n\t\tID:         \"456\",\n\t\tServerID:   \"123\",\n\t\tSysUserID:  \"789\",\n\t\tSysGroupID: \"2\",\n\t\tOrigin:     \"example.com.\",\n\t\tSerial:     \"2025102902\",\n\t\tActive:     \"Y\",\n\t}\n\n\tassert.Equal(t, expected, zone)\n}\n\nfunc TestClient_GetTXT(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"dns_txt_get.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"dns_txt_get-request.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"dns_txt_get\", \"\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord, err := client.GetTXT(t.Context(), \"sessionA\", \"example.com.\")\n\trequire.NoError(t, err)\n\n\texpected := &Record{ID: 123}\n\n\tassert.Equal(t, expected, record)\n}\n\nfunc TestClient_AddTXT(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"dns_txt_add.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"dns_txt_add-request.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"dns_txt_add\", \"\"),\n\t\t).\n\t\tBuild(t)\n\n\tnow := time.Date(2025, 12, 25, 1, 1, 1, 0, time.UTC)\n\n\tparams := RecordParams{\n\t\tServerID:     \"serverA\",\n\t\tZone:         \"example.com.\",\n\t\tName:         \"foo.example.com.\",\n\t\tType:         \"txt\",\n\t\tData:         \"txtTXTtxt\",\n\t\tAux:          \"0\",\n\t\tTTL:          \"3600\",\n\t\tActive:       \"y\",\n\t\tStamp:        now.Format(\"2006-01-02 15:04:05\"),\n\t\tUpdateSerial: true,\n\t}\n\n\trecordID, err := client.AddTXT(t.Context(), \"sessionA\", \"clientA\", params)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"123\", recordID)\n}\n\nfunc TestClient_DeleteTXT(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"dns_txt_delete.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"dns_txt_delete-request.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"dns_txt_delete\", \"\"),\n\t\t).\n\t\tBuild(t)\n\n\tcount, err := client.DeleteTXT(t.Context(), \"sessionA\", \"123\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 1, count)\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/client_get_id-request.json",
    "content": "{\n  \"session_id\": \"sessionA\",\n  \"sys_userid\": \"sysA\"\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/client_get_id.json",
    "content": "{\n  \"code\": \"ok\",\n  \"message\": \"foo\",\n  \"response\": 123\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/dns_txt_add-request.json",
    "content": "{\n  \"session_id\": \"sessionA\",\n  \"client_id\": \"clientA\",\n  \"params\": {\n    \"server_id\": \"serverA\",\n    \"zone\": \"example.com.\",\n    \"name\": \"foo.example.com.\",\n    \"type\": \"txt\",\n    \"data\": \"txtTXTtxt\",\n    \"aux\": \"0\",\n    \"ttl\": \"3600\",\n    \"active\": \"y\",\n    \"stamp\": \"2025-12-25 01:01:01\",\n    \"update_serial\": true\n  },\n  \"update_serial\": true\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/dns_txt_add.json",
    "content": "{\n  \"code\": \"ok\",\n  \"message\": \"foo\",\n  \"response\": \"123\"\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/dns_txt_delete-request.json",
    "content": "{\n  \"session_id\": \"sessionA\",\n  \"primary_id\": \"123\",\n  \"update_serial\": true\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/dns_txt_delete.json",
    "content": "{\n  \"code\": \"ok\",\n  \"message\": \"foo\",\n  \"response\": 1\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/dns_txt_get-request.json",
    "content": "{\n  \"session_id\": \"sessionA\",\n  \"primary_id\": {\n    \"name\": \"example.com.\",\n    \"type\": \"txt\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/dns_txt_get.json",
    "content": "{\n  \"code\": \"ok\",\n  \"message\": \"foo\",\n  \"response\": {\n    \"id\": 123\n  }\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/dns_zone_get-request.json",
    "content": "{\n  \"primary_id\": \"example.com.\",\n  \"session_id\": \"sessionA\"\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/dns_zone_get.json",
    "content": "{\n  \"code\": \"ok\",\n  \"message\": \"foo\",\n  \"response\": {\n    \"id\": \"456\",\n    \"sys_userid\": \"789\",\n    \"sys_groupid\": \"2\",\n    \"sys_perm_user\": \"riud\",\n    \"sys_perm_group\": \"riud\",\n    \"sys_perm_other\": \"\",\n    \"server_id\": \"123\",\n    \"origin\": \"example.com.\",\n    \"ns\": \"ns1.example.org.\",\n    \"mbox\": \"support.example.net.\",\n    \"serial\": \"2025102902\",\n    \"refresh\": \"7200\",\n    \"retry\": \"540\",\n    \"expire\": \"604800\",\n    \"minimum\": \"3600\",\n    \"ttl\": \"3600\",\n    \"active\": \"Y\",\n    \"xfer\": \"\",\n    \"also_notify\": \"\",\n    \"update_acl\": \"\",\n    \"dnssec_initialized\": \"N\",\n    \"dnssec_wanted\": \"N\",\n    \"dnssec_algo\": \"ECDSAP256SHA256\",\n    \"dnssec_last_signed\": \"0\",\n    \"dnssec_info\": \"\",\n    \"rendered_zone\": \"\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/dns_zone_get_id-request.json",
    "content": "{\n  \"origin\": \"example.com\",\n  \"session_id\": \"sessionA\"\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/dns_zone_get_id.json",
    "content": "{\n  \"code\": \"ok\",\n  \"message\": \"foo\",\n  \"response\": 123\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/error.json",
    "content": "{\n  \"code\": \"remote_fault\",\n  \"message\": \"The login failed. Username or password wrong.\",\n  \"response\": false\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/login-request.json",
    "content": "{\n  \"username\": \"user\",\n  \"password\": \"secret\",\n  \"client_login\": false\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/fixtures/login.json",
    "content": "{\n  \"code\": \"ok\",\n  \"message\": \"foo\",\n  \"response\": \"abc\"\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/readme.md",
    "content": "## Error Response\n\n```json\n{\n  \"code\":  \"<TODO>\",\n  \"message\":  \"<TODO>\",\n  \"response\": false\n}\n```\n\n## Login Endpoint\n\n* URL: `<server>?login`\n* HTTP Method: `POST`\n\n- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/login.html\n- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/login.php\n\n### Request Body (JSON)\n\n```json\n{\n  \"username\": \"<username>\",\n  \"password\": \"<password>\",\n  \"client_login\": false\n}\n```\n\n### Response Body (JSON)\n\n```json\n{\n  \"code\": \"ok\",\n  \"message\": \"foo\",\n  \"response\": \"abc\"\n}\n```\n\n- `response`: is the `sessionID`\n\n## Get Client ID Endpoint\n\n* URL: `<server>?client_get_id`\n* HTTP Method: `POST`\n\n- function `client_get_id`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/client.inc.php#L97\n- TABLE `sys_user`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L1852\n- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/client_get_id.html\n- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/client_get_id.php\n\n### Request Body (JSON)\n\n```json\n{\n  \"session_id\": \"<sessionID>\",\n  \"sys_userid\": \"<sys_userid>\"\n}\n```\n\n### Response Body (JSON)\n\n```json\n{\n  \"code\": \"ok\",\n  \"message\": \"foo\",\n  \"response\": 123\n}\n```\n\n## DNS Zone Get ID Endpoint\n\n* URL: `<server>?dns_zone_get_id`\n* HTTP Method: `POST`\n\n- function `dns_zone_get_id`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L142\n- TABLE `dns_soa`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L615\n\n### Request Body (JSON)\n\n```json\n{\n  \"session_id\": \"<session_id>\",\n  \"origin\": \"<zone_name>\"\n}\n```\n\n### Response Body (JSON)\n\n```json\n{\n  \"code\": \"ok\",\n  \"message\": \"foo\",\n  \"response\": 123\n}\n```\n\n## DNS Zone Get Endpoint\n\n* URL: `<server>?dns_zone_get`\n* HTTP Method: `POST`\n\n- function `dns_zone_get`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L87\n- function `getDataRecord`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remoting_lib.inc.php#L248\n- TABLE `dns_soa`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L615\n- Depending on the request, the response may be an array or an object (`primary_id` can be a string, an array or an object).\n- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_zone_get.html\n- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_zone_get.php\n\n### Request Body (JSON)\n\n```json\n{\n  \"session_id\": \"<session_id>\",\n  \"primary_id\": \"<zone_id>\"\n}\n```\n\n### Response Body (JSON)\n\n```json\n{\n  \"code\": \"ok\",\n  \"message\": \"foo\",\n  \"response\": {\n    \"id\": 456,\n    \"server_id\": 123,\n    \"sys_userid\": 789\n  }\n}\n```\n\n## DNS TXT Get Endpoint\n\n* URL: `<server>?dns_txt_get`\n* HTTP Method: `POST`\n\n- function `dns_txt_get`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L640\n- function `dns_rr_get`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L195\n- form: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/web/dns/form/dns_txt.tform.php\n- TABLE `dns_rr`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/install/sql/ispconfig3.sql?ref_type=heads#L490\n- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_txt_get.html\n- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_txt_get.php\n\n### Request Body (JSON)\n\n```json\n{\n  \"session_id\": \"<session_id>\",\n  \"primary_id\": {\n    \"name\": \"<fulldomain>.\",\n    \"type\": \"TXT\"\n  }\n}\n```\n\n### Response Body (JSON)\n\n```json\n{\n  \"code\": \"ok\",\n  \"message\": \"foo\",\n  \"response\": {\n    \"id\": 123\n  }\n}\n```\n\n## DNS TXT Add Endpoint\n\n* URL: `<server>?dns_txt_add`\n* HTTP Method: `POST`\n\n- function `dns_txt_add`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L645\n- function `dns_rr_add` https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L212\n- form: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/web/dns/form/dns_txt.tform.php\n- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_txt_add.html\n- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_txt_add.php\n\n### Request Body (JSON)\n\n```json\n{\n  \"session_id\": \"<session_id>\",\n  \"client_id\": \"<client_id>\",\n  \"params\": {\n    \"server_id\": \"<server_id>\",\n    \"zone\": \"<zone>\",\n    \"name\": \"<fulldomain>.\",\n    \"type\": \"txt\",\n    \"data\": \"<txtvalue>\",\n    \"aux\": \"0\",\n    \"ttl\": \"3600\",\n    \"active\": \"y\",\n    \"stamp\": \"<curStamp>\",\n    \"update_serial\": true\n  },\n  \"update_serial\": true\n}\n```\n\n- `stamp`: (ex: `2025-12-17 23:35:58`)\n- `serial`: (ex: `1766010947`)\n\n### Response Body (JSON)\n\n```json\n{\n  \"code\": \"ok\",\n  \"message\": \"foo\",\n  \"response\": \"123\"\n}\n```\n\n## DNS TXT Delete Endpoint\n\n* URL: `<server>?dns_txt_delete`\n* HTTP Method: `POST`\n\n- function `dns_txt_delete`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L655\n- function `dns_rr_delete`: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/lib/classes/remote.d/dns.inc.php#L247\n- form: https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/interface/web/dns/form/dns_txt.tform.php\n- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/dns_txt_delete.html\n- https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/examples/dns_txt_delete.php\n\n### Request Body (JSON)\n\n```json\n{\n  \"session_id\": \"<session_id>\",\n  \"primary_id\": \"<record_id>\",\n  \"update_serial\": true\n}\n```\n\n### Response Body (JSON)\n\n```json\n{\n  \"code\": \"ok\",\n  \"message\": \"foo\",\n  \"response\": 1\n}\n```\n\n---\n\nhttps://www.ispconfig.org/\nhttps://git.ispconfig.org/ispconfig/ispconfig3\nhttps://forum.howtoforge.com/#ispconfig-3.23\n"
  },
  {
    "path": "providers/dns/ispconfig/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n)\n\ntype APIError struct {\n\tAPIResponse\n}\n\nfunc (e *APIError) Error() string {\n\tvar msg strings.Builder\n\n\tmsg.WriteString(\"code: \" + e.Code)\n\n\tif e.Message != \"\" {\n\t\tmsg.WriteString(\", message: \" + e.Message)\n\t}\n\n\tif len(e.Response) > 0 {\n\t\tmsg.WriteString(\", response: \" + string(e.Response))\n\t}\n\n\treturn msg.String()\n}\n\ntype APIResponse struct {\n\tCode     string          `json:\"code\"`\n\tMessage  string          `json:\"message\"`\n\tResponse json.RawMessage `json:\"response\"`\n}\n\ntype LoginRequest struct {\n\tUsername    string `json:\"username\"`\n\tPassword    string `json:\"password\"`\n\tClientLogin bool   `json:\"client_login\"`\n}\n\ntype ClientIDRequest struct {\n\tSessionID string `json:\"session_id\"`\n\tSysUserID string `json:\"sys_userid\"`\n}\n\ntype Zone struct {\n\tID         string `json:\"id\"`\n\tServerID   string `json:\"server_id\"`\n\tSysUserID  string `json:\"sys_userid\"`\n\tSysGroupID string `json:\"sys_groupid\"`\n\tOrigin     string `json:\"origin\"`\n\tSerial     string `json:\"serial\"`\n\tActive     string `json:\"active\"`\n}\n\ntype GetTXTRequest struct {\n\tSessionID string `json:\"session_id\"`\n\tPrimaryID struct {\n\t\tName string `json:\"name\"`\n\t\tType string `json:\"type\"`\n\t} `json:\"primary_id\"`\n}\n\ntype Record struct {\n\tID int `json:\"id\"`\n}\n\ntype AddTXTRequest struct {\n\tSessionID    string        `json:\"session_id\"`\n\tClientID     string        `json:\"client_id\"`\n\tParams       *RecordParams `json:\"params,omitempty\"`\n\tUpdateSerial bool          `json:\"update_serial\"`\n}\n\ntype RecordParams struct {\n\tServerID string `json:\"server_id\"`\n\tZone     string `json:\"zone\"`\n\tName     string `json:\"name\"`\n\t// 'a','aaaa','alias','cname','hinfo','mx','naptr','ns','ds','ptr','rp','srv','txt'\n\tType string `json:\"type\"`\n\tData string `json:\"data\"`\n\t// \"0\"\n\tAux string `json:\"aux\"`\n\tTTL string `json:\"ttl\"`\n\t// 'n','y'\n\tActive string `json:\"active\"`\n\t// `2025-12-17 23:35:58`\n\tStamp        string `json:\"stamp\"`\n\tUpdateSerial bool   `json:\"update_serial\"`\n}\n\ntype DeleteTXTRequest struct {\n\tSessionID    string `json:\"session_id\"`\n\tPrimaryID    string `json:\"primary_id\"`\n\tUpdateSerial bool   `json:\"update_serial\"`\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/ispconfig.go",
    "content": "// Package ispconfig implements a DNS provider for solving the DNS-01 challenge using ISPConfig.\npackage ispconfig\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ispconfig/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"ISPCONFIG_\"\n\n\tEnvServerURL = envNamespace + \"SERVER_URL\"\n\tEnvUsername  = envNamespace + \"USERNAME\"\n\tEnvPassword  = envNamespace + \"PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvInsecureSkipVerify = envNamespace + \"INSECURE_SKIP_VERIFY\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tServerURL string\n\tUsername  string\n\tPassword  string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n\tInsecureSkipVerify bool\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for ISPConfig.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvServerURL, EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ispconfig: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.ServerURL = values[EnvServerURL]\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\tconfig.InsecureSkipVerify = env.GetOrDefaultBool(EnvInsecureSkipVerify, false)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for ISPConfig.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"ispconfig: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.ServerURL == \"\" {\n\t\treturn nil, errors.New(\"ispconfig: missing server URL\")\n\t}\n\n\tif config.Username == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"ispconfig: credentials missing\")\n\t}\n\n\tclient, err := internal.NewClient(config.ServerURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ispconfig: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tif config.InsecureSkipVerify {\n\t\tclient.HTTPClient.Transport = &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t}\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tsessionID, err := d.client.Login(ctx, d.config.Username, d.config.Password)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ispconfig: login: %w\", err)\n\t}\n\n\tzoneID, err := d.findZone(ctx, sessionID, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ispconfig: get zone id: %w\", err)\n\t}\n\n\tzone, err := d.client.GetZone(ctx, sessionID, strconv.Itoa(zoneID))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ispconfig: get zone: %w\", err)\n\t}\n\n\tclientID, err := d.client.GetClientID(ctx, sessionID, zone.SysUserID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ispconfig: get client id: %w\", err)\n\t}\n\n\tparams := internal.RecordParams{\n\t\tServerID: \"serverA\",\n\t\tZone:     zone.ID,\n\t\tName:     info.EffectiveFQDN,\n\t\tType:     \"txt\",\n\t\tData:     info.Value,\n\t\tAux:      \"0\",\n\t\tTTL:      strconv.Itoa(d.config.TTL),\n\t\tActive:   \"y\",\n\t\tStamp:    time.Now().UTC().Format(\"2006-01-02 15:04:05\"),\n\t}\n\n\trecordID, err := d.client.AddTXT(ctx, sessionID, strconv.Itoa(clientID), params)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ispconfig: add txt record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = recordID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// gets the record's unique ID\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"ispconfig: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\tsessionID, err := d.client.Login(ctx, d.config.Username, d.config.Password)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ispconfig: login: %w\", err)\n\t}\n\n\t_, err = d.client.DeleteTXT(ctx, sessionID, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ispconfig: delete txt record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) findZone(ctx context.Context, sessionID, fqdn string) (int, error) {\n\tfor domain := range dns01.UnFqdnDomainsSeq(fqdn) {\n\t\tzoneID, err := d.client.GetZoneID(ctx, sessionID, domain)\n\t\tif err == nil {\n\t\t\treturn zoneID, nil\n\t\t}\n\t}\n\n\treturn 0, fmt.Errorf(\"zone not found for %q\", fqdn)\n}\n"
  },
  {
    "path": "providers/dns/ispconfig/ispconfig.toml",
    "content": "Name = \"ISPConfig 3\"\nDescription = ''''''\nURL = \"https://www.ispconfig.org/\"\nCode = \"ispconfig\"\nSince = \"v4.31.0\"\n\nExample = '''\nISPCONFIG_SERVER_URL=\"https://example.com:8080/remote/json.php\" \\\nISPCONFIG_USERNAME=\"xxx\" \\\nISPCONFIG_PASSWORD=\"yyy\" \\\nlego --dns ispconfig -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    ISPCONFIG_SERVER_URL = \"Server URL\"\n    ISPCONFIG_USERNAME = \"Username\"\n    ISPCONFIG_PASSWORD = \"Password\"\n  [Configuration.Additional]\n    ISPCONFIG_INSECURE_SKIP_VERIFY = \"Whether to verify the API certificate\"\n    ISPCONFIG_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    ISPCONFIG_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    ISPCONFIG_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    ISPCONFIG_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://git.ispconfig.org/ispconfig/ispconfig3/-/blob/develop/remoting_client/API-docs/index.html\"\n"
  },
  {
    "path": "providers/dns/ispconfig/ispconfig_test.go",
    "content": "package ispconfig\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvServerURL,\n\tEnvUsername,\n\tEnvPassword,\n).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL: \"https://example.com:80/\",\n\t\t\t\tEnvUsername:  \"user\",\n\t\t\t\tEnvPassword:  \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing server URL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL: \"\",\n\t\t\t\tEnvUsername:  \"user\",\n\t\t\t\tEnvPassword:  \"secret\",\n\t\t\t},\n\t\t\texpected: \"ispconfig: some credentials information are missing: ISPCONFIG_SERVER_URL\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL: \"https://example.com:80/\",\n\t\t\t\tEnvUsername:  \"\",\n\t\t\t\tEnvPassword:  \"secret\",\n\t\t\t},\n\t\t\texpected: \"ispconfig: some credentials information are missing: ISPCONFIG_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL: \"https://example.com:80/\",\n\t\t\t\tEnvUsername:  \"user\",\n\t\t\t\tEnvPassword:  \"\",\n\t\t\t},\n\t\t\texpected: \"ispconfig: some credentials information are missing: ISPCONFIG_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"ispconfig: some credentials information are missing: ISPCONFIG_SERVER_URL,ISPCONFIG_USERNAME,ISPCONFIG_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tserverURL string\n\t\tusername  string\n\t\tpassword  string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tserverURL: \"https://example.com:80/\",\n\t\t\tusername:  \"user\",\n\t\t\tpassword:  \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing server URL\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"ispconfig: missing server URL\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing username\",\n\t\t\tserverURL: \"https://example.com:80/\",\n\t\t\tpassword:  \"secret\",\n\t\t\texpected:  \"ispconfig: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing password\",\n\t\t\tserverURL: \"https://example.com:80/\",\n\t\t\tusername:  \"user\",\n\t\t\texpected:  \"ispconfig: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"ispconfig: missing server URL\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.ServerURL = test.serverURL\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/ispconfigddns/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\nconst (\n\taddAction    = \"add\"\n\tdeleteAction = \"delete\"\n)\n\ntype Client struct {\n\ttoken     string\n\tserverURL string\n\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(serverURL, token string) (*Client, error) {\n\t_, err := url.Parse(serverURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"server URL: %w\", err)\n\t}\n\n\treturn &Client{\n\t\tserverURL:  serverURL,\n\t\ttoken:      token,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) AddTXTRecord(ctx context.Context, zone, fqdn, content string) error {\n\treturn c.updateRecord(ctx, UpdateRecord{Action: addAction, Zone: zone, Type: \"TXT\", Record: fqdn, Data: content})\n}\n\nfunc (c *Client) DeleteTXTRecord(ctx context.Context, zone, fqdn, recordContent string) error {\n\treturn c.updateRecord(ctx, UpdateRecord{Action: deleteAction, Zone: zone, Type: \"TXT\", Record: fqdn, Data: recordContent})\n}\n\nfunc (c *Client) updateRecord(ctx context.Context, action UpdateRecord) error {\n\treq, err := c.newRequest(ctx, action)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req)\n}\n\nfunc (c *Client) do(req *http.Request) error {\n\tuseragent.SetHeader(req.Header)\n\n\treq.SetBasicAuth(\"anonymous\", c.token)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\t// The endpoint uses the `DefaultDdnsResponseWriter`,\n\t// and this writer uses HTTP status code to determine if the request was successful or not.\n\t// - https://github.com/mhofer117/ispconfig-ddns-module/blob/8b011a5bb138881d9f13360a5c4fec10c0084613/lib/updater/DdnsUpdater.php#L53-L57\n\t// - https://github.com/mhofer117/ispconfig-ddns-module/blob/master/lib/updater/response/DefaultDdnsResponseWriter.php\n\tif resp.StatusCode/100 != 2 {\n\t\traw, _ := io.ReadAll(resp.Body)\n\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) newRequest(ctx context.Context, action UpdateRecord) (*http.Request, error) {\n\tendpoint, err := url.Parse(c.serverURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tendpoint = endpoint.JoinPath(\"ddns\", \"update.php\")\n\n\tvalues, err := querystring.Values(action)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tendpoint.RawQuery = values.Encode()\n\n\tmethod := http.MethodPost\n\tif action.Action == deleteAction {\n\t\tmethod = http.MethodDelete\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/ispconfigddns/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient, err := NewClient(server.URL, \"secret\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.HTTPClient = server.Client()\n\n\treturn client, nil\n}\n\nfunc TestClient_AddTXTRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /ddns/update.php\",\n\t\t\tservermock.Noop(),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithBasicAuth(\"anonymous\", \"secret\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"action\", \"add\").\n\t\t\t\tWith(\"zone\", \"example.com\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"record\", \"_acme-challenge.example.com.\").\n\t\t\t\tWith(\"data\", \"token\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.AddTXTRecord(t.Context(), \"example.com\", \"_acme-challenge.example.com.\", \"token\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddTXTRecord_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /ddns/update.php\",\n\t\t\tservermock.RawStringResponse(\"Missing or invalid token.\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.AddTXTRecord(t.Context(), \"example.com\", \"_acme-challenge.example.com.\", \"token\")\n\trequire.EqualError(t, err, \"unexpected status code: [status code: 401] body: Missing or invalid token.\")\n}\n\nfunc TestClient_DeleteTXTRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"DELETE /ddns/update.php\",\n\t\t\tservermock.Noop(),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithBasicAuth(\"anonymous\", \"secret\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"action\", \"delete\").\n\t\t\t\tWith(\"zone\", \"example.com\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"record\", \"_acme-challenge.example.com.\").\n\t\t\t\tWith(\"data\", \"token\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.DeleteTXTRecord(t.Context(), \"example.com\", \"_acme-challenge.example.com.\", \"token\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteTXTRecord_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"DELETE /ddns/update.php\",\n\t\t\tservermock.RawStringResponse(\"Missing or invalid token.\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.DeleteTXTRecord(t.Context(), \"example.com\", \"_acme-challenge.example.com.\", \"token\")\n\trequire.EqualError(t, err, \"unexpected status code: [status code: 401] body: Missing or invalid token.\")\n}\n"
  },
  {
    "path": "providers/dns/ispconfigddns/internal/types.go",
    "content": "package internal\n\ntype UpdateRecord struct {\n\tAction string `url:\"action,omitempty\"`\n\tZone   string `url:\"zone,omitempty\"`\n\tType   string `url:\"type,omitempty\"`\n\tRecord string `url:\"record,omitempty\"`\n\tData   string `url:\"data,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/ispconfigddns/ispconfigddns.go",
    "content": "// Package ispconfigddns implements a DNS provider for solving the DNS-01 challenge using ISPConfig 3 Dynamic DNS (DDNS) Module.\npackage ispconfigddns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ispconfigddns/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"ISPCONFIG_DDNS_\"\n\n\tEnvServerURL = envNamespace + \"SERVER_URL\"\n\tEnvToken     = envNamespace + \"TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tServerURL string\n\tToken     string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 3600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for ISPConfig 3 Dynamic DNS (DDNS) Module.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvServerURL, EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ispconfig (DDNS module): %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.ServerURL = values[EnvServerURL]\n\tconfig.Token = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for ISPConfig 3 Dynamic DNS (DDNS) Module.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"ispconfig (DDNS module): the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.ServerURL == \"\" {\n\t\treturn nil, errors.New(\"ispconfig (DDNS module): missing server URL\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"ispconfig (DDNS module): missing token\")\n\t}\n\n\tclient, err := internal.NewClient(config.ServerURL, config.Token)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ispconfig (DDNS module): %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to control checking compliance to spec.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ispconfig (DDNS module): could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\terr = d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(zone), info.EffectiveFQDN, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ispconfig (DDNS module): add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ispconfig (DDNS module): could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\terr = d.client.DeleteTXTRecord(context.Background(), dns01.UnFqdn(zone), info.EffectiveFQDN, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ispconfig (DDNS module): delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/ispconfigddns/ispconfigddns.toml",
    "content": "Name = \"ISPConfig 3 - Dynamic DNS (DDNS) Module\"\nDescription = ''''''\nURL = \"https://www.ispconfig.org/\"\nCode = \"ispconfigddns\"\nSince = \"v4.31.0\"\n\nExample = '''\nISPCONFIG_DDNS_SERVER_URL=\"https://panel.example.com:8080\" \\\nISPCONFIG_DDNS_TOKEN=xxxxxx \\\nlego --dns ispconfigddns -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\nISPConfig DNS provider supports leveraging the [ISPConfig 3 Dynamic DNS (DDNS) Module](https://github.com/mhofer117/ispconfig-ddns-module).\n\nRequires the DDNS module described at https://www.ispconfig.org/ispconfig/download/\n\nSee https://www.howtoforge.com/community/threads/ispconfig-3-danymic-dns-ddns-module.87967/ for additional details.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    ISPCONFIG_DDNS_SERVER_URL = \"API server URL (ex: https://panel.example.com:8080)\"\n    ISPCONFIG_DDNS_TOKEN = \"DDNS API token\"\n  [Configuration.Additional]\n    ISPCONFIG_DDNS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    ISPCONFIG_DDNS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    ISPCONFIG_DDNS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)\"\n    ISPCONFIG_DDNS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://github.com/mhofer117/ispconfig-ddns-module/tree/master/lib/updater\"\n"
  },
  {
    "path": "providers/dns/ispconfigddns/ispconfigddns_test.go",
    "content": "package ispconfigddns\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvServerURL, EnvToken).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL: \"https://example.com\",\n\t\t\t\tEnvToken:     \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing server URL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL: \"\",\n\t\t\t\tEnvToken:     \"secret\",\n\t\t\t},\n\t\t\texpected: \"ispconfig (DDNS module): some credentials information are missing: ISPCONFIG_DDNS_SERVER_URL\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerURL: \"https://example.com\",\n\t\t\t\tEnvToken:     \"\",\n\t\t\t},\n\t\t\texpected: \"ispconfig (DDNS module): some credentials information are missing: ISPCONFIG_DDNS_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"ispconfig (DDNS module): some credentials information are missing: ISPCONFIG_DDNS_SERVER_URL,ISPCONFIG_DDNS_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tserverURL string\n\t\ttoken     string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tserverURL: \"https://example.com\",\n\t\t\ttoken:     \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing server URL\",\n\t\t\tserverURL: \"\",\n\t\t\ttoken:     \"secret\",\n\t\t\texpected:  \"ispconfig (DDNS module): missing server URL\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing token\",\n\t\t\tserverURL: \"https://example.com\",\n\t\t\ttoken:     \"\",\n\t\t\texpected:  \"ispconfig (DDNS module): missing token\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.ServerURL = test.serverURL\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {\n\t\tconfig := NewDefaultConfig()\n\t\tconfig.HTTPClient = server.Client()\n\t\tconfig.Token = \"secret\"\n\t\tconfig.ServerURL = server.URL\n\n\t\treturn NewDNSProviderConfig(config)\n\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithBasicAuth(\"anonymous\", \"secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /ddns/update.php\",\n\t\t\tservermock.DumpRequest(),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"action\", \"add\").\n\t\t\t\tWith(\"zone\", \"example.com\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"record\", \"_acme-challenge.example.com.\").\n\t\t\t\tWith(\"data\", \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"DELETE /ddns/update.php\",\n\t\t\tservermock.DumpRequest(),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"action\", \"delete\").\n\t\t\t\tWith(\"zone\", \"example.com\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"record\", \"_acme-challenge.example.com.\").\n\t\t\t\tWith(\"data\", \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/iwantmyname/iwantmyname.go",
    "content": "// Package iwantmyname implements a DNS provider for solving the DNS-01 challenge using iwantmyname.\npackage iwantmyname\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"IWANTMYNAME_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername           string\n\tPassword           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for iwantmyname.\n// Credentials must be passed in the environment variables: IWANTMYNAME_USERNAME, IWANTMYNAME_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"iwantmyname: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for iwantmyname.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\treturn nil, errors.New(\"iwantmyname: the iwantmyname API has shut down https://github.com/go-acme/lego/issues/2563\")\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, _, keyAuth string) error {\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/iwantmyname/iwantmyname.toml",
    "content": "Name = \"iwantmyname (Deprecated)\"\nDescription = '''\nThe iwantmyname API has shut down.\n\nhttps://github.com/go-acme/lego/issues/2563\n'''\nURL = \"https://iwantmyname.com\"\nCode = \"iwantmyname\"\nSince = \"v4.7.0\"\n\nExample = '''\nIWANTMYNAME_USERNAME=xxxxxxxx \\\nIWANTMYNAME_PASSWORD=xxxxxxxx \\\nlego --dns iwantmyname -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    IWANTMYNAME_USERNAME = \"API username\"\n    IWANTMYNAME_PASSWORD = \"API password\"\n  [Configuration.Additional]\n    IWANTMYNAME_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    IWANTMYNAME_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    IWANTMYNAME_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    IWANTMYNAME_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://iwantmyname.com/developer/domain-dns-api\"\n"
  },
  {
    "path": "providers/dns/jdcloud/fixtures/create_record-request.json",
    "content": "{\n  \"domainId\": \"20\",\n  \"regionId\": \"cn-north-1\",\n  \"req\": {\n    \"hostRecord\": \"_acme-challenge\",\n    \"hostValue\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n    \"jcloudRes\": null,\n    \"mxPriority\": null,\n    \"port\": null,\n    \"ttl\": 120,\n    \"type\": \"TXT\",\n    \"viewValue\": -1,\n    \"weight\": null\n  }\n}\n"
  },
  {
    "path": "providers/dns/jdcloud/fixtures/create_record.json",
    "content": "{\n  \"requestId\": \"azerty\",\n  \"error\": {\n    \"code\": 0,\n    \"status\": \"\",\n    \"message\": \"\"\n  },\n  \"result\": {\n    \"dataList\": {\n      \"id\": 123,\n      \"hostRecord\": \"_acme-challenge\",\n      \"hostValue\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n      \"jcloudRes\": false,\n      \"mxPriority\": 0,\n      \"port\": 0,\n      \"ttl\": 120,\n      \"type\": \"TXT\",\n      \"weight\": 0,\n      \"viewValue\": [\n        1,\n        2\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/jdcloud/fixtures/delete_record.json",
    "content": "{\n  \"requestId\": \"azerty\",\n  \"error\": {\n    \"code\": 0,\n    \"status\": \"\",\n    \"message\": \"\"\n  },\n  \"result\": {}\n}\n"
  },
  {
    "path": "providers/dns/jdcloud/fixtures/describe_domains_page1.json",
    "content": "{\n  \"requestId\": \"azerty\",\n  \"error\": {\n    \"code\": 0,\n    \"status\": \"\",\n    \"message\": \"\"\n  },\n  \"result\": {\n    \"dataList\": [\n      {\n        \"id\": 1,\n        \"domainName\": \"1.example\"\n      },\n      {\n        \"id\": 2,\n        \"domainName\": \"2.example\"\n      },\n      {\n        \"id\": 3,\n        \"domainName\": \"3.example\"\n      },\n      {\n        \"id\": 4,\n        \"domainName\": \"4.example\"\n      },\n      {\n        \"id\": 5,\n        \"domainName\": \"5.example\"\n      },\n      {\n        \"id\": 6,\n        \"domainName\": \"6.example\"\n      },\n      {\n        \"id\": 7,\n        \"domainName\": \"7.example\"\n      },\n      {\n        \"id\": 8,\n        \"domainName\": \"8.example\"\n      },\n      {\n        \"id\": 9,\n        \"domainName\": \"9.example\"\n      },\n      {\n        \"id\": 10,\n        \"domainName\": \"10.example\"\n      }\n    ],\n    \"currentCount\": 10,\n    \"totalCount\": 20,\n    \"totalPage\": 2\n  }\n}\n"
  },
  {
    "path": "providers/dns/jdcloud/fixtures/describe_domains_page2.json",
    "content": "{\n  \"requestId\": \"azerty\",\n  \"error\": {\n    \"code\": 0,\n    \"status\": \"\",\n    \"message\": \"\"\n  },\n  \"result\": {\n    \"dataList\": [\n      {\n        \"id\": 11,\n        \"domainName\": \"11.example\"\n      },\n      {\n        \"id\": 12,\n        \"domainName\": \"12.example\"\n      },\n      {\n        \"id\": 13,\n        \"domainName\": \"13.example\"\n      },\n      {\n        \"id\": 14,\n        \"domainName\": \"14.example\"\n      },\n      {\n        \"id\": 15,\n        \"domainName\": \"15.example\"\n      },\n      {\n        \"id\": 16,\n        \"domainName\": \"16.example\"\n      },\n      {\n        \"id\": 17,\n        \"domainName\": \"17.example\"\n      },\n      {\n        \"id\": 18,\n        \"domainName\": \"18.example\"\n      },\n      {\n        \"id\": 19,\n        \"domainName\": \"19.example\"\n      },\n      {\n        \"id\": 20,\n        \"domainName\": \"example.com\"\n      }\n    ],\n    \"currentCount\": 10,\n    \"totalCount\": 20,\n    \"totalPage\": 2\n  }\n}\n"
  },
  {
    "path": "providers/dns/jdcloud/jdcloud.go",
    "content": "// Package jdcloud implements a DNS provider for solving the DNS-01 challenge using JD Cloud.\npackage jdcloud\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/jdcloud-sdk-go/core\"\n\t\"github.com/go-acme/jdcloud-sdk-go/services/domainservice/apis\"\n\tjdcclient \"github.com/go-acme/jdcloud-sdk-go/services/domainservice/client\"\n\tdomainservice \"github.com/go-acme/jdcloud-sdk-go/services/domainservice/models\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"JDCLOUD_\"\n\n\tEnvAccessKeyID     = envNamespace + \"ACCESS_KEY_ID\"\n\tEnvAccessKeySecret = envNamespace + \"ACCESS_KEY_SECRET\"\n\tEnvRegionID        = envNamespace + \"REGION_ID\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAccessKeyID     string\n\tAccessKeySecret string\n\tRegionID        string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *jdcclient.DomainserviceClient\n\n\trecordIDs   map[string]int\n\tdomainIDs   map[string]int\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for JD Cloud.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAccessKeyID, EnvAccessKeySecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"jdcloud: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AccessKeyID = values[EnvAccessKeyID]\n\tconfig.AccessKeySecret = values[EnvAccessKeySecret]\n\n\t// https://docs.jdcloud.com/en/common-declaration/api/introduction#Region%20Code\n\tconfig.RegionID = env.GetOrDefaultString(EnvRegionID, \"cn-north-1\")\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for JD Cloud.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"jdcloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.AccessKeyID == \"\" || config.AccessKeySecret == \"\" {\n\t\treturn nil, errors.New(\"jdcloud: missing credentials\")\n\t}\n\n\tcred := core.NewCredentials(config.AccessKeyID, config.AccessKeySecret)\n\n\tclient := jdcclient.NewDomainserviceClient(cred)\n\tclient.DisableLogger()\n\tclient.Config.SetTimeout(config.HTTPTimeout)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]int),\n\t\tdomainIDs: make(map[string]int),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"jdcloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"jdcloud: %w\", err)\n\t}\n\n\tzone, err := d.findZone(dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"jdcloud: %w\", err)\n\t}\n\n\t// https://docs.jdcloud.com/cn/jd-cloud-dns/api/createresourcerecord\n\tcrrr := apis.NewCreateResourceRecordRequestWithAllParams(\n\t\td.config.RegionID,\n\t\tstrconv.Itoa(zone.Id),\n\t\t&domainservice.AddRR{\n\t\t\tHostRecord: subDomain,\n\t\t\tHostValue:  info.Value,\n\t\t\tTtl:        d.config.TTL,\n\t\t\tType:       \"TXT\",\n\t\t\tViewValue:  -1,\n\t\t},\n\t)\n\n\trecord, err := jdcclient.CreateResourceRecord(d.client, crrr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"jdcloud: create resource record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.domainIDs[token] = zone.Id\n\td.recordIDs[token] = record.Result.DataList.Id\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordIDsMu.Lock()\n\trecordID, recordOK := d.recordIDs[token]\n\tdomainID, domainOK := d.domainIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !recordOK {\n\t\treturn fmt.Errorf(\"jdcloud: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\tif !domainOK {\n\t\treturn fmt.Errorf(\"jdcloud: unknown domain ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\t// https://docs.jdcloud.com/cn/jd-cloud-dns/api/deleteresourcerecord\n\tdrrr := apis.NewDeleteResourceRecordRequestWithAllParams(\n\t\td.config.RegionID,\n\t\tstrconv.Itoa(domainID),\n\t\tstrconv.Itoa(recordID),\n\t)\n\n\t_, err := jdcclient.DeleteResourceRecord(d.client, drrr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"jdcloud: delete resource record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) findZone(zone string) (*domainservice.DomainInfo, error) {\n\t// https://docs.jdcloud.com/cn/jd-cloud-dns/api/describedomains\n\tddr := apis.NewDescribeDomainsRequestWithoutParam()\n\tddr.SetRegionId(d.config.RegionID)\n\tddr.SetPageNumber(1)\n\tddr.SetPageSize(10)\n\tddr.SetDomainName(zone)\n\n\tfor {\n\t\tresponse, err := jdcclient.DescribeDomains(d.client, ddr)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"describe domains: %w\", err)\n\t\t}\n\n\t\tfor _, d := range response.Result.DataList {\n\t\t\tif d.DomainName == zone {\n\t\t\t\treturn &d, nil\n\t\t\t}\n\t\t}\n\n\t\tif len(response.Result.DataList) < ddr.PageSize || response.Result.TotalPage <= ddr.PageNumber {\n\t\t\tbreak\n\t\t}\n\n\t\tddr.SetPageNumber(ddr.PageNumber + 1)\n\t}\n\n\treturn nil, errors.New(\"zone not found\")\n}\n"
  },
  {
    "path": "providers/dns/jdcloud/jdcloud.toml",
    "content": "Name = \"JD Cloud\"\nDescription = ''''''\nURL = \"https://www.jdcloud.com/\"\nCode = \"jdcloud\"\nSince = \"v4.31.0\"\n\nExample = '''\nJDCLOUD_ACCESS_KEY_ID=\"xxx\" \\\nJDCLOUD_ACCESS_KEY_SECRET=\"yyy\" \\\nlego --dns jdcloud -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    JDCLOUD_ACCESS_KEY_ID = \"Access key ID\"\n    JDCLOUD_ACCESS_KEY_SECRET = \"Access key secret\"\n  [Configuration.Additional]\n    JDCLOUD_REGION_ID = \"Region ID (Default: cn-north-1)\"\n    JDCLOUD_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    JDCLOUD_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    JDCLOUD_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    JDCLOUD_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://docs.jdcloud.com/cn/jd-cloud-dns/api/overview\"\n  Common = \"https://docs.jdcloud.com/en/common-declaration/api/introduction\"\n  GoClient = \"https://github.com/jdcloud-api/jdcloud-sdk-go\"\n"
  },
  {
    "path": "providers/dns/jdcloud/jdcloud_test.go",
    "content": "package jdcloud\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAccessKeyID,\n\tEnvAccessKeySecret,\n\tEnvRegionID,\n).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKeyID:     \"abc123\",\n\t\t\t\tEnvAccessKeySecret: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access key ID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKeyID:     \"\",\n\t\t\t\tEnvAccessKeySecret: \"secret\",\n\t\t\t},\n\t\t\texpected: \"jdcloud: some credentials information are missing: JDCLOUD_ACCESS_KEY_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access key secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKeyID:     \"abc123\",\n\t\t\t\tEnvAccessKeySecret: \"\",\n\t\t\t},\n\t\t\texpected: \"jdcloud: some credentials information are missing: JDCLOUD_ACCESS_KEY_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"jdcloud: some credentials information are missing: JDCLOUD_ACCESS_KEY_ID,JDCLOUD_ACCESS_KEY_SECRET\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc            string\n\t\taccessKeyID     string\n\t\taccessKeySecret string\n\t\texpected        string\n\t}{\n\t\t{\n\t\t\tdesc:            \"success\",\n\t\t\taccessKeyID:     \"abc123\",\n\t\t\taccessKeySecret: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:            \"missing access key ID\",\n\t\t\taccessKeySecret: \"secret\",\n\t\t\texpected:        \"jdcloud: missing credentials\",\n\t\t},\n\t\t{\n\t\t\tdesc:        \"missing access key secret\",\n\t\t\taccessKeyID: \"abc123\",\n\t\t\texpected:    \"jdcloud: missing credentials\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"jdcloud: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AccessKeyID = test.accessKeyID\n\t\t\tconfig.AccessKeySecret = test.accessKeySecret\n\t\t\tconfig.RegionID = \"cn-north-1\"\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AccessKeyID = \"abc123\"\n\t\t\tconfig.AccessKeySecret = \"secret\"\n\t\t\tconfig.RegionID = \"cn-north-1\"\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tserverURL, _ := url.Parse(server.URL)\n\n\t\t\tp.client.Config.SetEndpoint(net.JoinHostPort(serverURL.Hostname(), serverURL.Port()))\n\t\t\tp.client.Config.SetScheme(serverURL.Scheme)\n\t\t\tp.client.Config.SetTimeout(server.Client().Timeout)\n\n\t\t\treturn p, nil\n\t\t},\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /v2/regions/cn-north-1/domain\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\tpageNumber := req.URL.Query().Get(\"pageNumber\")\n\n\t\t\t\tservermock.ResponseFromFixture(\n\t\t\t\t\tfmt.Sprintf(\"describe_domains_page%s.json\", pageNumber),\n\t\t\t\t).ServeHTTP(rw, req)\n\t\t\t}),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"domainName\", \"example.com\").\n\t\t\t\tWithRegexp(\"pageNumber\", `(1|2)`).\n\t\t\t\tWith(\"pageSize\", \"10\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithRegexp(\"Authorization\",\n\t\t\t\t\t`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+`).\n\t\t\t\tWithRegexp(\"X-Jdcloud-Date\", `\\d{8}T\\d{6}Z`).\n\t\t\t\tWithRegexp(\"X-Jdcloud-Nonce\", `[\\w-]+`),\n\t\t).\n\t\tRoute(\"POST /v2/regions/cn-north-1/domain/20/ResourceRecord\",\n\t\t\tservermock.ResponseFromFixture(\"create_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_record-request.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithRegexp(\"Authorization\",\n\t\t\t\t\t`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+`).\n\t\t\t\tWithRegexp(\"X-Jdcloud-Date\", `\\d{8}T\\d{6}Z`).\n\t\t\t\tWithRegexp(\"X-Jdcloud-Nonce\", `[\\w-]+`),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n\n\trequire.Len(t, provider.domainIDs, 1)\n\trequire.Len(t, provider.recordIDs, 1)\n\n\tassert.Equal(t, 20, provider.domainIDs[\"abc\"])\n\tassert.Equal(t, 123, provider.recordIDs[\"abc\"])\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"DELETE /v2/regions/cn-north-1/domain/20/ResourceRecord/123\",\n\t\t\tservermock.ResponseFromFixture(\"delete_record.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithRegexp(\"Authorization\",\n\t\t\t\t\t`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+`).\n\t\t\t\tWithRegexp(\"X-Jdcloud-Date\", `\\d{8}T\\d{6}Z`).\n\t\t\t\tWithRegexp(\"X-Jdcloud-Nonce\", `[\\w-]+`),\n\t\t).\n\t\tBuild(t)\n\n\tprovider.domainIDs[\"abc\"] = 20\n\tprovider.recordIDs[\"abc\"] = 123\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/joker/internal/dmapi/client.go",
    "content": "// Package dmapi Client for DMAPI joker.com.\n// https://joker.com/faq/category/39/22-dmapi.html\npackage dmapi\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://dmapi.joker.com/request/\"\n\n// Response Joker DMAPI Response.\ntype Response struct {\n\tHeaders    url.Values\n\tBody       string\n\tStatusCode int\n\tStatusText string\n\tAuthSid    string\n}\n\ntype AuthInfo struct {\n\tAPIKey   string\n\tUsername string\n\tPassword string\n}\n\n// Client a DMAPI Client.\ntype Client struct {\n\tapiKey   string\n\tusername string\n\tpassword string\n\n\ttoken   *Token\n\tmuToken sync.Mutex\n\n\tDebug      bool\n\tBaseURL    string\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new DMAPI Client.\nfunc NewClient(authInfo AuthInfo) *Client {\n\treturn &Client{\n\t\tapiKey:     authInfo.APIKey,\n\t\tusername:   authInfo.Username,\n\t\tpassword:   authInfo.Password,\n\t\tBaseURL:    defaultBaseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// GetZone returns content of DNS zone for domain.\nfunc (c *Client) GetZone(ctx context.Context, domain string) (*Response, error) {\n\tif getSessionID(ctx) == \"\" {\n\t\treturn nil, errors.New(\"must be logged in to get zone\")\n\t}\n\n\treturn c.postRequest(ctx, \"dns-zone-get\", url.Values{\"domain\": {dns01.UnFqdn(domain)}})\n}\n\n// PutZone uploads DNS zone to Joker DMAPI.\nfunc (c *Client) PutZone(ctx context.Context, domain, zone string) (*Response, error) {\n\tif getSessionID(ctx) == \"\" {\n\t\treturn nil, errors.New(\"must be logged in to put zone\")\n\t}\n\n\treturn c.postRequest(ctx, \"dns-zone-put\", url.Values{\"domain\": {dns01.UnFqdn(domain)}, \"zone\": {strings.TrimSpace(zone)}})\n}\n\n// postRequest performs actual HTTP request.\nfunc (c *Client) postRequest(ctx context.Context, cmd string, data url.Values) (*Response, error) {\n\tendpoint, err := url.JoinPath(c.BaseURL, cmd)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif getSessionID(ctx) != \"\" {\n\t\tdata.Set(\"auth-sid\", getSessionID(ctx))\n\t}\n\n\tif c.Debug {\n\t\tlog.Infof(\"postRequest:\\n\\tURL: %q\\n\\tData: %v\", endpoint, data)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(data.Encode()))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\treturn parseResponse(string(raw)), nil\n}\n\n// parseResponse parses HTTP response body.\nfunc parseResponse(message string) *Response {\n\tr := &Response{Headers: url.Values{}, StatusCode: -1}\n\n\tlines, body, _ := strings.Cut(message, \"\\n\\n\")\n\n\tfor line := range strings.Lines(lines) {\n\t\tif strings.TrimSpace(line) == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tk, v, _ := strings.Cut(line, \":\")\n\n\t\tval := strings.TrimSpace(v)\n\n\t\tr.Headers.Add(k, val)\n\n\t\tswitch k {\n\t\tcase \"Status-Code\":\n\t\t\ti, err := strconv.Atoi(val)\n\t\t\tif err == nil {\n\t\t\t\tr.StatusCode = i\n\t\t\t}\n\t\tcase \"Status-Text\":\n\t\t\tr.StatusText = val\n\t\tcase \"Auth-Sid\":\n\t\t\tr.AuthSid = val\n\t\t}\n\t}\n\n\tr.Body = body\n\n\treturn r\n}\n\n// Temporary workaround, until it get fixed on API side.\nfunc fixTxtLines(line string) string {\n\tfields := strings.Fields(line)\n\n\tif len(fields) < 6 || fields[1] != \"TXT\" {\n\t\treturn line\n\t}\n\n\tif fields[3][0] == '\"' && fields[4] == `\"` {\n\t\tfields[3] = strings.TrimSpace(fields[3]) + `\"`\n\t\tfields = append(fields[:4], fields[5:]...)\n\t}\n\n\treturn strings.Join(fields, \" \")\n}\n\n// RemoveTxtEntryFromZone clean-ups all TXT records with given name.\nfunc RemoveTxtEntryFromZone(zone, relative string) (string, bool) {\n\tprefix := fmt.Sprintf(\"%s TXT 0 \", relative)\n\n\tmodified := false\n\n\tvar zoneEntries []string\n\n\tfor line := range strings.Lines(zone) {\n\t\tif strings.HasPrefix(line, prefix) {\n\t\t\tmodified = true\n\t\t\tcontinue\n\t\t}\n\n\t\tzoneEntries = append(zoneEntries, line)\n\t}\n\n\treturn strings.TrimSpace(strings.Join(zoneEntries, \"\\n\")), modified\n}\n\n// AddTxtEntryToZone returns DNS zone with added TXT record.\nfunc AddTxtEntryToZone(zone, relative, value string, ttl int) string {\n\tvar zoneEntries []string\n\n\tfor line := range strings.Lines(zone) {\n\t\tzoneEntries = append(zoneEntries, fixTxtLines(line))\n\t}\n\n\tnewZoneEntry := fmt.Sprintf(\"%s TXT 0 %q %d\", relative, value, ttl)\n\tzoneEntries = append(zoneEntries, newZoneEntry)\n\n\treturn strings.TrimSpace(strings.Join(zoneEntries, \"\\n\"))\n}\n"
  },
  {
    "path": "providers/dns/joker/internal/dmapi/client_test.go",
    "content": "package dmapi\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tcorrectAPIKey     = \"123\"\n\tincorrectAPIKey   = \"321\"\n\tserverErrorAPIKey = \"500\"\n)\n\nconst (\n\tcorrectUsername     = \"lego\"\n\tincorrectUsername   = \"not_lego\"\n\tserverErrorUsername = \"error\"\n)\n\nfunc mockBuilder(auth AuthInfo) *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(auth)\n\t\t\tclient.BaseURL = server.URL\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded())\n}\n\nfunc TestClient_GetZone(t *testing.T) {\n\ttestZone := \"@ A 0 192.0.2.2 3600\"\n\n\ttestCases := []struct {\n\t\tdesc               string\n\t\tauthSid            string\n\t\tdomain             string\n\t\tzone               string\n\t\texpectedError      bool\n\t\texpectedStatusCode int\n\t}{\n\t\t{\n\t\t\tdesc:               \"correct auth-sid, known domain\",\n\t\t\tauthSid:            correctAPIKey,\n\t\t\tdomain:             \"known\",\n\t\t\tzone:               testZone,\n\t\t\texpectedStatusCode: 0,\n\t\t},\n\t\t{\n\t\t\tdesc:               \"incorrect auth-sid, known domain\",\n\t\t\tauthSid:            incorrectAPIKey,\n\t\t\tdomain:             \"known\",\n\t\t\texpectedStatusCode: 2202,\n\t\t},\n\t\t{\n\t\t\tdesc:               \"correct auth-sid, unknown domain\",\n\t\t\tauthSid:            correctAPIKey,\n\t\t\tdomain:             \"unknown\",\n\t\t\texpectedStatusCode: 2202,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"server error\",\n\t\t\tauthSid:       \"500\",\n\t\t\texpectedError: true,\n\t\t},\n\t}\n\n\tclient := mockBuilder(AuthInfo{APIKey: \"12345\"}).\n\t\tRoute(\"POST /dns-zone-get\", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\tauthSid := req.FormValue(\"auth-sid\")\n\t\t\tdomain := req.FormValue(\"domain\")\n\n\t\t\tswitch {\n\t\t\tcase authSid == correctAPIKey && domain == \"known\":\n\t\t\t\t_, _ = io.WriteString(rw, \"Status-Code: 0\\nStatus-Text: OK\\n\\n\"+testZone)\n\t\t\tcase authSid == incorrectAPIKey || (authSid == correctAPIKey && domain == \"unknown\"):\n\t\t\t\t_, _ = io.WriteString(rw, \"Status-Code: 2202\\nStatus-Text: Authorization error\")\n\t\t\tdefault:\n\t\t\t\thttp.NotFound(rw, req)\n\t\t\t}\n\t\t})).\n\t\tBuild(t)\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tresponse, err := client.GetZone(mockContext(t, test.authSid), test.domain)\n\t\t\tif test.expectedError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, response)\n\t\t\t\tassert.Equal(t, test.expectedStatusCode, response.StatusCode)\n\t\t\t\tassert.Equal(t, test.zone, response.Body)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_parseResponse(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tinput    string\n\t\texpected *Response\n\t}{\n\t\t{\n\t\t\tdesc:  \"Empty response\",\n\t\t\tinput: \"\",\n\t\t\texpected: &Response{\n\t\t\t\tHeaders:    url.Values{},\n\t\t\t\tStatusCode: -1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:  \"No headers, just body\",\n\t\t\tinput: \"\\n\\nTest body\",\n\t\t\texpected: &Response{\n\t\t\t\tHeaders:    url.Values{},\n\t\t\t\tBody:       \"Test body\",\n\t\t\t\tStatusCode: -1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:  \"Headers and body\",\n\t\t\tinput: \"Test-Header: value\\n\\nTest body\",\n\t\t\texpected: &Response{\n\t\t\t\tHeaders:    url.Values{\"Test-Header\": {\"value\"}},\n\t\t\t\tBody:       \"Test body\",\n\t\t\t\tStatusCode: -1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:  \"Headers and body + Auth-Sid\",\n\t\t\tinput: \"Test-Header: value\\nAuth-Sid: 123\\n\\nTest body\",\n\t\t\texpected: &Response{\n\t\t\t\tHeaders:    url.Values{\"Test-Header\": {\"value\"}, \"Auth-Sid\": {\"123\"}},\n\t\t\t\tBody:       \"Test body\",\n\t\t\t\tStatusCode: -1,\n\t\t\t\tAuthSid:    \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:  \"Headers and body + Status-Text\",\n\t\t\tinput: \"Test-Header: value\\nStatus-Text: OK\\n\\nTest body\",\n\t\t\texpected: &Response{\n\t\t\t\tHeaders:    url.Values{\"Test-Header\": {\"value\"}, \"Status-Text\": {\"OK\"}},\n\t\t\t\tBody:       \"Test body\",\n\t\t\t\tStatusText: \"OK\",\n\t\t\t\tStatusCode: -1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:  \"Headers and body + Status-Code\",\n\t\t\tinput: \"Test-Header: value\\nStatus-Code: 2020\\n\\nTest body\",\n\t\t\texpected: &Response{\n\t\t\t\tHeaders:    url.Values{\"Test-Header\": {\"value\"}, \"Status-Code\": {\"2020\"}},\n\t\t\t\tBody:       \"Test body\",\n\t\t\t\tStatusCode: 2020,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresponse := parseResponse(test.input)\n\n\t\t\tassert.Equal(t, test.expected, response)\n\t\t})\n\t}\n}\n\nfunc Test_RemoveTxtEntryFromZone(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tinput    string\n\t\texpected string\n\t\tmodified bool\n\t}{\n\t\t{\n\t\t\tdesc:     \"empty zone\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\",\n\t\t\tmodified: false,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"zone with only A entry\",\n\t\t\tinput:    \"@ A 0 192.0.2.2 3600\",\n\t\t\texpected: \"@ A 0 192.0.2.2 3600\",\n\t\t\tmodified: false,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"zone with only clenup entry\",\n\t\t\tinput:    \"_acme-challenge TXT 0  \\\"old \\\" 120\",\n\t\t\texpected: \"\",\n\t\t\tmodified: true,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"zone with one A and one cleanup entries\",\n\t\t\tinput:    \"@ A 0 192.0.2.2 3600\\n_acme-challenge TXT 0  \\\"old \\\" 120\",\n\t\t\texpected: \"@ A 0 192.0.2.2 3600\",\n\t\t\tmodified: true,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"zone with one A and multiple cleanup entries\",\n\t\t\tinput:    \"@ A 0 192.0.2.2 3600\\n_acme-challenge TXT 0  \\\"old \\\" 120\\n_acme-challenge TXT 0  \\\"another \\\" 120\",\n\t\t\texpected: \"@ A 0 192.0.2.2 3600\",\n\t\t\tmodified: true,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tzone, modified := RemoveTxtEntryFromZone(test.input, \"_acme-challenge\")\n\t\t\tassert.Equal(t, test.expected, zone)\n\t\t\tassert.Equal(t, test.modified, modified)\n\t\t})\n\t}\n}\n\nfunc Test_AddTxtEntryToZone(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"empty zone\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"_acme-challenge TXT 0 \\\"test\\\" 120\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"zone with A entry\",\n\t\t\tinput:    \"@ A 0 192.0.2.2 3600\",\n\t\t\texpected: \"@ A 0 192.0.2.2 3600\\n_acme-challenge TXT 0 \\\"test\\\" 120\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"zone with required cleanup entry\",\n\t\t\tinput:    \"_acme-challenge TXT 0  \\\"old \\\" 120\",\n\t\t\texpected: \"_acme-challenge TXT 0 \\\"old\\\" 120\\n_acme-challenge TXT 0 \\\"test\\\" 120\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tzone := AddTxtEntryToZone(test.input, \"_acme-challenge\", \"test\", 120)\n\t\t\tassert.Equal(t, test.expected, zone)\n\t\t})\n\t}\n}\n\nfunc Test_fixTxtLines(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"clean-up\",\n\t\t\tinput:    `_acme-challenge TXT 0  \"SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE \" 120`,\n\t\t\texpected: `_acme-challenge TXT 0 \"SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE\" 120`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"already cleaned\",\n\t\t\tinput:    `_acme-challenge TXT 0 \"SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE\" 120`,\n\t\t\texpected: `_acme-challenge TXT 0 \"SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE\" 120`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"special DNS entry\",\n\t\t\tinput:    \"$dyndns=yes:username:password\",\n\t\t\texpected: \"$dyndns=yes:username:password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"SRV entry\",\n\t\t\tinput:    \"_jabber._tcp SRV 20/0 xmpp-server1.l.google.com:5269 300\",\n\t\t\texpected: \"_jabber._tcp SRV 20/0 xmpp-server1.l.google.com:5269 300\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"MX entry\",\n\t\t\tinput:    \"@ MX 10 ASPMX.L.GOOGLE.COM 300\",\n\t\t\texpected: \"@ MX 10 ASPMX.L.GOOGLE.COM 300\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tline := fixTxtLines(test.input)\n\t\t\tassert.Equal(t, test.expected, line)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/joker/internal/dmapi/identity.go",
    "content": "package dmapi\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"time\"\n)\n\ntype token string\n\nconst sessionIDKey token = \"session-id\"\n\n// Token session ID.\n// > Every request (except \"login\") requires the presence of the Auth-Sid variable (\"Session ID\"),\n// > which is returned by the \"login\" request (login). An active session will expire after some inactivity period (default: 1 hour).\n// https://joker.com/faq/content/22/12/en/commonalities-for-all-requests.html\ntype Token struct {\n\tSessionID string\n\tExpireAt  time.Time\n}\n\n// login performs a log in to Joker's DMAPI.\nfunc (c *Client) login(ctx context.Context) (*Response, error) {\n\tvar values url.Values\n\n\tswitch {\n\tcase c.username != \"\" && c.password != \"\":\n\t\tvalues = url.Values{\n\t\t\t\"username\": {c.username},\n\t\t\t\"password\": {c.password},\n\t\t}\n\tcase c.apiKey != \"\":\n\t\tvalues = url.Values{\"api-key\": {c.apiKey}}\n\tdefault:\n\t\treturn nil, errors.New(\"no username and password or api-key\")\n\t}\n\n\tresponse, err := c.postRequest(ctx, \"login\", values)\n\tif err != nil {\n\t\treturn response, err\n\t}\n\n\tif response == nil {\n\t\treturn nil, errors.New(\"login returned nil response\")\n\t}\n\n\tif response.AuthSid == \"\" {\n\t\treturn response, errors.New(\"login did not return valid Auth-Sid\")\n\t}\n\n\treturn response, nil\n}\n\n// Logout closes authenticated session with Joker's DMAPI.\nfunc (c *Client) Logout(ctx context.Context) (*Response, error) {\n\tif c.token == nil {\n\t\treturn nil, errors.New(\"already logged out\")\n\t}\n\n\tresponse, err := c.postRequest(ctx, \"logout\", url.Values{})\n\n\tc.muToken.Lock()\n\tc.token = nil\n\tc.muToken.Unlock()\n\n\tif err != nil {\n\t\treturn response, err\n\t}\n\n\treturn response, nil\n}\n\nfunc (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) {\n\tc.muToken.Lock()\n\tdefer c.muToken.Unlock()\n\n\tif c.token != nil && time.Now().UTC().Before(c.token.ExpireAt) {\n\t\treturn context.WithValue(ctx, sessionIDKey, c.token.SessionID), nil\n\t}\n\n\tresponse, err := c.login(ctx)\n\tif err != nil {\n\t\treturn nil, formatResponseError(response, err)\n\t}\n\n\tc.token = &Token{\n\t\tSessionID: response.AuthSid,\n\t\tExpireAt:  time.Now().UTC().Add(1 * time.Hour),\n\t}\n\n\treturn context.WithValue(ctx, sessionIDKey, response.AuthSid), nil\n}\n\nfunc getSessionID(ctx context.Context) string {\n\ttok, ok := ctx.Value(sessionIDKey).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn tok\n}\n\n// formatResponseError formats error with optional details from DMAPI response.\nfunc formatResponseError(response *Response, err error) error {\n\tif response != nil {\n\t\treturn fmt.Errorf(\"joker: DMAPI error: %w Response: %v\", err, response.Headers)\n\t}\n\n\treturn fmt.Errorf(\"joker: DMAPI error: %w\", err)\n}\n"
  },
  {
    "path": "providers/dns/joker/internal/dmapi/identity_test.go",
    "content": "package dmapi\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockContext(t *testing.T, sessionID string) context.Context {\n\tt.Helper()\n\n\tif sessionID == \"\" {\n\t\tsessionID = \"xxx\"\n\t}\n\n\treturn context.WithValue(t.Context(), sessionIDKey, sessionID)\n}\n\nfunc TestClient_login_apikey(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc               string\n\t\tapiKey             string\n\t\texpectedError      bool\n\t\texpectedStatusCode int\n\t\texpectedAuthSid    string\n\t}{\n\t\t{\n\t\t\tdesc:               \"correct key\",\n\t\t\tapiKey:             correctAPIKey,\n\t\t\texpectedStatusCode: 0,\n\t\t\texpectedAuthSid:    correctAPIKey,\n\t\t},\n\t\t{\n\t\t\tdesc:               \"incorrect key\",\n\t\t\tapiKey:             incorrectAPIKey,\n\t\t\texpectedStatusCode: 2200,\n\t\t\texpectedError:      true,\n\t\t},\n\t\t{\n\t\t\tdesc:               \"server error\",\n\t\t\tapiKey:             serverErrorAPIKey,\n\t\t\texpectedStatusCode: -500,\n\t\t\texpectedError:      true,\n\t\t},\n\t\t{\n\t\t\tdesc:               \"non-ok status code\",\n\t\t\tapiKey:             \"333\",\n\t\t\texpectedStatusCode: 2202,\n\t\t\texpectedError:      true,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := mockBuilder(AuthInfo{APIKey: test.apiKey}).\n\t\t\t\tRoute(\"POST /login\", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\t\tswitch req.FormValue(\"api-key\") {\n\t\t\t\t\tcase correctAPIKey:\n\t\t\t\t\t\t_, _ = io.WriteString(rw, \"Status-Code: 0\\nStatus-Text: OK\\nAuth-Sid: 123\\n\\ncom\\nnet\")\n\t\t\t\t\tcase incorrectAPIKey:\n\t\t\t\t\t\t_, _ = io.WriteString(rw, \"Status-Code: 2200\\nStatus-Text: Authentication error\")\n\t\t\t\t\tcase serverErrorAPIKey:\n\t\t\t\t\t\thttp.NotFound(rw, req)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\t_, _ = io.WriteString(rw, \"Status-Code: 2202\\nStatus-Text: OK\\n\\ncom\\nnet\")\n\t\t\t\t\t}\n\t\t\t\t})).\n\t\t\t\tBuild(t)\n\n\t\t\tresponse, err := client.login(t.Context())\n\t\t\tif test.expectedError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, response)\n\t\t\t\tassert.Equal(t, test.expectedStatusCode, response.StatusCode)\n\t\t\t\tassert.Equal(t, test.expectedAuthSid, response.AuthSid)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_login_username(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc               string\n\t\tusername           string\n\t\tpassword           string\n\t\texpectedError      bool\n\t\texpectedStatusCode int\n\t\texpectedAuthSid    string\n\t}{\n\t\t{\n\t\t\tdesc:               \"correct username and password\",\n\t\t\tusername:           correctUsername,\n\t\t\tpassword:           \"go-acme\",\n\t\t\texpectedError:      false,\n\t\t\texpectedStatusCode: 0,\n\t\t\texpectedAuthSid:    correctAPIKey,\n\t\t},\n\t\t{\n\t\t\tdesc:               \"incorrect username\",\n\t\t\tusername:           incorrectUsername,\n\t\t\tpassword:           \"go-acme\",\n\t\t\texpectedStatusCode: 2200,\n\t\t\texpectedError:      true,\n\t\t},\n\t\t{\n\t\t\tdesc:               \"server error\",\n\t\t\tusername:           serverErrorUsername,\n\t\t\tpassword:           \"go-acme\",\n\t\t\texpectedStatusCode: -500,\n\t\t\texpectedError:      true,\n\t\t},\n\t\t{\n\t\t\tdesc:               \"non-ok status code\",\n\t\t\tusername:           \"random\",\n\t\t\tpassword:           \"go-acme\",\n\t\t\texpectedStatusCode: 2202,\n\t\t\texpectedError:      true,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := mockBuilder(AuthInfo{Username: test.username, Password: test.password}).\n\t\t\t\tRoute(\"POST /login\", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\t\tswitch req.FormValue(\"username\") {\n\t\t\t\t\tcase correctUsername:\n\t\t\t\t\t\t_, _ = io.WriteString(rw, \"Status-Code: 0\\nStatus-Text: OK\\nAuth-Sid: 123\\n\\ncom\\nnet\")\n\t\t\t\t\tcase incorrectUsername:\n\t\t\t\t\t\t_, _ = io.WriteString(rw, \"Status-Code: 2200\\nStatus-Text: Authentication error\")\n\t\t\t\t\tcase serverErrorUsername:\n\t\t\t\t\t\thttp.NotFound(rw, req)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\t_, _ = io.WriteString(rw, \"Status-Code: 2202\\nStatus-Text: OK\\n\\ncom\\nnet\")\n\t\t\t\t\t}\n\t\t\t\t})).\n\t\t\t\tBuild(t)\n\n\t\t\tresponse, err := client.login(t.Context())\n\t\t\tif test.expectedError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, response)\n\t\t\t\tassert.Equal(t, test.expectedStatusCode, response.StatusCode)\n\t\t\t\tassert.Equal(t, test.expectedAuthSid, response.AuthSid)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_logout(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc               string\n\t\tauthSid            string\n\t\texpectedError      bool\n\t\texpectedStatusCode int\n\t}{\n\t\t{\n\t\t\tdesc:               \"correct auth-sid\",\n\t\t\tauthSid:            correctAPIKey,\n\t\t\texpectedStatusCode: 0,\n\t\t},\n\t\t{\n\t\t\tdesc:               \"incorrect auth-sid\",\n\t\t\tauthSid:            incorrectAPIKey,\n\t\t\texpectedStatusCode: 2200,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"already logged out\",\n\t\t\tauthSid:       \"\",\n\t\t\texpectedError: true,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"server error\",\n\t\t\tauthSid:       serverErrorAPIKey,\n\t\t\texpectedError: true,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := mockBuilder(AuthInfo{APIKey: \"12345\"}).\n\t\t\t\tRoute(\"POST /logout\", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\t\tswitch req.FormValue(\"auth-sid\") {\n\t\t\t\t\tcase correctAPIKey:\n\t\t\t\t\t\t_, _ = io.WriteString(rw, \"Status-Code: 0\\nStatus-Text: OK\\n\")\n\t\t\t\t\tcase incorrectAPIKey:\n\t\t\t\t\t\t_, _ = io.WriteString(rw, \"Status-Code: 2200\\nStatus-Text: Authentication error\")\n\t\t\t\t\tdefault:\n\t\t\t\t\t\thttp.NotFound(rw, req)\n\t\t\t\t\t}\n\t\t\t\t})).\n\t\t\t\tBuild(t)\n\n\t\t\tclient.token = &Token{SessionID: test.authSid}\n\n\t\t\tresponse, err := client.Logout(mockContext(t, test.authSid))\n\t\t\tif test.expectedError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, response)\n\t\t\t\tassert.Equal(t, test.expectedStatusCode, response.StatusCode)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_CreateAuthenticatedContext(t *testing.T) {\n\tid := atomic.Int32{}\n\tid.Add(100)\n\n\tclient := mockBuilder(AuthInfo{Username: correctUsername, Password: \"secret\"}).\n\t\tRoute(\"POST /login\", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\tswitch req.FormValue(\"username\") {\n\t\t\tcase correctUsername:\n\t\t\t\t_, _ = fmt.Fprintf(rw, \"Status-Code: 0\\nStatus-Text: OK\\nAuth-Sid: %d\\n\\ncom\\nnet\", id.Load())\n\t\t\t\tid.Add(100)\n\n\t\t\tdefault:\n\t\t\t\t_, _ = io.WriteString(rw, \"Status-Code: 2200\\nStatus-Text: Authentication error\")\n\t\t\t}\n\t\t})).\n\t\tBuild(t)\n\n\tctx, err := client.CreateAuthenticatedContext(t.Context())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"100\", getSessionID(ctx))\n\n\t// the token is not expired then we use the \"cache\".\n\tclient.muToken.Lock()\n\tclient.token.SessionID = \"cache\"\n\tclient.muToken.Unlock()\n\n\tctx, err = client.CreateAuthenticatedContext(t.Context())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"cache\", getSessionID(ctx))\n\n\t// force the expiration of the token\n\tclient.muToken.Lock()\n\tclient.token.ExpireAt = time.Now().UTC().Add(-1 * time.Hour)\n\tclient.muToken.Unlock()\n\n\tctx, err = client.CreateAuthenticatedContext(t.Context())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"200\", getSessionID(ctx))\n}\n"
  },
  {
    "path": "providers/dns/joker/internal/svc/client.go",
    "content": "// Package svc Client for the SVC API.\n// https://joker.com/faq/content/6/496/en/let_s-encrypt-support.html\npackage svc\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\nconst defaultBaseURL = \"https://svc.joker.com/nic/replace\"\n\ntype request struct {\n\tUsername string `url:\"username\"`\n\tPassword string `url:\"password\"`\n\tZone     string `url:\"zone\"`\n\tLabel    string `url:\"label\"`\n\tType     string `url:\"type\"`\n\tValue    string `url:\"value\"`\n}\n\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tBaseURL    string\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(username, password string) *Client {\n\treturn &Client{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\tBaseURL:    defaultBaseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\nfunc (c *Client) SendRequest(ctx context.Context, zone, label, value string) error {\n\tpayload := request{\n\t\tUsername: c.username,\n\t\tPassword: c.password,\n\t\tZone:     zone,\n\t\tLabel:    label,\n\t\tType:     \"TXT\",\n\t\tValue:    value,\n\t}\n\n\tv, err := querystring.Values(payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL, strings.NewReader(v.Encode()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif resp.StatusCode == http.StatusOK && strings.HasPrefix(string(raw), \"OK\") {\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"error: %d: %s\", resp.StatusCode, string(raw))\n}\n"
  },
  {
    "path": "providers/dns/joker/internal/svc/client_test.go",
    "content": "package svc\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"test\", \"secret\")\n\t\t\tclient.BaseURL = server.URL\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded())\n}\n\nfunc TestClient_Send(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.RawStringResponse(\"OK: 1 inserted, 0 deleted\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"zone\", \"example.com\").\n\t\t\t\tWith(\"label\", \"_acme-challenge\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"value\", \"123\").\n\t\t\t\tWith(\"username\", \"test\").\n\t\t\t\tWith(\"password\", \"secret\"),\n\t\t).\n\t\tBuild(t)\n\n\tzone := \"example.com\"\n\tlabel := \"_acme-challenge\"\n\tvalue := \"123\"\n\n\terr := client.SendRequest(t.Context(), zone, label, value)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_Send_empty(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.RawStringResponse(\"OK: 1 inserted, 0 deleted\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"zone\", \"example.com\").\n\t\t\t\tWith(\"label\", \"_acme-challenge\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"value\", \"\").\n\t\t\t\tWith(\"username\", \"test\").\n\t\t\t\tWith(\"password\", \"secret\"),\n\t\t).\n\t\tBuild(t)\n\n\tzone := \"example.com\"\n\tlabel := \"_acme-challenge\"\n\tvalue := \"\"\n\n\terr := client.SendRequest(t.Context(), zone, label, value)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/joker/joker.go",
    "content": "// Package joker implements a DNS provider for solving the DNS-01 challenge using joker.com.\npackage joker\n\nimport (\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"JOKER_\"\n\n\tEnvAPIKey   = envNamespace + \"API_KEY\"\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\tEnvDebug    = envNamespace + \"DEBUG\"\n\tEnvMode     = envNamespace + \"API_MODE\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst (\n\tmodeDMAPI = \"DMAPI\"\n\tmodeSVC   = \"SVC\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tDebug              bool\n\tAPIKey             string\n\tUsername           string\n\tPassword           string\n\tAPIMode            string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tAPIMode:            env.GetOrDefaultString(EnvMode, modeDMAPI),\n\t\tDebug:              env.GetOrDefaultBool(EnvDebug, false),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second),\n\t\t},\n\t}\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Joker.\n// Credentials must be passed in the environment variable JOKER_API_KEY.\nfunc NewDNSProvider() (challenge.ProviderTimeout, error) {\n\tif os.Getenv(EnvMode) == modeSVC {\n\t\treturn newSvcProvider()\n\t}\n\n\treturn newDmapiProvider()\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Joker.\nfunc NewDNSProviderConfig(config *Config) (challenge.ProviderTimeout, error) {\n\tif config.APIMode == modeSVC {\n\t\treturn newSvcProviderConfig(config)\n\t}\n\n\treturn newDmapiProviderConfig(config)\n}\n"
  },
  {
    "path": "providers/dns/joker/joker.toml",
    "content": "Name = \"Joker\"\nDescription = ''''''\nURL = \"https://joker.com\"\nCode = \"joker\"\nSince = \"v2.6.0\"\n\nExample = '''\n# SVC\nJOKER_API_MODE=SVC \\\nJOKER_USERNAME=<your email> \\\nJOKER_PASSWORD=<your password> \\\nlego --dns joker -d '*.example.com' -d example.com run\n\n# DMAPI\nJOKER_API_MODE=DMAPI \\\nJOKER_USERNAME=<your email> \\\nJOKER_PASSWORD=<your password> \\\nlego --dns joker -d '*.example.com' -d example.com run\n## or\nJOKER_API_MODE=DMAPI \\\nJOKER_API_KEY=<your API key> \\\nlego --dns joker -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## SVC mode\n\nIn the SVC mode, username and passsword are not your email and account passwords, but those displayed in Joker.com domain dashboard when enabling Dynamic DNS.\n\nAs per [Joker.com documentation](https://joker.com/faq/content/6/496/en/let_s-encrypt-support.html):\n\n> 1. please log in at Joker.com, visit 'My Domains',\n>    find the domain you want to add  Let's Encrypt certificate for, and chose \"DNS\" in the menu\n>\n> 2. on the top right, you will find the setting for 'Dynamic DNS'.\n>    If not already active, please activate it.\n>    It will not affect any other already existing DNS records of this domain.\n>\n> 3. please take a note of the credentials which are now shown as 'Dynamic DNS Authentication', consisting of a 'username' and a 'password'.\n>\n> 4. this is all you have to do here - and only once per domain.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    JOKER_API_MODE = \"'DMAPI' or 'SVC'. DMAPI is for resellers accounts. (Default: DMAPI)\"\n    JOKER_USERNAME = \"Joker.com username\"\n    JOKER_PASSWORD = \"Joker.com password\"\n    JOKER_API_KEY = \"API key (only with DMAPI mode)\"\n  [Configuration.Additional]\n    JOKER_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    JOKER_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    JOKER_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    JOKER_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 60)\"\n    JOKER_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60), only with 'SVC' mode\"\n\n[Links]\n  API = \"https://joker.com/faq/category/39/22-dmapi.html\"\n  API_SVC = \"https://joker.com/faq/content/6/496/en/let_s-encrypt-support.html\"\n"
  },
  {
    "path": "providers/dns/joker/joker_test.go",
    "content": "package joker\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey, EnvUsername, EnvPassword, EnvMode).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected any\n\t}{\n\t\t{\n\t\t\tdesc: \"mode DMAPI (default)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvPassword: \"123\",\n\t\t\t},\n\t\t\texpected: &dmapiProvider{},\n\t\t},\n\t\t{\n\t\t\tdesc: \"mode DMAPI\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvMode:     modeDMAPI,\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvPassword: \"123\",\n\t\t\t},\n\t\t\texpected: &dmapiProvider{},\n\t\t},\n\t\t{\n\t\t\tdesc: \"mode SVC\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvMode:     modeSVC,\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvPassword: \"123\",\n\t\t\t},\n\t\t\texpected: &svcProvider{},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tfmt.Println(os.Getenv(EnvMode))\n\n\t\t\tp, err := NewDNSProvider()\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, p)\n\n\t\t\tassert.IsType(t, test.expected, p)\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tmode     string\n\t\texpected any\n\t}{\n\t\t{\n\t\t\tdesc:     \"mode DMAPI (default)\",\n\t\t\texpected: &dmapiProvider{},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"mode DMAPI\",\n\t\t\tmode:     modeDMAPI,\n\t\t\texpected: &dmapiProvider{},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"mode SVC\",\n\t\t\tmode:     modeSVC,\n\t\t\texpected: &svcProvider{},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = \"123\"\n\t\t\tconfig.Password = \"123\"\n\t\t\tconfig.APIMode = test.mode\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, p)\n\n\t\t\tassert.IsType(t, test.expected, p)\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/joker/provider_dmapi.go",
    "content": "package joker\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/joker/internal/dmapi\"\n)\n\nvar _ challenge.ProviderTimeout = (*dmapiProvider)(nil)\n\n// dmapiProvider implements the challenge.Provider interface.\ntype dmapiProvider struct {\n\tconfig *Config\n\tclient *dmapi.Client\n}\n\n// newDmapiProvider returns a DNSProvider instance configured for Joker.\n// Credentials must be passed in the environment variable: JOKER_USERNAME, JOKER_PASSWORD or JOKER_API_KEY.\nfunc newDmapiProvider() (*dmapiProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\tvar errU error\n\n\t\tvalues, errU = env.Get(EnvUsername, EnvPassword)\n\t\tif errU != nil {\n\t\t\t//nolint:errorlint // false-positive\n\t\t\treturn nil, fmt.Errorf(\"joker: %v or %v\", errU, err)\n\t\t}\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn newDmapiProviderConfig(config)\n}\n\n// newDmapiProviderConfig return a DNSProvider instance configured for Joker.\nfunc newDmapiProviderConfig(config *Config) (*dmapiProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"joker: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\tif config.Username == \"\" || config.Password == \"\" {\n\t\t\treturn nil, errors.New(\"joker: credentials missing\")\n\t\t}\n\t}\n\n\tclient := dmapi.NewClient(dmapi.AuthInfo{\n\t\tAPIKey:   config.APIKey,\n\t\tUsername: config.Username,\n\t\tPassword: config.Password,\n\t})\n\n\tclient.Debug = config.Debug\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &dmapiProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *dmapiProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *dmapiProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"joker: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"joker: %w\", err)\n\t}\n\n\tif d.config.Debug {\n\t\tlog.Infof(\"[%s] joker: adding TXT record %q to zone %q with value %q\", domain, subDomain, zone, info.Value)\n\t}\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresponse, err := d.client.GetZone(ctx, zone)\n\tif err != nil || response.StatusCode != 0 {\n\t\treturn formatResponseError(response, err)\n\t}\n\n\tdnsZone := dmapi.AddTxtEntryToZone(response.Body, subDomain, info.Value, d.config.TTL)\n\n\tresponse, err = d.client.PutZone(ctx, zone, dnsZone)\n\tif err != nil || response.StatusCode != 0 {\n\t\treturn formatResponseError(response, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *dmapiProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"joker: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"joker: %w\", err)\n\t}\n\n\tif d.config.Debug {\n\t\tlog.Infof(\"[%s] joker: removing entry %q from zone %q\", domain, subDomain, zone)\n\t}\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() {\n\t\t// Try to log out in case of errors\n\t\t_, _ = d.client.Logout(ctx)\n\t}()\n\n\tresponse, err := d.client.GetZone(ctx, zone)\n\tif err != nil || response.StatusCode != 0 {\n\t\treturn formatResponseError(response, err)\n\t}\n\n\tdnsZone, modified := dmapi.RemoveTxtEntryFromZone(response.Body, subDomain)\n\tif modified {\n\t\tresponse, err = d.client.PutZone(ctx, zone, dnsZone)\n\t\tif err != nil || response.StatusCode != 0 {\n\t\t\treturn formatResponseError(response, err)\n\t\t}\n\t}\n\n\tresponse, err = d.client.Logout(ctx)\n\tif err != nil {\n\t\treturn formatResponseError(response, err)\n\t}\n\n\treturn nil\n}\n\n// formatResponseError formats error with optional details from DMAPI response.\nfunc formatResponseError(response *dmapi.Response, err error) error {\n\tif response != nil {\n\t\treturn fmt.Errorf(\"joker: DMAPI error: %w Response: %v\", err, response.Headers)\n\t}\n\n\treturn fmt.Errorf(\"joker: DMAPI error: %w\", err)\n}\n"
  },
  {
    "path": "providers/dns/joker/provider_dmapi_test.go",
    "content": "package joker\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_newDmapiProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success username password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvPassword: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:   \"\",\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"joker: some credentials information are missing: JOKER_USERNAME,JOKER_PASSWORD or some credentials information are missing: JOKER_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:   \"\",\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"joker: some credentials information are missing: JOKER_PASSWORD or some credentials information are missing: JOKER_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:   \"\",\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"123\",\n\t\t\t},\n\t\t\texpected: \"joker: some credentials information are missing: JOKER_USERNAME or some credentials information are missing: JOKER_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := newDmapiProvider()\n\n\t\t\tif test.expected != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_newDmapiProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success api key\",\n\t\t\tapiKey: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"success username and password\",\n\t\t\tusername: \"123\",\n\t\t\tpassword: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"joker: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials: username\",\n\t\t\texpected: \"joker: credentials missing\",\n\t\t\tusername: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials: password\",\n\t\t\texpected: \"joker: credentials missing\",\n\t\t\tpassword: \"123\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := newDmapiProviderConfig(config)\n\n\t\t\tif test.expected != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/joker/provider_svc.go",
    "content": "package joker\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/joker/internal/svc\"\n)\n\nvar _ challenge.ProviderTimeout = (*svcProvider)(nil)\n\n// svcProvider implements the challenge.Provider interface.\ntype svcProvider struct {\n\tconfig *Config\n\tclient *svc.Client\n}\n\n// newSvcProvider returns a DNSProvider instance configured for Joker.\n// Credentials must be passed in the environment variable: JOKER_USERNAME, JOKER_PASSWORD.\nfunc newSvcProvider() (*svcProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"joker: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn newSvcProviderConfig(config)\n}\n\n// newSvcProviderConfig return a DNSProvider instance configured for Joker.\nfunc newSvcProviderConfig(config *Config) (*svcProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"joker: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Username == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"joker: credentials missing\")\n\t}\n\n\tclient := svc.NewClient(config.Username, config.Password)\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &svcProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *svcProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *svcProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"joker: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"joker: %w\", err)\n\t}\n\n\treturn d.client.SendRequest(context.Background(), dns01.UnFqdn(zone), subDomain, info.Value)\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *svcProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"joker: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"joker: %w\", err)\n\t}\n\n\treturn d.client.SendRequest(context.Background(), dns01.UnFqdn(zone), subDomain, \"\")\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *svcProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n"
  },
  {
    "path": "providers/dns/joker/provider_svc_test.go",
    "content": "package joker\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_newSvcProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success username password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvPassword: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"joker: some credentials information are missing: JOKER_USERNAME,JOKER_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"joker: some credentials information are missing: JOKER_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"123\",\n\t\t\t},\n\t\t\texpected: \"joker: some credentials information are missing: JOKER_USERNAME\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := newSvcProvider()\n\n\t\t\tif test.expected != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_newSvcProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success username and password\",\n\t\t\tusername: \"123\",\n\t\t\tpassword: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"joker: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials: username\",\n\t\t\texpected: \"joker: credentials missing\",\n\t\t\tusername: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials: password\",\n\t\t\texpected: \"joker: credentials missing\",\n\t\t\tpassword: \"123\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := newSvcProviderConfig(config)\n\n\t\t\tif test.expected != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/keyhelp/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// APIKeyHeader API key header.\nconst APIKeyHeader = \"X-Api-Key\"\n\n// Client the KeyHelp API client.\ntype Client struct {\n\tapiKey string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(baseURL, apiKey string) (*Client, error) {\n\tif baseURL == \"\" {\n\t\treturn nil, errors.New(\"missing base URL\")\n\t}\n\n\tif apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbase, err := url.Parse(baseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parse base URL:  %w\", err)\n\t}\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tbaseURL:    base.JoinPath(\"api\", \"v2\"),\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(APIKeyHeader, c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {\n\tendpoint := c.baseURL.JoinPath(\"domains\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"sort\", \"domain_utf8\")\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result []Domain\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\nfunc (c *Client) ListDomainRecords(ctx context.Context, domainID int) (*DomainRecords, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns\", strconv.Itoa(domainID))\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result DomainRecords\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &result, nil\n}\n\nfunc (c *Client) UpdateDomainRecords(ctx context.Context, domainID int, records DomainRecords) (*DomainID, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns\", strconv.Itoa(domainID))\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, records)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result DomainID\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &result, nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/keyhelp/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(server.URL, \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWith(APIKeyHeader, \"secret\").\n\t\t\tWithJSONHeaders(),\n\t)\n}\n\nfunc TestClient_ListDomains(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/v2/domains\",\n\t\t\tservermock.ResponseFromFixture(\"get_domains.json\"),\n\t\t\tservermock.CheckQueryParameter().\n\t\t\t\tWith(\"sort\", \"domain_utf8\").\n\t\t\t\tStrict()).\n\t\tBuild(t)\n\n\tdomains, err := client.ListDomains(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []Domain{{\n\t\tID:             8,\n\t\tUserID:         4,\n\t\tParentDomainID: 0,\n\t\tStatus:         1,\n\t\tDomain:         \"example.com\",\n\t\tDomainUTF8:     \"example.com\",\n\t\tIsEmailDomain:  true,\n\t}}\n\n\tassert.Equal(t, expected, domains)\n}\n\nfunc TestClient_ListDomains_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/v2/domains\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.ListDomains(t.Context())\n\n\trequire.EqualError(t, err, \"401 Unauthorized: API key is missing or invalid.\")\n}\n\nfunc TestClient_ListDomainRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/v2/dns/123\",\n\t\t\tservermock.ResponseFromFixture(\"get_domain_records.json\")).\n\t\tBuild(t)\n\n\tdomainRecords, err := client.ListDomainRecords(t.Context(), 123)\n\trequire.NoError(t, err)\n\n\texpected := &DomainRecords{\n\t\tDkimRecord: `default._domainkey IN TXT ( \"v=DKIM1; k=rsa; s=email; \" \"...DKIM KEY...\" )`,\n\t\tRecords: &Records{\n\t\t\tSoa: &SOARecord{\n\t\t\t\tTTL:       86400,\n\t\t\t\tPrimaryNs: \"ns.example.com.\",\n\t\t\t\tRName:     \"root.example.com.\",\n\t\t\t\tRefresh:   14400,\n\t\t\t\tRetry:     1800,\n\t\t\t\tExpire:    604800,\n\t\t\t\tMinimum:   3600,\n\t\t\t},\n\t\t\tOther: []Record{{\n\t\t\t\tHost:  \"@\",\n\t\t\t\tTTL:   86400,\n\t\t\t\tType:  \"A\",\n\t\t\t\tValue: \"192.168.178.1\",\n\t\t\t}},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, domainRecords)\n}\n\nfunc TestClient_ListDomainRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/v2/dns/8\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.ListDomainRecords(t.Context(), 8)\n\n\trequire.EqualError(t, err, \"401 Unauthorized: API key is missing or invalid.\")\n}\n\nfunc TestClient_UpdateDomainRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /api/v2/dns/8\",\n\t\t\tservermock.ResponseFromFixture(\"update_domain_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"update_domain_records-request.json\")).\n\t\tBuild(t)\n\n\trecords := DomainRecords{\n\t\tDkimRecord: `default._domainkey IN TXT ( \"v=DKIM1; k=rsa; s=email; \" \"...DKIM KEY...\" )`,\n\t\tRecords: &Records{\n\t\t\tSoa: &SOARecord{\n\t\t\t\tTTL:       86400,\n\t\t\t\tPrimaryNs: \"ns.example.com.\",\n\t\t\t\tRName:     \"root.example.com.\",\n\t\t\t\tRefresh:   14400,\n\t\t\t\tRetry:     1800,\n\t\t\t\tExpire:    604800,\n\t\t\t\tMinimum:   3600,\n\t\t\t},\n\t\t\tOther: []Record{\n\t\t\t\t{\n\t\t\t\t\tHost:  \"@\",\n\t\t\t\t\tTTL:   86400,\n\t\t\t\t\tType:  \"A\",\n\t\t\t\t\tValue: \"192.168.178.1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHost:  \"_acme-challenge\",\n\t\t\t\t\tTTL:   120,\n\t\t\t\t\tType:  \"TXT\",\n\t\t\t\t\tValue: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tdomainID, err := client.UpdateDomainRecords(t.Context(), 8, records)\n\trequire.NoError(t, err)\n\n\texpected := &DomainID{ID: 8}\n\n\tassert.Equal(t, expected, domainID)\n}\n\nfunc TestClient_UpdateDomainRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /api/v2/dns/123\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\trecords := DomainRecords{}\n\n\t_, err := client.UpdateDomainRecords(t.Context(), 123, records)\n\n\trequire.EqualError(t, err, \"401 Unauthorized: API key is missing or invalid.\")\n}\n"
  },
  {
    "path": "providers/dns/keyhelp/internal/fixtures/error.json",
    "content": "{\n  \"code\": \"401 Unauthorized\",\n  \"message\": \"API key is missing or invalid.\"\n}\n"
  },
  {
    "path": "providers/dns/keyhelp/internal/fixtures/get_domain_records.json",
    "content": "{\n  \"is_custom_dns\": false,\n  \"is_dns_disabled\": false,\n  \"dkim_record\": \"default._domainkey IN TXT ( \\\"v=DKIM1; k=rsa; s=email; \\\" \\\"...DKIM KEY...\\\" )\",\n  \"records\": {\n    \"soa\": {\n      \"ttl\": 86400,\n      \"primary_ns\": \"ns.example.com.\",\n      \"rname\": \"root.example.com.\",\n      \"refresh\": 14400,\n      \"retry\": 1800,\n      \"expire\": 604800,\n      \"minimum\": 3600\n    },\n    \"other\": [\n      {\n        \"host\": \"@\",\n        \"ttl\": 86400,\n        \"type\": \"A\",\n        \"value\": \"192.168.178.1\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/keyhelp/internal/fixtures/get_domain_records2.json",
    "content": "{\n  \"is_custom_dns\": false,\n  \"is_dns_disabled\": false,\n  \"dkim_record\": \"default._domainkey IN TXT ( \\\"v=DKIM1; k=rsa; s=email; \\\" \\\"...DKIM KEY...\\\" )\",\n  \"records\": {\n    \"soa\": {\n      \"ttl\": 86400,\n      \"primary_ns\": \"ns.example.com.\",\n      \"rname\": \"root.example.com.\",\n      \"refresh\": 14400,\n      \"retry\": 1800,\n      \"expire\": 604800,\n      \"minimum\": 3600\n    },\n    \"other\": [\n      {\n        \"host\": \"@\",\n        \"ttl\": 86400,\n        \"type\": \"A\",\n        \"value\": \"192.168.178.1\"\n      },\n      {\n        \"host\": \"_acme-challenge\",\n        \"ttl\": 120,\n        \"type\": \"TXT\",\n        \"value\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/keyhelp/internal/fixtures/get_domains.json",
    "content": "[\n  {\n    \"id\": 8,\n    \"id_user\": 4,\n    \"id_parent_domain\": 0,\n    \"status\": 1,\n    \"domain\": \"example.com\",\n    \"domain_utf8\": \"example.com\",\n    \"created_at\": \"2019-08-15T11:29:13+02:00\",\n    \"php_version\": \"\",\n    \"traffic\": 32434624,\n    \"is_disabled\": false,\n    \"delete_on\": \"2025-09-02T19:31:14+0000\",\n    \"dkim_selector\": \"default\",\n    \"dkim_record\": \"default._domainkey IN TXT ( \\\"v=DKIM1; k=rsa; s=email; \\\" \\\"...DKIM KEY...\\\" )\",\n    \"is_custom_dns\": false,\n    \"is_dns_disabled\": false,\n    \"is_subdomain\": false,\n    \"is_system_domain\": false,\n    \"is_email_domain\": true,\n    \"is_email_sending_only\": false,\n    \"target\": {\n      \"target\": \"https://www.keyhelp.de\",\n      \"is_forwarding\": true,\n      \"forwarding_type\": 301\n    },\n    \"security\": {\n      \"id_certificate\": 0,\n      \"lets_encrypt\": true,\n      \"is_prefer_https\": true,\n      \"is_hsts\": true,\n      \"hsts_max_age\": 10368000,\n      \"hsts_include\": true,\n      \"hsts_preload\": true\n    },\n    \"apache\": {\n      \"http_directives\": \"# My custom HTTP directives\",\n      \"https_directives\": \"# My custom HTTPS directives\"\n    }\n  }\n]\n"
  },
  {
    "path": "providers/dns/keyhelp/internal/fixtures/update_domain_records-request.json",
    "content": "{\n  \"dkim_record\": \"default._domainkey IN TXT ( \\\"v=DKIM1; k=rsa; s=email; \\\" \\\"...DKIM KEY...\\\" )\",\n  \"records\": {\n    \"soa\": {\n      \"ttl\": 86400,\n      \"primary_ns\": \"ns.example.com.\",\n      \"rname\": \"root.example.com.\",\n      \"refresh\": 14400,\n      \"retry\": 1800,\n      \"expire\": 604800,\n      \"minimum\": 3600\n    },\n    \"other\": [\n      {\n        \"host\": \"@\",\n        \"ttl\": 86400,\n        \"type\": \"A\",\n        \"value\": \"192.168.178.1\"\n      },\n      {\n        \"host\": \"_acme-challenge\",\n        \"ttl\": 120,\n        \"type\": \"TXT\",\n        \"value\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/keyhelp/internal/fixtures/update_domain_records-request2.json",
    "content": "{\n  \"dkim_record\": \"default._domainkey IN TXT ( \\\"v=DKIM1; k=rsa; s=email; \\\" \\\"...DKIM KEY...\\\" )\",\n  \"records\": {\n    \"soa\": {\n      \"ttl\": 86400,\n      \"primary_ns\": \"ns.example.com.\",\n      \"rname\": \"root.example.com.\",\n      \"refresh\": 14400,\n      \"retry\": 1800,\n      \"expire\": 604800,\n      \"minimum\": 3600\n    },\n    \"other\": [\n      {\n        \"host\": \"@\",\n        \"ttl\": 86400,\n        \"type\": \"A\",\n        \"value\": \"192.168.178.1\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/keyhelp/internal/fixtures/update_domain_records.json",
    "content": "{\n  \"id\": 8\n}\n"
  },
  {
    "path": "providers/dns/keyhelp/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n)\n\ntype APIError struct {\n\tCode    string `json:\"code,omitempty\"`\n\tMessage string `json:\"message,omitempty\"`\n}\n\nfunc (a *APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", a.Code, a.Message)\n}\n\ntype Domain struct {\n\tID                 int    `json:\"id,omitempty\"`\n\tUserID             int    `json:\"id_user,omitempty\"`\n\tParentDomainID     int    `json:\"id_parent_domain,omitempty\"`\n\tStatus             int    `json:\"status,omitempty\"`\n\tDomain             string `json:\"domain,omitempty\"`\n\tDomainUTF8         string `json:\"domain_utf8,omitempty\"`\n\tIsDisabled         bool   `json:\"is_disabled,omitempty\"`\n\tIsCustomDNS        bool   `json:\"is_custom_dns,omitempty\"`\n\tIsDNSDisabled      bool   `json:\"is_dns_disabled,omitempty\"`\n\tIsSubdomain        bool   `json:\"is_subdomain,omitempty\"`\n\tIsSystemDomain     bool   `json:\"is_system_domain,omitempty\"`\n\tIsEmailDomain      bool   `json:\"is_email_domain,omitempty\"`\n\tIsEmailSendingOnly bool   `json:\"is_email_sending_only,omitempty\"`\n}\n\ntype DomainID struct {\n\tID int `json:\"id,omitempty\"`\n}\n\ntype DomainRecords struct {\n\tIsCustomDNS   bool     `json:\"is_custom_dns,omitempty\"`\n\tIsDNSDisabled bool     `json:\"is_dns_disabled,omitempty\"`\n\tDkimRecord    string   `json:\"dkim_record,omitempty\"`\n\tRecords       *Records `json:\"records,omitempty\"`\n}\n\ntype Records struct {\n\tSoa   *SOARecord `json:\"soa,omitempty\"`\n\tOther []Record   `json:\"other,omitempty\"`\n}\n\ntype SOARecord struct {\n\tTTL       int    `json:\"ttl,omitempty\"`\n\tPrimaryNs string `json:\"primary_ns,omitempty\"`\n\tRName     string `json:\"rname,omitempty\"`\n\tRefresh   int    `json:\"refresh,omitempty\"`\n\tRetry     int    `json:\"retry,omitempty\"`\n\tExpire    int    `json:\"expire,omitempty\"`\n\tMinimum   int    `json:\"minimum,omitempty\"`\n}\n\ntype Record struct {\n\tHost  string `json:\"host\"`\n\tTTL   int    `json:\"ttl\"`\n\tType  string `json:\"type\"`\n\tValue string `json:\"value\"`\n}\n"
  },
  {
    "path": "providers/dns/keyhelp/keyhelp.go",
    "content": "// Package keyhelp implements a DNS provider for solving the DNS-01 challenge using KeyHelp.\npackage keyhelp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/keyhelp/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"KEYHELP_\"\n\n\tEnvBaseURL = envNamespace + \"BASE_URL\"\n\tEnvAPIKey  = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL string\n\tAPIKey  string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\tdomainIDs   map[string]int\n\tdomainIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for KeyHelp.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvBaseURL, EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"keyhelp: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.BaseURL = values[EnvBaseURL]\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for KeyHelp.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"keyhelp: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.BaseURL, config.APIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"keyhelp: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\tdomainIDs: make(map[string]int),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"keyhelp: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tdomainInfo, err := d.findDomain(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"keyhelp: %w\", err)\n\t}\n\n\tdomainRecords, err := d.client.ListDomainRecords(ctx, domainInfo.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"keyhelp: list domain records: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"keyhelp: %w\", err)\n\t}\n\n\trecords := domainRecords.Records.Other\n\trecords = append(records, internal.Record{\n\t\tHost:  subDomain,\n\t\tTTL:   d.config.TTL,\n\t\tType:  \"TXT\",\n\t\tValue: info.Value,\n\t})\n\n\treq := internal.DomainRecords{\n\t\tDkimRecord: domainRecords.DkimRecord,\n\t\tRecords: &internal.Records{\n\t\t\tSoa:   domainRecords.Records.Soa,\n\t\t\tOther: records,\n\t\t},\n\t}\n\n\t_, err = d.client.UpdateDomainRecords(ctx, domainInfo.ID, req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"keyhelp: update domain records (add): %w\", err)\n\t}\n\n\td.domainIDsMu.Lock()\n\td.domainIDs[token] = domainInfo.ID\n\td.domainIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// get the domain's unique ID from when we created it\n\td.domainIDsMu.Lock()\n\tdomainID, ok := d.domainIDs[token]\n\td.domainIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"keyhelp: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\tdomainRecords, err := d.client.ListDomainRecords(ctx, domainID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"keyhelp: list domain records: %w\", err)\n\t}\n\n\tvar records []internal.Record\n\n\tfor _, record := range domainRecords.Records.Other {\n\t\tif record.Type == \"TXT\" && record.Value == info.Value {\n\t\t\tcontinue\n\t\t}\n\n\t\trecords = append(records, record)\n\t}\n\n\treq := internal.DomainRecords{\n\t\tDkimRecord: domainRecords.DkimRecord,\n\t\tRecords: &internal.Records{\n\t\t\tSoa:   domainRecords.Records.Soa,\n\t\t\tOther: records,\n\t\t},\n\t}\n\n\t_, err = d.client.UpdateDomainRecords(ctx, domainID, req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"keyhelp: update domain records (delete): %w\", err)\n\t}\n\n\t// Delete domain ID from map\n\td.domainIDsMu.Lock()\n\tdelete(d.domainIDs, token)\n\td.domainIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) findDomain(ctx context.Context, zone string) (internal.Domain, error) {\n\tdomains, err := d.client.ListDomains(ctx)\n\tif err != nil {\n\t\treturn internal.Domain{}, fmt.Errorf(\"list domains: %w\", err)\n\t}\n\n\tfor _, domain := range domains {\n\t\tif domain.DomainUTF8 == zone || domain.Domain == zone {\n\t\t\treturn domain, nil\n\t\t}\n\t}\n\n\treturn internal.Domain{}, fmt.Errorf(\"domain not found: %s\", zone)\n}\n"
  },
  {
    "path": "providers/dns/keyhelp/keyhelp.toml",
    "content": "Name = \"KeyHelp\"\nDescription = ''''''\nURL = \"https://www.keyweb.de/en/keyhelp/keyhelp/\"\nCode = \"keyhelp\"\nSince = \"v4.26.0\"\n\nExample = '''\nKEYHELP_BASE_URL=\"https://keyhelp.example.com\" \\\nKEYHELP_API_KEY=\"xxx\" \\\nlego --dns keyhelp -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    KEYHELP_BASE_URL= \"Server URL\"\n    KEYHELP_API_KEY = \"API key\"\n  [Configuration.Additional]\n    KEYHELP_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    KEYHELP_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    KEYHELP_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    KEYHELP_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://app.swaggerhub.com/apis-docs/keyhelp/api/\"\n"
  },
  {
    "path": "providers/dns/keyhelp/keyhelp_test.go",
    "content": "package keyhelp\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-acme/lego/v4/providers/dns/keyhelp/internal\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvBaseURL, EnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvBaseURL: \"https://keyhelp.example.com\",\n\t\t\t\tEnvAPIKey:  \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing base URL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"secret\",\n\t\t\t},\n\t\t\texpected: \"keyhelp: some credentials information are missing: KEYHELP_BASE_URL\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvBaseURL: \"https://keyhelp.example.com\",\n\t\t\t},\n\t\t\texpected: \"keyhelp: some credentials information are missing: KEYHELP_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"keyhelp: some credentials information are missing: KEYHELP_BASE_URL,KEYHELP_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tbaseURL  string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:    \"success\",\n\t\t\tbaseURL: \"https://keyhelp.example.com\",\n\t\t\tapiKey:  \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing base URL\",\n\t\t\tapiKey:   \"secret\",\n\t\t\texpected: \"keyhelp: missing base URL\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\tbaseURL:  \"https://keyhelp.example.com\",\n\t\t\texpected: \"keyhelp: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"keyhelp: missing base URL\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.BaseURL = test.baseURL\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {\n\t\tconfig := NewDefaultConfig()\n\t\tconfig.HTTPClient = server.Client()\n\t\tconfig.APIKey = \"secret\"\n\t\tconfig.BaseURL = server.URL\n\n\t\treturn NewDNSProviderConfig(config)\n\t},\n\t\tservermock.CheckHeader().\n\t\t\tWith(internal.APIKeyHeader, \"secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /api/v2/domains\",\n\t\t\tservermock.ResponseFromInternal(\"get_domains.json\"),\n\t\t\tservermock.CheckQueryParameter().\n\t\t\t\tWith(\"sort\", \"domain_utf8\").\n\t\t\t\tStrict()).\n\t\tRoute(\"GET /api/v2/dns/8\",\n\t\t\tservermock.ResponseFromInternal(\"get_domain_records.json\")).\n\t\tRoute(\"PUT /api/v2/dns/8\",\n\t\t\tservermock.ResponseFromInternal(\"update_domain_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"update_domain_records-request.json\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 8, provider.domainIDs[\"abc\"])\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /api/v2/dns/8\",\n\t\t\tservermock.ResponseFromInternal(\"get_domain_records2.json\")).\n\t\tRoute(\"PUT /api/v2/dns/8\",\n\t\t\tservermock.ResponseFromInternal(\"update_domain_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"update_domain_records-request2.json\")).\n\t\tBuild(t)\n\n\tprovider.domainIDs[\"abc\"] = 8\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\nconst defaultBaseURL = \"https://api.leaseweb.com/hosting/v2\"\n\nconst AuthHeader = \"X-LSW-Auth\"\n\n// Client the Leaseweb API client.\ntype Client struct {\n\tapiKey string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiKey string) (*Client, error) {\n\tif apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// CreateRRSet creates a resource record set.\n// https://developer.leaseweb.com/docs/#tag/DNS/operation/createResourceRecordSet\nfunc (c *Client) CreateRRSet(ctx context.Context, domainName string, rrset RRSet) (*RRSet, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", domainName, \"resourceRecordSets\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, rrset)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &RRSet{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// GetRRSet gets a resource record set.\n// https://developer.leaseweb.com/docs/#tag/DNS/operation/getResourceRecordSet\nfunc (c *Client) GetRRSet(ctx context.Context, domainName, name, rType string) (*RRSet, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", domainName, \"resourceRecordSets\", name, rType)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &RRSet{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// UpdateRRSet updates a resource record set.\n// https://developer.leaseweb.com/docs/#tag/DNS/operation/updateResourceRecordSet\nfunc (c *Client) UpdateRRSet(ctx context.Context, domainName string, rrset RRSet) (*RRSet, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", domainName, \"resourceRecordSets\", rrset.Name, rrset.Type)\n\n\t// Reset values that are not allowed to be updated.\n\trrset.Name = \"\"\n\trrset.Type = \"\"\n\trrset.Editable = false\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, rrset)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &RRSet{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// DeleteRRSet deletes a resource record set.\n// https://developer.leaseweb.com/docs/#tag/DNS/operation/deleteResourceRecordSet\nfunc (c *Client) DeleteRRSet(ctx context.Context, domainName, name, rType string) error {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", domainName, \"resourceRecordSets\", name, rType)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\treq.Header.Add(AuthHeader, c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\tif resp.StatusCode == http.StatusNotFound {\n\t\t\treturn &NotFoundError{APIError{\n\t\t\t\tCorrelationID: resp.Header.Get(\"Correlation-Id\"),\n\t\t\t\tErrorCode:     strconv.Itoa(http.StatusNotFound),\n\t\t\t\tErrorMessage:  string(raw),\n\t\t\t}}\n\t\t}\n\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\tif errAPI.ErrorCode == strconv.Itoa(http.StatusNotFound) {\n\t\treturn &NotFoundError{APIError: errAPI}\n\t}\n\n\treturn &errAPI\n}\n\n// TTLRounder rounds the given TTL in seconds to the next accepted value.\n// Accepted TTL values are: 60, 300, 1800, 3600, 14400, 28800, 43200, 86400.\nfunc TTLRounder(ttl int) int {\n\tfor _, validTTL := range []int{60, 300, 1800, 3600, 14400, 28800, 43200, 86400} {\n\t\tif ttl <= validTTL {\n\t\t\treturn validTTL\n\t\t}\n\t}\n\n\treturn 3600\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(AuthHeader, \"secret\"),\n\t)\n}\n\nfunc TestClient_CreateRRSet(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/example.com/resourceRecordSets\",\n\t\t\tservermock.ResponseFromFixture(\"createResourceRecordSet.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"createResourceRecordSet-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trrset := RRSet{\n\t\tContent: []string{\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"},\n\t\tName:    \"_acme-challenge.example.com.\",\n\t\tTTL:     300,\n\t\tType:    \"TXT\",\n\t}\n\n\tresult, err := client.CreateRRSet(t.Context(), \"example.com\", rrset)\n\trequire.NoError(t, err)\n\n\texpected := &RRSet{\n\t\tContent:  []string{\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"},\n\t\tName:     \"_acme-challenge.example.com.\",\n\t\tEditable: true,\n\t\tTTL:      300,\n\t\tType:     \"TXT\",\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_GetRRSet(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\",\n\t\t\tservermock.ResponseFromFixture(\"getResourceRecordSet.json\"),\n\t\t).\n\t\tBuild(t)\n\n\tresult, err := client.GetRRSet(t.Context(), \"example.com\", \"_acme-challenge.example.com.\", \"TXT\")\n\trequire.NoError(t, err)\n\n\texpected := &RRSet{\n\t\tContent:  []string{\"foo\", \"Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo\"},\n\t\tName:     \"_acme-challenge.example.com.\",\n\t\tEditable: true,\n\t\tTTL:      3600,\n\t\tType:     \"TXT\",\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_GetRRSet_error_404(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\",\n\t\t\tservermock.ResponseFromFixture(\"error_404.json\").\n\t\t\t\tWithStatusCode(http.StatusNotFound),\n\t\t).\n\t\tBuild(t)\n\n\t_, err := client.GetRRSet(t.Context(), \"example.com\", \"_acme-challenge.example.com.\", \"TXT\")\n\trequire.EqualError(t, err, \"404: Resource not found (289346a1-3eaf-4da4-b707-62ef12eb08be)\")\n\n\ttarget := &NotFoundError{}\n\trequire.ErrorAs(t, err, &target)\n}\n\nfunc TestClient_UpdateRRSet(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\",\n\t\t\tservermock.ResponseFromFixture(\"updateResourceRecordSet.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"updateResourceRecordSet-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trrset := RRSet{\n\t\tContent: []string{\"foo\", \"Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo\", \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"},\n\t\tName:    \"_acme-challenge.example.com.\",\n\t\tTTL:     3600,\n\t\tType:    \"TXT\",\n\t}\n\n\tresult, err := client.UpdateRRSet(t.Context(), \"example.com\", rrset)\n\trequire.NoError(t, err)\n\n\texpected := &RRSet{\n\t\tContent:  []string{\"foo\", \"Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo\", \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"},\n\t\tName:     \"_acme-challenge.example.com.\",\n\t\tEditable: true,\n\t\tTTL:      3600,\n\t\tType:     \"TXT\",\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_DeleteRRSet(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.DeleteRRSet(t.Context(), \"example.com\", \"_acme-challenge.example.com.\", \"TXT\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRRSet_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\",\n\t\t\tservermock.ResponseFromFixture(\"error_401.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.DeleteRRSet(t.Context(), \"example.com\", \"_acme-challenge.example.com.\", \"TXT\")\n\trequire.EqualError(t, err, \"401: You are not authorized to view this resource. (289346a1-3eaf-4da4-b707-62ef12eb08be)\")\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/internal/fixtures/createResourceRecordSet-request.json",
    "content": "{\n  \"content\": [\n    \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"\n  ],\n  \"name\": \"_acme-challenge.example.com.\",\n  \"ttl\": 300,\n  \"type\": \"TXT\"\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/internal/fixtures/createResourceRecordSet.json",
    "content": "{\n  \"_links\": {\n    \"self\": {\n      \"href\": \"/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\"\n    },\n    \"collection\": {\n      \"href\": \"/domains/example.com/resourceRecordSets\"\n    }\n  },\n  \"content\": [\n    \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"\n  ],\n  \"editable\": true,\n  \"name\": \"_acme-challenge.example.com.\",\n  \"ttl\": 300,\n  \"type\": \"TXT\"\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/internal/fixtures/error_400.json",
    "content": "{\n  \"correlationId\": \"289346a1-3eaf-4da4-b707-62ef12eb08be\",\n  \"errorCode\": \"400\",\n  \"errorDetails\": {},\n  \"errorMessage\": \"The API could not interpret your request correctly.\"\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/internal/fixtures/error_401.json",
    "content": "{\n  \"correlationId\": \"289346a1-3eaf-4da4-b707-62ef12eb08be\",\n  \"errorCode\": \"401\",\n  \"errorMessage\": \"You are not authorized to view this resource.\"\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/internal/fixtures/error_404.json",
    "content": "{\n  \"correlationId\": \"289346a1-3eaf-4da4-b707-62ef12eb08be\",\n  \"errorCode\": \"404\",\n  \"errorMessage\": \"Resource not found\"\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/internal/fixtures/getResourceRecordSet.json",
    "content": "{\n  \"_links\": {\n    \"self\": {\n      \"href\": \"/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\"\n    },\n    \"collection\": {\n      \"href\": \"/domains/example.com/resourceRecordSets\"\n    }\n  },\n  \"content\": [\n    \"foo\",\n    \"Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo\"\n  ],\n  \"editable\": true,\n  \"name\": \"_acme-challenge.example.com.\",\n  \"ttl\": 3600,\n  \"type\": \"TXT\"\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/internal/fixtures/getResourceRecordSet2.json",
    "content": "{\n  \"_links\": {\n    \"self\": {\n      \"href\": \"/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\"\n    },\n    \"collection\": {\n      \"href\": \"/domains/example.com/resourceRecordSets\"\n    }\n  },\n  \"content\": [\n    \"Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo\"\n  ],\n  \"editable\": true,\n  \"name\": \"_acme-challenge.example.com.\",\n  \"ttl\": 3600,\n  \"type\": \"TXT\"\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request.json",
    "content": "{\n  \"content\": [\n    \"foo\",\n    \"Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo\",\n    \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"\n  ],\n  \"ttl\": 3600\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet-request2.json",
    "content": "{\n  \"content\": [\n    \"foo\"\n  ],\n  \"ttl\": 3600\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/internal/fixtures/updateResourceRecordSet.json",
    "content": "{\n  \"_links\": {\n    \"self\": {\n      \"href\": \"/domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\"\n    },\n    \"collection\": {\n      \"href\": \"/domains/example.com/resourceRecordSets\"\n    }\n  },\n  \"content\":  [\n    \"foo\",\n    \"Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo\",\n    \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"\n  ],\n  \"editable\": true,\n  \"name\": \"_acme-challenge.example.com.\",\n  \"ttl\": 3600,\n  \"type\": \"TXT\"\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\ntype NotFoundError struct {\n\tAPIError\n}\n\ntype APIError struct {\n\tCorrelationID string          `json:\"correlationId,omitempty\"`\n\tErrorCode     string          `json:\"errorCode,omitempty\"`\n\tErrorMessage  string          `json:\"errorMessage,omitempty\"`\n\tErrorDetails  json.RawMessage `json:\"errorDetails,omitempty\"`\n}\n\nfunc (a *APIError) Error() string {\n\tmsg := fmt.Sprintf(\"%s: %s (%s)\", a.ErrorCode, a.ErrorMessage, a.CorrelationID)\n\n\tif len(a.ErrorDetails) > 0 {\n\t\tmsg += fmt.Sprintf(\": %s\", string(a.ErrorDetails))\n\t}\n\n\treturn msg\n}\n\ntype RRSet struct {\n\tContent  []string `json:\"content,omitempty\"`\n\tName     string   `json:\"name,omitempty\"`\n\tEditable bool     `json:\"editable,omitempty\"`\n\tTTL      int      `json:\"ttl,omitempty\"`\n\tType     string   `json:\"type,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/leaseweb.go",
    "content": "// Package leaseweb implements a DNS provider for solving the DNS-01 challenge using Leaseweb.\npackage leaseweb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/leaseweb/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"LEASEWEB_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Leaseweb.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"leaseweb: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Leaseweb.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"leaseweb: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.APIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"leaseweb: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"leaseweb: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\texistingRRSet, err := d.client.GetRRSet(ctx, dns01.UnFqdn(authZone), info.EffectiveFQDN, \"TXT\")\n\tif err != nil {\n\t\tnotfoundErr := &internal.NotFoundError{}\n\t\tif !errors.As(err, &notfoundErr) {\n\t\t\treturn fmt.Errorf(\"leaseweb: get RRSet: %w\", err)\n\t\t}\n\n\t\t// Create the RRSet.\n\n\t\trrset := internal.RRSet{\n\t\t\tContent: []string{info.Value},\n\t\t\tName:    info.EffectiveFQDN,\n\t\t\tTTL:     internal.TTLRounder(d.config.TTL),\n\t\t\tType:    \"TXT\",\n\t\t}\n\n\t\t_, err = d.client.CreateRRSet(ctx, dns01.UnFqdn(authZone), rrset)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"leaseweb: create RRSet: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Update the RRSet.\n\n\texistingRRSet.Content = append(existingRRSet.Content, info.Value)\n\n\t_, err = d.client.UpdateRRSet(ctx, dns01.UnFqdn(authZone), *existingRRSet)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"leaseweb: update RRSet: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"leaseweb: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\texistingRRSet, err := d.client.GetRRSet(ctx, dns01.UnFqdn(authZone), info.EffectiveFQDN, \"TXT\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"leaseweb: get RRSet: %w\", err)\n\t}\n\n\tvar content []string\n\n\tfor _, s := range existingRRSet.Content {\n\t\tif s != info.Value {\n\t\t\tcontent = append(content, s)\n\t\t}\n\t}\n\n\tif len(content) == 0 {\n\t\terr = d.client.DeleteRRSet(ctx, dns01.UnFqdn(authZone), info.EffectiveFQDN, \"TXT\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"leaseweb: delete RRSet: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\texistingRRSet.Content = content\n\n\t_, err = d.client.UpdateRRSet(ctx, dns01.UnFqdn(authZone), *existingRRSet)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"leaseweb: update RRSet: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/leaseweb/leaseweb.toml",
    "content": "Name = \"Leaseweb\"\nDescription = ''''''\nURL = \"https://www.leaseweb.com/en/\"\nCode = \"leaseweb\"\nSince = \"v4.32.0\"\n\nExample = '''\nLEASEWEB_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns leaseweb -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    LEASEWEB_API_KEY = \"API key\"\n  [Configuration.Additional]\n    LEASEWEB_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    LEASEWEB_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    LEASEWEB_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    LEASEWEB_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://developer.leaseweb.com/docs/#tag/DNS\"\n"
  },
  {
    "path": "providers/dns/leaseweb/leaseweb_test.go",
    "content": "package leaseweb\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-acme/lego/v4/providers/dns/leaseweb/internal\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"leaseweb: some credentials information are missing: LEASEWEB_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"leaseweb: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(internal.AuthHeader, \"secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present_create(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\",\n\t\t\tservermock.ResponseFromInternal(\"error_404.json\").\n\t\t\t\tWithStatusCode(http.StatusNotFound),\n\t\t).\n\t\tRoute(\"POST /domains/example.com/resourceRecordSets\",\n\t\t\tservermock.ResponseFromInternal(\"createResourceRecordSet.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"createResourceRecordSet-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_Present_update(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\",\n\t\t\tservermock.ResponseFromInternal(\"getResourceRecordSet.json\"),\n\t\t).\n\t\tRoute(\"PUT /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\",\n\t\t\tservermock.ResponseFromInternal(\"updateResourceRecordSet.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"updateResourceRecordSet-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp_delete(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\",\n\t\t\tservermock.ResponseFromInternal(\"getResourceRecordSet2.json\"),\n\t\t).\n\t\tRoute(\"DELETE /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"1234d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp_update(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\",\n\t\t\tservermock.ResponseFromInternal(\"getResourceRecordSet.json\"),\n\t\t).\n\t\tRoute(\"PUT /domains/example.com/resourceRecordSets/_acme-challenge.example.com./TXT\",\n\t\t\tservermock.ResponseFromInternal(\"updateResourceRecordSet.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"updateResourceRecordSet-request2.json\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"1234d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/liara/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"golang.org/x/oauth2\"\n)\n\nconst defaultBaseURL = \"https://dns-service.iran.liara.ir\"\n\n// Client a Liara DNS API client.\ntype Client struct {\n\tbaseURL    *url.URL\n\thttpClient *http.Client\n\n\tteamID string\n}\n\n// NewClient creates a new Client.\nfunc NewClient(hc *http.Client, teamID string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\tif hc == nil {\n\t\thc = &http.Client{Timeout: 10 * time.Second}\n\t}\n\n\treturn &Client{\n\t\thttpClient: hc,\n\t\tbaseURL:    baseURL,\n\t\tteamID:     teamID,\n\t}\n}\n\n// GetRecords gets the records of a domain.\n// https://openapi.liara.ir/?urls.primaryName=DNS\nfunc (c *Client) GetRecords(ctx context.Context, domainName string) ([]Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v1\", \"zones\", domainName, \"dns-records\")\n\n\treq, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, parseError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar response Response[[]Record]\n\n\terr = json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn response.Data, nil\n}\n\n// CreateRecord creates a record.\nfunc (c *Client) CreateRecord(ctx context.Context, domainName string, record Record) (*Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v1\", \"zones\", domainName, \"dns-records\")\n\n\treq, err := c.newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusCreated {\n\t\treturn nil, parseError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar response Response[*Record]\n\n\terr = json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn response.Data, nil\n}\n\n// GetRecord gets a specific record.\nfunc (c *Client) GetRecord(ctx context.Context, domainName, recordID string) (*Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v1\", \"zones\", domainName, \"dns-records\", recordID)\n\n\treq, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, parseError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar response Response[*Record]\n\n\terr = json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn response.Data, nil\n}\n\n// DeleteRecord deletes a record.\nfunc (c *Client) DeleteRecord(ctx context.Context, domainName, recordID string) error {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"v1\", \"zones\", domainName, \"dns-records\", recordID)\n\n\treq, err := c.newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound {\n\t\treturn parseError(req, resp)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tif c.teamID != \"\" {\n\t\tquery := endpoint.Query()\n\t\tquery.Set(\"teamID\", c.teamID)\n\n\t\tendpoint.RawQuery = query.Encode()\n\t}\n\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code: %d] %w\", resp.StatusCode, &errAPI)\n}\n\nfunc OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {\n\tif client == nil {\n\t\tclient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tclient.Transport = &oauth2.Transport{\n\t\tSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),\n\t\tBase:   client.Transport,\n\t}\n\n\treturn client\n}\n"
  },
  {
    "path": "providers/dns/liara/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst apiKey = \"key\"\n\nfunc mockBuilder(teamID string) *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(OAuthStaticAccessToken(server.Client(), apiKey), teamID)\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer \"+apiKey))\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder(\"\").\n\t\tRoute(\"GET /api/v1/zones/example.com/dns-records\", servermock.ResponseFromFixture(\"RecordsResponse.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tID:   \"string\",\n\t\t\tType: \"string\",\n\t\t\tName: \"string\",\n\t\t\tContents: []Content{\n\t\t\t\t{\n\t\t\t\t\tText: \"string\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tTTL: 3600,\n\t\t},\n\t}\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetRecord(t *testing.T) {\n\tclient := mockBuilder(\"\").\n\t\tRoute(\"GET /api/v1/zones/example.com/dns-records/123\", servermock.ResponseFromFixture(\"RecordResponse.json\")).\n\t\tBuild(t)\n\n\trecord, err := client.GetRecord(t.Context(), \"example.com\", \"123\")\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tID:   \"string\",\n\t\tType: \"string\",\n\t\tName: \"string\",\n\t\tContents: []Content{\n\t\t\t{\n\t\t\t\tText: \"string\",\n\t\t\t},\n\t\t},\n\t\tTTL: 3600,\n\t}\n\tassert.Equal(t, expected, record)\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := mockBuilder(\"\").\n\t\tRoute(\"POST /api/v1/zones/example.com/dns-records\",\n\t\t\tservermock.ResponseFromFixture(\"RecordResponse.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBody(`{\"name\":\"string\",\"type\":\"string\",\"ttl\":3600,\"contents\":[{\"text\":\"string\"}]}`)).\n\t\tBuild(t)\n\n\tdata := Record{\n\t\tType: \"string\",\n\t\tName: \"string\",\n\t\tContents: []Content{\n\t\t\t{\n\t\t\t\tText: \"string\",\n\t\t\t},\n\t\t},\n\t\tTTL: 3600,\n\t}\n\n\trecord, err := client.CreateRecord(t.Context(), \"example.com\", data)\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tID:   \"string\",\n\t\tType: \"string\",\n\t\tName: \"string\",\n\t\tContents: []Content{\n\t\t\t{\n\t\t\t\tText: \"string\",\n\t\t\t},\n\t\t},\n\t\tTTL: 3600,\n\t}\n\n\tassert.Equal(t, expected, record)\n}\n\nfunc TestClient_CreateRecord_withTeamID(t *testing.T) {\n\tclient := mockBuilder(\"123\").\n\t\tRoute(\"POST /api/v1/zones/example.com/dns-records\",\n\t\t\tservermock.ResponseFromFixture(\"RecordResponse.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBody(`{\"name\":\"string\",\"type\":\"string\",\"ttl\":3600,\"contents\":[{\"text\":\"string\"}]}`),\n\t\t\tservermock.CheckQueryParameter().Strict().With(\"teamID\", \"123\"),\n\t\t).\n\t\tBuild(t)\n\n\tdata := Record{\n\t\tType: \"string\",\n\t\tName: \"string\",\n\t\tContents: []Content{\n\t\t\t{\n\t\t\t\tText: \"string\",\n\t\t\t},\n\t\t},\n\t\tTTL: 3600,\n\t}\n\n\trecord, err := client.CreateRecord(t.Context(), \"example.com\", data)\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tID:   \"string\",\n\t\tType: \"string\",\n\t\tName: \"string\",\n\t\tContents: []Content{\n\t\t\t{\n\t\t\t\tText: \"string\",\n\t\t\t},\n\t\t},\n\t\tTTL: 3600,\n\t}\n\n\tassert.Equal(t, expected, record)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder(\"\").\n\t\tRoute(\"DELETE /api/v1/zones/example.com/dns-records/123\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", \"123\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_NotFound_Response(t *testing.T) {\n\tclient := mockBuilder(\"\").\n\t\tRoute(\"DELETE /api/v1/zones/example.com/dns-records/123\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNotFound)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", \"123\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder(\"\").\n\t\tRoute(\"DELETE /api/v1/zones/example.com/dns-records/123\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", \"123\")\n\trequire.EqualError(t, err, \"[status code: 401] Unauthorized: Invalid token missing header\")\n}\n"
  },
  {
    "path": "providers/dns/liara/internal/fixtures/RecordResponse.json",
    "content": "{\n  \"status\": \"string\",\n  \"data\": {\n    \"id\": \"string\",\n    \"name\": \"string\",\n    \"type\": \"string\",\n    \"ttl\": 3600,\n    \"contents\": [{ \"text\": \"string\" }]\n  }\n}\n"
  },
  {
    "path": "providers/dns/liara/internal/fixtures/RecordsResponse.json",
    "content": "{\n  \"status\": \"string\",\n  \"data\": [\n    {\n      \"id\": \"string\",\n      \"name\": \"string\",\n      \"type\": \"string\",\n      \"ttl\": 3600,\n      \"contents\": [{ \"text\": \"string\" }]\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/liara/internal/fixtures/error.json",
    "content": "{\n  \"statusCode\": 401,\n  \"error\": \"Unauthorized\",\n  \"message\": \"Invalid token missing header\"\n}\n"
  },
  {
    "path": "providers/dns/liara/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype Content struct {\n\tText string `json:\"text,omitempty\"`\n}\n\ntype Record struct {\n\tID       string    `json:\"id,omitempty\"`\n\tName     string    `json:\"name\"`\n\tType     string    `json:\"type\"`\n\tTTL      int       `json:\"ttl\"`\n\tContents []Content `json:\"contents\"`\n}\n\ntype Response[D any] struct {\n\tStatus string `json:\"status\"`\n\tData   D      `json:\"data\"`\n}\n\ntype APIError struct {\n\tStatusCode   int    `json:\"statusCode\"`\n\tErrorCode    string `json:\"error\"`\n\tErrorMessage string `json:\"message\"`\n}\n\nfunc (a APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", a.ErrorCode, a.ErrorMessage)\n}\n"
  },
  {
    "path": "providers/dns/liara/liara.go",
    "content": "// Package liara implements a DNS provider for solving the DNS-01 challenge using Liara DNS.\npackage liara\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/liara/internal\"\n\t\"github.com/hashicorp/go-retryablehttp\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"LIARA_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\tEnvTeamID = envNamespace + \"TEAM_ID\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst (\n\tminTTL = 120\n\tmaxTTL = 432000\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey string\n\tTeamID string\n\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 3600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Liara DNS.\n// Liara_API_KEY must be passed in the environment variables.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"liara: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.TeamID = env.GetOrFile(EnvTeamID)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Liara DNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"liara: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"liara: APIKey is missing\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"liara: invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\tif config.TTL > maxTTL {\n\t\treturn nil, fmt.Errorf(\"liara: invalid TTL, TTL (%d) must be lower than %d\", config.TTL, maxTTL)\n\t}\n\n\tretryClient := retryablehttp.NewClient()\n\n\tretryClient.RetryMax = 5\n\tif config.HTTPClient != nil {\n\t\tretryClient.HTTPClient = config.HTTPClient\n\t}\n\n\tretryClient.Logger = log.Logger\n\n\tclient := internal.NewClient(\n\t\tclientdebug.Wrap(\n\t\t\tinternal.OAuthStaticAccessToken(retryClient.StandardClient(), config.APIKey),\n\t\t),\n\t\tconfig.TeamID,\n\t)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"liara: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"liara: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tType:     \"TXT\",\n\t\tName:     subDomain,\n\t\tContents: []internal.Content{{Text: info.Value}},\n\t\tTTL:      d.config.TTL,\n\t}\n\n\tnewRecord, err := d.client.CreateRecord(context.Background(), dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"liara: failed to create TXT record, fqdn=%s: %w\", info.EffectiveFQDN, err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = newRecord.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"liara: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// gets the record's unique ID\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"liara: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\terr = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"liara: failed to delete TXT record, id=%s: %w\", recordID, err)\n\t}\n\n\t// deletes record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/liara/liara.toml",
    "content": "Name = \"Liara\"\nDescription = ''''''\nURL = \"https://liara.ir\"\nCode = \"liara\"\nSince = \"v4.10.0\"\n\nExample = '''\nLIARA_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns liara -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    LIARA_API_KEY = \"The API key\"\n  [Configuration.Additional]\n    LIARA_TEAM_ID = \"The team ID to access services in a team\"\n    LIARA_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    LIARA_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    LIARA_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)\"\n    LIARA_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://openapi.liara.ir/?urls.primaryName=DNS\"\n"
  },
  {
    "path": "providers/dns/liara/liara_test.go",
    "content": "package liara\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tenvDomain         = envNamespace + \"DOMAIN\"\n\tlowerThanMinTTL   = 100\n\tgreaterThanMaxTTL = 440000\n)\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"key\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"liara: some credentials information are missing: LIARA_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tttl      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"key\",\n\t\t\tttl:    minTTL,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\tttl:      maxTTL,\n\t\t\texpected: \"liara: APIKey is missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"invalid TTL\",\n\t\t\tttl:      lowerThanMinTTL,\n\t\t\tapiKey:   \"key\",\n\t\t\texpected: fmt.Sprintf(\"liara: invalid TTL, TTL (%d) must be greater than %d\", lowerThanMinTTL, minTTL),\n\t\t},\n\t\t{\n\t\t\tdesc:     \"invalid TTL\",\n\t\t\tttl:      greaterThanMaxTTL,\n\t\t\tapiKey:   \"key\",\n\t\t\texpected: fmt.Sprintf(\"liara: invalid TTL, TTL (%d) must be lower than %d\", greaterThanMaxTTL, maxTTL),\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.TTL = test.ttl\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/lightsail/lightsail.go",
    "content": "// Package lightsail implements a DNS provider for solving the DNS-01 challenge using AWS Lightsail DNS.\npackage lightsail\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/aws/retry\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/service/lightsail\"\n\tawstypes \"github.com/aws/aws-sdk-go-v2/service/lightsail/types\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"LIGHTSAIL_\"\n\n\tEnvRegion  = envNamespace + \"REGION\"\n\tEnvDNSZone = \"DNS_ZONE\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nconst maxRetries = 5\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tDNSZone            string\n\tRegion             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *lightsail.Client\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for the AWS Lightsail service.\n//\n// AWS Credentials are automatically detected in the following locations\n// and prioritized in the following order:\n//  1. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,\n//     [AWS_SESSION_TOKEN], [DNS_ZONE], [LIGHTSAIL_REGION]\n//  2. Shared credentials file (defaults to ~/.aws/credentials)\n//  3. Amazon EC2 IAM role\n//\n// public hosted zone via the FQDN.\n//\n// See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tconfig := NewDefaultConfig()\n\n\tconfig.DNSZone = env.GetOrFile(EnvDNSZone)\n\tconfig.Region = env.GetOrDefaultString(EnvRegion, \"us-east-1\")\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for AWS Lightsail.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"lightsail: the configuration of the DNS provider is nil\")\n\t}\n\n\tctx := context.Background()\n\n\tcfg, err := awsconfig.LoadDefaultConfig(ctx,\n\t\tawsconfig.WithRegion(config.Region),\n\t\tawsconfig.WithRetryer(func() aws.Retryer {\n\t\t\treturn retry.NewStandard(func(options *retry.StandardOptions) {\n\t\t\t\toptions.MaxAttempts = maxRetries\n\n\t\t\t\t// It uses a basic exponential backoff algorithm that returns an initial\n\t\t\t\t// delay of ~400ms with an upper limit of ~30 seconds which should prevent\n\t\t\t\t// causing a high number of consecutive throttling errors.\n\t\t\t\t// For reference: Route 53 enforces an account-wide(!) 5req/s query limit.\n\t\t\t\toptions.Backoff = retry.BackoffDelayerFunc(func(attempt int, err error) (time.Duration, error) {\n\t\t\t\t\tretryCount := min(attempt, 7)\n\n\t\t\t\t\tdelay := (1 << uint(retryCount)) * (rand.Intn(50) + 200)\n\n\t\t\t\t\treturn time.Duration(delay) * time.Millisecond, nil\n\t\t\t\t})\n\t\t\t})\n\t\t}),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: lightsail.NewFromConfig(cfg),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, _, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tparams := &lightsail.CreateDomainEntryInput{\n\t\tDomainName: aws.String(d.config.DNSZone),\n\t\tDomainEntry: &awstypes.DomainEntry{\n\t\t\tName:   aws.String(info.EffectiveFQDN),\n\t\t\tTarget: aws.String(strconv.Quote(info.Value)),\n\t\t\tType:   aws.String(\"TXT\"),\n\t\t},\n\t}\n\n\t_, err := d.client.CreateDomainEntry(ctx, params)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"lightsail: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tparams := &lightsail.DeleteDomainEntryInput{\n\t\tDomainName: aws.String(d.config.DNSZone),\n\t\tDomainEntry: &awstypes.DomainEntry{\n\t\t\tName:   aws.String(info.EffectiveFQDN),\n\t\t\tType:   aws.String(\"TXT\"),\n\t\t\tTarget: aws.String(strconv.Quote(info.Value)),\n\t\t},\n\t}\n\n\t_, err := d.client.DeleteDomainEntry(ctx, params)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"lightsail: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/lightsail/lightsail.toml",
    "content": "Name = \"Amazon Lightsail\"\nDescription = ''''''\nURL = \"https://aws.amazon.com/lightsail/\"\nCode = \"lightsail\"\nSince = \"v0.5.0\"\n\nExample = ''''''\n\nAdditional = '''\n## Description\n\nAWS Credentials are automatically detected in the following locations and prioritized in the following order:\n\n1. Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, [`AWS_SESSION_TOKEN`]\n2. Shared credentials file (defaults to `~/.aws/credentials`, profiles can be specified using `AWS_PROFILE`)\n3. Amazon EC2 IAM role\n\nAWS region is not required to set as the Lightsail DNS zone is in global (us-east-1) region.\n\n## Policy\n\nThe following AWS IAM policy document describes the minimum permissions required for lego to complete the DNS challenge.\n\n```json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"lightsail:DeleteDomainEntry\",\n        \"lightsail:CreateDomainEntry\"\n      ],\n      \"Resource\": \"<Lightsail DNS zone ARN>\"\n    }\n  ]\n}\n```\n\nReplace the `Resource` value with your Lightsail DNS zone ARN.\nYou can retrieve the ARN using aws cli by running `aws lightsail get-domains --region us-east-1` (Lightsail web console does not show the ARN, unfortunately).\nIt should be in the format of `arn:aws:lightsail:global:<ACCOUNT ID>:Domain/<DOMAIN ID>`.\nYou also need to replace the region in the ARN to `us-east-1` (instead of `global`).\n\nAlternatively, you can also set the `Resource` to `*` (wildcard), which allow to access all domain, but this is not recommended.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    AWS_ACCESS_KEY_ID = \"Managed by the AWS client. Access key ID (`AWS_ACCESS_KEY_ID_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead)\"\n    AWS_SECRET_ACCESS_KEY = \"Managed by the AWS client. Secret access key (`AWS_SECRET_ACCESS_KEY_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead)\"\n    DNS_ZONE = \"Domain name of the DNS zone\"\n  [Configuration.Additional]\n    AWS_SHARED_CREDENTIALS_FILE = \"Managed by the AWS client. Shared credentials file.\"\n    LIGHTSAIL_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    LIGHTSAIL_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n\n[Links]\n  GoClient = \"https://github.com/aws/aws-sdk-go-v2\"\n"
  },
  {
    "path": "providers/dns/lightsail/lightsail_integration_test.go",
    "content": "package lightsail\n\nimport (\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/service/lightsail\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLiveTTL(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\tdomain := envTest.GetDomain()\n\n\terr = provider.Present(domain, \"foo\", \"bar\")\n\trequire.NoError(t, err)\n\n\t// we need a separate Lightsail client here as the one in the DNS provider is unexported.\n\tfqdn := \"_acme-challenge.\" + domain\n\n\tctx := t.Context()\n\n\tcfg, err := awsconfig.LoadDefaultConfig(ctx)\n\trequire.NoError(t, err)\n\n\tsvc := lightsail.NewFromConfig(cfg)\n\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\terrC := provider.CleanUp(domain, \"foo\", \"bar\")\n\t\tif errC != nil {\n\t\t\tt.Log(errC)\n\t\t}\n\t}()\n\n\tparams := &lightsail.GetDomainInput{\n\t\tDomainName: aws.String(domain),\n\t}\n\n\tresp, err := svc.GetDomain(ctx, params)\n\trequire.NoError(t, err)\n\n\tentries := resp.Domain.DomainEntries\n\tfor _, entry := range entries {\n\t\tif ptr.Deref(entry.Type) == \"TXT\" && ptr.Deref(entry.Name) == fqdn {\n\t\t\treturn\n\t\t}\n\t}\n\n\tt.Fatalf(\"Could not find a TXT record for _acme-challenge.%s\", domain)\n}\n"
  },
  {
    "path": "providers/dns/lightsail/lightsail_test.go",
    "content": "package lightsail\n\nimport (\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/lightsail\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tenvAwsNamespace = \"AWS_\"\n\n\tenvAwsAccessKeyID     = envAwsNamespace + \"ACCESS_KEY_ID\"\n\tenvAwsSecretAccessKey = envAwsNamespace + \"SECRET_ACCESS_KEY\"\n\tenvAwsRegion          = envAwsNamespace + \"REGION\"\n\tenvAwsHostedZoneID    = envAwsNamespace + \"HOSTED_ZONE_ID\"\n)\n\nvar envTest = tester.NewEnvTest(\n\tenvAwsAccessKeyID,\n\tenvAwsSecretAccessKey,\n\tenvAwsRegion,\n\tenvAwsHostedZoneID).\n\tWithDomain(EnvDNSZone).\n\tWithLiveTestRequirements(envAwsAccessKeyID, envAwsSecretAccessKey, EnvDNSZone)\n\nfunc TestCredentialsFromEnv(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\tenvTest.ClearEnv()\n\n\t_ = os.Setenv(envAwsAccessKeyID, \"123\")\n\t_ = os.Setenv(envAwsSecretAccessKey, \"123\")\n\t_ = os.Setenv(envAwsRegion, \"us-east-1\")\n\n\tctx := t.Context()\n\tcfg, err := awsconfig.LoadDefaultConfig(ctx)\n\trequire.NoError(t, err)\n\n\tcs, err := cfg.Credentials.Retrieve(ctx)\n\trequire.NoError(t, err, \"Expected credentials to be set from environment\")\n\n\texpected := aws.Credentials{\n\t\tAccessKeyID:     \"123\",\n\t\tSecretAccessKey: \"123\",\n\t\tSource:          \"EnvConfigCredentials\",\n\t}\n\tassert.Equal(t, expected, cs)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\treturn &DNSProvider{\n\t\t\t\tclient: lightsail.NewFromConfig(aws.Config{\n\t\t\t\t\tHTTPClient:       server.Client(),\n\t\t\t\t\tCredentials:      credentials.NewStaticCredentialsProvider(\"abc\", \"123\", \" \"),\n\t\t\t\t\tRegion:           \"mock-region\",\n\t\t\t\t\tBaseEndpoint:     aws.String(server.URL),\n\t\t\t\t\tRetryMaxAttempts: 1,\n\t\t\t\t}),\n\t\t\t\tconfig: NewDefaultConfig(),\n\t\t\t}, nil\n\t\t}).\n\t\tRoute(\"POST /\", nil).\n\t\tBuild(t)\n\n\tdomain := \"example.com\"\n\tkeyAuth := \"123456d==\"\n\n\terr := provider.Present(domain, \"\", keyAuth)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/limacity/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://www.lima-city.de/usercp\"\n\ntype Client struct {\n\tapiKey     string\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(apiKey string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}\n}\n\nfunc (c *Client) GetDomains(ctx context.Context) ([]Domain, error) {\n\tendpoint := c.baseURL.JoinPath(\"domains.json\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar results DomainsResponse\n\n\terr = c.do(req, &results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn results.Data, nil\n}\n\nfunc (c *Client) GetRecords(ctx context.Context, domainID int) ([]Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"domains\", strconv.Itoa(domainID), \"records.json\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar results RecordsResponse\n\n\terr = c.do(req, &results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn results.Data, nil\n}\n\nfunc (c *Client) AddRecord(ctx context.Context, domainID int, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"domains\", strconv.Itoa(domainID), \"records.json\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, NameserverRecordPayload{Data: record})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar results APIResponse\n\n\terr = c.do(req, &results)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) UpdateRecord(ctx context.Context, domainID, recordID int, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"domains\", strconv.Itoa(domainID), \"records\", strconv.Itoa(recordID))\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, NameserverRecordPayload{Data: record})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar results APIResponse\n\n\terr = c.do(req, &results)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int) error {\n\t// /domains/{domainId}/records/{recordId} DELETE\n\tendpoint := c.baseURL.JoinPath(\"domains\", strconv.Itoa(domainID), \"records\", strconv.Itoa(recordID))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar results APIResponse\n\n\terr = c.do(req, &results)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.SetBasicAuth(\"api\", c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIResponse\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code: %d] %w\", resp.StatusCode, &errAPI)\n}\n"
  },
  {
    "path": "providers/dns/limacity/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst apiKey = \"secret\"\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(apiKey)\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithBasicAuth(\"api\", apiKey),\n\t)\n}\n\nfunc TestClient_GetDomains(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains.json\", servermock.ResponseFromFixture(\"get-domains.json\")).\n\t\tBuild(t)\n\n\tdomains, err := client.GetDomains(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []Domain{{\n\t\tID:          123,\n\t\tUnicodeFqdn: \"example.com\",\n\t\tDomain:      \"example\",\n\t\tTLD:         \"com\",\n\t\tStatus:      \"ok\",\n\t}}\n\tassert.Equal(t, expected, domains)\n}\n\nfunc TestClient_GetDomains_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains.json\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\t_, err := client.GetDomains(t.Context())\n\trequire.EqualError(t, err, \"[status code: 400] status: invalid_resource, details: name: [muss ausgefüllt werden]\")\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains/123/records.json\", servermock.ResponseFromFixture(\"get-records.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), 123)\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tID:      1234,\n\t\t\tContent: \"ns1.lima-city.de\",\n\t\t\tName:    \"example.com\",\n\t\t\tTTL:     36000,\n\t\t\tType:    \"NS\",\n\t\t},\n\t\t{\n\t\t\tID:      5678,\n\t\t\tContent: `\"foobar\"`,\n\t\t\tName:    \"_acme-challenge.example.com\",\n\t\t\tTTL:     36000,\n\t\t\tType:    \"TXT\",\n\t\t},\n\t}\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains/123/records.json\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\t_, err := client.GetRecords(t.Context(), 123)\n\trequire.EqualError(t, err, \"[status code: 400] status: invalid_resource, details: name: [muss ausgefüllt werden]\")\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/123/records.json\",\n\t\t\tservermock.ResponseFromFixture(\"ok.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"nameserver_record\":{\"name\":\"foo\",\"content\":\"bar\",\"ttl\":12,\"type\":\"TXT\"}}`)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:    \"foo\",\n\t\tContent: \"bar\",\n\t\tTTL:     12,\n\t\tType:    \"TXT\",\n\t}\n\n\terr := client.AddRecord(t.Context(), 123, record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/123/records.json\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:    \"foo\",\n\t\tContent: \"bar\",\n\t\tTTL:     12,\n\t\tType:    \"TXT\",\n\t}\n\n\terr := client.AddRecord(t.Context(), 123, record)\n\trequire.EqualError(t, err, \"[status code: 400] status: invalid_resource, details: name: [muss ausgefüllt werden]\")\n}\n\nfunc TestClient_UpdateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /domains/123/records/456\",\n\t\t\tservermock.ResponseFromFixture(\"ok.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"nameserver_record\":{}}`)).\n\t\tBuild(t)\n\n\terr := client.UpdateRecord(t.Context(), 123, 456, Record{})\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_UpdateRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /domains/123/records/456\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\terr := client.UpdateRecord(t.Context(), 123, 456, Record{})\n\trequire.EqualError(t, err, \"[status code: 400] status: invalid_resource, details: name: [muss ausgefüllt werden]\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domains/123/records/456\",\n\t\t\tservermock.ResponseFromFixture(\"ok.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), 123, 456)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domains/123/records/456\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), 123, 456)\n\trequire.EqualError(t, err, \"[status code: 400] status: invalid_resource, details: name: [muss ausgefüllt werden]\")\n}\n"
  },
  {
    "path": "providers/dns/limacity/internal/fixtures/error.json",
    "content": "{\n  \"status\": \"invalid_resource\",\n  \"errors\": {\n    \"name\": [\n      \"muss ausgefüllt werden\"\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/limacity/internal/fixtures/get-domains.json",
    "content": "{\n  \"domains\": [\n    {\n      \"id\": 123,\n      \"mode\": \"CREATE\",\n      \"tld\": \"com\",\n      \"domain\": \"example\",\n      \"in_subscription\": false,\n      \"auto_renew\": false,\n      \"status\": \"ok\",\n      \"unicode_fqdn\": \"example.com\",\n      \"registered_at\": \"1970-01-01T00:00:00+00:00\",\n      \"registered_until\": \"2000-01-01T00:00:00+00:00\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/limacity/internal/fixtures/get-records.json",
    "content": "{\n  \"records\": [\n    {\n      \"id\": 1234,\n      \"content\": \"ns1.lima-city.de\",\n      \"name\": \"example.com\",\n      \"ttl\": 36000,\n      \"type\": \"NS\",\n      \"priority\": null\n    },\n    {\n      \"id\": 5678,\n      \"content\": \"\\\"foobar\\\"\",\n      \"name\": \"_acme-challenge.example.com\",\n      \"subdomain\": \"_acme-challenge\",\n      \"ttl\": 36000,\n      \"type\": \"TXT\",\n      \"priority\": null\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/limacity/internal/fixtures/ok.json",
    "content": "{\n  \"status\": \"ok\"\n}\n"
  },
  {
    "path": "providers/dns/limacity/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype RecordsResponse struct {\n\tData []Record `json:\"records,omitempty\"`\n}\n\ntype NameserverRecordPayload struct {\n\tData Record `json:\"nameserver_record\"`\n}\n\ntype DomainsResponse struct {\n\tData []Domain `json:\"domains,omitempty\"`\n}\n\ntype APIResponse struct {\n\tStatus  string              `json:\"status,omitempty\"`\n\tDetails map[string][]string `json:\"errors,omitempty\"`\n}\n\nfunc (a APIResponse) Error() string {\n\tvar details []string\n\tfor k, v := range a.Details {\n\t\tdetails = append(details, fmt.Sprintf(\"%s: %s\", k, v))\n\t}\n\n\treturn fmt.Sprintf(\"status: %s, details: %s\", a.Status, strings.Join(details, \",\"))\n}\n\ntype Record struct {\n\tID      int    `json:\"id,omitempty\"`\n\tName    string `json:\"name,omitempty\"`\n\tContent string `json:\"content,omitempty\"`\n\tTTL     int    `json:\"ttl,omitempty\"`\n\tType    string `json:\"type,omitempty\"`\n}\n\ntype Domain struct {\n\tID          int    `json:\"id,omitempty\"`\n\tUnicodeFqdn string `json:\"unicode_fqdn,omitempty\"`\n\tDomain      string `json:\"domain,omitempty\"`\n\tTLD         string `json:\"tld,omitempty\"`\n\tStatus      string `json:\"status,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/limacity/limacity.go",
    "content": "// Package limacity implements a DNS provider for solving the DNS-01 challenge using Lima-City DNS.\npackage limacity\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/limacity/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"LIMACITY_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 60),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 8*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 80*time.Second),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, 90*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\tdomainIDs   map[string]int\n\tdomainIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Lima-City DNS.\n// LIMACITY_API_KEY must be passed in the environment variables.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"limacity: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Lima-City DNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"limacity: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"limacity: APIKey is missing\")\n\t}\n\n\tclient := internal.NewClient(config.APIKey)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\tdomainIDs: make(map[string]int),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tdomains, err := d.client.GetDomains(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"limacity: get domains: %w\", err)\n\t}\n\n\tdom, err := findDomain(domains, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"limacity: find domain: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, dom.UnicodeFqdn)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"limacity: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName:    subDomain,\n\t\tContent: info.Value,\n\t\tTTL:     d.config.TTL,\n\t\tType:    \"TXT\",\n\t}\n\n\terr = d.client.AddRecord(ctx, dom.ID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"limacity: add record: %w\", err)\n\t}\n\n\td.domainIDsMu.Lock()\n\td.domainIDs[token] = dom.ID\n\td.domainIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// gets the domain's unique ID\n\td.domainIDsMu.Lock()\n\tdomainID, ok := d.domainIDs[token]\n\td.domainIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"limacity: unknown domain ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, domainID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"limacity: get records: %w\", err)\n\t}\n\n\tvar recordID int\n\n\tfor _, record := range records {\n\t\tif record.Type == \"TXT\" && record.Content == strconv.Quote(info.Value) {\n\t\t\trecordID = record.ID\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif recordID == 0 {\n\t\treturn errors.New(\"limacity: TXT record not found\")\n\t}\n\n\terr = d.client.DeleteRecord(ctx, domainID, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"limacity: delete record (domain ID=%d, record ID=%d): %w\", domainID, recordID, err)\n\t}\n\n\td.domainIDsMu.Lock()\n\tdelete(d.domainIDs, info.EffectiveFQDN)\n\td.domainIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc findDomain(domains []internal.Domain, fqdn string) (internal.Domain, error) {\n\tfor f := range dns01.DomainsSeq(fqdn) {\n\t\tdomain := dns01.UnFqdn(f)\n\n\t\tfor _, dom := range domains {\n\t\t\tif dom.UnicodeFqdn == domain || dom.UnicodeFqdn == f {\n\t\t\t\treturn dom, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn internal.Domain{}, fmt.Errorf(\"domain %s not found\", fqdn)\n}\n"
  },
  {
    "path": "providers/dns/limacity/limacity.toml",
    "content": "Name = \"Lima-City\"\nDescription = ''''''\nURL = \"https://www.lima-city.de\"\nCode = \"limacity\"\nSince = \"v4.18.0\"\n\nExample = '''\nLIMACITY_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns limacity -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    LIMACITY_API_KEY = \"The API key\"\n  [Configuration.Additional]\n    LIMACITY_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 80)\"\n    LIMACITY_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 480)\"\n    LIMACITY_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 90)\"\n    LIMACITY_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    LIMACITY_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.lima-city.de/hilfe/lima-city-api\"\n"
  },
  {
    "path": "providers/dns/limacity/limacity_test.go",
    "content": "package limacity\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"key\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"limacity: some credentials information are missing: LIMACITY_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"key\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\texpected: \"limacity: APIKey is missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/linode/linode.go",
    "content": "// Package linode implements a DNS provider for solving the DNS-01 challenge using Linode DNS and Linode's APIv4\npackage linode\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\t\"github.com/linode/linodego\"\n\t\"golang.org/x/oauth2\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"LINODE_\"\n\n\tEnvToken = envNamespace + \"TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst (\n\tminTTL             = 300\n\tdnsUpdateFreqMins  = 15\n\tdnsUpdateFudgeSecs = 120\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken              string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 15*time.Second),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\ntype hostedZoneInfo struct {\n\tdomainID     int\n\tresourceName string\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *linodego.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Linode.\n// Credentials must be passed in the environment variable: LINODE_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"linode: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Linode.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"linode: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"linode: Linode Access Token missing\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"linode: invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\toauth2Client := &http.Client{\n\t\tTimeout: config.HTTPTimeout,\n\t\tTransport: &oauth2.Transport{\n\t\t\tSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: config.Token}),\n\t\t},\n\t}\n\n\tclient := linodego.NewClient(clientdebug.Wrap(oauth2Client))\n\tclient.SetUserAgent(useragent.Get())\n\n\treturn &DNSProvider{config: config, client: &client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (time.Duration, time.Duration) {\n\ttimeout := d.config.PropagationTimeout\n\tif d.config.PropagationTimeout <= 0 {\n\t\t// Since Linode only updates their zone files every X minutes, we need\n\t\t// to figure out how many minutes we have to wait until we hit the next\n\t\t// interval of X.  We then wait another couple of minutes, just to be\n\t\t// safe.  Hopefully at some point during all of this, the record will\n\t\t// have propagated throughout Linode's network.\n\t\tminsRemaining := dnsUpdateFreqMins - (time.Now().Minute() % dnsUpdateFreqMins)\n\n\t\ttimeout = (time.Duration(minsRemaining) * time.Minute) +\n\t\t\t(minTTL * time.Second) +\n\t\t\t(dnsUpdateFudgeSecs * time.Second)\n\t}\n\n\treturn timeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getHostedZoneInfo(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcreateOpts := linodego.DomainRecordCreateOptions{\n\t\tName:   dns01.UnFqdn(info.EffectiveFQDN),\n\t\tTarget: info.Value,\n\t\tTTLSec: d.config.TTL,\n\t\tType:   linodego.RecordTypeTXT,\n\t}\n\n\t_, err = d.client.CreateDomainRecord(ctx, zone.domainID, createOpts)\n\n\treturn err\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getHostedZoneInfo(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Get all TXT records for the specified domain.\n\tlistOpts := linodego.NewListOptions(0, `{\"type\":\"TXT\"}`)\n\n\tresources, err := d.client.ListDomainRecords(ctx, zone.domainID, listOpts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Remove the specified resource, if it exists.\n\tfor _, resource := range resources {\n\t\tif (resource.Name == dns01.UnFqdn(info.EffectiveFQDN) || resource.Name == zone.resourceName) &&\n\t\t\tresource.Target == info.Value {\n\t\t\tif err := d.client.DeleteDomainRecord(ctx, zone.domainID, resource.ID); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) getHostedZoneInfo(ctx context.Context, fqdn string) (*hostedZoneInfo, error) {\n\t// Lookup the zone that handles the specified FQDN.\n\tauthZone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\t// Query the authority zone.\n\tfilter, err := json.Marshal(map[string]string{\"domain\": dns01.UnFqdn(authZone)})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create JSON filter: %w\", err)\n\t}\n\n\tlistOpts := linodego.NewListOptions(0, string(filter))\n\n\tdomains, err := d.client.ListDomains(ctx, listOpts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(domains) == 0 {\n\t\treturn nil, errors.New(\"domain not found\")\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, authZone)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &hostedZoneInfo{\n\t\tdomainID:     domains[0].ID,\n\t\tresourceName: subDomain,\n\t}, nil\n}\n"
  },
  {
    "path": "providers/dns/linode/linode.toml",
    "content": "Name = \"Linode (v4)\"\nDescription = ''''''\nURL = \"https://www.linode.com/\"\nCode = \"linode\"\nAliases = [\"linodev4\"] # \"linodev4\" is for compatibility with v3, must be dropped in v5\nSince = \"v1.1.0\"\n\nExample = '''\nLINODE_TOKEN=xxxxx \\\nlego --dns linode -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    LINODE_TOKEN = \"API token\"\n  [Configuration.Additional]\n    LINODE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 15)\"\n    LINODE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    LINODE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    LINODE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://developers.linode.com/api/v4\"\n  GoClient = \"https://github.com/linode/linodego\"\n"
  },
  {
    "path": "providers/dns/linode/linode_test.go",
    "content": "package linode\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/linode/linodego\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar envTest = tester.NewEnvTest(EnvToken)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"\",\n\t\t\t},\n\t\t\texpected: \"linode: some credentials information are missing: LINODE_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"linode: Linode Access Token missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\tos.Setenv(EnvToken, \"testing\")\n\n\tdomain := \"example.com\"\n\tkeyAuth := \"dGVzdGluZw==\"\n\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tbuilder       *servermock.Builder[*DNSProvider]\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc: \"Success\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"GET /v4/domains\",\n\t\t\t\t\tservermock.JSONEncode(linodego.DomainsPagedResponse{\n\t\t\t\t\t\tPageOptions: &linodego.PageOptions{\n\t\t\t\t\t\t\tPages:   1,\n\t\t\t\t\t\t\tResults: 1,\n\t\t\t\t\t\t\tPage:    1,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tData: []linodego.Domain{{\n\t\t\t\t\t\t\tDomain: domain,\n\t\t\t\t\t\t\tID:     1234,\n\t\t\t\t\t\t}},\n\t\t\t\t\t})).\n\t\t\t\tRoute(\"POST /v4/domains/1234/records\", servermock.JSONEncode(linodego.DomainRecord{\n\t\t\t\t\tID: 1234,\n\t\t\t\t})),\n\t\t},\n\t\t{\n\t\t\tdesc: \"NoDomain\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"GET /v4/domains\",\n\t\t\t\t\tservermock.JSONEncode(linodego.APIError{\n\t\t\t\t\t\tErrors: []linodego.APIErrorReason{{\n\t\t\t\t\t\t\tReason: \"Not found\",\n\t\t\t\t\t\t}},\n\t\t\t\t\t}).\n\t\t\t\t\t\tWithStatusCode(http.StatusNotFound)),\n\t\t\texpectedError: \"[404] Not found\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"CreateFailed\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"GET /v4/domains\",\n\t\t\t\t\tservermock.JSONEncode(&linodego.DomainsPagedResponse{\n\t\t\t\t\t\tPageOptions: &linodego.PageOptions{\n\t\t\t\t\t\t\tPages:   1,\n\t\t\t\t\t\t\tResults: 1,\n\t\t\t\t\t\t\tPage:    1,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tData: []linodego.Domain{{\n\t\t\t\t\t\t\tDomain: \"example.com\",\n\t\t\t\t\t\t\tID:     1234,\n\t\t\t\t\t\t}},\n\t\t\t\t\t})).\n\t\t\t\tRoute(\"POST /v4/domains/1234/records\",\n\t\t\t\t\tservermock.JSONEncode(linodego.APIError{\n\t\t\t\t\t\tErrors: []linodego.APIErrorReason{{\n\t\t\t\t\t\t\tReason: \"Failed to create domain resource\",\n\t\t\t\t\t\t\tField:  \"somefield\",\n\t\t\t\t\t\t}},\n\t\t\t\t\t}).\n\t\t\t\t\t\tWithStatusCode(http.StatusBadRequest)),\n\t\t\texpectedError: \"[400] [somefield] Failed to create domain resource\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tprovider := test.builder.Build(t)\n\n\t\t\terr := provider.Present(domain, \"\", keyAuth)\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\tos.Setenv(EnvToken, \"testing\")\n\n\tdomain := \"example.com\"\n\tkeyAuth := \"dGVzdGluZw==\"\n\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tbuilder       *servermock.Builder[*DNSProvider]\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc: \"Success\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"GET /v4/domains\",\n\t\t\t\t\tservermock.JSONEncode(&linodego.DomainsPagedResponse{\n\t\t\t\t\t\tPageOptions: &linodego.PageOptions{\n\t\t\t\t\t\t\tPages:   1,\n\t\t\t\t\t\t\tResults: 1,\n\t\t\t\t\t\t\tPage:    1,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tData: []linodego.Domain{{\n\t\t\t\t\t\t\tDomain: \"foobar.com\",\n\t\t\t\t\t\t\tID:     1234,\n\t\t\t\t\t\t}},\n\t\t\t\t\t})).\n\t\t\t\tRoute(\"GET /v4/domains/1234/records\",\n\t\t\t\t\tservermock.JSONEncode(&linodego.DomainRecordsPagedResponse{\n\t\t\t\t\t\tPageOptions: &linodego.PageOptions{\n\t\t\t\t\t\t\tPages:   1,\n\t\t\t\t\t\t\tResults: 1,\n\t\t\t\t\t\t\tPage:    1,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tData: []linodego.DomainRecord{{\n\t\t\t\t\t\t\tID:     1234,\n\t\t\t\t\t\t\tName:   \"_acme-challenge\",\n\t\t\t\t\t\t\tTarget: \"ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM\",\n\t\t\t\t\t\t\tType:   \"TXT\",\n\t\t\t\t\t\t}},\n\t\t\t\t\t})).\n\t\t\t\tRoute(\"DELETE /v4/domains/1234/records/1234\",\n\t\t\t\t\tservermock.RawStringResponse(\"{}\").WithHeader(\"Content-Type\", \"application/json\")),\n\t\t},\n\t\t{\n\t\t\tdesc: \"NoDomain\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"GET /v4/domains\",\n\t\t\t\t\tservermock.JSONEncode(linodego.APIError{\n\t\t\t\t\t\tErrors: []linodego.APIErrorReason{{\n\t\t\t\t\t\t\tReason: \"Not found\",\n\t\t\t\t\t\t}},\n\t\t\t\t\t}).\n\t\t\t\t\t\tWithStatusCode(http.StatusNotFound)).\n\t\t\t\tRoute(\"GET /v4/domains/1234/records\",\n\t\t\t\t\tservermock.JSONEncode(linodego.APIError{\n\t\t\t\t\t\tErrors: []linodego.APIErrorReason{{\n\t\t\t\t\t\t\tReason: \"Not found\",\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t\t).\n\t\t\t\t\t\tWithStatusCode(http.StatusNotFound)),\n\t\t\texpectedError: \"[404] Not found\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"DeleteFailed\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"GET /v4/domains\",\n\t\t\t\t\tservermock.JSONEncode(linodego.DomainsPagedResponse{\n\t\t\t\t\t\tPageOptions: &linodego.PageOptions{\n\t\t\t\t\t\t\tPages:   1,\n\t\t\t\t\t\t\tResults: 1,\n\t\t\t\t\t\t\tPage:    1,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tData: []linodego.Domain{{\n\t\t\t\t\t\t\tID:     1234,\n\t\t\t\t\t\t\tDomain: \"example.com\",\n\t\t\t\t\t\t}},\n\t\t\t\t\t})).\n\t\t\t\tRoute(\"GET /v4/domains/1234/records\",\n\t\t\t\t\tservermock.JSONEncode(linodego.DomainRecordsPagedResponse{\n\t\t\t\t\t\tPageOptions: &linodego.PageOptions{\n\t\t\t\t\t\t\tPages:   1,\n\t\t\t\t\t\t\tResults: 1,\n\t\t\t\t\t\t\tPage:    1,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tData: []linodego.DomainRecord{{\n\t\t\t\t\t\t\tID:     1234,\n\t\t\t\t\t\t\tName:   \"_acme-challenge\",\n\t\t\t\t\t\t\tTarget: \"ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM\",\n\t\t\t\t\t\t\tType:   \"TXT\",\n\t\t\t\t\t\t}},\n\t\t\t\t\t})).\n\t\t\t\tRoute(\"DELETE /v4/domains/1234/records/1234\",\n\t\t\t\t\tservermock.JSONEncode(linodego.APIError{\n\t\t\t\t\t\tErrors: []linodego.APIErrorReason{{\n\t\t\t\t\t\t\tReason: \"Failed to delete domain resource\",\n\t\t\t\t\t\t}},\n\t\t\t\t\t}).\n\t\t\t\t\t\tWithStatusCode(http.StatusBadRequest)),\n\t\t\texpectedError: \"[400] Failed to delete domain resource\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tprovider := test.builder.Build(t)\n\n\t\t\terr := provider.CleanUp(domain, \"\", keyAuth)\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"Skipping live test\")\n\t}\n\t// TODO implement this test\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"Skipping live test\")\n\t}\n\t// TODO implement this test\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {\n\t\tp, err := NewDNSProvider()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tp.client.SetBaseURL(server.URL)\n\n\t\treturn p, nil\n\t})\n}\n"
  },
  {
    "path": "providers/dns/liquidweb/liquidweb.go",
    "content": "// Package liquidweb implements a DNS provider for solving the DNS-01 challenge using Liquid Web.\npackage liquidweb\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\tlw \"github.com/liquidweb/liquidweb-go/client\"\n\t\"github.com/liquidweb/liquidweb-go/network\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace    = \"LIQUID_WEB_\"\n\taltEnvNamespace = \"LWAPI_\"\n\n\tEnvURL      = envNamespace + \"URL\"\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\tEnvZone     = envNamespace + \"ZONE\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultBaseURL = \"https://api.liquidweb.com\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL            string\n\tUsername           string\n\tPassword           string\n\tZone               string\n\tTTL                int\n\tPollingInterval    time.Duration\n\tPropagationTimeout time.Duration\n\tHTTPTimeout        time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tBaseURL:            defaultBaseURL,\n\t\tTTL:                env.GetOneWithFallback(EnvTTL, 300, strconv.Atoi, altEnvName(EnvTTL)),\n\t\tPropagationTimeout: env.GetOneWithFallback(EnvPropagationTimeout, 2*time.Minute, env.ParseSecond, altEnvName(EnvPropagationTimeout)),\n\t\tPollingInterval:    env.GetOneWithFallback(EnvPollingInterval, dns01.DefaultPollingInterval, env.ParseSecond, altEnvName(EnvPollingInterval)),\n\t\tHTTPTimeout:        env.GetOneWithFallback(EnvHTTPTimeout, 1*time.Minute, env.ParseSecond, altEnvName(EnvHTTPTimeout)),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *lw.API\n\n\trecordIDs   map[string]int\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Liquid Web.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.GetWithFallback(\n\t\t[]string{EnvUsername, altEnvName(EnvUsername)},\n\t\t[]string{EnvPassword, altEnvName(EnvPassword)},\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"liquidweb: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.BaseURL = env.GetOneWithFallback(EnvURL, defaultBaseURL, env.ParseString, altEnvName(EnvURL))\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\tconfig.Zone = env.GetOneWithFallback(EnvZone, \"\", env.ParseString, altEnvName(EnvZone))\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Liquid Web.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"liquidweb: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.BaseURL == \"\" {\n\t\tconfig.BaseURL = defaultBaseURL\n\t}\n\n\tclient, err := lw.NewAPI(config.Username, config.Password, config.BaseURL, int(config.HTTPTimeout.Seconds()))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"liquidweb: could not create Liquid Web API client: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\trecordIDs: make(map[string]int),\n\t\tclient:    client,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (time.Duration, time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tparams := &network.DNSRecordParams{\n\t\tName:  dns01.UnFqdn(info.EffectiveFQDN),\n\t\tRData: strconv.Quote(info.Value),\n\t\tType:  \"TXT\",\n\t\tZone:  d.config.Zone,\n\t\tTTL:   d.config.TTL,\n\t}\n\n\tif params.Zone == \"\" {\n\t\tbestZone, err := d.findZone(params.Name)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"liquidweb: %w\", err)\n\t\t}\n\n\t\tparams.Zone = bestZone\n\t}\n\n\tdnsEntry, err := d.client.NetworkDNS.Create(params)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"liquidweb: could not create TXT record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = int(dnsEntry.ID)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"liquidweb: unknown record ID for '%s'\", domain)\n\t}\n\n\tparams := &network.DNSRecordParams{ID: recordID}\n\n\t_, err := d.client.NetworkDNS.Delete(params)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"liquidweb: could not remove TXT record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) findZone(domain string) (string, error) {\n\tzones, err := d.client.NetworkDNSZone.ListAll()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to retrieve zones for account: %w\", err)\n\t}\n\n\t// filter the zones on the account to only ones that match\n\tvar zs []network.DNSZone\n\n\tfor _, item := range zones.Items {\n\t\tif strings.HasSuffix(domain, item.Name) {\n\t\t\tzs = append(zs, item)\n\t\t}\n\t}\n\n\tif len(zs) < 1 {\n\t\treturn \"\", fmt.Errorf(\"no valid zone in account for certificate '%s'\", domain)\n\t}\n\n\t// powerdns _only_ looks for records on the longest matching subdomain zone aka,\n\t// for test.sub.example.com if sub.example.com exists,\n\t// it will look there it will not look atexample.com even if it also exists\n\tsort.Slice(zs, func(i, j int) bool {\n\t\treturn len(zs[i].Name) > len(zs[j].Name)\n\t})\n\n\treturn zs[0].Name, nil\n}\n\nfunc altEnvName(v string) string {\n\treturn strings.ReplaceAll(v, envNamespace, altEnvNamespace)\n}\n"
  },
  {
    "path": "providers/dns/liquidweb/liquidweb.toml",
    "content": "Name = \"Liquid Web\"\nDescription = ''''''\nURL = \"https://liquidweb.com\"\nCode = \"liquidweb\"\nSince = \"v3.1.0\"\n\nExample = '''\nLWAPI_USERNAME=someuser \\\nLWAPI_PASSWORD=somepass \\\nlego --dns liquidweb -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    LWAPI_USERNAME = \"Liquid Web API Username\"\n    LWAPI_PASSWORD = \"Liquid Web API Password\"\n  [Configuration.Additional]\n    LWAPI_ZONE = \"DNS Zone\"\n    LWAPI_URL = \"Liquid Web API endpoint\"\n    LWAPI_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    LWAPI_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    LWAPI_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    LWAPI_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 60)\"\n\n[Links]\n  API = \"https://api.liquidweb.com/docs/\"\n  GoClient = \"https://github.com/liquidweb/liquidweb-go\"\n"
  },
  {
    "path": "providers/dns/liquidweb/liquidweb_test.go",
    "content": "package liquidweb\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/liquidweb/liquidweb-go/network\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvURL,\n\tEnvUsername,\n\tEnvPassword,\n\tEnvZone).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"minimum-success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"set-everything\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvURL:      \"https://storm.example\",\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t\tEnvZone:     \"blars.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"liquidweb: some credentials information are missing: LIQUID_WEB_USERNAME,LIQUID_WEB_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t\tEnvZone:     \"blars.example\",\n\t\t\t},\n\t\t\texpected: \"liquidweb: some credentials information are missing: LIQUID_WEB_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvZone:     \"blars.example\",\n\t\t\t},\n\t\t\texpected: \"liquidweb: some credentials information are missing: LIQUID_WEB_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\tzone     string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"acme\",\n\t\t\tpassword: \"secret\",\n\t\t\tzone:     \"example.com\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tusername: \"\",\n\t\t\tpassword: \"\",\n\t\t\tzone:     \"\",\n\t\t\texpected: \"liquidweb: could not create Liquid Web API client: provided username is empty\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tusername: \"\",\n\t\t\tpassword: \"secret\",\n\t\t\tzone:     \"example.com\",\n\t\t\texpected: \"liquidweb: could not create Liquid Web API client: provided username is empty\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tusername: \"acme\",\n\t\t\tpassword: \"\",\n\t\t\tzone:     \"example.com\",\n\t\t\texpected: \"liquidweb: could not create Liquid Web API client: provided password is empty\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\t\t\tconfig.Zone = test.zone\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockProvider(t)\n\n\terr := provider.Present(\"tacoman.example\", \"\", \"\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockProvider(t, network.DNSRecord{\n\t\tName:   \"_acme-challenge.tacoman.example\",\n\t\tRData:  \"123d==\",\n\t\tType:   \"TXT\",\n\t\tTTL:    300,\n\t\tID:     1234567,\n\t\tZoneID: 42,\n\t})\n\n\tprovider.recordIDs[\"123d==\"] = 1234567\n\n\terr := provider.CleanUp(\"tacoman.example.\", \"123d==\", \"\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tinitRecs      []network.DNSRecord\n\t\tdomain        string\n\t\ttoken         string\n\t\tkeyAuth       string\n\t\tpresent       bool\n\t\texpPresentErr string\n\t\tcleanup       bool\n\t}{\n\t\t{\n\t\t\tdesc:    \"expected successful\",\n\t\t\tdomain:  \"tacoman.example\",\n\t\t\ttoken:   \"123\",\n\t\t\tkeyAuth: \"456\",\n\t\t\tpresent: true,\n\t\t\tcleanup: true,\n\t\t},\n\t\t{\n\t\t\tdesc:    \"other successful\",\n\t\t\tdomain:  \"banana.example\",\n\t\t\ttoken:   \"123\",\n\t\t\tkeyAuth: \"456\",\n\t\t\tpresent: true,\n\t\t\tcleanup: true,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"zone not on account\",\n\t\t\tdomain:        \"huckleberry.example\",\n\t\t\ttoken:         \"123\",\n\t\t\tkeyAuth:       \"456\",\n\t\t\tpresent:       true,\n\t\t\texpPresentErr: \"no valid zone in account for certificate '_acme-challenge.huckleberry.example'\",\n\t\t\tcleanup:       false,\n\t\t},\n\t\t{\n\t\t\tdesc:    \"ssl for domain\",\n\t\t\tdomain:  \"sundae.cherry.example\",\n\t\t\ttoken:   \"5847953\",\n\t\t\tkeyAuth: \"34872934\",\n\t\t\tpresent: true,\n\t\t\tcleanup: true,\n\t\t},\n\t\t{\n\t\t\tdesc:    \"complicated domain\",\n\t\t\tdomain:  \"always.money.stand.banana.example\",\n\t\t\ttoken:   \"5847953\",\n\t\t\tkeyAuth: \"there is always money in the banana stand\",\n\t\t\tpresent: true,\n\t\t\tcleanup: true,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tprovider := mockProvider(t, test.initRecs...)\n\n\t\t\tif test.present {\n\t\t\t\terr := provider.Present(test.domain, test.token, test.keyAuth)\n\t\t\t\tif test.expPresentErr == \"\" {\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t} else {\n\t\t\t\t\trequire.ErrorContains(t, err, test.expPresentErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif test.cleanup {\n\t\t\t\terr := provider.CleanUp(test.domain, test.token, test.keyAuth)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/liquidweb/servermock_test.go",
    "content": "package liquidweb\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/liquidweb/liquidweb-go/network\"\n\t\"github.com/liquidweb/liquidweb-go/types\"\n)\n\nfunc mockProvider(t *testing.T, initRecs ...network.DNSRecord) *DNSProvider {\n\tt.Helper()\n\n\trecs := make(map[int]network.DNSRecord)\n\n\tfor _, rec := range initRecs {\n\t\trecs[int(rec.ID)] = rec\n\t}\n\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = \"user\"\n\t\t\tconfig.Password = \"secret\"\n\t\t\tconfig.BaseURL = server.URL\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithBasicAuth(\"user\", \"secret\"),\n\t).\n\t\tRoute(\"/v1/Network/DNS/Record/delete\", mockAPIDelete(recs)).\n\t\tRoute(\"/v1/Network/DNS/Record/create\", mockAPICreate(recs)).\n\t\tRoute(\"/v1/Network/DNS/Zone/list\", mockAPIListZones()).\n\t\tRoute(\"/bleed/Network/DNS/Record/delete\", mockAPIDelete(recs)).\n\t\tRoute(\"/bleed/Network/DNS/Record/create\", mockAPICreate(recs)).\n\t\tRoute(\"/bleed/Network/DNS/Zone/list\", mockAPIListZones()).\n\t\tBuild(t)\n}\n\nfunc mockAPICreate(recs map[int]network.DNSRecord) http.HandlerFunc {\n\t_, mockAPIServerZones := makeMockZones()\n\n\treturn func(rw http.ResponseWriter, req *http.Request) {\n\t\tbody, err := io.ReadAll(req.Body)\n\t\tif err != nil {\n\t\t\thttp.Error(rw, \"invalid request\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tpayload := struct {\n\t\t\tParams network.DNSRecord `json:\"params\"`\n\t\t}{}\n\n\t\tif err = json.Unmarshal(body, &payload); err != nil {\n\t\t\thttp.Error(rw, makeEncodingError(body), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tpayload.Params.ID = types.FlexInt(rand.Intn(10000000))\n\t\tpayload.Params.ZoneID = types.FlexInt(mockAPIServerZones[payload.Params.Name])\n\n\t\tif _, exists := recs[int(payload.Params.ID)]; exists {\n\t\t\thttp.Error(rw, \"dns record already exists\", http.StatusTeapot)\n\t\t\treturn\n\t\t}\n\n\t\trecs[int(payload.Params.ID)] = payload.Params\n\n\t\tresp, err := json.Marshal(payload.Params)\n\t\tif err != nil {\n\t\t\thttp.Error(rw, \"\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\thttp.Error(rw, string(resp), http.StatusOK)\n\t}\n}\n\nfunc mockAPIDelete(recs map[int]network.DNSRecord) http.HandlerFunc {\n\treturn func(rw http.ResponseWriter, req *http.Request) {\n\t\tbody, err := io.ReadAll(req.Body)\n\t\tif err != nil {\n\t\t\thttp.Error(rw, \"invalid request\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tpayload := struct {\n\t\t\tParams struct {\n\t\t\t\tName string `json:\"name\"`\n\t\t\t\tID   int    `json:\"id\"`\n\t\t\t} `json:\"params\"`\n\t\t}{}\n\n\t\tif err := json.Unmarshal(body, &payload); err != nil {\n\t\t\thttp.Error(rw, makeEncodingError(body), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tif payload.Params.ID == 0 {\n\t\t\thttp.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)\n\t\t\treturn\n\t\t}\n\n\t\tif _, ok := recs[payload.Params.ID]; !ok {\n\t\t\thttp.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)\n\t\t\treturn\n\t\t}\n\n\t\tdelete(recs, payload.Params.ID)\n\t\thttp.Error(rw, fmt.Sprintf(\"{\\\"deleted\\\":%d}\", payload.Params.ID), http.StatusOK)\n\t}\n}\n\nfunc mockAPIListZones() http.HandlerFunc {\n\tmockZones, mockAPIServerZones := makeMockZones()\n\n\treturn func(rw http.ResponseWriter, req *http.Request) {\n\t\tbody, err := io.ReadAll(req.Body)\n\t\tif err != nil {\n\t\t\thttp.Error(rw, \"invalid request\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tpayload := struct {\n\t\t\tParams struct {\n\t\t\t\tPageNum int `json:\"page_num\"`\n\t\t\t} `json:\"params\"`\n\t\t}{}\n\n\t\tif err = json.Unmarshal(body, &payload); err != nil {\n\t\t\thttp.Error(rw, makeEncodingError(body), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tswitch {\n\t\tcase payload.Params.PageNum < 1:\n\t\t\tpayload.Params.PageNum = 1\n\t\tcase payload.Params.PageNum > len(mockZones):\n\t\t\tpayload.Params.PageNum = len(mockZones)\n\t\t}\n\n\t\tresp := mockZones[payload.Params.PageNum]\n\t\tresp.ItemTotal = types.FlexInt(len(mockAPIServerZones))\n\t\tresp.PageNum = types.FlexInt(payload.Params.PageNum)\n\t\tresp.PageSize = 5\n\t\tresp.PageTotal = types.FlexInt(len(mockZones))\n\n\t\tvar respBody []byte\n\t\tif respBody, err = json.Marshal(resp); err == nil {\n\t\t\thttp.Error(rw, string(respBody), http.StatusOK)\n\t\t\treturn\n\t\t}\n\n\t\thttp.Error(rw, \"\", http.StatusInternalServerError)\n\t}\n}\n\nfunc makeEncodingError(buf []byte) string {\n\treturn fmt.Sprintf(`{\"data\":\"%q\",\"encoding\":\"JSON\",\"error\":\"unexpected end of string while parsing JSON string, at character offset 32 (before \\\"(end of string)\\\") at /usr/local/lp/libs/perl/LW/Base/Role/Serializer.pm line 16.\\n\",\"error_class\":\"LW::Exception::Deserialize\",\"full_message\":\"Could not deserialize \\\"%q\\\" from JSON: unexpected end of string while parsing JSON string, at character offset 32 (before \\\"(end of string)\\\") at /usr/local/lp/libs/perl/LW/Base/Role/Serializer.pm line 16.\\n\"}⏎`, string(buf), string(buf))\n}\n\nfunc makeMockZones() (map[int]network.DNSZoneList, map[string]int) {\n\tmockZones := map[int]network.DNSZoneList{\n\t\t1: {\n\t\t\tItems: []network.DNSZone{\n\t\t\t\t{\n\t\t\t\t\tID:                1,\n\t\t\t\t\tName:              \"blars.example\",\n\t\t\t\t\tActive:            1,\n\t\t\t\t\tDelegationStatus:  \"CORRECT\",\n\t\t\t\t\tPrimaryNameserver: \"ns.example.org\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                2,\n\t\t\t\t\tName:              \"tacoman.example\",\n\t\t\t\t\tActive:            1,\n\t\t\t\t\tDelegationStatus:  \"CORRECT\",\n\t\t\t\t\tPrimaryNameserver: \"ns.example.org\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                3,\n\t\t\t\t\tName:              \"storm.example\",\n\t\t\t\t\tActive:            1,\n\t\t\t\t\tDelegationStatus:  \"CORRECT\",\n\t\t\t\t\tPrimaryNameserver: \"ns.example.org\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                4,\n\t\t\t\t\tName:              \"not-apple.example\",\n\t\t\t\t\tActive:            1,\n\t\t\t\t\tDelegationStatus:  \"BAD_NAMESERVERS\",\n\t\t\t\t\tPrimaryNameserver: \"ns.example.org\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                5,\n\t\t\t\t\tName:              \"example.com\",\n\t\t\t\t\tActive:            1,\n\t\t\t\t\tDelegationStatus:  \"BAD_NAMESERVERS\",\n\t\t\t\t\tPrimaryNameserver: \"ns.example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t2: {\n\t\t\tItems: []network.DNSZone{\n\t\t\t\t{\n\t\t\t\t\tID:                6,\n\t\t\t\t\tName:              \"banana.example\",\n\t\t\t\t\tActive:            1,\n\t\t\t\t\tDelegationStatus:  \"NXDOMAIN\",\n\t\t\t\t\tPrimaryNameserver: \"ns.example.org\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                7,\n\t\t\t\t\tName:              \"cherry.example\",\n\t\t\t\t\tActive:            1,\n\t\t\t\t\tDelegationStatus:  \"SERVFAIL\",\n\t\t\t\t\tPrimaryNameserver: \"ns.example.org\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                8,\n\t\t\t\t\tName:              \"dates.example\",\n\t\t\t\t\tActive:            1,\n\t\t\t\t\tDelegationStatus:  \"SERVFAIL\",\n\t\t\t\t\tPrimaryNameserver: \"ns.example.org\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                9,\n\t\t\t\t\tName:              \"eggplant.example\",\n\t\t\t\t\tActive:            1,\n\t\t\t\t\tDelegationStatus:  \"SERVFAIL\",\n\t\t\t\t\tPrimaryNameserver: \"ns.example.org\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                10,\n\t\t\t\t\tName:              \"fig.example\",\n\t\t\t\t\tActive:            1,\n\t\t\t\t\tDelegationStatus:  \"UNKNOWN\",\n\t\t\t\t\tPrimaryNameserver: \"ns.example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t3: {\n\t\t\tItems: []network.DNSZone{\n\t\t\t\t{\n\t\t\t\t\tID:                11,\n\t\t\t\t\tName:              \"grapes.example\",\n\t\t\t\t\tActive:            1,\n\t\t\t\t\tDelegationStatus:  \"UNKNOWN\",\n\t\t\t\t\tPrimaryNameserver: \"ns.example.org\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                12,\n\t\t\t\t\tName:              \"money.banana.example\",\n\t\t\t\t\tActive:            1,\n\t\t\t\t\tDelegationStatus:  \"UNKNOWN\",\n\t\t\t\t\tPrimaryNameserver: \"ns.example.org\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                13,\n\t\t\t\t\tName:              \"money.stand.banana.example\",\n\t\t\t\t\tActive:            1,\n\t\t\t\t\tDelegationStatus:  \"UNKNOWN\",\n\t\t\t\t\tPrimaryNameserver: \"ns.example.org\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                14,\n\t\t\t\t\tName:              \"stand.banana.example\",\n\t\t\t\t\tActive:            1,\n\t\t\t\t\tDelegationStatus:  \"UNKNOWN\",\n\t\t\t\t\tPrimaryNameserver: \"ns.example.org\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tmockAPIServerZones := make(map[string]int)\n\n\tfor _, page := range mockZones {\n\t\tfor _, zone := range page.Items {\n\t\t\tmockAPIServerZones[zone.Name] = int(zone.ID)\n\t\t}\n\t}\n\n\treturn mockZones, mockAPIServerZones\n}\n"
  },
  {
    "path": "providers/dns/loopia/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// DefaultBaseURL is url to the XML-RPC api.\nconst DefaultBaseURL = \"https://api.loopia.se/RPCSERV\"\n\n// Client the Loopia client.\ntype Client struct {\n\tapiUser     string\n\tapiPassword string\n\n\tBaseURL    string\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Loopia Client.\nfunc NewClient(apiUser, apiPassword string) *Client {\n\treturn &Client{\n\t\tapiUser:     apiUser,\n\t\tapiPassword: apiPassword,\n\t\tBaseURL:     DefaultBaseURL,\n\t\tHTTPClient:  &http.Client{Timeout: 10 * time.Second},\n\t}\n}\n\n// AddTXTRecord adds a TXT record.\nfunc (c *Client) AddTXTRecord(ctx context.Context, domain, subdomain string, ttl int, value string) error {\n\tcall := &methodCall{\n\t\tMethodName: \"addZoneRecord\",\n\t\tParams: []param{\n\t\t\tparamString{Value: c.apiUser},\n\t\t\tparamString{Value: c.apiPassword},\n\t\t\tparamString{Value: domain},\n\t\t\tparamString{Value: subdomain},\n\t\t\tparamStruct{\n\t\t\t\tStructMembers: []structMember{\n\t\t\t\t\tstructMemberString{Name: \"type\", Value: \"TXT\"},\n\t\t\t\t\tstructMemberInt{Name: \"ttl\", Value: ttl},\n\t\t\t\t\tstructMemberInt{Name: \"priority\", Value: 0},\n\t\t\t\t\tstructMemberString{Name: \"rdata\", Value: value},\n\t\t\t\t\tstructMemberInt{Name: \"record_id\", Value: 0},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tresp := &responseString{}\n\n\terr := c.rpcCall(ctx, call, resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn checkResponse(resp.Value)\n}\n\n// RemoveTXTRecord removes a TXT record.\nfunc (c *Client) RemoveTXTRecord(ctx context.Context, domain, subdomain string, recordID int) error {\n\tcall := &methodCall{\n\t\tMethodName: \"removeZoneRecord\",\n\t\tParams: []param{\n\t\t\tparamString{Value: c.apiUser},\n\t\t\tparamString{Value: c.apiPassword},\n\t\t\tparamString{Value: domain},\n\t\t\tparamString{Value: subdomain},\n\t\t\tparamInt{Value: recordID},\n\t\t},\n\t}\n\tresp := &responseString{}\n\n\terr := c.rpcCall(ctx, call, resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn checkResponse(resp.Value)\n}\n\n// GetTXTRecords gets TXT records.\nfunc (c *Client) GetTXTRecords(ctx context.Context, domain, subdomain string) ([]RecordObj, error) {\n\tcall := &methodCall{\n\t\tMethodName: \"getZoneRecords\",\n\t\tParams: []param{\n\t\t\tparamString{Value: c.apiUser},\n\t\t\tparamString{Value: c.apiPassword},\n\t\t\tparamString{Value: domain},\n\t\t\tparamString{Value: subdomain},\n\t\t},\n\t}\n\tresp := &recordObjectsResponse{}\n\n\terr := c.rpcCall(ctx, call, resp)\n\n\treturn resp.Params, err\n}\n\n// RemoveSubdomain remove a sub-domain.\nfunc (c *Client) RemoveSubdomain(ctx context.Context, domain, subdomain string) error {\n\tcall := &methodCall{\n\t\tMethodName: \"removeSubdomain\",\n\t\tParams: []param{\n\t\t\tparamString{Value: c.apiUser},\n\t\t\tparamString{Value: c.apiPassword},\n\t\t\tparamString{Value: domain},\n\t\t\tparamString{Value: subdomain},\n\t\t},\n\t}\n\tresp := &responseString{}\n\n\terr := c.rpcCall(ctx, call, resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn checkResponse(resp.Value)\n}\n\n// rpcCall makes an XML-RPC call to Loopia's RPC endpoint by marshaling the data given in the call argument to XML\n// and sending that via HTTP Post to Loopia.\n// The response is then unmarshalled into the resp argument.\nfunc (c *Client) rpcCall(ctx context.Context, call *methodCall, result response) error {\n\treq, err := newXMLRequest(ctx, c.BaseURL, call)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = xml.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unmarshal error: %w\", err)\n\t}\n\n\tif result.faultCode() != 0 {\n\t\treturn RPCError{\n\t\t\tFaultCode:   result.faultCode(),\n\t\t\tFaultString: strings.TrimSpace(result.faultString()),\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc newXMLRequest(ctx context.Context, endpoint string, payload any) (*http.Request, error) {\n\tbody := new(bytes.Buffer)\n\tbody.WriteString(xml.Header)\n\n\tencoder := xml.NewEncoder(body)\n\tencoder.Indent(\"\", \"  \")\n\n\terr := encoder.Encode(payload)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"text/xml\")\n\n\treturn req, nil\n}\n\nfunc checkResponse(value string) error {\n\tswitch v := strings.TrimSpace(value); v {\n\tcase \"OK\":\n\t\treturn nil\n\tcase \"AUTH_ERROR\":\n\t\treturn errors.New(\"authentication error\")\n\tdefault:\n\t\treturn fmt.Errorf(\"unknown error: %q\", v)\n\t}\n}\n"
  },
  {
    "path": "providers/dns/loopia/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"encoding/xml\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder(password string) *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"apiuser\", password)\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.BaseURL = server.URL + \"/\"\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithContentType(\"text/xml\"),\n\t)\n}\n\nfunc TestClient_AddZoneRecord(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tpassword string\n\t\tdomain   string\n\t\trequest  string\n\t\tresponse string\n\t\terr      string\n\t}{\n\t\t{\n\t\t\tdesc:     \"auth ok\",\n\t\t\tpassword: \"goodpassword\",\n\t\t\tdomain:   exampleDomain,\n\t\t\trequest:  addZoneRecordGoodAuth,\n\t\t\tresponse: responseOk,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"auth error\",\n\t\t\tpassword: \"badpassword\",\n\t\t\tdomain:   exampleDomain,\n\t\t\trequest:  addZoneRecordBadAuth,\n\t\t\tresponse: responseAuthError,\n\t\t\terr:      \"authentication error\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"unknown error\",\n\t\t\tpassword: \"goodpassword\",\n\t\t\tdomain:   \"badexample.com\",\n\t\t\trequest:  addZoneRecordNonValidDomain,\n\t\t\tresponse: responseUnknownError,\n\t\t\terr:      `unknown error: \"UNKNOWN_ERROR\"`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"empty response\",\n\t\t\tpassword: \"goodpassword\",\n\t\t\tdomain:   \"empty.com\",\n\t\t\trequest:  addZoneRecordEmptyResponse,\n\t\t\tresponse: \"\",\n\t\t\terr:      \"unmarshal error: EOF\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := mockBuilder(test.password).\n\t\t\t\tRoute(\"POST /\",\n\t\t\t\t\tservermock.RawStringResponse(test.response),\n\t\t\t\t\tservermock.CheckRequestBody(test.request)).\n\t\t\t\tBuild(t)\n\n\t\t\terr := client.AddTXTRecord(t.Context(), test.domain, exampleSubDomain, 123, \"TXTrecord\")\n\t\t\tif test.err == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.EqualError(t, err, test.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_RemoveSubdomain(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tpassword string\n\t\tdomain   string\n\t\trequest  string\n\t\tresponse string\n\t\terr      string\n\t}{\n\t\t{\n\t\t\tdesc:     \"auth ok\",\n\t\t\tpassword: \"goodpassword\",\n\t\t\tdomain:   exampleDomain,\n\t\t\trequest:  removeSubdomainGoodAuth,\n\t\t\tresponse: responseOk,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"auth error\",\n\t\t\tpassword: \"badpassword\",\n\t\t\tdomain:   exampleDomain,\n\t\t\trequest:  removeSubdomainBadAuth,\n\t\t\tresponse: responseAuthError,\n\t\t\terr:      \"authentication error\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"unknown error\",\n\t\t\tpassword: \"goodpassword\",\n\t\t\tdomain:   \"badexample.com\",\n\t\t\trequest:  removeSubdomainNonValidDomain,\n\t\t\tresponse: responseUnknownError,\n\t\t\terr:      `unknown error: \"UNKNOWN_ERROR\"`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"empty response\",\n\t\t\tpassword: \"goodpassword\",\n\t\t\tdomain:   \"empty.com\",\n\t\t\trequest:  removeSubdomainEmptyResponse,\n\t\t\tresponse: \"\",\n\t\t\terr:      \"unmarshal error: EOF\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := mockBuilder(test.password).\n\t\t\t\tRoute(\"POST /\",\n\t\t\t\t\tservermock.RawStringResponse(test.response),\n\t\t\t\t\tservermock.CheckRequestBody(test.request)).\n\t\t\t\tBuild(t)\n\n\t\t\terr := client.RemoveSubdomain(t.Context(), test.domain, exampleSubDomain)\n\t\t\tif test.err == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.EqualError(t, err, test.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_RemoveZoneRecord(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tpassword string\n\t\tdomain   string\n\t\trequest  string\n\t\tresponse string\n\t\terr      string\n\t}{\n\t\t{\n\t\t\tdesc:     \"auth ok\",\n\t\t\tpassword: \"goodpassword\",\n\t\t\tdomain:   exampleDomain,\n\t\t\trequest:  removeRecordGoodAuth,\n\t\t\tresponse: responseOk,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"auth error\",\n\t\t\tpassword: \"badpassword\",\n\t\t\tdomain:   exampleDomain,\n\t\t\trequest:  removeRecordBadAuth,\n\t\t\tresponse: responseAuthError,\n\t\t\terr:      \"authentication error\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"uknown error\",\n\t\t\tpassword: \"goodpassword\",\n\t\t\tdomain:   \"badexample.com\",\n\t\t\trequest:  removeRecordNonValidDomain,\n\t\t\tresponse: responseUnknownError,\n\t\t\terr:      `unknown error: \"UNKNOWN_ERROR\"`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"empty response\",\n\t\t\tpassword: \"goodpassword\",\n\t\t\tdomain:   \"empty.com\",\n\t\t\trequest:  removeRecordEmptyResponse,\n\t\t\tresponse: \"\",\n\t\t\terr:      \"unmarshal error: EOF\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := mockBuilder(test.password).\n\t\t\t\tRoute(\"POST /\",\n\t\t\t\t\tservermock.RawStringResponse(test.response),\n\t\t\t\t\tservermock.CheckRequestBody(test.request)).\n\t\t\t\tBuild(t)\n\n\t\t\terr := client.RemoveTXTRecord(t.Context(), test.domain, exampleSubDomain, 12345678)\n\t\t\tif test.err == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.EqualError(t, err, test.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_GetZoneRecord(t *testing.T) {\n\tclient := mockBuilder(\"goodpassword\").\n\t\tRoute(\"POST /\",\n\t\t\tservermock.RawStringResponse(getZoneRecordsResponse),\n\t\t\tservermock.CheckRequestBody(getZoneRecords)).\n\t\tBuild(t)\n\n\trecordObjs, err := client.GetTXTRecords(t.Context(), exampleDomain, exampleSubDomain)\n\trequire.NoError(t, err)\n\n\texpected := []RecordObj{\n\t\t{\n\t\t\tType:     \"TXT\",\n\t\t\tTTL:      300,\n\t\t\tPriority: 0,\n\t\t\tRdata:    exampleRdata,\n\t\t\tRecordID: 12345678,\n\t\t},\n\t}\n\tassert.Equal(t, expected, recordObjs)\n}\n\nfunc TestClient_rpcCall_404(t *testing.T) {\n\tclient := mockBuilder(\"apipassword\").\n\t\tRoute(\"POST /\",\n\t\t\tservermock.RawStringResponse(\"<?xml version='1.0' encoding='UTF-8'?>\").\n\t\t\t\tWithStatusCode(http.StatusNotFound)).\n\t\tBuild(t)\n\n\tcall := &methodCall{\n\t\tMethodName: \"dummyMethod\",\n\t\tParams: []param{\n\t\t\tparamString{Value: \"test1\"},\n\t\t},\n\t}\n\n\terr := client.rpcCall(t.Context(), call, &responseString{})\n\trequire.EqualError(t, err, \"unexpected status code: [status code: 404] body: <?xml version='1.0' encoding='UTF-8'?>\")\n}\n\nfunc TestClient_rpcCall_RPCError(t *testing.T) {\n\tclient := mockBuilder(\"apipassword\").\n\t\tRoute(\"POST /\",\n\t\t\tservermock.RawStringResponse(responseRPCError)).\n\t\tBuild(t)\n\n\tcall := &methodCall{\n\t\tMethodName: \"getDomains\",\n\t\tParams: []param{\n\t\t\tparamString{Value: \"test1\"},\n\t\t},\n\t}\n\n\terr := client.rpcCall(t.Context(), call, &responseString{})\n\trequire.EqualError(t, err, \"RPC Error: (201) Method signature error: 42\")\n}\n\nfunc TestUnmarshallFaultyRecordObject(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc string\n\t\txml  string\n\t}{\n\t\t{\n\t\t\tdesc: \"faulty name\",\n\t\t\txml:  \"<name>name<name>\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"faulty string\",\n\t\t\txml:  \"<value><string>foo<string></value>\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"faulty int\",\n\t\t\txml:  \"<value><int>1<int></value>\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tresp := &RecordObj{}\n\n\t\t\terr := xml.Unmarshal([]byte(test.xml), resp)\n\t\t\trequire.Error(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/loopia/internal/mock_test.go",
    "content": "package internal\n\nconst (\n\texampleDomain    = \"example.com\"\n\texampleSubDomain = \"_acme-challenge\"\n\texampleRdata     = \"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM\"\n)\n\n// Testdata based on real traffic between a xml-rpc client and the api.\nconst responseOk = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\t\t<methodResponse>\n\t\t  <params>\n\t\t\t<param>\n\t\t\t  <value>\n\t\t\t\t<string>\n\t\t\t\t  OK\n\t\t\t\t</string>\n\t\t\t  </value>\n\t\t\t</param>\n\t\t  </params>\n\t\t</methodResponse>`\n\nconst responseAuthError = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\t\t<methodResponse>\n\t\t  <params>\n\t\t\t<param>\n\t\t\t  <value>\n\t\t\t\t<string>\n\t\t\t\t  AUTH_ERROR\n\t\t\t\t</string>\n\t\t\t  </value>\n\t\t\t</param>\n\t\t  </params>\n\t\t</methodResponse>`\n\nconst responseUnknownError = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\t\t<methodResponse>\n\t\t  <params>\n\t\t\t<param>\n\t\t\t  <value>\n\t\t\t\t<string>\n\t\t\t\t  UNKNOWN_ERROR\n\t\t\t\t</string>\n\t\t\t  </value>\n\t\t\t</param>\n\t\t  </params>\n\t\t</methodResponse>`\n\nconst responseRPCError = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodResponse>\n  <fault>\n    <value>\n      <struct>\n        <member>\n          <name>\n            faultCode\n          </name>\n          <value>\n            <int>\n              201\n            </int>\n          </value>\n        </member>\n        <member>\n          <name>\n            faultString\n          </name>\n          <value>\n            <string>\n              Method signature error: 42\n            </string>\n          </value>\n        </member>\n      </struct>\n    </value>\n  </fault>\n</methodResponse>`\n\nconst addZoneRecordGoodAuth = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>addZoneRecord</methodName>\n  <params>\n    <param>\n      <value>\n        <string>apiuser</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>goodpassword</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>example.com</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>_acme-challenge</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <struct>\n          <member>\n            <name>type</name>\n            <value>\n              <string>TXT</string>\n            </value>\n          </member>\n          <member>\n            <name>ttl</name>\n            <value>\n              <int>123</int>\n            </value>\n          </member>\n          <member>\n            <name>priority</name>\n            <value>\n              <int>0</int>\n            </value>\n          </member>\n          <member>\n            <name>rdata</name>\n            <value>\n              <string>TXTrecord</string>\n            </value>\n          </member>\n          <member>\n            <name>record_id</name>\n            <value>\n              <int>0</int>\n            </value>\n          </member>\n        </struct>\n      </value>\n    </param>\n  </params>\n</methodCall>`\n\nconst addZoneRecordBadAuth = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>addZoneRecord</methodName>\n  <params>\n    <param>\n      <value>\n        <string>apiuser</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>badpassword</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>example.com</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>_acme-challenge</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <struct>\n          <member>\n            <name>type</name>\n            <value>\n              <string>TXT</string>\n            </value>\n          </member>\n          <member>\n            <name>ttl</name>\n            <value>\n              <int>123</int>\n            </value>\n          </member>\n          <member>\n            <name>priority</name>\n            <value>\n              <int>0</int>\n            </value>\n          </member>\n          <member>\n            <name>rdata</name>\n            <value>\n              <string>TXTrecord</string>\n            </value>\n          </member>\n          <member>\n            <name>record_id</name>\n            <value>\n              <int>0</int>\n            </value>\n          </member>\n        </struct>\n      </value>\n    </param>\n  </params>\n</methodCall>`\n\nconst addZoneRecordNonValidDomain = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>addZoneRecord</methodName>\n  <params>\n    <param>\n      <value>\n        <string>apiuser</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>goodpassword</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>badexample.com</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>_acme-challenge</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <struct>\n          <member>\n            <name>type</name>\n            <value>\n              <string>TXT</string>\n            </value>\n          </member>\n          <member>\n            <name>ttl</name>\n            <value>\n              <int>123</int>\n            </value>\n          </member>\n          <member>\n            <name>priority</name>\n            <value>\n              <int>0</int>\n            </value>\n          </member>\n          <member>\n            <name>rdata</name>\n            <value>\n              <string>TXTrecord</string>\n            </value>\n          </member>\n          <member>\n            <name>record_id</name>\n            <value>\n              <int>0</int>\n            </value>\n          </member>\n        </struct>\n      </value>\n    </param>\n  </params>\n</methodCall>`\n\nconst addZoneRecordEmptyResponse = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>addZoneRecord</methodName>\n  <params>\n    <param>\n      <value>\n        <string>apiuser</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>goodpassword</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>empty.com</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>_acme-challenge</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <struct>\n          <member>\n            <name>type</name>\n            <value>\n              <string>TXT</string>\n            </value>\n          </member>\n          <member>\n            <name>ttl</name>\n            <value>\n              <int>123</int>\n            </value>\n          </member>\n          <member>\n            <name>priority</name>\n            <value>\n              <int>0</int>\n            </value>\n          </member>\n          <member>\n            <name>rdata</name>\n            <value>\n              <string>TXTrecord</string>\n            </value>\n          </member>\n          <member>\n            <name>record_id</name>\n            <value>\n              <int>0</int>\n            </value>\n          </member>\n        </struct>\n      </value>\n    </param>\n  </params>\n</methodCall>`\n\nconst getZoneRecords = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>getZoneRecords</methodName>\n  <params>\n    <param>\n      <value>\n        <string>apiuser</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>goodpassword</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>example.com</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>_acme-challenge</string>\n      </value>\n    </param>\n  </params>\n</methodCall>`\n\nconst getZoneRecordsResponse = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodResponse>\n  <params>\n    <param>\n      <value>\n        <array>\n          <data>\n            <value>\n              <struct>\n                <member>\n                  <name>\n                    rdata\n                  </name>\n                  <value>\n                    <string>\n                    LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM\n                    </string>\n                  </value>\n                </member>\n                <member>\n                  <name>\n                    record_id\n                  </name>\n                  <value>\n                    <int>\n                      12345678\n                    </int>\n                  </value>\n                </member>\n                <member>\n                  <name>\n                    priority\n                  </name>\n                  <value>\n                    <int>\n                      0\n                    </int>\n                  </value>\n                </member>\n                <member>\n                  <name>\n                    ttl\n                  </name>\n                  <value>\n                    <int>\n                      300\n                    </int>\n                  </value>\n                </member>\n                <member>\n                  <name>\n                    type\n                  </name>\n                  <value>\n                    <string>\n                      TXT\n                    </string>\n                  </value>\n                </member>\n              </struct>\n            </value>\n          </data>\n        </array>\n      </value>\n    </param>\n  </params>\n</methodResponse>`\n\nconst removeRecordGoodAuth = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>removeZoneRecord</methodName>\n  <params>\n    <param>\n      <value>\n        <string>apiuser</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>goodpassword</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>example.com</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>_acme-challenge</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <int>12345678</int>\n      </value>\n    </param>\n  </params>\n</methodCall>`\n\nconst removeRecordBadAuth = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>removeZoneRecord</methodName>\n  <params>\n    <param>\n      <value>\n        <string>apiuser</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>badpassword</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>example.com</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>_acme-challenge</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <int>12345678</int>\n      </value>\n    </param>\n  </params>\n</methodCall>`\n\nconst removeRecordNonValidDomain = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>removeZoneRecord</methodName>\n  <params>\n    <param>\n      <value>\n        <string>apiuser</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>goodpassword</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>badexample.com</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>_acme-challenge</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <int>12345678</int>\n      </value>\n    </param>\n  </params>\n</methodCall>`\n\nconst removeRecordEmptyResponse = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>removeZoneRecord</methodName>\n  <params>\n    <param>\n      <value>\n        <string>apiuser</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>goodpassword</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>empty.com</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>_acme-challenge</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <int>12345678</int>\n      </value>\n    </param>\n  </params>\n</methodCall>`\n\nconst removeSubdomainGoodAuth = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>removeSubdomain</methodName>\n  <params>\n    <param>\n      <value>\n        <string>apiuser</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>goodpassword</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>example.com</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>_acme-challenge</string>\n      </value>\n    </param>\n  </params>\n</methodCall>`\n\nconst removeSubdomainBadAuth = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>removeSubdomain</methodName>\n  <params>\n    <param>\n      <value>\n        <string>apiuser</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>badpassword</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>example.com</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>_acme-challenge</string>\n      </value>\n    </param>\n  </params>\n</methodCall>`\n\nconst removeSubdomainNonValidDomain = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>removeSubdomain</methodName>\n  <params>\n    <param>\n      <value>\n        <string>apiuser</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>goodpassword</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>badexample.com</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>_acme-challenge</string>\n      </value>\n    </param>\n  </params>\n</methodCall>`\n\nconst removeSubdomainEmptyResponse = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<methodCall>\n  <methodName>removeSubdomain</methodName>\n  <params>\n    <param>\n      <value>\n        <string>apiuser</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>goodpassword</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>empty.com</string>\n      </value>\n    </param>\n    <param>\n      <value>\n        <string>_acme-challenge</string>\n      </value>\n    </param>\n  </params>\n</methodCall>`\n"
  },
  {
    "path": "providers/dns/loopia/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"strings\"\n)\n\n// types for XML-RPC method calls and parameters\n\ntype param interface {\n\tparam()\n}\n\ntype paramString struct {\n\tXMLName xml.Name `xml:\"param\"`\n\tValue   string   `xml:\"value>string\"`\n}\n\nfunc (p paramString) param() {}\n\ntype paramInt struct {\n\tXMLName xml.Name `xml:\"param\"`\n\tValue   int      `xml:\"value>int\"`\n}\n\nfunc (p paramInt) param() {}\n\ntype paramStruct struct {\n\tXMLName       xml.Name       `xml:\"param\"`\n\tStructMembers []structMember `xml:\"value>struct>member\"`\n}\n\nfunc (p paramStruct) param() {}\n\ntype structMember interface {\n\tstructMember()\n}\n\ntype structMemberString struct {\n\tName  string `xml:\"name\"`\n\tValue string `xml:\"value>string\"`\n}\n\nfunc (m structMemberString) structMember() {}\n\ntype structMemberInt struct {\n\tName  string `xml:\"name\"`\n\tValue int    `xml:\"value>int\"`\n}\n\nfunc (m structMemberInt) structMember() {}\n\ntype methodCall struct {\n\tXMLName    xml.Name `xml:\"methodCall\"`\n\tMethodName string   `xml:\"methodName\"`\n\tParams     []param  `xml:\"params>param\"`\n}\n\n// types for XML-RPC responses\n\ntype response interface {\n\tfaultCode() int\n\tfaultString() string\n}\n\ntype responseString struct {\n\tresponseFault\n\n\tValue string `xml:\"params>param>value>string\"`\n}\n\ntype responseFault struct {\n\tFaultCode   int    `xml:\"fault>value>struct>member>value>int\"`\n\tFaultString string `xml:\"fault>value>struct>member>value>string\"`\n}\n\nfunc (r responseFault) faultCode() int      { return r.FaultCode }\nfunc (r responseFault) faultString() string { return r.FaultString }\n\ntype RPCError struct {\n\tFaultCode   int\n\tFaultString string\n}\n\nfunc (e RPCError) Error() string {\n\treturn fmt.Sprintf(\"RPC Error: (%d) %s\", e.FaultCode, e.FaultString)\n}\n\ntype recordObjectsResponse struct {\n\tresponseFault\n\n\tXMLName xml.Name    `xml:\"methodResponse\"`\n\tParams  []RecordObj `xml:\"params>param>value>array>data>value>struct\"`\n}\n\ntype RecordObj struct {\n\tType     string\n\tTTL      int\n\tPriority int\n\tRdata    string\n\tRecordID int\n}\n\nfunc (r *RecordObj) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {\n\tvar name string\n\n\tfor {\n\t\tt, err := d.Token()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch tt := t.(type) {\n\t\tcase xml.StartElement:\n\t\t\tswitch tt.Name.Local {\n\t\t\tcase \"name\": // The name of the record object: <name>\n\t\t\t\tvar s string\n\t\t\t\tif err = d.DecodeElement(&s, &start); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tname = strings.TrimSpace(s)\n\n\t\t\tcase \"string\": // A string value of the record object: <value><string>\n\t\t\t\tif err = r.decodeValueString(name, d, start); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\tcase \"int\": // An int value of the record object: <value><int>\n\t\t\t\tif err = r.decodeValueInt(name, d, start); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\tcase xml.EndElement:\n\t\t\tif tt == start.End() {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (r *RecordObj) decodeValueString(name string, d *xml.Decoder, start xml.StartElement) error {\n\tvar s string\n\tif err := d.DecodeElement(&s, &start); err != nil {\n\t\treturn err\n\t}\n\n\ts = strings.TrimSpace(s)\n\n\tswitch name {\n\tcase \"type\":\n\t\tr.Type = s\n\tcase \"rdata\":\n\t\tr.Rdata = s\n\t}\n\n\treturn nil\n}\n\nfunc (r *RecordObj) decodeValueInt(name string, d *xml.Decoder, start xml.StartElement) error {\n\tvar i int\n\tif err := d.DecodeElement(&i, &start); err != nil {\n\t\treturn err\n\t}\n\n\tswitch name {\n\tcase \"record_id\":\n\t\tr.RecordID = i\n\tcase \"ttl\":\n\t\tr.TTL = i\n\tcase \"priority\":\n\t\tr.Priority = i\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/loopia/loopia.go",
    "content": "// Package loopia implements a DNS provider for solving the DNS-01 challenge using loopia DNS.\npackage loopia\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/loopia/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"LOOPIA_\"\n\n\tEnvAPIUser     = envNamespace + \"API_USER\"\n\tEnvAPIPassword = envNamespace + \"API_PASSWORD\"\n\tEnvAPIURL      = envNamespace + \"API_URL\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst minTTL = 300\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\ntype dnsClient interface {\n\tAddTXTRecord(ctx context.Context, domain, subdomain string, ttl int, value string) error\n\tRemoveTXTRecord(ctx context.Context, domain, subdomain string, recordID int) error\n\tGetTXTRecords(ctx context.Context, domain, subdomain string) ([]internal.RecordObj, error)\n\tRemoveSubdomain(ctx context.Context, domain, subdomain string) error\n}\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL            string\n\tAPIUser            string\n\tAPIPassword        string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 40*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPropagationTimeout),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, time.Minute),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient dnsClient\n\n\tinProgressInfo map[string]int\n\tinProgressMu   sync.Mutex\n\n\t// only for testing purpose.\n\tfindZoneByFqdn func(fqdn string) (string, error)\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Loopia.\n// Credentials must be passed in the environment variables:\n// LOOPIA_API_USER, LOOPIA_API_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIUser, EnvAPIPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"loopia: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIUser = values[EnvAPIUser]\n\tconfig.APIPassword = values[EnvAPIPassword]\n\tconfig.BaseURL = env.GetOrDefaultString(EnvAPIURL, internal.DefaultBaseURL)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Loopia.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"loopia: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIUser == \"\" || config.APIPassword == \"\" {\n\t\treturn nil, errors.New(\"loopia: credentials missing\")\n\t}\n\n\t// Min value for TTL is 300\n\tif config.TTL < 300 {\n\t\tconfig.TTL = 300\n\t}\n\n\tclient := internal.NewClient(config.APIUser, config.APIPassword)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\tif config.BaseURL != \"\" {\n\t\tclient.BaseURL = config.BaseURL\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:         config,\n\t\tclient:         client,\n\t\tfindZoneByFqdn: dns01.FindZoneByFqdn,\n\t\tinProgressInfo: make(map[string]int),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tsubDomain, authZone, err := d.splitDomain(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"loopia: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\terr = d.client.AddTXTRecord(ctx, authZone, subDomain, d.config.TTL, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"loopia: failed to add TXT record: %w\", err)\n\t}\n\n\ttxtRecords, err := d.client.GetTXTRecords(ctx, authZone, subDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"loopia: failed to get TXT records: %w\", err)\n\t}\n\n\td.inProgressMu.Lock()\n\tdefer d.inProgressMu.Unlock()\n\n\tfor _, r := range txtRecords {\n\t\tif r.Rdata == info.Value {\n\t\t\td.inProgressInfo[token] = r.RecordID\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn errors.New(\"loopia: failed to find the stored TXT record\")\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tsubDomain, authZone, err := d.splitDomain(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"loopia: %w\", err)\n\t}\n\n\td.inProgressMu.Lock()\n\tdefer d.inProgressMu.Unlock()\n\n\tctx := context.Background()\n\n\terr = d.client.RemoveTXTRecord(ctx, authZone, subDomain, d.inProgressInfo[token])\n\tif err != nil {\n\t\treturn fmt.Errorf(\"loopia: failed to remove TXT record: %w\", err)\n\t}\n\n\trecords, err := d.client.GetTXTRecords(ctx, authZone, subDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"loopia: failed to get TXT records: %w\", err)\n\t}\n\n\tif len(records) > 0 {\n\t\treturn nil\n\t}\n\n\terr = d.client.RemoveSubdomain(ctx, authZone, subDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"loopia: failed to remove subdomain: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) splitDomain(fqdn string) (string, string, error) {\n\tauthZone, err := d.findZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, authZone)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\treturn subDomain, dns01.UnFqdn(authZone), nil\n}\n"
  },
  {
    "path": "providers/dns/loopia/loopia.toml",
    "content": "Name = \"Loopia\"\nDescription = ''''''\nURL = \"https://loopia.com\"\nCode = \"loopia\"\nSince = \"v4.2.0\"\n\nExample = '''\nLOOPIA_API_USER=xxxxxxxx \\\nLOOPIA_API_PASSWORD=yyyyyyyy \\\nlego --dns loopia -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n### API user\n\nYou can [generate a new API user](https://customerzone.loopia.com/api/) from your account page.\n\nIt needs to have the following permissions:\n\n* addZoneRecord\n* getZoneRecords\n* removeZoneRecord\n* removeSubdomain\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    LOOPIA_API_USER = \"API username\"\n    LOOPIA_API_PASSWORD = \"API password\"\n  [Configuration.Additional]\n    LOOPIA_API_URL = \"API endpoint. Ex: https://api.loopia.se/RPCSERV or https://api.loopia.rs/RPCSERV\"\n    LOOPIA_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2400)\"\n    LOOPIA_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    LOOPIA_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    LOOPIA_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 60)\"\n\n[Links]\n  API = \"https://www.loopia.com/api\"\n"
  },
  {
    "path": "providers/dns/loopia/loopia_mock_test.go",
    "content": "package loopia\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/loopia/internal\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\texampleDomain    = \"example.com\"\n\texampleSubDomain = \"_acme-challenge\"\n\texampleRdata     = \"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM\"\n)\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tmockedFindZoneByFqdn := func(fqdn string) (string, error) {\n\t\treturn exampleDomain + \".\", nil\n\t}\n\n\ttestCases := []struct {\n\t\tdesc string\n\n\t\tgetTXTRecordsError  error\n\t\tgetTXTRecordsReturn []internal.RecordObj\n\t\taddTXTRecordError   error\n\t\tcallAddTXTRecord    bool\n\t\tcallGetTXTRecords   bool\n\n\t\texpectedError               string\n\t\texpectedInProgressTokenInfo int\n\t}{\n\t\t{\n\t\t\tdesc: \"Present OK\",\n\n\t\t\tgetTXTRecordsReturn: []internal.RecordObj{{Type: \"TXT\", Rdata: exampleRdata, RecordID: 12345678}},\n\t\t\tcallAddTXTRecord:    true,\n\t\t\tcallGetTXTRecords:   true,\n\n\t\t\texpectedInProgressTokenInfo: 12345678,\n\t\t},\n\t\t{\n\t\t\tdesc: \"AddTXTRecord fails\",\n\n\t\t\taddTXTRecordError: errors.New(\"unknown error: 'ADDTXT'\"),\n\t\t\tcallAddTXTRecord:  true,\n\n\t\t\texpectedError: \"loopia: failed to add TXT record: unknown error: 'ADDTXT'\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"GetTXTRecords fails\",\n\n\t\t\tgetTXTRecordsError: errors.New(\"unknown error: 'GETTXT'\"),\n\t\t\tcallAddTXTRecord:   true,\n\t\t\tcallGetTXTRecords:  true,\n\n\t\t\texpectedError: \"loopia: failed to get TXT records: unknown error: 'GETTXT'\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"Failed to get ID\",\n\n\t\t\tcallAddTXTRecord:  true,\n\t\t\tcallGetTXTRecords: true,\n\n\t\t\texpectedError: \"loopia: failed to find the stored TXT record\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIUser = \"apiuser\"\n\t\t\tconfig.APIPassword = \"password\"\n\n\t\t\tclient := &mockedClient{}\n\n\t\t\tprovider, err := NewDNSProviderConfig(config)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tprovider.findZoneByFqdn = mockedFindZoneByFqdn\n\t\t\tprovider.client = client\n\n\t\t\tif test.callAddTXTRecord {\n\t\t\t\tclient.On(\"AddTXTRecord\", exampleDomain, exampleSubDomain, config.TTL, exampleRdata).Return(test.addTXTRecordError)\n\t\t\t}\n\n\t\t\tif test.callGetTXTRecords {\n\t\t\t\tclient.On(\"GetTXTRecords\", exampleDomain, exampleSubDomain).Return(test.getTXTRecordsReturn, test.getTXTRecordsError)\n\t\t\t}\n\n\t\t\terr = provider.Present(exampleDomain, \"token\", \"key\")\n\n\t\t\tclient.AssertExpectations(t)\n\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.expectedInProgressTokenInfo, provider.inProgressInfo[\"token\"])\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_Cleanup(t *testing.T) {\n\tmockedFindZoneByFqdn := func(fqdn string) (string, error) {\n\t\treturn \"example.com.\", nil\n\t}\n\n\ttestCases := []struct {\n\t\tdesc string\n\n\t\tgetTXTRecordsError   error\n\t\tgetTXTRecordsReturn  []internal.RecordObj\n\t\tremoveTXTRecordError error\n\t\tremoveSubdomainError error\n\t\tcallAddTXTRecord     bool\n\t\tcallGetTXTRecords    bool\n\t\tcallRemoveSubdomain  bool\n\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc: \"Cleanup Ok\",\n\n\t\t\tcallAddTXTRecord:    true,\n\t\t\tcallGetTXTRecords:   true,\n\t\t\tcallRemoveSubdomain: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"removeTXTRecord failed\",\n\n\t\t\tremoveTXTRecordError: errors.New(\"authentication error\"),\n\t\t\tcallAddTXTRecord:     true,\n\n\t\t\texpectedError: \"loopia: failed to remove TXT record: authentication error\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"removeSubdomain failed\",\n\n\t\t\tremoveSubdomainError: errors.New(`unknown error: \"UNKNOWN_ERROR\"`),\n\t\t\tcallAddTXTRecord:     true,\n\t\t\tcallGetTXTRecords:    true,\n\t\t\tcallRemoveSubdomain:  true,\n\n\t\t\texpectedError: `loopia: failed to remove subdomain: unknown error: \"UNKNOWN_ERROR\"`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"Don't call removeSubdomain when records\",\n\n\t\t\tgetTXTRecordsReturn: []internal.RecordObj{{Type: \"TXT\", Rdata: \"LEFTOVER\"}},\n\t\t\tcallAddTXTRecord:    true,\n\t\t\tcallGetTXTRecords:   true,\n\t\t\tcallRemoveSubdomain: false,\n\t\t},\n\t\t{\n\t\t\tdesc: \"getTXTRecords failed\",\n\n\t\t\tgetTXTRecordsError:  errors.New(`unknown error: \"UNKNOWN_ERROR\"`),\n\t\t\tcallAddTXTRecord:    true,\n\t\t\tcallGetTXTRecords:   true,\n\t\t\tcallRemoveSubdomain: false,\n\n\t\t\texpectedError: `loopia: failed to get TXT records: unknown error: \"UNKNOWN_ERROR\"`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIUser = \"apiuser\"\n\t\t\tconfig.APIPassword = \"password\"\n\n\t\t\tclient := &mockedClient{}\n\n\t\t\tprovider, err := NewDNSProviderConfig(config)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tprovider.findZoneByFqdn = mockedFindZoneByFqdn\n\t\t\tprovider.client = client\n\t\t\tprovider.inProgressInfo[\"token\"] = 12345678\n\n\t\t\tif test.callAddTXTRecord {\n\t\t\t\tclient.On(\"RemoveTXTRecord\", \"example.com\", \"_acme-challenge\", 12345678).Return(test.removeTXTRecordError)\n\t\t\t}\n\n\t\t\tif test.callGetTXTRecords {\n\t\t\t\tclient.On(\"GetTXTRecords\", \"example.com\", \"_acme-challenge\").Return(test.getTXTRecordsReturn, test.getTXTRecordsError)\n\t\t\t}\n\n\t\t\tif test.callRemoveSubdomain {\n\t\t\t\tclient.On(\"RemoveSubdomain\", \"example.com\", \"_acme-challenge\").Return(test.removeSubdomainError)\n\t\t\t}\n\n\t\t\terr = provider.CleanUp(\"example.com\", \"token\", \"key\")\n\n\t\t\tclient.AssertExpectations(t)\n\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype mockedClient struct {\n\tmock.Mock\n}\n\nfunc (c *mockedClient) RemoveTXTRecord(ctx context.Context, domain, subdomain string, recordID int) error {\n\targs := c.Called(domain, subdomain, recordID)\n\treturn args.Error(0)\n}\n\nfunc (c *mockedClient) AddTXTRecord(ctx context.Context, domain, subdomain string, ttl int, value string) error {\n\targs := c.Called(domain, subdomain, ttl, value)\n\treturn args.Error(0)\n}\n\nfunc (c *mockedClient) GetTXTRecords(ctx context.Context, domain, subdomain string) ([]internal.RecordObj, error) {\n\targs := c.Called(domain, subdomain)\n\treturn args.Get(0).([]internal.RecordObj), args.Error(1)\n}\n\nfunc (c *mockedClient) RemoveSubdomain(ctx context.Context, domain, subdomain string) error {\n\targs := c.Called(domain, subdomain)\n\treturn args.Error(0)\n}\n"
  },
  {
    "path": "providers/dns/loopia/loopia_test.go",
    "content": "package loopia\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIUser,\n\tEnvAPIPassword,\n\tEnvTTL,\n\tEnvPollingInterval,\n\tEnvPropagationTimeout,\n\tEnvHTTPTimeout).\n\tWithDomain(envDomain)\n\nfunc TestSplitDomain(t *testing.T) {\n\tprovider := &DNSProvider{\n\t\tfindZoneByFqdn: func(fqdn string) (string, error) {\n\t\t\treturn \"example.com.\", nil\n\t\t},\n\t}\n\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tfqdn      string\n\t\tsubdomain string\n\t\tdomain    string\n\t}{\n\t\t{\n\t\t\tdesc:      \"single subdomain\",\n\t\t\tfqdn:      \"subdomain.example.com\",\n\t\t\tsubdomain: \"subdomain\",\n\t\t\tdomain:    \"example.com\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"double subdomain\",\n\t\t\tfqdn:      \"sub.domain.example.com\",\n\t\t\tsubdomain: \"sub.domain\",\n\t\t\tdomain:    \"example.com\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"asterisk subdomain\",\n\t\t\tfqdn:      \"*.example.com\",\n\t\t\tsubdomain: \"*\",\n\t\t\tdomain:    \"example.com\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tsubdomain, domain, err := provider.splitDomain(test.fqdn)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.subdomain, subdomain)\n\t\t\tassert.Equal(t, test.domain, domain)\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tenvVars       map[string]string\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser:     \"user\",\n\t\t\t\tEnvAPIPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API user\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser:     \"\",\n\t\t\t\tEnvAPIPassword: \"secret\",\n\t\t\t},\n\t\t\texpectedError: \"loopia: some credentials information are missing: LOOPIA_API_USER\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser:     \"user\",\n\t\t\t\tEnvAPIPassword: \"\",\n\t\t\t},\n\t\t\texpectedError: \"loopia: some credentials information are missing: LOOPIA_API_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc:          \"missing credentials\",\n\t\t\tenvVars:       map[string]string{},\n\t\t\texpectedError: \"loopia: some credentials information are missing: LOOPIA_API_USER,LOOPIA_API_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tconfig        *Config\n\t\texpectedTTL   int\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tconfig: &Config{\n\t\t\t\tAPIUser:     \"user\",\n\t\t\t\tAPIPassword: \"secret\",\n\t\t\t\tTTL:         3600,\n\t\t\t},\n\t\t\texpectedTTL: 3600,\n\t\t},\n\t\t{\n\t\t\tdesc:          \"nil config user\",\n\t\t\texpectedError: \"loopia: the configuration of the DNS provider is nil\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"empty user\",\n\t\t\tconfig: &Config{\n\t\t\t\tAPIUser:     \"\",\n\t\t\t\tAPIPassword: \"secret\",\n\t\t\t\tTTL:         3600,\n\t\t\t},\n\t\t\texpectedError: \"loopia: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"empty password\",\n\t\t\tconfig: &Config{\n\t\t\t\tAPIUser:     \"user\",\n\t\t\t\tAPIPassword: \"\",\n\t\t\t\tTTL:         3600,\n\t\t\t},\n\t\t\texpectedTTL:   3600,\n\t\t\texpectedError: \"loopia: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"too low TTL\",\n\t\t\tconfig: &Config{\n\t\t\t\tAPIUser:     \"user\",\n\t\t\t\tAPIPassword: \"secret\",\n\t\t\t\tTTL:         299,\n\t\t\t},\n\t\t\texpectedTTL: 300,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tp, err := NewDNSProviderConfig(test.config)\n\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.Equal(t, test.expectedTTL, p.config.TTL)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/luadns/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// defaultBaseURL represents the API endpoint to call.\nconst defaultBaseURL = \"https://api.luadns.com\"\n\n// Client Lua DNS API client.\ntype Client struct {\n\tapiUsername string\n\tapiToken    string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiUsername, apiToken string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiUsername: apiUsername,\n\t\tapiToken:    apiToken,\n\t\tbaseURL:     baseURL,\n\t\tHTTPClient:  &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// ListZones gets all the hosted zones.\n// https://luadns.com/api.html#list-zones\nfunc (c *Client) ListZones(ctx context.Context) ([]DNSZone, error) {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"zones\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar zones []DNSZone\n\n\terr = c.do(req, &zones)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not list zones: %w\", err)\n\t}\n\n\treturn zones, nil\n}\n\n// CreateRecord creates a new record in a zone.\n// https://luadns.com/api.html#create-a-record\nfunc (c *Client) CreateRecord(ctx context.Context, zone DNSZone, newRecord DNSRecord) (*DNSRecord, error) {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"zones\", strconv.Itoa(zone.ID), \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, newRecord)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar record *DNSRecord\n\n\terr = c.do(req, &record)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not create record %#v: %w\", record, err)\n\t}\n\n\treturn record, nil\n}\n\n// DeleteRecord deletes a record.\n// https://luadns.com/api.html#delete-a-record\nfunc (c *Client) DeleteRecord(ctx context.Context, record *DNSRecord) error {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"zones\", strconv.Itoa(record.ZoneID), \"records\", strconv.Itoa(record.ID))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not delete record %#v: %w\", record, err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.SetBasicAuth(c.apiUsername, c.apiToken)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errResp errorResponse\n\n\terr := json.Unmarshal(raw, &errResp)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"status=%d: %w\", resp.StatusCode, errResp)\n}\n"
  },
  {
    "path": "providers/dns/luadns/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder(apiToken string) *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"me\", apiToken)\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithBasicAuth(\"me\", apiToken))\n}\n\nfunc TestClient_ListZones(t *testing.T) {\n\tclient := mockBuilder(\"secretA\").\n\t\tRoute(\"GET /v1/zones\", servermock.ResponseFromFixture(\"list_zones.json\")).\n\t\tBuild(t)\n\n\tzones, err := client.ListZones(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []DNSZone{\n\t\t{\n\t\t\tID:             1,\n\t\t\tName:           \"example.com\",\n\t\t\tSynced:         false,\n\t\t\tQueriesCount:   0,\n\t\t\tRecordsCount:   3,\n\t\t\tAliasesCount:   0,\n\t\t\tRedirectsCount: 0,\n\t\t\tForwardsCount:  0,\n\t\t\tTemplateID:     0,\n\t\t},\n\t\t{\n\t\t\tID:             2,\n\t\t\tName:           \"example.net\",\n\t\t\tSynced:         false,\n\t\t\tQueriesCount:   0,\n\t\t\tRecordsCount:   3,\n\t\t\tAliasesCount:   0,\n\t\t\tRedirectsCount: 0,\n\t\t\tForwardsCount:  0,\n\t\t\tTemplateID:     0,\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := mockBuilder(\"secretB\").\n\t\tRoute(\"POST /v1/zones/1/records\",\n\t\t\tservermock.ResponseFromFixture(\"create_record.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"name\":\"example.com.\",\"type\":\"MX\",\"content\":\"10 mail.example.com.\",\"ttl\":300}`)).\n\t\tBuild(t)\n\n\tzone := DNSZone{ID: 1}\n\n\trecord := DNSRecord{\n\t\tName:    \"example.com.\",\n\t\tType:    \"MX\",\n\t\tContent: \"10 mail.example.com.\",\n\t\tTTL:     300,\n\t}\n\n\tnewRecord, err := client.CreateRecord(t.Context(), zone, record)\n\trequire.NoError(t, err)\n\n\texpected := &DNSRecord{\n\t\tID:      100,\n\t\tName:    \"example.com.\",\n\t\tType:    \"MX\",\n\t\tContent: \"10 mail.example.com.\",\n\t\tTTL:     300,\n\t\tZoneID:  1,\n\t}\n\n\tassert.Equal(t, expected, newRecord)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder(\"secretC\").\n\t\tRoute(\"DELETE /v1/zones/1/records/2\",\n\t\t\tservermock.ResponseFromFixture(\"delete_record.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"id\":2,\"name\":\"example.com.\",\"type\":\"MX\",\"content\":\"10 mail.example.com.\",\"ttl\":300,\"zone_id\":1}`)).\n\t\tBuild(t)\n\n\trecord := &DNSRecord{\n\t\tID:      2,\n\t\tName:    \"example.com.\",\n\t\tType:    \"MX\",\n\t\tContent: \"10 mail.example.com.\",\n\t\tTTL:     300,\n\t\tZoneID:  1,\n\t}\n\n\terr := client.DeleteRecord(t.Context(), record)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/luadns/internal/fixtures/create_record.json",
    "content": "{\n  \"id\": 100,\n  \"name\": \"example.com.\",\n  \"type\": \"MX\",\n  \"content\": \"10 mail.example.com.\",\n  \"ttl\": 300,\n  \"zone_id\": 1,\n  \"created_at\": \"2015-01-17T14:04:35.251785849Z\",\n  \"updated_at\": \"2015-01-17T14:04:35.251785972Z\"\n}"
  },
  {
    "path": "providers/dns/luadns/internal/fixtures/delete_record.json",
    "content": "{\n  \"id\": 100,\n  \"name\": \"example.com.\",\n  \"type\": \"MX\",\n  \"content\": \"10 mail.example.com.\",\n  \"ttl\": 300,\n  \"zone_id\": 1,\n  \"created_at\": \"2015-01-17T14:04:35.251785849Z\",\n  \"updated_at\": \"2015-01-17T14:04:35.251785972Z\"\n}"
  },
  {
    "path": "providers/dns/luadns/internal/fixtures/list_zones.json",
    "content": "[\n  {\n    \"id\": 1,\n    \"name\": \"example.com\",\n    \"synced\": false,\n    \"queries_count\": 0,\n    \"records_count\": 3,\n    \"aliases_count\": 0,\n    \"redirects_count\": 0,\n    \"forwards_count\": 0,\n    \"template_id\": 0\n  },\n  {\n    \"id\": 2,\n    \"name\": \"example.net\",\n    \"synced\": false,\n    \"queries_count\": 0,\n    \"records_count\": 3,\n    \"aliases_count\": 0,\n    \"redirects_count\": 0,\n    \"forwards_count\": 0,\n    \"template_id\": 0\n  }\n]"
  },
  {
    "path": "providers/dns/luadns/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype errorResponse struct {\n\tStatus    string `json:\"status\"`\n\tRequestID string `json:\"request_id\"`\n\tMessage   string `json:\"message\"`\n}\n\nfunc (e errorResponse) Error() string {\n\treturn fmt.Sprintf(\"status=%s, message=%s\", e.Status, e.Message)\n}\n\n// DNSZone a DNS zone.\ntype DNSZone struct {\n\tID             int    `json:\"id\"`\n\tName           string `json:\"name,omitempty\"`\n\tSynced         bool   `json:\"synced,omitempty\"`\n\tQueriesCount   int    `json:\"queries_count,omitempty\"`\n\tRecordsCount   int    `json:\"records_count,omitempty\"`\n\tAliasesCount   int    `json:\"aliases_count,omitempty\"`\n\tRedirectsCount int    `json:\"redirects_count,omitempty\"`\n\tForwardsCount  int    `json:\"forwards_count,omitempty\"`\n\tTemplateID     int    `json:\"template_id,omitempty\"`\n}\n\n// DNSRecord a DNS record.\ntype DNSRecord struct {\n\tID      int    `json:\"id,omitempty\"`\n\tName    string `json:\"name,omitempty\"`\n\tType    string `json:\"type,omitempty\"`\n\tContent string `json:\"content,omitempty\"`\n\tTTL     int    `json:\"ttl,omitempty\"`\n\tZoneID  int    `json:\"zone_id,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/luadns/luadns.go",
    "content": "// Package luadns implements a DNS provider for solving the DNS-01 challenge using LuaDNS.\npackage luadns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/luadns/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"LUADNS_\"\n\n\tEnvAPIUsername = envNamespace + \"API_USERNAME\"\n\tEnvAPIToken    = envNamespace + \"API_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst minTTL = 300\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIUsername        string\n\tAPIToken           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordsMu sync.Mutex\n\trecords   map[string]*internal.DNSRecord\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for LuaDNS.\n// Credentials must be passed in the environment variables:\n// LUADNS_API_USERNAME and LUADNS_API_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIUsername, EnvAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"luadns: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIUsername = values[EnvAPIUsername]\n\tconfig.APIToken = values[EnvAPIToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for LuaDNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"luadns: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIUsername == \"\" || config.APIToken == \"\" {\n\t\treturn nil, errors.New(\"luadns: credentials missing\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"luadns: invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\tclient := internal.NewClient(config.APIUsername, config.APIToken)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:  config,\n\t\tclient:  client,\n\t\trecords: make(map[string]*internal.DNSRecord),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzones, err := d.client.ListZones(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"luadns: failed to get zones: %w\", err)\n\t}\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"luadns: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzone := findZone(zones, dns01.UnFqdn(authZone))\n\tif zone == nil {\n\t\treturn fmt.Errorf(\"luadns: no matching zone found for domain %s\", domain)\n\t}\n\n\tnewRecord := internal.DNSRecord{\n\t\tName:    info.EffectiveFQDN,\n\t\tType:    \"TXT\",\n\t\tContent: info.Value,\n\t\tTTL:     d.config.TTL,\n\t}\n\n\trecord, err := d.client.CreateRecord(ctx, *zone, newRecord)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"luadns: failed to create record: %w\", err)\n\t}\n\n\td.recordsMu.Lock()\n\td.records[token] = record\n\td.recordsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordsMu.Lock()\n\trecord, ok := d.records[token]\n\td.recordsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"luadns: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\terr := d.client.DeleteRecord(context.Background(), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"luadns: failed to delete record: %w\", err)\n\t}\n\n\t// Delete record from map\n\td.recordsMu.Lock()\n\tdelete(d.records, token)\n\td.recordsMu.Unlock()\n\n\treturn nil\n}\n\nfunc findZone(zones []internal.DNSZone, domain string) *internal.DNSZone {\n\tvar result *internal.DNSZone\n\n\tfor _, zone := range zones {\n\t\tif zone.Name != \"\" && strings.HasSuffix(domain, zone.Name) {\n\t\t\tif result == nil || len(zone.Name) > len(result.Name) {\n\t\t\t\tresult = &zone\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "providers/dns/luadns/luadns.toml",
    "content": "Name = \"LuaDNS\"\nDescription = ''''''\nURL = \"https://luadns.com\"\nCode = \"luadns\"\nSince = \"v3.7.0\"\n\nExample = '''\nLUADNS_API_USERNAME=youremail \\\nLUADNS_API_TOKEN=xxxxxxxx \\\nlego --dns luadns -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    LUADNS_API_USERNAME = \"Username (your email)\"\n    LUADNS_API_TOKEN = \"API token\"\n  [Configuration.Additional]\n    LUADNS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    LUADNS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    LUADNS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    LUADNS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://luadns.com/api.html\"\n"
  },
  {
    "path": "providers/dns/luadns/luadns_test.go",
    "content": "package luadns\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/providers/dns/luadns/internal\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIUsername,\n\tEnvAPIToken).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUsername: \"123\",\n\t\t\t\tEnvAPIToken:    \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUsername: \"\",\n\t\t\t\tEnvAPIToken:    \"\",\n\t\t\t},\n\t\t\texpected: \"luadns: some credentials information are missing: LUADNS_API_USERNAME,LUADNS_API_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUsername: \"\",\n\t\t\t\tEnvAPIToken:    \"456\",\n\t\t\t},\n\t\t\texpected: \"luadns: some credentials information are missing: LUADNS_API_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUsername: \"123\",\n\t\t\t\tEnvAPIToken:    \"\",\n\t\t\t},\n\t\t\texpected: \"luadns: some credentials information are missing: LUADNS_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tapiKey    string\n\t\tapiSecret string\n\t\ttll       int\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tapiKey:    \"123\",\n\t\t\tapiSecret: \"456\",\n\t\t\ttll:       minTTL,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\ttll:      minTTL,\n\t\t\texpected: \"luadns: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing username\",\n\t\t\tapiSecret: \"456\",\n\t\t\ttll:       minTTL,\n\t\t\texpected:  \"luadns: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api token\",\n\t\t\tapiKey:   \"123\",\n\t\t\ttll:      minTTL,\n\t\t\texpected: \"luadns: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"invalid TTL\",\n\t\t\tapiKey:    \"123\",\n\t\t\tapiSecret: \"456\",\n\t\t\ttll:       30,\n\t\t\texpected:  \"luadns: invalid TTL, TTL (30) must be greater than 300\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIUsername = test.apiKey\n\t\t\tconfig.APIToken = test.apiSecret\n\t\t\tconfig.TTL = test.tll\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_findZone(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tdomain   string\n\t\tzones    []internal.DNSZone\n\t\texpected *internal.DNSZone\n\t}{\n\t\t{\n\t\t\tdesc:   \"simple domain\",\n\t\t\tdomain: \"example.org\",\n\t\t\tzones: []internal.DNSZone{\n\t\t\t\t{Name: \"example.org\"},\n\t\t\t\t{Name: \"example.com\"},\n\t\t\t},\n\t\t\texpected: &internal.DNSZone{Name: \"example.org\"},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"sub domain\",\n\t\t\tdomain: \"aaa.example.org\",\n\t\t\tzones: []internal.DNSZone{\n\t\t\t\t{Name: \"example.org\"},\n\t\t\t\t{Name: \"aaa.example.org\"},\n\t\t\t\t{Name: \"bbb.example.org\"},\n\t\t\t\t{Name: \"example.com\"},\n\t\t\t},\n\t\t\texpected: &internal.DNSZone{Name: \"aaa.example.org\"},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"empty zone name\",\n\t\t\tdomain: \"example.org\",\n\t\t\tzones: []internal.DNSZone{\n\t\t\t\t{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"not found\",\n\t\t\tdomain: \"example.org\",\n\t\t\tzones: []internal.DNSZone{\n\t\t\t\t{Name: \"example.net\"},\n\t\t\t\t{Name: \"aaa.example.net\"},\n\t\t\t\t{Name: \"bbb.example.net\"},\n\t\t\t\t{Name: \"example.com\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tzone := findZone(test.zones, test.domain)\n\t\t\tassert.Equal(t, test.expected, zone)\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/mailinabox/mailinabox.go",
    "content": "// Package mailinabox implements a DNS provider for solving the DNS-01 challenge using Mail-in-a-Box.\npackage mailinabox\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/nrdcg/mailinabox\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"MAILINABOX_\"\n\n\tEnvEmail    = envNamespace + \"EMAIL\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\tEnvBaseURL  = envNamespace + \"BASE_URL\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tEmail              string\n\tPassword           string\n\tBaseURL            string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *mailinabox.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Mail-in-a-Box.\n// Credentials must be passed in the environment variables:\n// MAILINABOX_EMAIL, MAILINABOX_PASSWORD, and MAILINABOX_BASE_URL.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvBaseURL, EnvEmail, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mailinabox: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.BaseURL = values[EnvBaseURL]\n\tconfig.Email = values[EnvEmail]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for deSEC.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"mailinabox: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Email == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"mailinabox: incomplete credentials, missing email or password\")\n\t}\n\n\tif config.BaseURL == \"\" {\n\t\treturn nil, errors.New(\"mailinabox: missing base URL\")\n\t}\n\n\tif config.HTTPClient == nil {\n\t\tconfig.HTTPClient = &http.Client{Timeout: 30 * time.Second}\n\t}\n\n\tconfig.HTTPClient = clientdebug.Wrap(config.HTTPClient)\n\n\tclient, err := mailinabox.New(config.BaseURL, config.Email, config.Password, mailinabox.WithHTTPClient(config.HTTPClient))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mailinabox: %w\", err)\n\t}\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecord := mailinabox.Record{\n\t\tName:  dns01.UnFqdn(info.EffectiveFQDN),\n\t\tType:  \"TXT\",\n\t\tValue: info.Value,\n\t}\n\n\t_, err := d.client.DNS.AddRecord(ctx, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mailinabox: add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecord := mailinabox.Record{\n\t\tName:  dns01.UnFqdn(info.EffectiveFQDN),\n\t\tType:  \"TXT\",\n\t\tValue: info.Value,\n\t}\n\n\t_, err := d.client.DNS.RemoveRecord(ctx, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mailinabox: remove record: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/mailinabox/mailinabox.toml",
    "content": "Name = \"Mail-in-a-Box\"\nDescription = ''''''\nURL = \"https://mailinabox.email\"\nCode = \"mailinabox\"\nSince = \"v4.16.0\"\n\nExample = '''\nMAILINABOX_EMAIL=user@example.com \\\nMAILINABOX_PASSWORD=yyyy \\\nMAILINABOX_BASE_URL=https://box.example.com \\\nlego --dns mailinabox -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    MAILINABOX_EMAIL = \"User email\"\n    MAILINABOX_PASSWORD = \"User password\"\n    MAILINABOX_BASE_URL = \"Base API URL (ex: https://box.example.com)\"\n  [Configuration.Additional]\n    MAILINABOX_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 4)\"\n    MAILINABOX_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    MAILINABOX_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://mailinabox.email/api-docs.html\"\n"
  },
  {
    "path": "providers/dns/mailinabox/mailinabox_test.go",
    "content": "package mailinabox\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvBaseURL, EnvEmail, EnvPassword).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvBaseURL:  \"https://example.com\",\n\t\t\t\tEnvEmail:    \"user@example.com\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing base URL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEmail:    \"user@example.com\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"mailinabox: some credentials information are missing: MAILINABOX_BASE_URL\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing email\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvBaseURL:  \"https://example.com\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"mailinabox: some credentials information are missing: MAILINABOX_EMAIL\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvBaseURL: \"https://example.com\",\n\t\t\t\tEnvEmail:   \"user@example.com\",\n\t\t\t},\n\t\t\texpected: \"mailinabox: some credentials information are missing: MAILINABOX_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing all options\",\n\t\t\texpected: \"mailinabox: some credentials information are missing: MAILINABOX_BASE_URL,MAILINABOX_EMAIL,MAILINABOX_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tbaseURL  string\n\t\temail    string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\temail:    \"user@example.com\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing base URL\",\n\t\t\temail:    \"user@example.com\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"mailinabox: missing base URL\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing email\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"mailinabox: incomplete credentials, missing email or password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\temail:    \"user@example.com\",\n\t\t\texpected: \"mailinabox: incomplete credentials, missing email or password\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.BaseURL = test.baseURL\n\t\t\tconfig.Email = test.email\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/manageengine/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://clouddns.manageengine.com/v1\"\n\n// Client the ManageEngine CloudDNS API client.\ntype Client struct {\n\tbaseURL    *url.URL\n\thttpClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(hc *http.Client) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tbaseURL:    baseURL,\n\t\thttpClient: hc,\n\t}\n}\n\n// GetAllZones gets all zones.\n// https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#GET_All\nfunc (c *Client) GetAllZones(ctx context.Context) ([]Zone, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns\", \"domain\")\n\n\treq, err := newRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar results []Zone\n\n\terr = c.do(req, &results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn results, nil\n}\n\n// GetAllZoneRecords gets all \"zone records\" for a zone.\n// https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#GET_All_9\nfunc (c *Client) GetAllZoneRecords(ctx context.Context, zoneID int) ([]ZoneRecord, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns\", \"domain\", strconv.Itoa(zoneID), \"records\", \"SPF_TXT\")\n\n\treq, err := newRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar results []ZoneRecord\n\n\terr = c.do(req, &results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn results, nil\n}\n\n// DeleteZoneRecord deletes a \"zone record\".\n// https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#DEL_Delete_10\nfunc (c *Client) DeleteZoneRecord(ctx context.Context, zoneID, domainID int) error {\n\tendpoint := c.baseURL.JoinPath(\"dns\", \"domain\", strconv.Itoa(zoneID), \"records\", \"SPF_TXT\", strconv.Itoa(domainID))\n\n\treq, err := newRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar results APIResponse\n\n\treturn c.do(req, &results)\n}\n\n// CreateZoneRecord creates a \"zone record\".\n// https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#POST_Create_10\nfunc (c *Client) CreateZoneRecord(ctx context.Context, zoneID int, record ZoneRecord) error {\n\tendpoint := c.baseURL.JoinPath(\"dns\", \"domain\", strconv.Itoa(zoneID), \"records\", \"SPF_TXT\", \"/\")\n\n\treq, err := newRequest(ctx, http.MethodPost, endpoint, []ZoneRecord{record})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar results APIResponse\n\n\treturn c.do(req, &results)\n}\n\n// UpdateZoneRecord update an existing \"zone record\".\n// https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation#PUT_Update_10\nfunc (c *Client) UpdateZoneRecord(ctx context.Context, record ZoneRecord) error {\n\tif record.SpfTxtDomainID == 0 {\n\t\treturn errors.New(\"SpfTxtDomainID is empty\")\n\t}\n\n\tif record.ZoneID == 0 {\n\t\treturn errors.New(\"ZoneID is empty\")\n\t}\n\n\tendpoint := c.baseURL.JoinPath(\"dns\", \"domain\", strconv.Itoa(record.ZoneID), \"records\", \"SPF_TXT\", strconv.Itoa(record.SpfTxtDomainID), \"/\")\n\n\treq, err := newRequest(ctx, http.MethodPut, endpoint, []ZoneRecord{record})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar results APIResponse\n\n\treturn c.do(req, &results)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tvar body io.Reader = http.NoBody\n\n\tif payload != nil {\n\t\tbuf := new(bytes.Buffer)\n\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\n\t\tvalues := url.Values{}\n\t\tvalues.Set(\"config\", buf.String())\n\t\tbody = strings.NewReader(values.Encode())\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code: %d] %w\", resp.StatusCode, &errAPI)\n}\n"
  },
  {
    "path": "providers/dns/manageengine/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(server.Client())\n\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithAccept(\"application/json\"))\n}\n\nfunc TestClient_GetAllZones(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/domain\", servermock.ResponseFromFixture(\"zone_domains_all.json\")).\n\t\tBuild(t)\n\n\tgroups, err := client.GetAllZones(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []Zone{\n\t\t{\n\t\t\tZoneID:        1,\n\t\t\tZoneName:      \"test.com.\",\n\t\t\tZoneTTL:       500,\n\t\t\tZoneTargeting: true,\n\t\t\tRefresh:       43200,\n\t\t\tRetry:         3600,\n\t\t\tExpiry:        1209600,\n\t\t\tMinimum:       180,\n\t\t\tOrg:           2,\n\t\t\tNsID:          1,\n\t\t\tSerial:        2022042206,\n\t\t\tNss:           []string{\"ns11.zns-53.com.\", \"ns21.zns-53.net.\", \"ns31.zns-53.com.\", \"ns41.zns-53.net.\"},\n\t\t},\n\t\t{\n\t\t\tZoneID:   2,\n\t\t\tZoneName: \"yourdomain.com.\",\n\t\t\tZoneTTL:  1000,\n\t\t\tRefresh:  43200,\n\t\t\tRetry:    3600,\n\t\t\tExpiry:   1209600,\n\t\t\tMinimum:  180,\n\t\t\tOrg:      2,\n\t\t\tVanity:   true,\n\t\t\tNsID:     1,\n\t\t\tSerial:   2022040608,\n\t\t\tNss:      []string{\"ns11.yourdomain.com.\", \"ns21.yourdomain.net.\", \"ns31.yourdomain.com.\", \"ns41.yourdomain.net.\"},\n\t\t},\n\t\t{\n\t\t\tZoneID:   20,\n\t\t\tZoneName: \"hello45.com.\",\n\t\t\tZoneTTL:  3000,\n\t\t\tRefresh:  43200,\n\t\t\tRetry:    3600,\n\t\t\tExpiry:   1209600,\n\t\t\tMinimum:  180,\n\t\t\tOrg:      2,\n\t\t\tNsID:     1,\n\t\t\tSerial:   2022040711,\n\t\t\tNss:      []string{\"ns11.zns-53.com.\", \"ns21.zns-53.net.\", \"ns31.zns-53.com.\", \"ns41.zns-53.net.\"},\n\t\t},\n\t\t{\n\t\t\tZoneID:        22,\n\t\t\tZoneName:      \"zohoaccl.com.\",\n\t\t\tZoneTTL:       300,\n\t\t\tZoneTargeting: true,\n\t\t\tRefresh:       43200,\n\t\t\tRetry:         3600,\n\t\t\tExpiry:        1209600,\n\t\t\tMinimum:       180,\n\t\t\tOrg:           2,\n\t\t\tNsID:          1,\n\t\t\tSerial:        2022042206,\n\t\t\tNss:           []string{\"ns11.zns-53.com.\", \"ns21.zns-53.net.\", \"ns31.zns-53.com.\", \"ns41.zns-53.net.\"},\n\t\t},\n\t\t{\n\t\t\tZoneID:        23,\n\t\t\tZoneName:      \"zohocal.com.\",\n\t\t\tZoneTTL:       300,\n\t\t\tZoneTargeting: true,\n\t\t\tRefresh:       43200,\n\t\t\tRetry:         3600,\n\t\t\tExpiry:        1209600,\n\t\t\tMinimum:       180,\n\t\t\tOrg:           2,\n\t\t\tNsID:          1,\n\t\t\tSerial:        2022041310,\n\t\t\tNss:           []string{\"ns11.zns-53.com.\", \"ns21.zns-53.net.\", \"ns31.zns-53.com.\", \"ns41.zns-53.net.\"},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, groups)\n}\n\nfunc TestClient_GetAllZones_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/domain\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetAllZones(t.Context())\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"[status code: 401] Authentication credentials were not provided.\")\n}\n\nfunc TestClient_GetAllZoneRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/domain/4/records/SPF_TXT\", servermock.ResponseFromFixture(\"zone_records_all.json\")).\n\t\tBuild(t)\n\n\tgroups, err := client.GetAllZoneRecords(t.Context(), 4)\n\trequire.NoError(t, err)\n\n\texpected := []ZoneRecord{\n\t\t{\n\t\t\tZoneID:           4,\n\t\t\tSpfTxtDomainID:   6,\n\t\t\tDomainName:       \"spftest.example.com.\",\n\t\t\tDomainTTL:        300,\n\t\t\tDomainLocationID: 1,\n\t\t\tRecordType:       \"SPF\",\n\t\t\tRecords: []Record{{\n\t\t\t\tID:       1,\n\t\t\t\tValues:   []string{\"necwcltpwxbz-noelget3jush-vop2xxvapot3eyq_0\"},\n\t\t\t\tDomainID: 6,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tZoneID:           4,\n\t\t\tSpfTxtDomainID:   13,\n\t\t\tDomainName:       \"txt.example.com.\",\n\t\t\tDomainTTL:        300,\n\t\t\tDomainLocationID: 1,\n\t\t\tRecordType:       \"TXT\",\n\t\t\tRecords: []Record{{\n\t\t\t\tID:       1,\n\t\t\t\tValues:   []string{\"v=spf1include:transmail.netinclude:example.com~all\", \"c-68e3oc4trm8w7piplscg7vgojmtkjrnrabr4king8\"},\n\t\t\t\tDomainID: 13,\n\t\t\t}},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, groups)\n}\n\nfunc TestClient_GetAllZoneRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/domain/4/records/SPF_TXT\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetAllZoneRecords(t.Context(), 4)\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"[status code: 401] Authentication credentials were not provided.\")\n}\n\nfunc TestClient_DeleteZoneRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns/domain/4/records/SPF_TXT/6\", servermock.ResponseFromFixture(\"zone_record_delete.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteZoneRecord(t.Context(), 4, 6)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteZoneRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns/domain/4/records/SPF_TXT/6\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\terr := client.DeleteZoneRecord(t.Context(), 4, 6)\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"[status code: 401] Authentication credentials were not provided.\")\n}\n\nfunc TestClient_CreateZoneRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/domain/4/records/SPF_TXT/\",\n\t\t\tservermock.ResponseFromFixture(\"zone_record_create.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentTypeFromURLEncoded(),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"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}]}]\n`)).\n\t\tBuild(t)\n\n\trecord := ZoneRecord{\n\t\tZoneID:           1,\n\t\tSpfTxtDomainID:   2,\n\t\tDomainName:       \"example.com\",\n\t\tDomainTTL:        120,\n\t\tDomainLocationID: 3,\n\t\tRecordType:       \"TXT\",\n\t\tRecords: []Record{\n\t\t\t{\n\t\t\t\tID:       123,\n\t\t\t\tValues:   []string{\"value1\"},\n\t\t\t\tDisabled: false,\n\t\t\t\tDomainID: 1,\n\t\t\t},\n\t\t},\n\t}\n\n\terr := client.CreateZoneRecord(t.Context(), 4, record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_CreateZoneRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/domain/4/records/SPF_TXT/\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentTypeFromURLEncoded()).\n\t\tBuild(t)\n\n\trecord := ZoneRecord{}\n\n\terr := client.CreateZoneRecord(t.Context(), 4, record)\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"[status code: 401] Authentication credentials were not provided.\")\n}\n\nfunc TestClient_CreateZoneRecord_error_bad_request(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/domain/4/records/SPF_TXT/\",\n\t\t\tservermock.ResponseFromFixture(\"error_bad_request.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentTypeFromURLEncoded()).\n\t\tBuild(t)\n\n\trecord := ZoneRecord{}\n\n\terr := client.CreateZoneRecord(t.Context(), 4, record)\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"[status code: 400] Invalid record format, Record should be in list.\")\n}\n\nfunc TestClient_UpdateZoneRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /dns/domain/4/records/SPF_TXT/6/\",\n\t\t\tservermock.ResponseFromFixture(\"zone_record_update.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentTypeFromURLEncoded(),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"config\", `[{\"zone_id\":4,\"spf_txt_domain_id\":6,\"records\":null}]\n`)).\n\t\tBuild(t)\n\n\trecord := ZoneRecord{\n\t\tSpfTxtDomainID: 6,\n\t\tZoneID:         4,\n\t}\n\n\terr := client.UpdateZoneRecord(t.Context(), record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_UpdateZoneRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /dns/domain/4/records/SPF_TXT/6/\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentTypeFromURLEncoded()).\n\t\tBuild(t)\n\n\trecord := ZoneRecord{\n\t\tSpfTxtDomainID: 6,\n\t\tZoneID:         4,\n\t}\n\n\terr := client.UpdateZoneRecord(t.Context(), record)\n\trequire.Error(t, err)\n\n\trequire.EqualError(t, err, \"[status code: 401] Authentication credentials were not provided.\")\n}\n"
  },
  {
    "path": "providers/dns/manageengine/internal/fixtures/error.json",
    "content": "{\n  \"detail\": \"Authentication credentials were not provided.\"\n}\n"
  },
  {
    "path": "providers/dns/manageengine/internal/fixtures/error_bad_request.json",
    "content": "{\n  \"error\": \"Invalid record format, Record should be in list.\"\n}\n"
  },
  {
    "path": "providers/dns/manageengine/internal/fixtures/zone_domains_all.json",
    "content": "[\n  {\n    \"zone_id\": 1,\n    \"zone_name\": \"test.com.\",\n    \"zone_ttl\": 500,\n    \"zone_type\": 0,\n    \"zone_targeting\": true,\n    \"zone_logging\": \"{}\",\n    \"zone_contact\": \"mathes.zoho.com\",\n    \"refresh\": 43200,\n    \"retry\": 3600,\n    \"expiry\": 1209600,\n    \"minimum\": 180,\n    \"org\": 2,\n    \"any_query\": false,\n    \"dnssec\": true,\n    \"vanity\": false,\n    \"ns_id\": 1,\n    \"serial\": 2022042206,\n    \"ns\": [\n      \"ns11.zns-53.com.\",\n      \"ns21.zns-53.net.\",\n      \"ns31.zns-53.com.\",\n      \"ns41.zns-53.net.\"\n    ],\n    \"contact_group\": [\n      \"test_contact1\",\n      \"test_contact2\"\n    ],\n    \"ds\": [\n      {\n        \"record_id\": 59,\n        \"keyTag\": 36938,\n        \"algorithm\": 13,\n        \"digestType\": 1,\n        \"digest\": \"e9f03d176455d5d16f826b69f9ecb11f59be35e7\",\n        \"domain_id\": 30\n      },\n      {\n        \"record_id\": 60,\n        \"keyTag\": 36938,\n        \"algorithm\": 13,\n        \"digestType\": 2,\n        \"digest\": \"7ea640a8668eafd9d89a9b2e9994f5fcfb1dee0668d1e93ba556aa57ac047f96\",\n        \"domain_id\": 30\n      }\n    ]\n  },\n  {\n    \"zone_id\": 2,\n    \"zone_name\": \"yourdomain.com.\",\n    \"zone_ttl\": 1000,\n    \"zone_type\": 0,\n    \"zone_targeting\": false,\n    \"zone_logging\": \"{}\",\n    \"zone_contact\": \"contact.yourdomain.com\",\n    \"refresh\": 43200,\n    \"retry\": 3600,\n    \"expiry\": 1209600,\n    \"minimum\": 180,\n    \"org\": 2,\n    \"any_query\": false,\n    \"dnssec\": false,\n    \"vanity\": true,\n    \"vanity_grp\": \"yourdomain\",\n    \"ns_id\": 1,\n    \"serial\": 2022040608,\n    \"ns\": [\n      \"ns11.yourdomain.com.\",\n      \"ns21.yourdomain.net.\",\n      \"ns31.yourdomain.com.\",\n      \"ns41.yourdomain.net.\"\n    ]\n  },\n  {\n    \"zone_id\": 20,\n    \"zone_name\": \"hello45.com.\",\n    \"zone_ttl\": 3000,\n    \"zone_targeting\": false,\n    \"zone_logging\": \"{}\",\n    \"zone_contact\": \"mathes.zoho.com\",\n    \"refresh\": 43200,\n    \"retry\": 3600,\n    \"expiry\": 1209600,\n    \"minimum\": 180,\n    \"org\": 2,\n    \"any_query\": false,\n    \"dnssec\": false,\n    \"ns_id\": 1,\n    \"serial\": 2022040711,\n    \"ns\": [\n      \"ns11.zns-53.com.\",\n      \"ns21.zns-53.net.\",\n      \"ns31.zns-53.com.\",\n      \"ns41.zns-53.net.\"\n    ]\n  },\n  {\n    \"zone_id\": 22,\n    \"zone_name\": \"zohoaccl.com.\",\n    \"zone_ttl\": 300,\n    \"zone_type\": 0,\n    \"zone_targeting\": true,\n    \"zone_logging\": \"{}\",\n    \"zone_contact\": \"networkone.zohocorp.com\",\n    \"refresh\": 43200,\n    \"retry\": 3600,\n    \"expiry\": 1209600,\n    \"minimum\": 180,\n    \"org\": 2,\n    \"any_query\": false,\n    \"dnssec\": false,\n    \"ns_id\": 1,\n    \"serial\": 2022042206,\n    \"ns\": [\n      \"ns11.zns-53.com.\",\n      \"ns21.zns-53.net.\",\n      \"ns31.zns-53.com.\",\n      \"ns41.zns-53.net.\"\n    ]\n  },\n  {\n    \"zone_id\": 23,\n    \"zone_name\": \"zohocal.com.\",\n    \"zone_ttl\": 300,\n    \"zone_type\": 0,\n    \"zone_targeting\": true,\n    \"zone_logging\": \"{}\",\n    \"zone_contact\": \"mathes.zoho.com\",\n    \"refresh\": 43200,\n    \"retry\": 3600,\n    \"expiry\": 1209600,\n    \"minimum\": 180,\n    \"org\": 2,\n    \"any_query\": false,\n    \"dnssec\": false,\n    \"ns_id\": 1,\n    \"serial\": 2022041310,\n    \"ns\": [\n      \"ns11.zns-53.com.\",\n      \"ns21.zns-53.net.\",\n      \"ns31.zns-53.com.\",\n      \"ns41.zns-53.net.\"\n    ]\n  }\n]\n"
  },
  {
    "path": "providers/dns/manageengine/internal/fixtures/zone_record_create.json",
    "content": "{\n  \"message\": \"Record created successfully\"\n}\n"
  },
  {
    "path": "providers/dns/manageengine/internal/fixtures/zone_record_delete.json",
    "content": "{\n  \"message\": \"Record deleted successfully\"\n}\n"
  },
  {
    "path": "providers/dns/manageengine/internal/fixtures/zone_record_update.json",
    "content": "{\n  \"message\": \"Record updated successfully\"\n}\n"
  },
  {
    "path": "providers/dns/manageengine/internal/fixtures/zone_records_all.json",
    "content": "[\n  {\n    \"spf_txt_domain_id\": 6,\n    \"zone_id\": 4,\n    \"domain_name\": \"spftest.example.com.\",\n    \"domain_ttl\": 300,\n    \"domain_location_id\": 1,\n    \"record_type\": \"SPF\",\n    \"records\": [\n      {\n        \"record_id\": 1,\n        \"value\": [\n          \"necwcltpwxbz-noelget3jush-vop2xxvapot3eyq_0\"\n        ],\n        \"disabled\": false,\n        \"domain_id\": 6\n      }\n    ]\n  },\n  {\n    \"spf_txt_domain_id\": 13,\n    \"zone_id\": 4,\n    \"domain_name\": \"txt.example.com.\",\n    \"domain_ttl\": 300,\n    \"domain_maxhost\": 1,\n    \"domain_location_id\": 1,\n    \"record_type\": \"TXT\",\n    \"records\": [\n      {\n        \"record_id\": 1,\n        \"value\": [\n          \"v=spf1include:transmail.netinclude:example.com~all\",\n          \"c-68e3oc4trm8w7piplscg7vgojmtkjrnrabr4king8\"\n        ],\n        \"disabled\": false,\n        \"domain_id\": 13\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "providers/dns/manageengine/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"golang.org/x/oauth2/clientcredentials\"\n)\n\nconst defaultAuthURL = \"https://clouddns.manageengine.com/oauth2/token/\"\n\nfunc CreateOAuthClient(ctx context.Context, clientID, clientSecret string) *http.Client {\n\tconfig := &clientcredentials.Config{\n\t\tTokenURL:     defaultAuthURL,\n\t\tClientID:     clientID,\n\t\tClientSecret: clientSecret,\n\t}\n\n\treturn config.Client(ctx)\n}\n"
  },
  {
    "path": "providers/dns/manageengine/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"strings\"\n)\n\ntype APIError struct {\n\tMessage string `json:\"error\"`\n\tDetail  string `json:\"detail\"`\n}\n\nfunc (a *APIError) Error() string {\n\tvar msg []string\n\n\tif a.Message != \"\" {\n\t\tmsg = append(msg, a.Message)\n\t}\n\n\tif a.Detail != \"\" {\n\t\tmsg = append(msg, a.Detail)\n\t}\n\n\treturn strings.Join(msg, \" \")\n}\n\ntype APIResponse struct {\n\tMessage string `json:\"message,omitempty\"`\n}\n\ntype ZoneRecord struct {\n\tZoneID           int      `json:\"zone_id,omitempty\"`\n\tSpfTxtDomainID   int      `json:\"spf_txt_domain_id,omitempty\"`\n\tDomainName       string   `json:\"domain_name,omitempty\"`\n\tDomainTTL        int      `json:\"domain_ttl,omitempty\"`\n\tDomainLocationID int      `json:\"domain_location_id,omitempty\"`\n\tRecordType       string   `json:\"record_type,omitempty\"`\n\tRecords          []Record `json:\"records\"`\n}\n\ntype Record struct {\n\tID       int      `json:\"record_id,omitempty\"`\n\tValues   []string `json:\"value,omitempty\"`\n\tDisabled bool     `json:\"disabled,omitempty\"`\n\tDomainID int      `json:\"domain_id,omitempty\"`\n}\n\ntype Zone struct {\n\tZoneID        int      `json:\"zone_id\"`\n\tZoneName      string   `json:\"zone_name\"`\n\tZoneTTL       int      `json:\"zone_ttl\"`\n\tZoneType      int      `json:\"zone_type,omitempty\"`\n\tZoneTargeting bool     `json:\"zone_targeting\"`\n\tRefresh       int      `json:\"refresh\"`\n\tRetry         int      `json:\"retry\"`\n\tExpiry        int      `json:\"expiry\"`\n\tMinimum       int      `json:\"minimum\"`\n\tOrg           int      `json:\"org\"`\n\tAnyQuery      bool     `json:\"any_query\"`\n\tVanity        bool     `json:\"vanity,omitempty\"`\n\tNsID          int      `json:\"ns_id\"`\n\tSerial        int      `json:\"serial\"`\n\tNss           []string `json:\"ns\"`\n}\n"
  },
  {
    "path": "providers/dns/manageengine/manageengine.go",
    "content": "// Package manageengine implements a DNS provider for solving the DNS-01 challenge using ManageEngine CloudDNS.\npackage manageengine\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/manageengine/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"MANAGEENGINE_\"\n\n\tEnvClientID     = envNamespace + \"CLIENT_ID\"\n\tEnvClientSecret = envNamespace + \"CLIENT_SECRET\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tClientID     string\n\tClientSecret string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for ManageEngine CloudDNS.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvClientID, EnvClientSecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"manageengine: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.ClientID = values[EnvClientID]\n\tconfig.ClientSecret = values[EnvClientSecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for ManageEngine CloudDNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"manageengine: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.ClientID == \"\" || config.ClientSecret == \"\" {\n\t\treturn nil, errors.New(\"manageengine: credentials missing\")\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: internal.NewClient(\n\t\t\tclientdebug.Wrap(\n\t\t\t\tinternal.CreateOAuthClient(context.Background(), config.ClientID, config.ClientSecret),\n\t\t\t),\n\t\t),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"manageengine: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzoneID, err := d.findZoneID(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"manageengine: find zone ID: %w\", err)\n\t}\n\n\tzoneRecord, err := d.findZoneRecord(ctx, zoneID, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"manageengine: find zone record: %w\", err)\n\t}\n\n\t// Update the existing zone record.\n\tif zoneRecord != nil {\n\t\tfor _, record := range zoneRecord.Records {\n\t\t\tif slices.Contains(record.Values, info.Value) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tzr := internal.ZoneRecord{\n\t\t\t\tZoneID:         zoneID,\n\t\t\t\tSpfTxtDomainID: zoneRecord.SpfTxtDomainID,\n\t\t\t\tDomainName:     info.EffectiveFQDN,\n\t\t\t\tDomainTTL:      d.config.TTL,\n\t\t\t\tRecordType:     \"TXT\",\n\t\t\t\tRecords: []internal.Record{{\n\t\t\t\t\tValues:   append(record.Values, info.Value),\n\t\t\t\t\tDomainID: zoneRecord.SpfTxtDomainID,\n\t\t\t\t}},\n\t\t\t}\n\n\t\t\t// Update the zone record.\n\t\t\terr = d.client.UpdateZoneRecord(ctx, zr)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"manageengine: update zone record: %w\", err)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn errors.New(\"manageengine: zone already contains the TXT record value\")\n\t}\n\n\t// Create a new zone record.\n\trecord := internal.ZoneRecord{\n\t\tZoneID:     zoneID,\n\t\tDomainName: info.EffectiveFQDN,\n\t\tDomainTTL:  d.config.TTL,\n\t\tRecordType: \"TXT\",\n\t\tRecords: []internal.Record{{\n\t\t\tValues: []string{info.Value},\n\t\t}},\n\t}\n\n\terr = d.client.CreateZoneRecord(ctx, zoneID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"manageengine: create zone record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"manageengine: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzoneID, err := d.findZoneID(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"manageengine: find zone ID: %w\", err)\n\t}\n\n\tzoneRecord, err := d.findZoneRecord(ctx, zoneID, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"manageengine: find zone record: %w\", err)\n\t}\n\n\tfor _, record := range zoneRecord.Records {\n\t\tif !slices.Contains(record.Values, info.Value) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Delete the zone record.\n\t\tif len(record.Values) <= 1 {\n\t\t\terr = d.client.DeleteZoneRecord(ctx, zoneID, zoneRecord.SpfTxtDomainID)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"manageengine: delete zone record: %w\", err)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\n\t\t// Update the zone record.\n\t\tvar values []string\n\n\t\tfor _, value := range record.Values {\n\t\t\tif value != info.Value {\n\t\t\t\tvalues = append(values, value)\n\t\t\t}\n\t\t}\n\n\t\tzr := internal.ZoneRecord{\n\t\t\tZoneID:         zoneID,\n\t\t\tSpfTxtDomainID: zoneRecord.SpfTxtDomainID,\n\t\t\tDomainName:     info.EffectiveFQDN,\n\t\t\tDomainTTL:      d.config.TTL,\n\t\t\tRecordType:     \"TXT\",\n\t\t\tRecords: []internal.Record{{\n\t\t\t\tValues:   values,\n\t\t\t\tDomainID: zoneRecord.SpfTxtDomainID,\n\t\t\t}},\n\t\t}\n\n\t\terr = d.client.UpdateZoneRecord(ctx, zr)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"manageengine: create zone record: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) findZoneID(ctx context.Context, authZone string) (int, error) {\n\tzones, err := d.client.GetAllZones(ctx)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"get all zone groups: %w\", err)\n\t}\n\n\tfor _, zone := range zones {\n\t\tif strings.EqualFold(zone.ZoneName, authZone) {\n\t\t\treturn zone.ZoneID, nil\n\t\t}\n\t}\n\n\treturn 0, fmt.Errorf(\"zone not found %s\", authZone)\n}\n\nfunc (d *DNSProvider) findZoneRecord(ctx context.Context, zoneID int, fqdn string) (*internal.ZoneRecord, error) {\n\tzoneRecords, err := d.client.GetAllZoneRecords(ctx, zoneID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get all zone records: %w\", err)\n\t}\n\n\tfor _, zoneRecord := range zoneRecords {\n\t\tif !strings.EqualFold(zoneRecord.DomainName, fqdn) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.EqualFold(zoneRecord.RecordType, \"TXT\") {\n\t\t\treturn &zoneRecord, nil\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n"
  },
  {
    "path": "providers/dns/manageengine/manageengine.toml",
    "content": "Name = \"ManageEngine CloudDNS\"\nDescription = ''''''\nURL = \"https://clouddns.manageengine.com\"\nCode = \"manageengine\"\nSince = \"v4.21.0\"\n\nExample = '''\nMANAGEENGINE_CLIENT_ID=\"xxx\" \\\nMANAGEENGINE_CLIENT_SECRET=\"yyy\" \\\nlego --dns manageengine -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    MANAGEENGINE_CLIENT_ID = \"Client ID\"\n    MANAGEENGINE_CLIENT_SECRET = \"Client Secret\"\n  [Configuration.Additional]\n    MANAGEENGINE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    MANAGEENGINE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    MANAGEENGINE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n\n[Links]\n  API = \"https://pitstop.manageengine.com/portal/en/kb/articles/manageengine-clouddns-rest-api-documentation\"\n"
  },
  {
    "path": "providers/dns/manageengine/manageengine_test.go",
    "content": "package manageengine\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvClientID, EnvClientSecret).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvClientID:     \"abc\",\n\t\t\t\tEnvClientSecret: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing client ID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvClientID:     \"\",\n\t\t\t\tEnvClientSecret: \"secret\",\n\t\t\t},\n\t\t\texpected: \"manageengine: some credentials information are missing: MANAGEENGINE_CLIENT_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing client secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvClientID:     \"abc\",\n\t\t\t\tEnvClientSecret: \"\",\n\t\t\t},\n\t\t\texpected: \"manageengine: some credentials information are missing: MANAGEENGINE_CLIENT_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"manageengine: some credentials information are missing: MANAGEENGINE_CLIENT_ID,MANAGEENGINE_CLIENT_SECRET\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc         string\n\t\tclientID     string\n\t\tclientSecret string\n\t\texpected     string\n\t}{\n\t\t{\n\t\t\tdesc:         \"success\",\n\t\t\tclientID:     \"abc\",\n\t\t\tclientSecret: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"missing client ID\",\n\t\t\tclientSecret: \"secret\",\n\t\t\texpected:     \"manageengine: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing client secret\",\n\t\t\tclientID: \"abc\",\n\t\t\texpected: \"manageengine: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"manageengine: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.ClientID = test.clientID\n\t\t\tconfig.ClientSecret = test.clientSecret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/manual/manual.go",
    "content": "package manual\n\nimport (\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n)\n\n// DNSProvider is an implementation of the ChallengeProvider interface.\ntype DNSProvider = dns01.DNSProviderManual\n\n// NewDNSProvider returns a DNSProvider instance.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\treturn &DNSProvider{}, nil\n}\n"
  },
  {
    "path": "providers/dns/manual/manual.toml",
    "content": "Name = \"Manual\"\nDescription = '''Solving the DNS-01 challenge using CLI prompt.'''\nCode = \"manual\"\nSince = \"v0.3.0\"\n\nExample = '''\nlego --dns manual -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Example\n\nTo start using the CLI prompt \"provider\", start lego with `--dns manual`:\n\n```console\n$ lego --dns manual -d example.com run\n```\n\nWhat follows are a few log print-outs, interspersed with some prompts, asking for you to do perform some actions:\n\n```txt\nNo key found for account you@example.com. Generating a P256 key.\nSaved key to ./.lego/accounts/acme-v02.api.letsencrypt.org/you@example.com/keys/you@example.com.key\nPlease review the TOS at https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf\nDo you accept the TOS? Y/n\n```\n\nIf you accept the linked Terms of Service, hit `Enter`.\n\n```txt\n[INFO] acme: Registering account for you@example.com\n!!!! HEADS UP !!!!\n\nYour account credentials have been saved in your\nconfiguration directory at \"./.lego/accounts\".\n\nYou should make a secure backup of this folder now. This\nconfiguration directory will also contain private keys\ngenerated by lego and certificates obtained from the ACME\nserver. Making regular backups of this folder is ideal.\n[INFO] [example.com] acme: Obtaining bundled SAN certificate\n[INFO] [example.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/2345678901\n[INFO] [example.com] acme: Could not find solver for: tls-alpn-01\n[INFO] [example.com] acme: Could not find solver for: http-01\n[INFO] [example.com] acme: use dns-01 solver\n[INFO] [example.com] acme: Preparing to solve DNS-01\nlego: Please create the following TXT record in your example.com. zone:\n_acme-challenge.example.com. 120 IN TXT \"hX0dPkG6Gfs9hUvBAchQclkyyoEKbShbpvJ9mY5q2JQ\"\nlego: Press 'Enter' when you are done\n```\n\nDo as instructed, and create the TXT records, and hit `Enter`.\n\n```txt\n[INFO] [example.com] acme: Trying to solve DNS-01\n[INFO] [example.com] acme: Checking DNS record propagation using [192.168.8.1:53]\n[INFO] Wait for propagation [timeout: 1m0s, interval: 2s]\n[INFO] [example.com] acme: Waiting for DNS record propagation.\n[INFO] [example.com] The server validated our request\n[INFO] [example.com] acme: Cleaning DNS-01 challenge\nlego: You can now remove this TXT record from your example.com. zone:\n_acme-challenge.example.com. 120 IN TXT \"hX0dPkG6Gfs9hUvBAchQclkyyoEKbShbpvJ9mY5q2JQ\"\n[INFO] [example.com] acme: Validations succeeded; requesting certificates\n[INFO] [example.com] Server responded with a certificate.\n```\n\nAs mentioned, you can now remove the TXT record again.\n\n'''\n"
  },
  {
    "path": "providers/dns/manual/manual_test.go",
    "content": "package manual\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDNSProviderManual(t *testing.T) {\n\tbackupStdin := os.Stdin\n\n\tdefer func() { os.Stdin = backupStdin }()\n\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tinput       string\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tdesc:  \"Press enter\",\n\t\t\tinput: \"ok\\n\",\n\t\t},\n\t\t{\n\t\t\tdesc:        \"Missing enter\",\n\t\t\tinput:       \"ok\",\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tfile, err := os.CreateTemp(t.TempDir(), \"lego_test\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tt.Cleanup(func() { _ = file.Close() })\n\n\t\t\t_, err = file.WriteString(test.input)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t_, err = file.Seek(0, io.SeekStart)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tos.Stdin = file\n\n\t\t\tmanualProvider, err := NewDNSProvider()\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = manualProvider.Present(\"example.com\", \"\", \"\")\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = manualProvider.CleanUp(\"example.com\", \"\", \"\")\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/metaname/metaname.go",
    "content": "// Package metaname implements a DNS provider for solving the DNS-01 challenge using Metaname.\npackage metaname\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/nzdjb/go-metaname\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"METANAME_\"\n\n\tEnvAccountReference = envNamespace + \"ACCOUNT_REFERENCE\"\n\tEnvAPIKey           = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAccountReference   string\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *metaname.MetanameClient\n\n\trecords   map[string]string\n\trecordsMu sync.Mutex\n}\n\n// NewDNSProvider returns a new DNS provider\n// using environment variable METANAME_API_KEY for adding and removing the DNS record.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAccountReference, EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"metaname: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AccountReference = values[EnvAccountReference]\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Metaname.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"metaname: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.AccountReference == \"\" {\n\t\treturn nil, errors.New(\"metaname: missing account reference\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"metaname: missing api key\")\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:  config,\n\t\tclient:  metaname.NewMetanameClient(config.AccountReference, config.APIKey),\n\t\trecords: make(map[string]string),\n\t}, nil\n}\n\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"metaname: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"metaname: could not extract subDomain: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\tr := metaname.ResourceRecord{\n\t\tName: subDomain,\n\t\tType: \"TXT\",\n\t\tAux:  nil,\n\t\tTtl:  d.config.TTL,\n\t\tData: info.Value,\n\t}\n\n\tref, err := d.client.CreateDnsRecord(ctx, authZone, r)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"metaname: add record: %w\", err)\n\t}\n\n\td.recordsMu.Lock()\n\td.records[token] = ref\n\td.recordsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"metaname: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tctx := context.Background()\n\n\td.recordsMu.Lock()\n\tref, ok := d.records[token]\n\td.recordsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"metaname: unknown ref for %s\", info.EffectiveFQDN)\n\t}\n\n\terr = d.client.DeleteDnsRecord(ctx, authZone, ref)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"metaname: delete record: %w\", err)\n\t}\n\n\td.recordsMu.Lock()\n\tdelete(d.records, token)\n\td.recordsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/metaname/metaname.toml",
    "content": "Name = \"Metaname\"\nDescription = ''''''\nURL = \"https://metaname.net\"\nCode = \"metaname\"\nSince = \"v4.13.0\"\n\nExample = '''\nMETANAME_ACCOUNT_REFERENCE=xxxx \\\nMETANAME_API_KEY=yyyyyyy \\\nlego --dns metaname -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    METANAME_ACCOUNT_REFERENCE = \"The four-digit reference of a Metaname account\"\n    METANAME_API_KEY = \"API Key\"\n  [Configuration.Additional]\n    METANAME_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    METANAME_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    METANAME_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n\n[Links]\n  API = \"https://metaname.net/api/1.1/doc\"\n  GoClient = \"https://github.com/nzdjb/go-metaname\"\n"
  },
  {
    "path": "providers/dns/metaname/metaname_test.go",
    "content": "package metaname\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAccountReference, EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccountReference: \"user\",\n\t\t\t\tEnvAPIKey:           \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccountReference: \"\",\n\t\t\t\tEnvAPIKey:           \"secret\",\n\t\t\t},\n\t\t\texpected: \"metaname: some credentials information are missing: METANAME_ACCOUNT_REFERENCE\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccountReference: \"user\",\n\t\t\t\tEnvAPIKey:           \"\",\n\t\t\t},\n\t\t\texpected: \"metaname: some credentials information are missing: METANAME_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"metaname: some credentials information are missing: METANAME_ACCOUNT_REFERENCE,METANAME_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc             string\n\t\taccountReference string\n\t\tapiKey           string\n\t\texpected         string\n\t}{\n\t\t{\n\t\t\tdesc:             \"success\",\n\t\t\taccountReference: \"user\",\n\t\t\tapiKey:           \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tapiKey:   \"secret\",\n\t\t\texpected: \"metaname: missing account reference\",\n\t\t},\n\t\t{\n\t\t\tdesc:             \"missing password\",\n\t\t\taccountReference: \"user\",\n\t\t\texpected:         \"metaname: missing api key\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing all\",\n\t\t\texpected: \"metaname: missing account reference\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\n\t\t\tconfig.AccountReference = test.accountReference\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/metaregistrar/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api.metaregistrar.com\"\n\nconst tokenHeader = \"token\"\n\n// Client is a client to interact with the Metaregistrar API.\ntype Client struct {\n\ttoken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(token string) (*Client, error) {\n\tif token == \"\" {\n\t\treturn nil, errors.New(\"token missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\ttoken:      token,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// UpdateDNSZone updates the DNS zone for a domain.\n// To add or remove a TXT record we make a PATCH request.\n// https://metaregistrar.dev/docu/metaapi/requests/patch_Update_dns_zone.html\nfunc (c *Client) UpdateDNSZone(ctx context.Context, domain string, updateRequest DNSZoneUpdateRequest) (*DNSZoneUpdateResponse, error) {\n\tendpoint := c.baseURL.JoinPath(\"dnszone\", domain)\n\n\treq, err := newJSONRequest(ctx, http.MethodPatch, endpoint, updateRequest)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &DNSZoneUpdateResponse{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Add(tokenHeader, c.token)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/metaregistrar/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(tokenHeader, \"secret\"))\n}\n\nfunc TestClient_UpdateDNSZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PATCH /dnszone/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"update-dns-zone.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"add\":[{\"name\":\"@\",\"type\":\"TXT\",\"ttl\":60,\"content\":\"value\"}]}`)).\n\t\tBuild(t)\n\n\tupdateRequest := DNSZoneUpdateRequest{\n\t\tAdd: []Record{{\n\t\t\tName:    \"@\",\n\t\t\tType:    \"TXT\",\n\t\t\tTTL:     60,\n\t\t\tContent: \"value\",\n\t\t}},\n\t}\n\n\tresponse, err := client.UpdateDNSZone(t.Context(), \"example.com\", updateRequest)\n\trequire.NoError(t, err)\n\n\texpected := &DNSZoneUpdateResponse{\n\t\tResponseID: \"mapi1_cb46ad8790b62b76535bd3102bd282aec83b894c\",\n\t\tStatus:     \"ok\",\n\t\tMessage:    \"Command completed successfully\",\n\t}\n\n\tassert.Equal(t, expected, response)\n}\n\nfunc TestClient_UpdateDNSZone_error(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tfilename string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"authentication error\",\n\t\t\tfilename: \"error.json\",\n\t\t\texpected: \"invalid_token: the supplied token is invalid\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"API error\",\n\t\t\tfilename: \"error-response.json\",\n\t\t\texpected: \"error: does_not_exist: This server does not exist\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(\"PATCH /dnszone/example.com\",\n\t\t\t\t\tservermock.ResponseFromFixture(test.filename).\n\t\t\t\t\t\tWithStatusCode(http.StatusUnprocessableEntity)).\n\t\t\t\tBuild(t)\n\n\t\t\tupdateRequest := DNSZoneUpdateRequest{\n\t\t\t\tAdd: []Record{{\n\t\t\t\t\tName:    \"@\",\n\t\t\t\t\tType:    \"TXT\",\n\t\t\t\t\tTTL:     60,\n\t\t\t\t\tContent: \"value\",\n\t\t\t\t}},\n\t\t\t}\n\n\t\t\t_, err := client.UpdateDNSZone(t.Context(), \"example.com\", updateRequest)\n\t\t\trequire.EqualError(t, err, test.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/metaregistrar/internal/fixtures/error-response.json",
    "content": "{\n  \"responseId\": \"1_0a407cb0634a56374ba80f863fda53ae37fd0042\",\n  \"status\": \"error\",\n  \"errorCode\": \"does_not_exist\",\n  \"errorMessage\": \"This server does not exist\"\n}\n"
  },
  {
    "path": "providers/dns/metaregistrar/internal/fixtures/error.json",
    "content": "{\n    \"error\": \"invalid_token\",\n    \"message\": \"the supplied token is invalid\"\n}"
  },
  {
    "path": "providers/dns/metaregistrar/internal/fixtures/update-dns-zone.json",
    "content": "{\n  \"responseId\": \"mapi1_cb46ad8790b62b76535bd3102bd282aec83b894c\",\n  \"status\": \"ok\",\n  \"message\": \"Command completed successfully\"\n}\n"
  },
  {
    "path": "providers/dns/metaregistrar/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"strings\"\n)\n\n// APIError It's a mix of documented and undocumented fields.\n// Note: the documentation is inconsistent: the names of property are not the same as the JSON sample.\n// https://metaregistrar.dev/docu/metaapi/requests/response_ErrorResponse.html\ntype APIError struct {\n\tResponseID   string `json:\"responseId,omitempty\"`\n\tStatus       string `json:\"status,omitempty\"`\n\tMessage      string `json:\"message,omitempty\"`\n\tErr          string `json:\"error,omitempty\"`\n\tErrorCode    string `json:\"errorCode,omitempty\"`\n\tErrorMessage string `json:\"errorMessage,omitempty\"`\n}\n\nfunc (e *APIError) Error() string {\n\tvar msg []string\n\n\tif e.Status != \"\" {\n\t\tmsg = append(msg, e.Status)\n\t}\n\n\tif e.Err != \"\" {\n\t\tmsg = append(msg, e.Err)\n\t}\n\n\tif e.ErrorCode != \"\" {\n\t\tmsg = append(msg, e.ErrorCode)\n\t}\n\n\tif e.Message != \"\" {\n\t\tmsg = append(msg, e.Message)\n\t}\n\n\tif e.ErrorMessage != \"\" {\n\t\tmsg = append(msg, e.ErrorMessage)\n\t}\n\n\treturn strings.Join(msg, \": \")\n}\n\ntype Record struct {\n\tName     string `json:\"name,omitempty\"`\n\tType     string `json:\"type,omitempty\"`\n\tTTL      int    `json:\"ttl,omitempty\"`\n\tContent  string `json:\"content,omitempty\"`\n\tPriority int    `json:\"priority,omitempty\"`\n\tDisabled bool   `json:\"disabled,omitempty\"`\n}\n\n// DNSZoneUpdateRequest is the representation of DnszoneUpdateRequest object.\n// https://metaregistrar.dev/docu/metaapi/requests/request_DnszoneUpdateRequest.html\ntype DNSZoneUpdateRequest struct {\n\tAdd    []Record `json:\"add,omitempty\"`\n\tRemove []Record `json:\"rem,omitempty\"`\n}\n\n// DNSZoneUpdateResponse is the representation of DnszoneUpdateResponse object.\n// https://metaregistrar.dev/docu/metaapi/requests/response_DnszoneUpdateResponse.html\ntype DNSZoneUpdateResponse struct {\n\tResponseID string `json:\"responseId,omitempty\"`\n\tStatus     string `json:\"status,omitempty\"`\n\tMessage    string `json:\"message,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/metaregistrar/metaregistrar.go",
    "content": "// Package metaregistrar implements a DNS provider for solving the DNS-01 challenge using Metaregistrar.\npackage metaregistrar\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/metaregistrar/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"METAREGISTRAR_\"\n\n\tEnvToken = envNamespace + \"API_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Metaregistrar.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"metaregistrar: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIToken = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Metaregistrar.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"metaregistrar: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.APIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"metaregistrar: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"metaregistrar: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tupdateRequest := internal.DNSZoneUpdateRequest{\n\t\tAdd: []internal.Record{{\n\t\t\tName:    dns01.UnFqdn(info.EffectiveFQDN),\n\t\t\tType:    \"TXT\",\n\t\t\tTTL:     d.config.TTL,\n\t\t\tContent: info.Value,\n\t\t}},\n\t}\n\n\t_, err = d.client.UpdateDNSZone(context.Background(), dns01.UnFqdn(authZone), updateRequest)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"metaregistrar: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"metaregistrar: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tupdateRequest := internal.DNSZoneUpdateRequest{\n\t\tRemove: []internal.Record{{\n\t\t\tName:    dns01.UnFqdn(info.EffectiveFQDN),\n\t\t\tType:    \"TXT\",\n\t\t\tTTL:     d.config.TTL,\n\t\t\tContent: strconv.Quote(info.Value),\n\t\t}},\n\t}\n\n\t_, err = d.client.UpdateDNSZone(context.Background(), dns01.UnFqdn(authZone), updateRequest)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"metaregistrar: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/metaregistrar/metaregistrar.toml",
    "content": "Name = \"Metaregistrar\"\nDescription = ''''''\nURL = \"https://metaregistrar.com/\"\nCode = \"metaregistrar\"\nSince = \"v4.23.0\"\n\nExample = '''\nMETAREGISTRAR_API_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns metaregistrar -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    METAREGISTRAR_API_TOKEN = \"The API token\"\n  [Configuration.Additional]\n    METAREGISTRAR_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    METAREGISTRAR_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    METAREGISTRAR_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    METAREGISTRAR_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://metaregistrar.dev/docu/metaapi/\"\n"
  },
  {
    "path": "providers/dns/metaregistrar/metaregistrar_test.go",
    "content": "package metaregistrar\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"token\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"metaregistrar: some credentials information are missing: METAREGISTRAR_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiToken string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiToken: \"token\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"metaregistrar: token missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = test.apiToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/mijnhost/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://mijn.host/api/v2/\"\n\nconst authorizationHeader = \"API-Key\"\n\n// Client a mijn.host DNS API client.\ntype Client struct {\n\tapiKey string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiKey string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}\n}\n\n// ListDomains Retrieve all domains from an account.\n// https://mijn.host/api/doc/api-3563872\nfunc (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {\n\tendpoint := c.baseURL.JoinPath(\"domains\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tvar results Response[DomainData]\n\n\terr = c.do(req, &results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn results.Data.Domains, nil\n}\n\n// GetRecords Retrieve DNS records of specific domain.\n// https://mijn.host/api/doc/api-3563906\nfunc (c *Client) GetRecords(ctx context.Context, domain string) ([]Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"domains\", domain, \"dns\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tvar results Response[RecordData]\n\n\terr = c.do(req, &results)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn results.Data.Records, nil\n}\n\n// UpdateRecords Update DNS records of specific domain.\n// https://mijn.host/api/doc/api-3563907\nfunc (c *Client) UpdateRecords(ctx context.Context, domain string, records []Record) error {\n\tendpoint := c.baseURL.JoinPath(\"domains\", domain, \"dns\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, RecordData{Records: records})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\terr = c.do(req, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(authorizationHeader, c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/mijnhost/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst apiKey = \"secret\"\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(apiKey)\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(authorizationHeader, apiKey),\n\t)\n}\n\nfunc TestClient_ListDomains(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains\", servermock.ResponseFromFixture(\"list-domains.json\")).\n\t\tBuild(t)\n\n\tdomains, err := client.ListDomains(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []Domain{{\n\t\tID:          1000,\n\t\tDomain:      \"example.com\",\n\t\tRenewalDate: \"2030-01-01\",\n\t\tStatus:      \"Active\",\n\t\tStatusID:    1,\n\t\tTags:        []string{\"my-tag\"},\n\t}}\n\n\tassert.Equal(t, expected, domains)\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains/example.com/dns\", servermock.ResponseFromFixture(\"get-dns-records.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tType:  \"A\",\n\t\t\tName:  \"example.com.\",\n\t\t\tValue: \"135.226.123.12\",\n\t\t\tTTL:   900,\n\t\t},\n\t\t{\n\t\t\tType:  \"AAAA\",\n\t\t\tName:  \"example.com.\",\n\t\t\tValue: \"2009:21d0:322:6100::5:c92b\",\n\t\t\tTTL:   900,\n\t\t},\n\t\t{\n\t\t\tType:  \"MX\",\n\t\t\tName:  \"example.com.\",\n\t\t\tValue: \"10 mail.example.com.\",\n\t\t\tTTL:   900,\n\t\t},\n\t\t{\n\t\t\tType:  \"TXT\",\n\t\t\tName:  \"example.com.\",\n\t\t\tValue: \"v=spf1 include:spf.mijn.host ~all\",\n\t\t\tTTL:   900,\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_UpdateRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /domains/example.com/dns\",\n\t\t\tservermock.ResponseFromFixture(\"update-dns-records.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"records\":[{\"type\":\"TXT\",\"name\":\"foo\",\"value\":\"value1\",\"ttl\":120}]}`)).\n\t\tBuild(t)\n\n\trecords := []Record{{\n\t\tType:  \"TXT\",\n\t\tName:  \"foo\",\n\t\tValue: \"value1\",\n\t\tTTL:   120,\n\t}}\n\n\terr := client.UpdateRecords(t.Context(), \"example.com\", records)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/mijnhost/internal/fixtures/error.json",
    "content": "{\n  \"status\": 400,\n  \"status_description\": \"Wrong request method\"\n}\n"
  },
  {
    "path": "providers/dns/mijnhost/internal/fixtures/get-dns-records.json",
    "content": "{\n  \"status\": 200,\n  \"status_description\": \"Request successful\",\n  \"data\": {\n    \"domain\": \"example.com\",\n    \"records\": [\n      {\n        \"type\": \"A\",\n        \"name\": \"example.com.\",\n        \"value\": \"135.226.123.12\",\n        \"ttl\": 900\n      },\n      {\n        \"type\": \"AAAA\",\n        \"name\": \"example.com.\",\n        \"value\": \"2009:21d0:322:6100::5:c92b\",\n        \"ttl\": 900\n      },\n      {\n        \"type\": \"MX\",\n        \"name\": \"example.com.\",\n        \"value\": \"10 mail.example.com.\",\n        \"ttl\": 900\n      },\n      {\n        \"type\": \"TXT\",\n        \"name\": \"example.com.\",\n        \"value\": \"v=spf1 include:spf.mijn.host ~all\",\n        \"ttl\": 900\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/mijnhost/internal/fixtures/list-domains.json",
    "content": "{\n  \"status\": 200,\n  \"status_description\": \"Request successful\",\n  \"data\": {\n    \"domains\": [\n      {\n        \"id\": 1000,\n        \"domain\": \"example.com\",\n        \"renewal_date\": \"2030-01-01\",\n        \"status\": \"Active\",\n        \"status_id\": 1,\n        \"tags\": [\n          \"my-tag\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/mijnhost/internal/fixtures/update-dns-records.json",
    "content": "{\n  \"status\": 200,\n  \"status_description\": \"DNS successfully updated\"\n}\n"
  },
  {
    "path": "providers/dns/mijnhost/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype APIError struct {\n\tStatus            int    `json:\"status,omitempty\"`\n\tStatusDescription string `json:\"status_description,omitempty\"`\n}\n\nfunc (e APIError) Error() string {\n\treturn fmt.Sprintf(\"%d: %s\", e.Status, e.StatusDescription)\n}\n\ntype Response[T any] struct {\n\tStatus            int    `json:\"status,omitempty\"`\n\tStatusDescription string `json:\"status_description,omitempty\"`\n\tData              T      `json:\"data,omitempty\"`\n}\n\ntype RecordData struct {\n\tDomain  string   `json:\"domain,omitempty\"`\n\tRecords []Record `json:\"records,omitempty\"`\n}\n\ntype Record struct {\n\tType  string `json:\"type,omitempty\"`\n\tName  string `json:\"name,omitempty\"`\n\tValue string `json:\"value,omitempty\"`\n\tTTL   int    `json:\"ttl,omitempty\"`\n}\n\ntype DomainData struct {\n\tDomains []Domain `json:\"domains\"`\n}\n\ntype Domain struct {\n\tID          int      `json:\"id\"`\n\tDomain      string   `json:\"domain\"`\n\tRenewalDate string   `json:\"renewal_date\"`\n\tStatus      string   `json:\"status\"`\n\tStatusID    int      `json:\"status_id\"`\n\tTags        []string `json:\"tags\"`\n}\n"
  },
  {
    "path": "providers/dns/mijnhost/mijnhost.go",
    "content": "// Package mijnhost implements a DNS provider for solving the DNS-01 challenge using mijn.host DNS.\npackage mijnhost\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/mijnhost/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"MIJNHOST_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst txtType = \"TXT\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, 5*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for mijn.host DNS.\n// MIJNHOST_API_KEY must be passed in the environment variables.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mijnhost: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for mijn.host DNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"mijnhost: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"mijnhost: APIKey is missing\")\n\t}\n\n\tclient := internal.NewClient(config.APIKey)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tdomains, err := d.client.ListDomains(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mijnhost: list domains: %w\", err)\n\t}\n\n\tdom, err := findDomain(domains, domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mijnhost: find domain: %w\", err)\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, dom.Domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mijnhost: get records: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, dom.Domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mijnhost: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tType:  txtType,\n\t\tName:  subDomain,\n\t\tValue: info.Value,\n\t\tTTL:   d.config.TTL,\n\t}\n\n\t// mijn.host doesn't support multiple values for a domain,\n\t// so we removed existing record for the subdomain.\n\tcleanedRecords := filterRecords(records, func(record internal.Record) bool {\n\t\treturn record.Type == txtType && (record.Name == subDomain || record.Name == dns01.UnFqdn(info.EffectiveFQDN))\n\t})\n\n\tcleanedRecords = append(cleanedRecords, record)\n\n\terr = d.client.UpdateRecords(ctx, dom.Domain, cleanedRecords)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mijnhost: update records: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tdomains, err := d.client.ListDomains(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mijnhost: list domains: %w\", err)\n\t}\n\n\tdom, err := findDomain(domains, domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mijnhost: find domain: %w\", err)\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, dom.Domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mijnhost: get records: %w\", err)\n\t}\n\n\tcleanedRecords := filterRecords(records, func(record internal.Record) bool {\n\t\treturn record.Type == txtType && record.Value == info.Value\n\t})\n\n\terr = d.client.UpdateRecords(ctx, dom.Domain, cleanedRecords)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mijnhost: update records: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc findDomain(domains []internal.Domain, fqdn string) (internal.Domain, error) {\n\tfor domain := range dns01.UnFqdnDomainsSeq(fqdn) {\n\t\tfor _, dom := range domains {\n\t\t\tif dom.Domain == domain {\n\t\t\t\treturn dom, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn internal.Domain{}, fmt.Errorf(\"domain %s not found\", fqdn)\n}\n\nfunc filterRecords(records []internal.Record, fn func(record internal.Record) bool) []internal.Record {\n\tvar newRecords []internal.Record\n\n\tfor _, record := range records {\n\t\tif record.Type == \"TXT\" && fn(record) {\n\t\t\tcontinue\n\t\t}\n\n\t\tnewRecords = append(newRecords, record)\n\t}\n\n\treturn newRecords\n}\n"
  },
  {
    "path": "providers/dns/mijnhost/mijnhost.toml",
    "content": "Name = \"mijn.host\"\nDescription = ''''''\nURL = \"https://mijn.host/\"\nCode = \"mijnhost\"\nSince = \"v4.18.0\"\n\nExample = '''\nMIJNHOST_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns mijnhost -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    MIJNHOST_API_KEY = \"The API key\"\n  [Configuration.Additional]\n    MIJNHOST_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    MIJNHOST_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    MIJNHOST_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n    MIJNHOST_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    MIJNHOST_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://mijn.host/api/doc/\"\n"
  },
  {
    "path": "providers/dns/mijnhost/mijnhost_test.go",
    "content": "package mijnhost\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"key\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"mijnhost: some credentials information are missing: MIJNHOST_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tttl      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"key\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\texpected: \"mijnhost: APIKey is missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.TTL = test.ttl\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/mittwald/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api.mittwald.de/v2/\"\n\nconst authorizationHeader = \"Authorization\"\n\n// Client the Mittwald client.\ntype Client struct {\n\ttoken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(token string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\ttoken:      token,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// ListDomains List Domains.\n// https://api.mittwald.de/v2/docs/#/Domain/domain-list-domains\nfunc (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {\n\tendpoint := c.baseURL.JoinPath(\"domains\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result []Domain\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// GetDNSZone Get a DNSZone.\n// https://api.mittwald.de/v2/docs/#/Domain/dns-get-dns-zone\nfunc (c *Client) GetDNSZone(ctx context.Context, zoneID string) (*DNSZone, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns-zones\", zoneID)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &DNSZone{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// ListDNSZones List DNSZones belonging to a Project.\n// https://api.mittwald.de/v2/docs/#/Domain/dns-list-dns-zones\nfunc (c *Client) ListDNSZones(ctx context.Context, projectID string) ([]DNSZone, error) {\n\tendpoint := c.baseURL.JoinPath(\"projects\", projectID, \"dns-zones\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result []DNSZone\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// CreateDNSZone Create a DNSZone.\n// https://api.mittwald.de/v2/docs/#/Domain/dns-create-dns-zone\nfunc (c *Client) CreateDNSZone(ctx context.Context, zone CreateDNSZoneRequest) (*DNSZone, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns-zones\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, zone)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &DNSZone{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\n// UpdateTXTRecord Update a record set on a DNSZone.\n// https://api.mittwald.de/v2/docs/#/Domain/dns-update-record-set\nfunc (c *Client) UpdateTXTRecord(ctx context.Context, zoneID string, record TXTRecord) error {\n\tendpoint := c.baseURL.JoinPath(\"dns-zones\", zoneID, \"record-sets\", \"txt\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\n// DeleteDNSZone Delete a DNSZone.\n// https://api.mittwald.de/v2/docs/#/Domain/dns-delete-dns-zone\nfunc (c *Client) DeleteDNSZone(ctx context.Context, zoneID string) error {\n\tendpoint := c.baseURL.JoinPath(\"dns-zones\", zoneID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(authorizationHeader, \"Bearer \"+c.token)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar response APIError\n\n\terr := json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code %d] %w\", resp.StatusCode, response)\n}\n"
  },
  {
    "path": "providers/dns/mittwald/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer secret\"),\n\t)\n}\n\nfunc TestClient_ListDomains(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains\", servermock.ResponseFromFixture(\"domain-list-domains.json\")).\n\t\tBuild(t)\n\n\tdomains, err := client.ListDomains(t.Context())\n\trequire.NoError(t, err)\n\n\trequire.Len(t, domains, 1)\n\n\texpected := []Domain{{\n\t\tDomain:    \"string\",\n\t\tDomainID:  \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n\t\tProjectID: \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n\t}}\n\n\tassert.Equal(t, expected, domains)\n}\n\nfunc TestClient_ListDomains_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains\",\n\t\t\tservermock.ResponseFromFixture(\"error-client.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\t_, err := client.ListDomains(t.Context())\n\trequire.EqualError(t, err, \"[status code 400] ValidationError: Validation failed [format: should be string (.address.street, email)]\")\n}\n\nfunc TestClient_ListDNSZones(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /projects/my-project-id/dns-zones\", servermock.ResponseFromFixture(\"dns-list-dns-zones.json\")).\n\t\tBuild(t)\n\n\tzones, err := client.ListDNSZones(t.Context(), \"my-project-id\")\n\trequire.NoError(t, err)\n\n\trequire.Len(t, zones, 1)\n\n\texpected := []DNSZone{{\n\t\tID:     \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n\t\tDomain: \"string\",\n\t\tRecordSet: &RecordSet{\n\t\t\tTXT: &TXTRecord{},\n\t\t},\n\t}}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_GetDNSZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns-zones/my-zone-id\", servermock.ResponseFromFixture(\"dns-get-dns-zone.json\")).\n\t\tBuild(t)\n\n\tzone, err := client.GetDNSZone(t.Context(), \"my-zone-id\")\n\trequire.NoError(t, err)\n\n\texpected := &DNSZone{\n\t\tID:     \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n\t\tDomain: \"string\",\n\t\tRecordSet: &RecordSet{\n\t\t\tTXT: &TXTRecord{},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zone)\n}\n\nfunc TestClient_CreateDNSZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns-zones\",\n\t\t\tservermock.ResponseFromFixture(\"dns-create-dns-zone.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"name\":\"test\",\"parentZoneId\":\"my-parent-zone-id\"}`)).\n\t\tBuild(t)\n\n\trequest := CreateDNSZoneRequest{\n\t\tName:         \"test\",\n\t\tParentZoneID: \"my-parent-zone-id\",\n\t}\n\n\tzone, err := client.CreateDNSZone(t.Context(), request)\n\trequire.NoError(t, err)\n\n\texpected := &DNSZone{\n\t\tID: \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n\t}\n\n\tassert.Equal(t, expected, zone)\n}\n\nfunc TestClient_UpdateTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /dns-zones/my-zone-id/record-sets/txt\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent),\n\t\t\tservermock.CheckRequestJSONBody(`{\"settings\":{\"ttl\":{\"auto\":true}},\"entries\":[\"txt\"]}`)).\n\t\tBuild(t)\n\n\trecord := TXTRecord{\n\t\tSettings: Settings{\n\t\t\tTTL: TTL{Auto: true},\n\t\t},\n\t\tEntries: []string{\"txt\"},\n\t}\n\n\terr := client.UpdateTXTRecord(t.Context(), \"my-zone-id\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteDNSZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns-zones/my-zone-id\",\n\t\t\tservermock.Noop()).\n\t\tBuild(t)\n\n\terr := client.DeleteDNSZone(t.Context(), \"my-zone-id\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteDNSZone_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns-zones/my-zone-id\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusInternalServerError)).\n\t\tBuild(t)\n\n\terr := client.DeleteDNSZone(t.Context(), \"my-zone-id\")\n\tassert.EqualError(t, err, \"[status code 500] InternalServerError: Something went wrong\")\n}\n"
  },
  {
    "path": "providers/dns/mittwald/internal/fixtures/dns-create-dns-zone.json",
    "content": "{\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n}\n"
  },
  {
    "path": "providers/dns/mittwald/internal/fixtures/dns-get-dns-zone.json",
    "content": "{\n  \"domain\": \"string\",\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n  \"recordSet\": {\n    \"cname\": {},\n    \"combinedARecords\": {},\n    \"mx\": {},\n    \"srv\": {},\n    \"txt\": {}\n  }\n}\n"
  },
  {
    "path": "providers/dns/mittwald/internal/fixtures/dns-list-dns-zones.json",
    "content": "[\n  {\n    \"domain\": \"string\",\n    \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n    \"recordSet\": {\n      \"cname\": {},\n      \"combinedARecords\": {},\n      \"mx\": {},\n      \"srv\": {},\n      \"txt\": {}\n    }\n  }\n]\n"
  },
  {
    "path": "providers/dns/mittwald/internal/fixtures/domain-list-domains.json",
    "content": "[\n  {\n    \"authCode\": {\n      \"expires\": \"2024-06-04T15:11:59.964Z\",\n      \"value\": \"string\"\n    },\n    \"authCode2\": {\n      \"expires\": \"2024-06-04T15:11:59.964Z\"\n    },\n    \"connected\": true,\n    \"deleted\": true,\n    \"domain\": \"string\",\n    \"domainId\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n    \"handles\": {\n      \"adminC\": {\n        \"current\": {\n          \"handleFields\": [\n            {\n              \"name\": \"string\",\n              \"value\": \"jnoFDyCBDHC&70Zp&2JMErZBq(),fnsYIvn_bOed5e_.vmsrZ3-IH )Ms)Xc13KDWy2WMH((mJ.-uY_NEBu/3MO8)3\"\n            }\n          ],\n          \"handleRef\": \"string\"\n        },\n        \"desired\": {\n          \"handleFields\": [\n            {\n              \"name\": \"string\",\n              \"value\": \"1odACmUIyjG Xa-uEX7R+f4,ykqpZ71FFLzkl8B87/+I@s0bVMxA\"\n            }\n          ],\n          \"handleRef\": \"string\"\n        }\n      },\n      \"ownerC\": {\n        \"current\": {\n          \"handleFields\": [\n            {\n              \"name\": \"string\",\n              \"value\": \"oklq/PU.yBrSFq) .Qx_Uqb8NBZnwA(9jk@x4w Dp6lLd&+a-A.oG5sHw(jcRSOyv0\"\n            }\n          ],\n          \"handleRef\": \"string\"\n        },\n        \"desired\": {\n          \"handleFields\": [\n            {\n              \"name\": \"string\",\n              \"value\": \"iwt.q,vygqXwZ0_HK+j3kuw/,A,Z)L1Jg&fNgIxWdBc1xnGj(pjj8YQX1DG 9M1/_Vaam,\"\n            }\n          ],\n          \"handleRef\": \"string\"\n        }\n      }\n    },\n    \"nameservers\": [\n      \"string\"\n    ],\n    \"processes\": [\n      {\n        \"error\": \"string\",\n        \"lastUpdate\": \"2024-06-04T15:11:59.973Z\",\n        \"processType\": \"UNSPECIFIED\",\n        \"state\": \"UNSPECIFIED\",\n        \"status\": \"string\",\n        \"statusCode\": \"string\",\n        \"transactionId\": \"string\"\n      }\n    ],\n    \"projectId\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n    \"transferInAuthCode\": \"string\",\n    \"usesDefaultNameserver\": true\n  }\n]\n"
  },
  {
    "path": "providers/dns/mittwald/internal/fixtures/error-client.json",
    "content": "{\n  \"type\": \"ValidationError\",\n  \"message\": \"Validation failed\",\n  \"validationErrors\": [\n    {\n      \"message\": \"should be string\",\n      \"path\": \".address.street\",\n      \"type\": \"format\",\n      \"context\": {\n        \"format\": \"email\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/mittwald/internal/fixtures/error.json",
    "content": "{\n  \"message\": \"Something went wrong\",\n  \"type\": \"InternalServerError\"\n}\n"
  },
  {
    "path": "providers/dns/mittwald/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// https://api.mittwald.de/v2/docs/#/Domain/domain-list-domains\n\ntype Domain struct {\n\tDomain    string `json:\"domain,omitempty\"`\n\tDomainID  string `json:\"domainId,omitempty\"`\n\tProjectID string `json:\"projectId,omitempty\"`\n}\n\n// https://api.mittwald.de/v2/docs/#/Domain/dns-list-dns-zones\n\ntype DNSZone struct {\n\tID        string     `json:\"id,omitempty\"`\n\tDomain    string     `json:\"domain,omitempty\"`\n\tRecordSet *RecordSet `json:\"recordSet,omitempty\"`\n}\n\ntype RecordSet struct {\n\tTXT *TXTRecord `json:\"txt\"`\n}\n\n// https://api.mittwald.de/v2/docs/#/Domain/dns-create-dns-zone\n\ntype CreateDNSZoneRequest struct {\n\tName         string `json:\"name,omitempty\"`\n\tParentZoneID string `json:\"parentZoneId,omitempty\"`\n}\n\ntype NewDNSZone struct {\n\tID string `json:\"id\"`\n}\n\n// https://api.mittwald.de/v2/docs/#/Domain/dns-update-record-set\n\ntype TXTRecord struct {\n\tSettings Settings `json:\"settings\"`\n\tEntries  []string `json:\"entries,omitempty\"`\n}\n\ntype Settings struct {\n\tTTL TTL `json:\"ttl\"`\n}\n\ntype TTL struct {\n\tSeconds int  `json:\"seconds,omitempty\"`\n\tAuto    bool `json:\"auto,omitempty\"`\n}\n\n// Error\n\ntype APIError struct {\n\tType             string            `json:\"type,omitempty\"`\n\tMessage          string            `json:\"message,omitempty\"`\n\tValidationErrors []ValidationError `json:\"validationErrors,omitempty\"`\n}\n\nfunc (a APIError) Error() string {\n\tmsg := new(strings.Builder)\n\n\t_, _ = fmt.Fprintf(msg, \"%s: %s\", a.Type, a.Message)\n\n\tif len(a.ValidationErrors) > 0 {\n\t\tfor _, validationError := range a.ValidationErrors {\n\t\t\t_, _ = fmt.Fprintf(msg, \" [%s: %s (%s, %s)]\",\n\t\t\t\tvalidationError.Type, validationError.Message, validationError.Path, validationError.Context.Format)\n\t\t}\n\t}\n\n\treturn msg.String()\n}\n\ntype ValidationError struct {\n\tMessage string                 `json:\"message,omitempty\"`\n\tPath    string                 `json:\"path,omitempty\"`\n\tType    string                 `json:\"type,omitempty\"`\n\tContext ValidationErrorContext `json:\"context\"`\n}\n\ntype ValidationErrorContext struct {\n\tFormat string `json:\"format,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/mittwald/mittwald.go",
    "content": "// Package mittwald implements a DNS provider for solving the DNS-01 challenge using Mittwald.\npackage mittwald\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/mittwald/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"MITTWALD_\"\n\n\tEnvToken = envNamespace + \"TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst minTTL = 300\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken              string\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, 2*time.Minute),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\tzoneIDs   map[string]string\n\tzoneIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Mittwald.\n// Credentials must be passed in the environment variables: MITTWALD_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mittwald: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Mittwald.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"mittwald: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"mittwald: some credentials information are missing\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"mittwald: invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\tclient := internal.NewClient(config.Token)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:  config,\n\t\tclient:  client,\n\t\tzoneIDs: map[string]string{},\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getOrCreateZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mittwald: get effective zone: %w\", err)\n\t}\n\n\trecord := internal.TXTRecord{\n\t\tSettings: internal.Settings{\n\t\t\tTTL: internal.TTL{Seconds: d.config.TTL},\n\t\t},\n\t\tEntries: []string{info.Value},\n\t}\n\n\terr = d.client.UpdateTXTRecord(ctx, zone.ID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mittwald: update/add TXT record: %w\", err)\n\t}\n\n\td.zoneIDsMu.Lock()\n\td.zoneIDs[token] = zone.ID\n\td.zoneIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// get the record's unique ID from when we created it\n\td.zoneIDsMu.Lock()\n\tzoneID, ok := d.zoneIDs[token]\n\td.zoneIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"mittwald: unknown zone ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\trecord := internal.TXTRecord{Entries: make([]string, 0)}\n\n\terr := d.client.UpdateTXTRecord(ctx, zoneID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mittwald: update/delete TXT record: %w\", err)\n\t}\n\n\td.zoneIDsMu.Lock()\n\tdelete(d.zoneIDs, token)\n\td.zoneIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) getOrCreateZone(ctx context.Context, fqdn string) (*internal.DNSZone, error) {\n\tdomains, err := d.client.ListDomains(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"list domains: %w\", err)\n\t}\n\n\tdom, err := findDomain(domains, fqdn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"find domain: %w\", err)\n\t}\n\n\tzones, err := d.client.ListDNSZones(ctx, dom.ProjectID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"list DNS zones: %w\", err)\n\t}\n\n\tfor _, zone := range zones {\n\t\tif zone.Domain == dns01.UnFqdn(fqdn) {\n\t\t\treturn &zone, nil\n\t\t}\n\t}\n\n\t// Looking for parent zone to create a new zone for the subdomain.\n\n\tparentZone, err := findZone(zones, fqdn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"find zone: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, parentZone.Domain)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trequest := internal.CreateDNSZoneRequest{\n\t\tName:         subDomain,\n\t\tParentZoneID: parentZone.ID,\n\t}\n\n\tzone, err := d.client.CreateDNSZone(ctx, request)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create DNS zone: %w\", err)\n\t}\n\n\treturn zone, nil\n}\n\nfunc findDomain(domains []internal.Domain, fqdn string) (internal.Domain, error) {\n\tfor domain := range dns01.UnFqdnDomainsSeq(fqdn) {\n\t\tfor _, dom := range domains {\n\t\t\tif dom.Domain == domain {\n\t\t\t\treturn dom, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn internal.Domain{}, fmt.Errorf(\"domain %s not found\", fqdn)\n}\n\nfunc findZone(zones []internal.DNSZone, fqdn string) (internal.DNSZone, error) {\n\tfor domain := range dns01.UnFqdnDomainsSeq(fqdn) {\n\t\tfor _, zon := range zones {\n\t\t\tif zon.Domain == domain {\n\t\t\t\treturn zon, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn internal.DNSZone{}, fmt.Errorf(\"zone %s not found\", fqdn)\n}\n"
  },
  {
    "path": "providers/dns/mittwald/mittwald.toml",
    "content": "Name = \"Mittwald\"\nDescription = ''''''\nURL = \"https://www.mittwald.de/\"\nCode = \"mittwald\"\nSince = \"v1.48.0\"\n\nExample = '''\nMITTWALD_TOKEN=my-token \\\nlego --dns mittwald -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    MITTWALD_TOKEN = \"API token\"\n  [Configuration.Additional]\n    MITTWALD_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    MITTWALD_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    MITTWALD_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    MITTWALD_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 120)\"\n    MITTWALD_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api.mittwald.de/v2/docs/\"\n"
  },
  {
    "path": "providers/dns/mittwald/mittwald_test.go",
    "content": "package mittwald\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/providers/dns/mittwald/internal\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"\",\n\t\t\t},\n\t\t\texpected: \"mittwald: some credentials information are missing: MITTWALD_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, p)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\tttl      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"mittwald: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"invalid TTL\",\n\t\t\ttoken:    \"secret\",\n\t\t\tttl:      10,\n\t\t\texpected: \"mittwald: invalid TTL, TTL (10) must be greater than 300\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.token\n\n\t\t\tif test.ttl > 0 {\n\t\t\t\tconfig.TTL = test.ttl\n\t\t\t}\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, p)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc Test_findDomain(t *testing.T) {\n\tdomains := []internal.Domain{\n\t\t{\n\t\t\tDomain:    \"example.com\",\n\t\t\tProjectID: \"a1\",\n\t\t},\n\t\t{\n\t\t\tDomain:    \"foo.example.com\",\n\t\t\tProjectID: \"a2\",\n\t\t},\n\t\t{\n\t\t\tDomain:    \"example.org\",\n\t\t\tProjectID: \"b1\",\n\t\t},\n\t\t{\n\t\t\tDomain:    \"foo.example.org\",\n\t\t\tProjectID: \"b2\",\n\t\t},\n\t\t{\n\t\t\tDomain:    \"test.example.org\",\n\t\t\tProjectID: \"b3\",\n\t\t},\n\t}\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tfqdn     string\n\t\texpected internal.Domain\n\t}{\n\t\t{\n\t\t\tdesc:     \"exact match\",\n\t\t\tfqdn:     \"example.org.\",\n\t\t\texpected: internal.Domain{Domain: \"example.org\", ProjectID: \"b1\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"1 level parent\",\n\t\t\tfqdn:     \"_acme-challenge.test.example.org.\",\n\t\t\texpected: internal.Domain{Domain: \"test.example.org\", ProjectID: \"b3\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"2 levels parent\",\n\t\t\tfqdn:     \"_acme-challenge.test.example.com.\",\n\t\t\texpected: internal.Domain{Domain: \"example.com\", ProjectID: \"a1\"},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tdomain, err := findDomain(domains, test.fqdn)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.expected, domain)\n\t\t})\n\t}\n}\n\nfunc Test_findZone(t *testing.T) {\n\tzones := []internal.DNSZone{\n\t\t{\n\t\t\tDomain: \"example.com\",\n\t\t\tID:     \"a1\",\n\t\t},\n\t\t{\n\t\t\tDomain: \"foo.example.com\",\n\t\t\tID:     \"a2\",\n\t\t},\n\t\t{\n\t\t\tDomain: \"example.org\",\n\t\t\tID:     \"b1\",\n\t\t},\n\t\t{\n\t\t\tDomain: \"foo.example.org\",\n\t\t\tID:     \"b2\",\n\t\t},\n\t\t{\n\t\t\tDomain: \"test.example.org\",\n\t\t\tID:     \"b3\",\n\t\t},\n\t}\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tfqdn     string\n\t\texpected internal.DNSZone\n\t}{\n\t\t{\n\t\t\tdesc:     \"exact match\",\n\t\t\tfqdn:     \"example.org.\",\n\t\t\texpected: internal.DNSZone{Domain: \"example.org\", ID: \"b1\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"1 level parent\",\n\t\t\tfqdn:     \"_acme-challenge.test.example.org.\",\n\t\t\texpected: internal.DNSZone{Domain: \"test.example.org\", ID: \"b3\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"2 levels parent\",\n\t\t\tfqdn:     \"_acme-challenge.test.example.com.\",\n\t\t\texpected: internal.DNSZone{Domain: \"example.com\", ID: \"a1\"},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tzone, err := findZone(zones, test.fqdn)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.expected, zone)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/myaddr/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://myaddr.tools\"\n\n// Client the myaddr.{tools,dev,io} API client.\ntype Client struct {\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n\n\tcredentials map[string]string\n\tcredMu      sync.Mutex\n}\n\n// NewClient creates a new Client.\nfunc NewClient(credentials map[string]string) (*Client, error) {\n\tif len(credentials) == 0 {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tbaseURL:     baseURL,\n\t\tHTTPClient:  &http.Client{Timeout: 10 * time.Second},\n\t\tcredentials: credentials,\n\t}, nil\n}\n\nfunc (c *Client) AddTXTRecord(ctx context.Context, subdomain, value string) error {\n\tc.credMu.Lock()\n\tprivateKey, ok := c.credentials[subdomain]\n\tc.credMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"subdomain %s not found in credentials, check your credentials map\", subdomain)\n\t}\n\n\tpayload := ACMEChallenge{Key: privateKey, Data: value}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, c.baseURL.JoinPath(\"update\"), payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\traw, _ := io.ReadAll(resp.Body)\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/myaddr/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tcredentials := map[string]string{\n\t\t\t\t\"example\": \"secret\",\n\t\t\t}\n\n\t\t\tclient, err := NewClient(credentials)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t)\n}\n\nfunc TestClient_AddTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /update\", nil,\n\t\t\tservermock.CheckRequestJSONBody(`{\"key\":\"secret\",\"acme_challenge\":\"txt\"}`)).\n\t\tBuild(t)\n\n\terr := client.AddTXTRecord(t.Context(), \"example\", \"txt\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddTXTRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /update\",\n\t\t\tservermock.ResponseFromFixture(\"error.txt\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\terr := client.AddTXTRecord(t.Context(), \"example\", \"txt\")\n\trequire.EqualError(t, err, `unexpected status code: [status code: 400] body: invalid value for \"key\"`)\n}\n\nfunc TestClient_AddTXTRecord_error_credentials(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /update\", nil).\n\t\tBuild(t)\n\n\terr := client.AddTXTRecord(t.Context(), \"nx\", \"txt\")\n\trequire.EqualError(t, err, \"subdomain nx not found in credentials, check your credentials map\")\n}\n"
  },
  {
    "path": "providers/dns/myaddr/internal/fixtures/error.txt",
    "content": "invalid value for \"key\"\n"
  },
  {
    "path": "providers/dns/myaddr/internal/types.go",
    "content": "package internal\n\ntype ACMEChallenge struct {\n\tKey  string `json:\"key\"`\n\tData string `json:\"acme_challenge\"`\n}\n"
  },
  {
    "path": "providers/dns/myaddr/myaddr.go",
    "content": "// Package myaddr implements a DNS provider for solving the DNS-01 challenge using myaddr.{tools,dev,io}.\npackage myaddr\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/myaddr/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"MYADDR_\"\n\n\tEnvPrivateKeysMapping = envNamespace + \"PRIVATE_KEYS_MAPPING\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tCredentials map[string]string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for myaddr.{tools,dev,io}.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvPrivateKeysMapping)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"myaddr: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\n\tcredentials, err := env.ParsePairs(values[EnvPrivateKeysMapping])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"myaddr: %w\", err)\n\t}\n\n\tconfig.Credentials = credentials\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for myaddr.{tools,dev,io}.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"myaddr: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.Credentials)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"myaddr: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"myaddr: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tfullSubdomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"myaddr: %w\", err)\n\t}\n\n\t_, after, found := strings.Cut(fullSubdomain, \".\")\n\tif !found {\n\t\treturn fmt.Errorf(\"myaddr: subdomain not found in: %q (%s)\", fullSubdomain, info.EffectiveFQDN)\n\t}\n\n\terr = d.client.AddTXTRecord(context.Background(), after, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"myaddr: add TXT record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\t// There is no API endpoint to delete a TXT record:\n\t// TXT records are automatically removed after a few minutes.\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n"
  },
  {
    "path": "providers/dns/myaddr/myaddr.toml",
    "content": "Name = \"myaddr.{tools,dev,io}\"\nDescription = ''''''\nURL = \"https://myaddr.tools/\"\nCode = \"myaddr\"\nSince = \"v4.22.0\"\n\nExample = '''\nMYADDR_PRIVATE_KEYS_MAPPING=\"example:123,test:456\" \\\nlego --dns myaddr -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    MYADDR_PRIVATE_KEYS_MAPPING = \"Mapping between subdomains and private keys. The format is: `<subdomain1>:<private_key1>,<subdomain2>:<private_key2>,<subdomain3>:<private_key3>`\"\n  [Configuration.Additional]\n    MYADDR_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    MYADDR_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    MYADDR_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 2)\"\n    MYADDR_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    MYADDR_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://myaddr.tools/\"\n"
  },
  {
    "path": "providers/dns/myaddr/myaddr_test.go",
    "content": "package myaddr\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvPrivateKeysMapping).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPrivateKeysMapping: \"example:123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"myaddr: some credentials information are missing: MYADDR_PRIVATE_KEYS_MAPPING\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tcredentials map[string]string\n\t\texpected    string\n\t}{\n\t\t{\n\t\t\tdesc:        \"success\",\n\t\t\tcredentials: map[string]string{\"example\": \"123\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"myaddr: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Credentials = test.credentials\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/mydnsjp/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://www.mydns.jp/directedit.html\"\n\n// Client the MyDNS.jp client.\ntype Client struct {\n\tmasterID string\n\tpassword string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(masterID, password string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tmasterID:   masterID,\n\t\tpassword:   password,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\nfunc (c *Client) AddTXTRecord(ctx context.Context, domain, value string) error {\n\treturn c.doRequest(ctx, domain, value, \"REGIST\")\n}\n\nfunc (c *Client) DeleteTXTRecord(ctx context.Context, domain, value string) error {\n\treturn c.doRequest(ctx, domain, value, \"DELETE\")\n}\n\nfunc (c *Client) buildRequest(ctx context.Context, domain, value, cmd string) (*http.Request, error) {\n\tparams := url.Values{}\n\tparams.Set(\"CERTBOT_DOMAIN\", domain)\n\tparams.Set(\"CERTBOT_VALIDATION\", value)\n\tparams.Set(\"EDIT_CMD\", cmd)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL.String(), strings.NewReader(params.Encode()))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\treturn req, nil\n}\n\nfunc (c *Client) doRequest(ctx context.Context, domain, value, cmd string) error {\n\treq, err := c.buildRequest(ctx, domain, value, cmd)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.SetBasicAuth(c.masterID, c.password)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/mydnsjp/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"xxx\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded().\n\t\t\tWithBasicAuth(\"xxx\", \"secret\"))\n}\n\nfunc TestClient_AddTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", nil,\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"CERTBOT_DOMAIN\", \"example.com\").\n\t\t\t\tWith(\"CERTBOT_VALIDATION\", \"txt\").\n\t\t\t\tWith(\"EDIT_CMD\", \"REGIST\")).\n\t\tBuild(t)\n\n\terr := client.AddTXTRecord(t.Context(), \"example.com\", \"txt\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", nil,\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"CERTBOT_DOMAIN\", \"example.com\").\n\t\t\t\tWith(\"CERTBOT_VALIDATION\", \"txt\").\n\t\t\t\tWith(\"EDIT_CMD\", \"DELETE\")).\n\t\tBuild(t)\n\n\terr := client.DeleteTXTRecord(t.Context(), \"example.com\", \"txt\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/mydnsjp/mydnsjp.go",
    "content": "// Package mydnsjp implements a DNS provider for solving the DNS-01 challenge using MyDNS.jp.\npackage mydnsjp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/mydnsjp/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"MYDNSJP_\"\n\n\tEnvMasterID = envNamespace + \"MASTER_ID\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tMasterID           string\n\tPassword           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for MyDNS.jp.\n// Credentials must be passed in the environment variables: MYDNSJP_MASTER_ID and MYDNSJP_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvMasterID, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mydnsjp: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.MasterID = values[EnvMasterID]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for MyDNS.jp.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"mydnsjp: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.MasterID == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"mydnsjp: some credentials information are missing\")\n\t}\n\n\tclient := internal.NewClient(config.MasterID, config.Password)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\terr := d.client.AddTXTRecord(context.Background(), domain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mydnsjp: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\terr := d.client.DeleteTXTRecord(context.Background(), domain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mydnsjp: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/mydnsjp/mydnsjp.toml",
    "content": "Name = \"MyDNS.jp\"\nDescription = ''''''\nURL = \"https://www.mydns.jp\"\nCode = \"mydnsjp\"\nSince = \"v1.2.0\"\n\nExample = '''\nMYDNSJP_MASTER_ID=xxxxx \\\nMYDNSJP_PASSWORD=xxxxx \\\nlego --dns mydnsjp -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    MYDNSJP_MASTER_ID = \"Master ID\"\n    MYDNSJP_PASSWORD = \"Password\"\n  [Configuration.Additional]\n    MYDNSJP_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    MYDNSJP_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    MYDNSJP_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.mydns.jp/?MENU=030\"\n"
  },
  {
    "path": "providers/dns/mydnsjp/mydnsjp_test.go",
    "content": "package mydnsjp\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvMasterID, EnvPassword).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvMasterID: \"test@example.com\",\n\t\t\t\tEnvPassword: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvMasterID: \"\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"mydnsjp: some credentials information are missing: MYDNSJP_MASTER_ID,MYDNSJP_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing email\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvMasterID: \"\",\n\t\t\t\tEnvPassword: \"key\",\n\t\t\t},\n\t\t\texpected: \"mydnsjp: some credentials information are missing: MYDNSJP_MASTER_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvMasterID: \"awesome@possum.com\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"mydnsjp: some credentials information are missing: MYDNSJP_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, p)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tmasterID string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tmasterID: \"test@example.com\",\n\t\t\tpassword: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"mydnsjp: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing email\",\n\t\t\tpassword: \"123\",\n\t\t\texpected: \"mydnsjp: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api key\",\n\t\t\tmasterID: \"test@example.com\",\n\t\t\texpected: \"mydnsjp: some credentials information are missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.MasterID = test.masterID\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, p)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/mythicbeasts/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// Default API endpoints.\nconst (\n\tAPIBaseURL  = \"https://api.mythic-beasts.com/dns/v2\"\n\tAuthBaseURL = \"https://auth.mythic-beasts.com/login\"\n)\n\n// Client the Mythic Beasts API client.\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tAPIEndpoint  *url.URL\n\tAuthEndpoint *url.URL\n\tHTTPClient   *http.Client\n\n\ttoken   *Token\n\tmuToken sync.Mutex\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(username, password string) *Client {\n\tapiEndpoint, _ := url.Parse(APIBaseURL)\n\tauthEndpoint, _ := url.Parse(AuthBaseURL)\n\n\treturn &Client{\n\t\tusername:     username,\n\t\tpassword:     password,\n\t\tAPIEndpoint:  apiEndpoint,\n\t\tAuthEndpoint: authEndpoint,\n\t\tHTTPClient:   &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// CreateTXTRecord creates a TXT record.\n// https://www.mythic-beasts.com/support/api/dnsv2#ep-get-zoneszonerecords\nfunc (c *Client) CreateTXTRecord(ctx context.Context, zone, leaf, value string, ttl int) error {\n\tresp, err := c.createTXTRecord(ctx, zone, leaf, \"TXT\", value, ttl)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif resp.Added != 1 {\n\t\treturn fmt.Errorf(\"did not add TXT record for some reason: %s\", resp.Message)\n\t}\n\n\t// Success\n\treturn nil\n}\n\n// RemoveTXTRecord removes a TXT records.\n// https://www.mythic-beasts.com/support/api/dnsv2#ep-delete-zoneszonerecords\nfunc (c *Client) RemoveTXTRecord(ctx context.Context, zone, leaf, value string) error {\n\tresp, err := c.removeTXTRecord(ctx, zone, leaf, \"TXT\", value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif resp.Removed != 1 {\n\t\treturn fmt.Errorf(\"did not remove TXT record for some reason: %s\", resp.Message)\n\t}\n\n\t// Success\n\treturn nil\n}\n\n// https://www.mythic-beasts.com/support/api/dnsv2#ep-post-zoneszonerecords\nfunc (c *Client) createTXTRecord(ctx context.Context, zone, leaf, recordType, value string, ttl int) (*createTXTResponse, error) {\n\tendpoint := c.APIEndpoint.JoinPath(\"zones\", zone, \"records\", leaf, recordType)\n\n\tcreateReq := createTXTRequest{\n\t\tRecords: []createTXTRecord{{\n\t\t\tHost: leaf,\n\t\t\tTTL:  ttl,\n\t\t\tType: \"TXT\",\n\t\t\tData: value,\n\t\t}},\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, createReq)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp := &createTXTResponse{}\n\n\terr = c.do(req, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn resp, nil\n}\n\n// https://www.mythic-beasts.com/support/api/dnsv2#ep-delete-zoneszonerecords\nfunc (c *Client) removeTXTRecord(ctx context.Context, zone, leaf, recordType, value string) (*deleteTXTResponse, error) {\n\tendpoint := c.APIEndpoint.JoinPath(\"zones\", zone, \"records\", leaf, recordType)\n\n\tquery := endpoint.Query()\n\tquery.Add(\"data\", value)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp := &deleteTXTResponse{}\n\n\terr = c.do(req, resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn resp, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\ttok := getToken(req.Context())\n\tif tok != nil {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+tok.Token)\n\t} else {\n\t\treturn errors.New(\"not logged in\")\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/mythicbeasts/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.APIEndpoint, _ = url.Parse(server.URL)\n\t\t\tclient.token = &Token{\n\t\t\t\tToken:     \"secret\",\n\t\t\t\tLifetime:  60,\n\t\t\t\tTokenType: \"bearer\",\n\t\t\t\tDeadline:  time.Now().Add(1 * time.Minute),\n\t\t\t}\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer \"+fakeToken),\n\t)\n}\n\nfunc TestClient_CreateTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zones/example.com/records/foo/TXT\",\n\t\t\tservermock.ResponseFromFixture(\"post-zoneszonerecords.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"records\":[{\"host\":\"foo\",\"ttl\":120,\"type\":\"TXT\",\"data\":\"txt\"}]}`)).\n\t\tBuild(t)\n\n\terr := client.CreateTXTRecord(mockContext(t), \"example.com\", \"foo\", \"txt\", 120)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_RemoveTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /zones/example.com/records/foo/TXT\",\n\t\t\tservermock.ResponseFromFixture(\"delete-zoneszonerecords.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"data\", \"txt\")).\n\t\tBuild(t)\n\n\terr := client.RemoveTXTRecord(mockContext(t), \"example.com\", \"foo\", \"txt\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/mythicbeasts/internal/fixtures/delete-zoneszonerecords.json",
    "content": "{\n  \"records_removed\": 1,\n  \"message\": \"1 record removed\"\n}\n"
  },
  {
    "path": "providers/dns/mythicbeasts/internal/fixtures/post-zoneszonerecords.json",
    "content": "{\n  \"records_added\": 1,\n  \"message\": \"1 record added\"\n}\n"
  },
  {
    "path": "providers/dns/mythicbeasts/internal/fixtures/token.json",
    "content": "{\n  \"access_token\": \"xxx\",\n  \"expires_in\": 666,\n  \"token_type\": \"bearer\"\n}\n"
  },
  {
    "path": "providers/dns/mythicbeasts/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\ntype token string\n\nconst tokenKey token = \"token\"\n\n// obtainToken Logs into mythic beasts and acquires a bearer token for use in future API calls.\n// https://www.mythic-beasts.com/support/api/auth#sec-obtaining-a-token\nfunc (c *Client) obtainToken(ctx context.Context) (*Token, error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.AuthEndpoint.String(), strings.NewReader(\"grant_type=client_credentials\"))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.SetBasicAuth(c.username, c.password)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, parseError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\ttok := Token{}\n\n\terr = json.Unmarshal(raw, &tok)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tif tok.TokenType != \"bearer\" {\n\t\treturn nil, fmt.Errorf(\"received unexpected token type: %s\", tok.TokenType)\n\t}\n\n\ttok.Deadline = time.Now().Add(time.Duration(tok.Lifetime) * time.Second)\n\n\treturn &tok, nil\n}\n\nfunc (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) {\n\tc.muToken.Lock()\n\tdefer c.muToken.Unlock()\n\n\tif c.token != nil && time.Now().Before(c.token.Deadline) {\n\t\t// Already authenticated, stop now\n\t\treturn context.WithValue(ctx, tokenKey, c.token), nil\n\t}\n\n\ttok, err := c.obtainToken(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn context.WithValue(ctx, tokenKey, tok), nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\tif resp.StatusCode < 400 || resp.StatusCode > 499 {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, _ := io.ReadAll(resp.Body)\n\n\terrResp := &authResponseError{}\n\n\terr := json.Unmarshal(raw, errResp)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"%d: %w\", resp.StatusCode, errResp)\n}\n\nfunc getToken(ctx context.Context) *Token {\n\ttok, ok := ctx.Value(tokenKey).(*Token)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\treturn tok\n}\n"
  },
  {
    "path": "providers/dns/mythicbeasts/internal/identity_test.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst fakeToken = \"xxx\"\n\nfunc mockContext(t *testing.T) context.Context {\n\tt.Helper()\n\n\treturn context.WithValue(t.Context(), tokenKey, &Token{Token: fakeToken})\n}\n\nfunc mockBuilderIdentity() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.AuthEndpoint, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithBasicAuth(\"user\", \"secret\"),\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded())\n}\n\nfunc TestClient_obtainToken(t *testing.T) {\n\tclient := mockBuilderIdentity().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"token.json\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"grant_type\", \"client_credentials\")).\n\t\tBuild(t)\n\n\tassert.Nil(t, client.token)\n\n\ttok, err := client.obtainToken(t.Context())\n\trequire.NoError(t, err)\n\n\tassert.NotNil(t, tok)\n\tassert.NotZero(t, tok.Deadline)\n\tassert.Equal(t, fakeToken, tok.Token)\n}\n\nfunc TestClient_CreateAuthenticatedContext(t *testing.T) {\n\tclient := mockBuilderIdentity().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"token.json\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"grant_type\", \"client_credentials\")).\n\t\tBuild(t)\n\n\tassert.Nil(t, client.token)\n\n\tctx, err := client.CreateAuthenticatedContext(t.Context())\n\trequire.NoError(t, err)\n\n\ttok := getToken(ctx)\n\n\tassert.NotNil(t, tok)\n\tassert.NotZero(t, tok.Deadline)\n\tassert.Equal(t, fakeToken, tok.Token)\n}\n"
  },
  {
    "path": "providers/dns/mythicbeasts/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\ntype Token struct {\n\t// The bearer token for use in API requests\n\tToken string `json:\"access_token\"`\n\n\t// The maximum lifetime of the token in seconds\n\tLifetime int `json:\"expires_in\"`\n\n\t// The token type (must be 'bearer')\n\tTokenType string `json:\"token_type\"`\n\n\tDeadline time.Time `json:\"-\"`\n}\n\ntype authResponseError struct {\n\tErrorMsg         string `json:\"error\"`\n\tErrorDescription string `json:\"error_description\"`\n}\n\nfunc (a authResponseError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", a.ErrorMsg, a.ErrorDescription)\n}\n\ntype createTXTRequest struct {\n\tRecords []createTXTRecord `json:\"records\"`\n}\n\ntype createTXTRecord struct {\n\tHost string `json:\"host\"`\n\tTTL  int    `json:\"ttl\"`\n\tType string `json:\"type\"`\n\tData string `json:\"data\"`\n}\n\ntype createTXTResponse struct {\n\tAdded   int    `json:\"records_added\"`\n\tRemoved int    `json:\"records_removed\"`\n\tMessage string `json:\"message\"`\n}\n\ntype deleteTXTResponse struct {\n\tRemoved int    `json:\"records_removed\"`\n\tMessage string `json:\"message\"`\n}\n"
  },
  {
    "path": "providers/dns/mythicbeasts/mythicbeasts.go",
    "content": "// Package mythicbeasts implements a DNS provider for solving the DNS-01 challenge using Mythic Beasts API.\npackage mythicbeasts\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/mythicbeasts/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"MYTHICBEASTS_\"\n\n\tEnvUserName        = envNamespace + \"USERNAME\"\n\tEnvPassword        = envNamespace + \"PASSWORD\"\n\tEnvAPIEndpoint     = envNamespace + \"API_ENDPOINT\"\n\tEnvAuthAPIEndpoint = envNamespace + \"AUTH_API_ENDPOINT\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUserName           string\n\tPassword           string\n\tHTTPClient         *http.Client\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tAPIEndpoint        *url.URL\n\tAuthAPIEndpoint    *url.URL\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() (*Config, error) {\n\tapiEndpoint, err := url.Parse(env.GetOrDefaultString(EnvAPIEndpoint, internal.APIBaseURL))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mythicbeasts: Unable to parse API URL: %w\", err)\n\t}\n\n\tauthEndpoint, err := url.Parse(env.GetOrDefaultString(EnvAuthAPIEndpoint, internal.AuthBaseURL))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mythicbeasts: Unable to parse AUTH API URL: %w\", err)\n\t}\n\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tAPIEndpoint:        apiEndpoint,\n\t\tAuthAPIEndpoint:    authEndpoint,\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}, nil\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for mythicbeasts DNSv2 API.\n// Credentials must be passed in the environment variables:\n// MYTHICBEASTS_USERNAME and MYTHICBEASTS_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUserName, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mythicbeasts: %w\", err)\n\t}\n\n\tconfig, err := NewDefaultConfig()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mythicbeasts: %w\", err)\n\t}\n\n\tconfig.UserName = values[EnvUserName]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for mythicbeasts DNSv2 API.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"mythicbeasts: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.UserName == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"mythicbeasts: incomplete credentials, missing username and/or password\")\n\t}\n\n\tclient := internal.NewClient(config.UserName, config.Password)\n\n\tif config.APIEndpoint != nil {\n\t\tclient.APIEndpoint = config.APIEndpoint\n\t}\n\n\tif config.AuthAPIEndpoint != nil {\n\t\tclient.AuthEndpoint = config.AuthAPIEndpoint\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mythicbeasts: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mythicbeasts: %w\", err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mythicbeasts: login: %w\", err)\n\t}\n\n\terr = d.client.CreateTXTRecord(ctx, authZone, subDomain, info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mythicbeasts: CreateTXTRecord: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mythicbeasts: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mythicbeasts: %w\", err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tctx, err := d.client.CreateAuthenticatedContext(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mythicbeasts: login: %w\", err)\n\t}\n\n\terr = d.client.RemoveTXTRecord(ctx, authZone, subDomain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"mythicbeasts: RemoveTXTRecord: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/mythicbeasts/mythicbeasts.toml",
    "content": "Name = \"MythicBeasts\"\nDescription = ''''''\nURL = \"https://www.mythic-beasts.com/\"\nCode = \"mythicbeasts\"\nSince = \"v0.3.7\"\n\nExample = '''\nMYTHICBEASTS_USERNAME=myuser \\\nMYTHICBEASTS_PASSWORD=mypass \\\nlego --dns mythicbeasts -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\nIf you are using specific API keys, then the username is the API ID for your API key, and the password is the API secret.\n\nYour API key name is not needed to operate lego.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    MYTHICBEASTS_USERNAME = \"User name\"\n    MYTHICBEASTS_PASSWORD = \"Password\"\n  [Configuration.Additional]\n    MYTHICBEASTS_API_ENDPOINT = \"The endpoint for the API (must implement v2)\"\n    MYTHICBEASTS_AUTH_API_ENDPOINT = \"The endpoint for Mythic Beasts' Authentication\"\n    MYTHICBEASTS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    MYTHICBEASTS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    MYTHICBEASTS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    MYTHICBEASTS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://www.mythic-beasts.com/support/api/dnsv2\"\n  APIAuth = \"https://auth.mythic-beasts.com/login\"\n"
  },
  {
    "path": "providers/dns/mythicbeasts/mythicbeasts_test.go",
    "content": "package mythicbeasts\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvUserName,\n\tEnvPassword).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUserName: \"123\",\n\t\t\t\tEnvPassword: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUserName: \"\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"mythicbeasts: some credentials information are missing: MYTHICBEASTS_USERNAME,MYTHICBEASTS_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUserName: \"\",\n\t\t\t\tEnvPassword: \"api_password\",\n\t\t\t},\n\t\t\texpected: \"mythicbeasts: some credentials information are missing: MYTHICBEASTS_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUserName: \"api_username\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"mythicbeasts: some credentials information are missing: MYTHICBEASTS_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"api_username\",\n\t\t\tpassword: \"api_password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"mythicbeasts: incomplete credentials, missing username and/or password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tusername: \"\",\n\t\t\tpassword: \"api_password\",\n\t\t\texpected: \"mythicbeasts: incomplete credentials, missing username and/or password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tusername: \"api_username\",\n\t\t\tpassword: \"\",\n\t\t\texpected: \"mythicbeasts: incomplete credentials, missing username and/or password\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig, err := NewDefaultConfig()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tconfig.UserName = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/namecheap/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// Default API endpoints.\nconst (\n\tDefaultBaseURL = \"https://api.namecheap.com/xml.response\"\n\tSandboxBaseURL = \"https://api.sandbox.namecheap.com/xml.response\"\n)\n\n// Client the API client for Namecheap.\ntype Client struct {\n\tapiUser  string\n\tapiKey   string\n\tclientIP string\n\n\tBaseURL    string\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiUser, apiKey, clientIP string) *Client {\n\treturn &Client{\n\t\tapiUser:    apiUser,\n\t\tapiKey:     apiKey,\n\t\tclientIP:   clientIP,\n\t\tBaseURL:    DefaultBaseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// GetHosts reads the full list of DNS host records.\n// https://www.namecheap.com/support/api/methods/domains-dns/get-hosts.aspx\nfunc (c *Client) GetHosts(ctx context.Context, sld, tld string) ([]Record, error) {\n\trequest, err := c.newRequestGet(ctx, \"namecheap.domains.dns.getHosts\",\n\t\taddParam(\"SLD\", sld),\n\t\taddParam(\"TLD\", tld),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar ghr getHostsResponse\n\n\terr = c.do(request, &ghr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(ghr.Errors) > 0 {\n\t\treturn nil, ghr.Errors[0]\n\t}\n\n\treturn ghr.Hosts, nil\n}\n\n// SetHosts writes the full list of DNS host records .\n// https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx\nfunc (c *Client) SetHosts(ctx context.Context, sld, tld string, hosts []Record) error {\n\treq, err := c.newRequestPost(ctx, \"namecheap.domains.dns.setHosts\",\n\t\taddParam(\"SLD\", sld),\n\t\taddParam(\"TLD\", tld),\n\t\tfunc(values url.Values) {\n\t\t\tfor i, h := range hosts {\n\t\t\t\tind := strconv.Itoa(i + 1)\n\t\t\t\tvalues.Add(\"HostName\"+ind, h.Name)\n\t\t\t\tvalues.Add(\"RecordType\"+ind, h.Type)\n\t\t\t\tvalues.Add(\"Address\"+ind, h.Address)\n\t\t\t\tvalues.Add(\"MXPref\"+ind, h.MXPref)\n\t\t\t\tvalues.Add(\"TTL\"+ind, h.TTL)\n\t\t\t}\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar shr setHostsResponse\n\n\terr = c.do(req, &shr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(shr.Errors) > 0 {\n\t\treturn shr.Errors[0]\n\t}\n\n\tif shr.Result.IsSuccess != \"true\" {\n\t\treturn errors.New(\"setHosts failed\")\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode >= http.StatusBadRequest {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\treturn xml.Unmarshal(raw, result)\n}\n\nfunc (c *Client) newRequestGet(ctx context.Context, cmd string, params ...func(url.Values)) (*http.Request, error) {\n\tquery := c.makeQuery(cmd, params...)\n\n\tendpoint, err := url.Parse(c.BaseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treturn req, nil\n}\n\nfunc (c *Client) newRequestPost(ctx context.Context, cmd string, params ...func(url.Values)) (*http.Request, error) {\n\tquery := c.makeQuery(cmd, params...)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL, strings.NewReader(query.Encode()))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\treturn req, nil\n}\n\nfunc (c *Client) makeQuery(cmd string, params ...func(url.Values)) url.Values {\n\tqueryParams := make(url.Values)\n\tqueryParams.Set(\"ApiUser\", c.apiUser)\n\tqueryParams.Set(\"ApiKey\", c.apiKey)\n\tqueryParams.Set(\"UserName\", c.apiUser)\n\tqueryParams.Set(\"Command\", cmd)\n\tqueryParams.Set(\"ClientIp\", c.clientIP)\n\n\tfor _, param := range params {\n\t\tparam(queryParams)\n\t}\n\n\treturn queryParams\n}\n\nfunc addParam(key, value string) func(url.Values) {\n\treturn func(values url.Values) {\n\t\tvalues.Set(key, value)\n\t}\n}\n"
  },
  {
    "path": "providers/dns/namecheap/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"user\", \"secret\", \"127.0.0.1\")\n\tclient.HTTPClient = server.Client()\n\tclient.BaseURL = server.URL\n\n\treturn client, nil\n}\n\nfunc TestClient_GetHosts(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /\",\n\t\t\tservermock.ResponseFromFixture(\"getHosts.xml\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"ApiKey\", \"secret\").\n\t\t\t\tWith(\"ApiUser\", \"user\").\n\t\t\t\tWith(\"ClientIp\", \"127.0.0.1\").\n\t\t\t\tWith(\"Command\", \"namecheap.domains.dns.getHosts\").\n\t\t\t\tWith(\"SLD\", \"foo\").\n\t\t\t\tWith(\"TLD\", \"example.com\").\n\t\t\t\tWith(\"UserName\", \"user\"),\n\t\t).\n\t\tBuild(t)\n\n\thosts, err := client.GetHosts(t.Context(), \"foo\", \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{Type: \"A\", Name: \"@\", Address: \"1.2.3.4\", MXPref: \"10\", TTL: \"1800\"},\n\t\t{Type: \"A\", Name: \"www\", Address: \"122.23.3.7\", MXPref: \"10\", TTL: \"1800\"},\n\t}\n\n\tassert.Equal(t, expected, hosts)\n}\n\nfunc TestClient_GetHosts_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /\",\n\t\t\tservermock.ResponseFromFixture(\"getHosts_errorBadAPIKey1.xml\")).\n\t\tBuild(t)\n\n\t_, err := client.GetHosts(t.Context(), \"foo\", \"example.com\")\n\trequire.ErrorAs(t, err, &apiError{})\n}\n\nfunc TestClient_SetHosts(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithContentTypeFromURLEncoded()).\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"setHosts.xml\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"ApiKey\", \"secret\").\n\t\t\t\tWith(\"ApiUser\", \"user\").\n\t\t\t\tWith(\"ClientIp\", \"127.0.0.1\").\n\t\t\t\tWith(\"Command\", \"namecheap.domains.dns.setHosts\").\n\t\t\t\tWith(\"SLD\", \"foo\").\n\t\t\t\tWith(\"TLD\", \"example.com\").\n\t\t\t\tWith(\"UserName\", \"user\").\n\t\t\t\t// entry 1\n\t\t\t\tWith(\"HostName1\", \"_acme-challenge.test.example.com\").\n\t\t\t\tWith(\"RecordType1\", \"TXT\").\n\t\t\t\tWith(\"Address1\", \"txtTXTtxt\").\n\t\t\t\tWith(\"MXPref1\", \"10\").\n\t\t\t\tWith(\"TTL1\", \"120\").\n\t\t\t\t// entry 2\n\t\t\t\tWith(\"HostName2\", \"_acme-challenge.test.example.org\").\n\t\t\t\tWith(\"RecordType2\", \"TXT\").\n\t\t\t\tWith(\"Address2\", \"txtTXTtxt\").\n\t\t\t\tWith(\"MXPref2\", \"10\").\n\t\t\t\tWith(\"TTL2\", \"120\"),\n\t\t).\n\t\tBuild(t)\n\n\trecords := []Record{\n\t\t{Name: \"_acme-challenge.test.example.com\", Type: \"TXT\", Address: \"txtTXTtxt\", MXPref: \"10\", TTL: \"120\"},\n\t\t{Name: \"_acme-challenge.test.example.org\", Type: \"TXT\", Address: \"txtTXTtxt\", MXPref: \"10\", TTL: \"120\"},\n\t}\n\n\terr := client.SetHosts(t.Context(), \"foo\", \"example.com\", records)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_SetHosts_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"setHosts_errorBadAPIKey1.xml\")).\n\t\tBuild(t)\n\n\trecords := []Record{\n\t\t{Name: \"_acme-challenge.test.example.com\", Type: \"TXT\", Address: \"txtTXTtxt\", MXPref: \"10\", TTL: \"120\"},\n\t\t{Name: \"_acme-challenge.test.example.org\", Type: \"TXT\", Address: \"txtTXTtxt\", MXPref: \"10\", TTL: \"120\"},\n\t}\n\n\terr := client.SetHosts(t.Context(), \"foo\", \"example.com\", records)\n\trequire.ErrorAs(t, err, &apiError{})\n}\n"
  },
  {
    "path": "providers/dns/namecheap/internal/fixtures/getHosts.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ApiResponse xmlns=\"http://api.namecheap.com/xml.response\" Status=\"OK\">\n    <Errors />\n    <RequestedCommand>namecheap.domains.dns.getHosts</RequestedCommand>\n    <CommandResponse Type=\"namecheap.domains.dns.getHosts\">\n        <DomainDNSGetHostsResult Domain=\"domain.com\" IsUsingOurDNS=\"true\">\n            <host HostId=\"12\" Name=\"@\" Type=\"A\" Address=\"1.2.3.4\" MXPref=\"10\" TTL=\"1800\" />\n            <host HostId=\"14\" Name=\"www\" Type=\"A\" Address=\"122.23.3.7\" MXPref=\"10\" TTL=\"1800\" />\n        </DomainDNSGetHostsResult>\n    </CommandResponse>\n    <Server>SERVER-NAME</Server>\n    <GMTTimeDifference>+5</GMTTimeDifference>\n    <ExecutionTime>32.76</ExecutionTime>\n</ApiResponse>\n"
  },
  {
    "path": "providers/dns/namecheap/internal/fixtures/getHosts_errorBadAPIKey1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ApiResponse Status=\"ERROR\" xmlns=\"http://api.namecheap.com/xml.response\">\n    <Errors>\n        <Error Number=\"1011102\">API Key is invalid or API access has not been enabled</Error>\n    </Errors>\n    <Warnings />\n    <RequestedCommand />\n    <Server>PHX01SBAPI01</Server>\n    <GMTTimeDifference>--5:00</GMTTimeDifference>\n    <ExecutionTime>0</ExecutionTime>\n</ApiResponse>\n"
  },
  {
    "path": "providers/dns/namecheap/internal/fixtures/getHosts_success1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ApiResponse Status=\"OK\" xmlns=\"http://api.namecheap.com/xml.response\">\n    <Errors />\n    <Warnings />\n    <RequestedCommand>namecheap.domains.dns.getHosts</RequestedCommand>\n    <CommandResponse Type=\"namecheap.domains.dns.getHosts\">\n        <DomainDNSGetHostsResult Domain=\"example.com\" EmailType=\"MXE\" IsUsingOurDNS=\"true\">\n            <host HostId=\"217076\" Name=\"www\" Type=\"A\" Address=\"10.0.0.2\" MXPref=\"10\" TTL=\"1200\" AssociatedAppTitle=\"\" FriendlyName=\"\" IsActive=\"true\" IsDDNSEnabled=\"false\" />\n            <host HostId=\"217069\" Name=\"home\" Type=\"A\" Address=\"10.0.0.1\" MXPref=\"10\" TTL=\"1799\" AssociatedAppTitle=\"\" FriendlyName=\"\" IsActive=\"true\" IsDDNSEnabled=\"false\" />\n            <host HostId=\"217071\" Name=\"a\" Type=\"AAAA\" Address=\"::0\" MXPref=\"10\" TTL=\"1799\" AssociatedAppTitle=\"\" FriendlyName=\"\" IsActive=\"true\" IsDDNSEnabled=\"false\" />\n            <host HostId=\"217075\" Name=\"*\" Type=\"CNAME\" Address=\"example.com.\" MXPref=\"10\" TTL=\"1799\" AssociatedAppTitle=\"\" FriendlyName=\"\" IsActive=\"true\" IsDDNSEnabled=\"false\" />\n            <host HostId=\"217073\" Name=\"example.com\" Type=\"MXE\" Address=\"10.0.0.5\" MXPref=\"10\" TTL=\"1800\" AssociatedAppTitle=\"MXE\" FriendlyName=\"MXE1\" IsActive=\"true\" IsDDNSEnabled=\"false\" />\n            <host HostId=\"217077\" Name=\"xyz\" Type=\"URL\" Address=\"https://google.com\" MXPref=\"10\" TTL=\"1799\" AssociatedAppTitle=\"\" FriendlyName=\"\" IsActive=\"true\" IsDDNSEnabled=\"false\" />\n        </DomainDNSGetHostsResult>\n    </CommandResponse>\n    <Server>PHX01SBAPI01</Server>\n    <GMTTimeDifference>--5:00</GMTTimeDifference>\n    <ExecutionTime>3.338</ExecutionTime>\n</ApiResponse>\n"
  },
  {
    "path": "providers/dns/namecheap/internal/fixtures/getHosts_success2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ApiResponse Status=\"OK\" xmlns=\"http://api.namecheap.com/xml.response\">\n    <Errors />\n    <Warnings />\n    <RequestedCommand>namecheap.domains.dns.getHosts</RequestedCommand>\n    <CommandResponse Type=\"namecheap.domains.dns.getHosts\">\n        <DomainDNSGetHostsResult Domain=\"example.com\" EmailType=\"MXE\" IsUsingOurDNS=\"true\">\n            <host HostId=\"217076\" Name=\"@\" Type=\"A\" Address=\"10.0.0.2\" MXPref=\"10\" TTL=\"1200\" AssociatedAppTitle=\"\" FriendlyName=\"\" IsActive=\"true\" IsDDNSEnabled=\"false\" />\n            <host HostId=\"217069\" Name=\"www\" Type=\"A\" Address=\"10.0.0.3\" MXPref=\"10\" TTL=\"60\" AssociatedAppTitle=\"\" FriendlyName=\"\" IsActive=\"true\" IsDDNSEnabled=\"false\" />\n        </DomainDNSGetHostsResult>\n    </CommandResponse>\n    <Server>PHX01SBAPI01</Server>\n    <GMTTimeDifference>--5:00</GMTTimeDifference>\n    <ExecutionTime>3.338</ExecutionTime>\n</ApiResponse>\n"
  },
  {
    "path": "providers/dns/namecheap/internal/fixtures/setHosts.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ApiResponse xmlns=\"https://api.namecheap.com/xml.response\" Status=\"OK\">\n    <Errors />\n    <RequestedCommand>namecheap.domains.dns.setHosts</RequestedCommand>\n    <CommandResponse Type=\"namecheap.domains.dns.setHosts\">\n        <DomainDNSSetHostsResult Domain=\"domain51.com\" IsSuccess=\"true\" />\n    </CommandResponse>\n    <Server>SERVER-NAME</Server>\n    <GMTTimeDifference>+5</GMTTimeDifference>\n    <ExecutionTime>32.76</ExecutionTime>\n</ApiResponse>\n"
  },
  {
    "path": "providers/dns/namecheap/internal/fixtures/setHosts_errorBadAPIKey1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ApiResponse Status=\"ERROR\" xmlns=\"http://api.namecheap.com/xml.response\">\n    <Errors>\n        <Error Number=\"1011102\">API Key is invalid or API access has not been enabled</Error>\n    </Errors>\n    <Warnings />\n    <RequestedCommand />\n    <Server>PHX01SBAPI01</Server>\n    <GMTTimeDifference>--5:00</GMTTimeDifference>\n    <ExecutionTime>0</ExecutionTime>\n</ApiResponse>\n"
  },
  {
    "path": "providers/dns/namecheap/internal/fixtures/setHosts_success1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ApiResponse Status=\"OK\" xmlns=\"http://api.namecheap.com/xml.response\">\n    <Errors />\n    <Warnings />\n    <RequestedCommand>namecheap.domains.dns.setHosts</RequestedCommand>\n    <CommandResponse Type=\"namecheap.domains.dns.setHosts\">\n        <DomainDNSSetHostsResult Domain=\"example.com\" IsSuccess=\"true\">\n            <Warnings />\n        </DomainDNSSetHostsResult>\n    </CommandResponse>\n    <Server>PHX01SBAPI01</Server>\n    <GMTTimeDifference>--5:00</GMTTimeDifference>\n    <ExecutionTime>2.347</ExecutionTime>\n</ApiResponse>\n"
  },
  {
    "path": "providers/dns/namecheap/internal/fixtures/setHosts_success2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ApiResponse Status=\"OK\" xmlns=\"http://api.namecheap.com/xml.response\">\n    <Errors />\n    <Warnings />\n    <RequestedCommand>namecheap.domains.dns.setHosts</RequestedCommand>\n    <CommandResponse Type=\"namecheap.domains.dns.setHosts\">\n        <DomainDNSSetHostsResult Domain=\"example.com\" IsSuccess=\"true\">\n            <Warnings />\n        </DomainDNSSetHostsResult>\n    </CommandResponse>\n    <Server>PHX01SBAPI01</Server>\n    <GMTTimeDifference>--5:00</GMTTimeDifference>\n    <ExecutionTime>2.347</ExecutionTime>\n</ApiResponse>\n"
  },
  {
    "path": "providers/dns/namecheap/internal/ip.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst getIPURL = \"https://dynamicdns.park-your-domain.com/getip\"\n\n// GetClientIP returns the client's public IP address.\n// It uses namecheap's IP discovery service to perform the lookup.\nfunc GetClientIP(ctx context.Context, client *http.Client, debug bool) (addr string, err error) {\n\tif client == nil {\n\t\tclient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, getIPURL, http.NoBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tclientIP, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif debug {\n\t\tlog.Println(\"Client IP:\", string(clientIP))\n\t}\n\n\treturn string(clientIP), nil\n}\n"
  },
  {
    "path": "providers/dns/namecheap/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"encoding/xml\"\n\t\"fmt\"\n)\n\n// Record describes a DNS record returned by the Namecheap DNS gethosts API.\n// Namecheap uses the term \"host\" to refer to all DNS records that include\n// a host field (A, AAAA, CNAME, NS, TXT, URL).\ntype Record struct {\n\tType    string `xml:\",attr\"`\n\tName    string `xml:\",attr\"`\n\tAddress string `xml:\",attr\"`\n\tMXPref  string `xml:\",attr\"`\n\tTTL     string `xml:\",attr\"`\n}\n\n// apiError describes an error record in a namecheap API response.\ntype apiError struct {\n\tNumber      int    `xml:\",attr\"`\n\tDescription string `xml:\",innerxml\"`\n}\n\nfunc (a apiError) Error() string {\n\treturn fmt.Sprintf(\"%s [%d]\", a.Description, a.Number)\n}\n\ntype setHostsResponse struct {\n\tXMLName xml.Name   `xml:\"ApiResponse\"`\n\tStatus  string     `xml:\"Status,attr\"`\n\tErrors  []apiError `xml:\"Errors>Error\"`\n\tResult  struct {\n\t\tIsSuccess string `xml:\",attr\"`\n\t} `xml:\"CommandResponse>DomainDNSSetHostsResult\"`\n}\n\ntype getHostsResponse struct {\n\tXMLName xml.Name   `xml:\"ApiResponse\"`\n\tStatus  string     `xml:\"Status,attr\"`\n\tErrors  []apiError `xml:\"Errors>Error\"`\n\tHosts   []Record   `xml:\"CommandResponse>DomainDNSGetHostsResult>host\"`\n}\n"
  },
  {
    "path": "providers/dns/namecheap/namecheap.go",
    "content": "// Package namecheap implements a DNS provider for solving the DNS-01 challenge using namecheap DNS.\npackage namecheap\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/namecheap/internal\"\n\t\"golang.org/x/net/publicsuffix\"\n)\n\n// Notes about namecheap's tool API:\n// 1. Using the API requires registration.\n//    Once registered, use your account name and API key to access the API.\n// 2. There is no API to add or modify a single DNS record.\n//    Instead, you must read the entire list of records, make modifications,\n//    and then write the entire updated list of records. (Yuck.)\n// 3. Namecheap's DNS updates can be slow to propagate.\n//    I've seen them take as long as an hour.\n// 4. Namecheap requires you to whitelist the IP address from which you call its APIs.\n//    It also requires all API calls to include the whitelisted IP address as a form or query string value.\n//    This code uses a namecheap service to query the client's IP address.\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"NAMECHEAP_\"\n\n\tEnvAPIUser = envNamespace + \"API_USER\"\n\tEnvAPIKey  = envNamespace + \"API_KEY\"\n\n\tEnvSandbox = envNamespace + \"SANDBOX\"\n\tEnvDebug   = envNamespace + \"DEBUG\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tDebug              bool\n\tBaseURL            string\n\tAPIUser            string\n\tAPIKey             string\n\tClientIP           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\tbaseURL := internal.DefaultBaseURL\n\tif env.GetOrDefaultBool(EnvSandbox, false) {\n\t\tbaseURL = internal.SandboxBaseURL\n\t}\n\n\treturn &Config{\n\t\tBaseURL:            baseURL,\n\t\tDebug:              env.GetOrDefaultBool(EnvDebug, false),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, time.Hour),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 15*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout:   env.GetOrDefaultSecond(EnvHTTPTimeout, time.Minute),\n\t\t\tTransport: defaultTransport(envNamespace),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for namecheap.\n// Credentials must be passed in the environment variables:\n// NAMECHEAP_API_USER and NAMECHEAP_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIUser, EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"namecheap: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIUser = values[EnvAPIUser]\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Namecheap.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"namecheap: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIUser == \"\" || config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"namecheap: credentials missing\")\n\t}\n\n\tif config.ClientIP == \"\" {\n\t\tclientIP, err := internal.GetClientIP(context.Background(), config.HTTPClient, config.Debug)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"namecheap: %w\", err)\n\t\t}\n\n\t\tconfig.ClientIP = clientIP\n\t}\n\n\tclient := internal.NewClient(config.APIUser, config.APIKey, config.ClientIP)\n\tclient.BaseURL = config.BaseURL\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Namecheap can sometimes take a long time to complete an update, so wait up to 60 minutes for the update to propagate.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present installs a TXT record for the DNS challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\tpr, err := newPseudoRecord(domain, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namecheap: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\trecords, err := d.client.GetHosts(ctx, pr.sld, pr.tld)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namecheap: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName:    pr.key,\n\t\tType:    \"TXT\",\n\t\tAddress: pr.keyValue,\n\t\tMXPref:  \"10\",\n\t\tTTL:     strconv.Itoa(d.config.TTL),\n\t}\n\n\trecords = append(records, record)\n\n\tif d.config.Debug {\n\t\tfor _, h := range records {\n\t\t\tlog.Printf(\"%-5.5s %-30.30s %-6s %-70.70s\", h.Type, h.Name, h.TTL, h.Address)\n\t\t}\n\t}\n\n\terr = d.client.SetHosts(ctx, pr.sld, pr.tld, records)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namecheap: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes a TXT record used for a previous DNS challenge.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\tpr, err := newPseudoRecord(domain, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namecheap: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\trecords, err := d.client.GetHosts(ctx, pr.sld, pr.tld)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namecheap: %w\", err)\n\t}\n\n\t// Find the challenge TXT record and remove it if found.\n\tvar (\n\t\tfound      bool\n\t\tnewRecords []internal.Record\n\t)\n\n\tfor _, h := range records {\n\t\tif h.Name == pr.key && h.Type == \"TXT\" {\n\t\t\tfound = true\n\t\t} else {\n\t\t\tnewRecords = append(newRecords, h)\n\t\t}\n\t}\n\n\tif !found {\n\t\treturn nil\n\t}\n\n\terr = d.client.SetHosts(ctx, pr.sld, pr.tld, newRecords)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namecheap: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// A pseudoRecord represents all the data needed to specify a dns-01 challenge to lets-encrypt.\ntype pseudoRecord struct {\n\tdomain   string\n\tkey      string\n\tkeyFqdn  string\n\tkeyValue string\n\ttld      string\n\tsld      string\n\thost     string\n}\n\n// newPseudoRecord builds a challenge record from a domain name and a challenge authentication key.\nfunc newPseudoRecord(domain, keyAuth string) (*pseudoRecord, error) {\n\tdomain = dns01.UnFqdn(domain)\n\n\ttld, _ := publicsuffix.PublicSuffix(domain)\n\tif tld == domain {\n\t\treturn nil, fmt.Errorf(\"invalid domain name %q\", domain)\n\t}\n\n\tparts := strings.Split(domain, \".\")\n\tlongest := len(parts) - strings.Count(tld, \".\") - 1\n\tsld := parts[longest-1]\n\n\tvar host string\n\tif longest >= 1 {\n\t\thost = strings.Join(parts[:longest-1], \".\")\n\t}\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\treturn &pseudoRecord{\n\t\tdomain:   domain,\n\t\tkey:      \"_acme-challenge.\" + host,\n\t\tkeyFqdn:  info.EffectiveFQDN,\n\t\tkeyValue: info.Value,\n\t\ttld:      tld,\n\t\tsld:      sld,\n\t\thost:     host,\n\t}, nil\n}\n"
  },
  {
    "path": "providers/dns/namecheap/namecheap.toml",
    "content": "Name = \"Namecheap\"\nURL = \"https://www.namecheap.com\"\nCode = \"namecheap\"\nSince = \"v0.3.0\"\nDescription = '''\n\nConfiguration for [Namecheap](https://www.namecheap.com).\n\n**To enable API access on the Namecheap production environment, some opaque requirements must be met.**\nMore information in the section [Enabling API Access](https://www.namecheap.com/support/api/intro/) of the Namecheap documentation.\n(2020-08: Account balance of $50+, 20+ domains in your account, or purchases totaling $50+ within the last 2 years.)\n'''\n\nExample = '''\nNAMECHEAP_API_USER=user \\\nNAMECHEAP_API_KEY=key \\\nlego --dns namecheap -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    NAMECHEAP_API_USER = \"API user\"\n    NAMECHEAP_API_KEY = \"API key\"\n  [Configuration.Additional]\n    NAMECHEAP_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 15)\"\n    NAMECHEAP_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 3600)\"\n    NAMECHEAP_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    NAMECHEAP_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 60)\"\n    NAMECHEAP_SANDBOX = \"Activate the sandbox (boolean)\"\n\n[Links]\n  API = \"https://www.namecheap.com/support/api/methods.aspx\"\n"
  },
  {
    "path": "providers/dns/namecheap/namecheap_test.go",
    "content": "package namecheap\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tenvTestUser     = \"foo\"\n\tenvTestKey      = \"bar\"\n\tenvTestClientIP = \"10.0.0.1\"\n)\n\ntype testCase struct {\n\tname             string\n\tdomain           string\n\terrString        string\n\tgetHostsResponse string\n\tsetHostsResponse string\n}\n\nvar testCases = []testCase{\n\t{\n\t\tname:             \"Test:Success:1\",\n\t\tdomain:           \"test.example.com\",\n\t\tgetHostsResponse: \"getHosts_success1.xml\",\n\t\tsetHostsResponse: \"setHosts_success1.xml\",\n\t},\n\t{\n\t\tname:             \"Test:Success:2\",\n\t\tdomain:           \"example.com\",\n\t\tgetHostsResponse: \"getHosts_success2.xml\",\n\t\tsetHostsResponse: \"setHosts_success2.xml\",\n\t},\n\t{\n\t\tname:             \"Test:Error:BadApiKey:1\",\n\t\tdomain:           \"test.example.com\",\n\t\terrString:        \"API Key is invalid or API access has not been enabled [1011102]\",\n\t\tgetHostsResponse: \"getHosts_errorBadAPIKey1.xml\",\n\t},\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tfor _, test := range testCases {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tch, _ := newPseudoRecord(test.domain, \"\")\n\n\t\t\tprovider := mockBuilder().\n\t\t\t\tRoute(\"GET /\",\n\t\t\t\t\tservermock.ResponseFromInternal(test.getHostsResponse),\n\t\t\t\t\tservermock.CheckForm().Strict().\n\t\t\t\t\t\tWith(\"ClientIp\", \"10.0.0.1\").\n\t\t\t\t\t\tWith(\"Command\", \"namecheap.domains.dns.getHosts\").\n\t\t\t\t\t\tWith(\"SLD\", ch.sld).\n\t\t\t\t\t\tWith(\"TLD\", ch.tld).\n\t\t\t\t\t\tWith(\"UserName\", \"foo\").\n\t\t\t\t\t\tWith(\"ApiKey\", \"bar\").\n\t\t\t\t\t\tWith(\"ApiUser\", \"foo\"),\n\t\t\t\t).\n\t\t\t\tRoute(\"POST /\",\n\t\t\t\t\tservermock.ResponseFromInternal(test.setHostsResponse),\n\t\t\t\t\tservermock.CheckForm().\n\t\t\t\t\t\tWith(\"ClientIp\", \"10.0.0.1\").\n\t\t\t\t\t\tWith(\"Command\", \"namecheap.domains.dns.setHosts\").\n\t\t\t\t\t\tWith(\"SLD\", ch.sld).\n\t\t\t\t\t\tWith(\"TLD\", ch.tld).\n\t\t\t\t\t\tWith(\"UserName\", \"foo\").\n\t\t\t\t\t\tWith(\"ApiKey\", \"bar\").\n\t\t\t\t\t\tWith(\"ApiUser\", \"foo\"),\n\t\t\t\t).\n\t\t\t\tBuild(t)\n\n\t\t\terr := provider.Present(test.domain, \"\", \"dummyKey\")\n\t\t\tif test.errString != \"\" {\n\t\t\t\tassert.EqualError(t, err, \"namecheap: \"+test.errString)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tfor _, test := range testCases {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tch, _ := newPseudoRecord(test.domain, \"\")\n\n\t\t\tprovider := mockBuilder().\n\t\t\t\tRoute(\"GET /\",\n\t\t\t\t\tservermock.ResponseFromInternal(test.getHostsResponse),\n\t\t\t\t\tservermock.CheckForm().Strict().\n\t\t\t\t\t\tWith(\"ClientIp\", \"10.0.0.1\").\n\t\t\t\t\t\tWith(\"Command\", \"namecheap.domains.dns.getHosts\").\n\t\t\t\t\t\tWith(\"SLD\", ch.sld).\n\t\t\t\t\t\tWith(\"TLD\", ch.tld).\n\t\t\t\t\t\tWith(\"UserName\", \"foo\").\n\t\t\t\t\t\tWith(\"ApiKey\", \"bar\").\n\t\t\t\t\t\tWith(\"ApiUser\", \"foo\"),\n\t\t\t\t).\n\t\t\t\tRoute(\"POST /\",\n\t\t\t\t\tservermock.ResponseFromInternal(test.setHostsResponse),\n\t\t\t\t\tservermock.CheckForm().\n\t\t\t\t\t\tWith(\"ClientIp\", \"10.0.0.1\").\n\t\t\t\t\t\tWith(\"Command\", \"namecheap.domains.dns.setHosts\").\n\t\t\t\t\t\tWith(\"SLD\", ch.sld).\n\t\t\t\t\t\tWith(\"TLD\", ch.tld).\n\t\t\t\t\t\tWith(\"UserName\", \"foo\").\n\t\t\t\t\t\tWith(\"ApiKey\", \"bar\").\n\t\t\t\t\t\tWith(\"ApiUser\", \"foo\"),\n\t\t\t\t).\n\t\t\t\tBuild(t)\n\n\t\t\terr := provider.CleanUp(test.domain, \"\", \"dummyKey\")\n\t\t\tif test.errString != \"\" {\n\t\t\t\tassert.EqualError(t, err, \"namecheap: \"+test.errString)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_newPseudoRecord_domainSplit(t *testing.T) {\n\ttests := []struct {\n\t\tdomain string\n\t\tvalid  bool\n\t\ttld    string\n\t\tsld    string\n\t\thost   string\n\t}{\n\t\t{domain: \"a.b.c.test.co.uk\", valid: true, tld: \"co.uk\", sld: \"test\", host: \"a.b.c\"},\n\t\t{domain: \"test.co.uk\", valid: true, tld: \"co.uk\", sld: \"test\"},\n\t\t{domain: \"test.com\", valid: true, tld: \"com\", sld: \"test\"},\n\t\t{domain: \"test.co.com\", valid: true, tld: \"co.com\", sld: \"test\"},\n\t\t{domain: \"www.test.com.au\", valid: true, tld: \"com.au\", sld: \"test\", host: \"www\"},\n\t\t{domain: \"www.za.com\", valid: true, tld: \"za.com\", sld: \"www\"},\n\t\t{domain: \"my.test.tf\", valid: true, tld: \"tf\", sld: \"test\", host: \"my\"},\n\t\t{},\n\t\t{domain: \"a\"},\n\t\t{domain: \"com\"},\n\t\t{domain: \"com.au\"},\n\t\t{domain: \"co.com\"},\n\t\t{domain: \"co.uk\"},\n\t\t{domain: \"tf\"},\n\t\t{domain: \"za.com\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.domain, func(t *testing.T) {\n\t\t\tvalid := true\n\n\t\t\tch, err := newPseudoRecord(test.domain, \"\")\n\t\t\tif err != nil {\n\t\t\t\tvalid = false\n\t\t\t}\n\n\t\t\tif test.valid && !valid {\n\t\t\t\tt.Errorf(\"Expected '%s' to split\", test.domain)\n\t\t\t} else if !test.valid && valid {\n\t\t\t\tt.Errorf(\"Expected '%s' to produce error\", test.domain)\n\t\t\t}\n\n\t\t\tif test.valid && valid {\n\t\t\t\trequire.NotNil(t, ch)\n\t\t\t\tassert.Equal(t, test.domain, ch.domain, \"domain\")\n\t\t\t\tassert.Equal(t, test.tld, ch.tld, \"tld\")\n\t\t\t\tassert.Equal(t, test.sld, ch.sld, \"sld\")\n\t\t\t\tassert.Equal(t, test.host, ch.host, \"host\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {\n\t\tconfig := NewDefaultConfig()\n\t\tconfig.HTTPClient = server.Client()\n\t\tconfig.BaseURL = server.URL\n\t\tconfig.APIUser = envTestUser\n\t\tconfig.APIKey = envTestKey\n\t\tconfig.ClientIP = envTestClientIP\n\n\t\treturn NewDNSProviderConfig(config)\n\t})\n}\n"
  },
  {
    "path": "providers/dns/namecheap/transport.go",
    "content": "package namecheap\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"golang.org/x/net/http/httpproxy\"\n)\n\nconst (\n\tenvHTTPProxy       = \"HTTP_PROXY\"\n\tenvHTTPProxyLower  = \"http_proxy\"\n\tenvHTTPSProxy      = \"HTTPS_PROXY\"\n\tenvHTTPSProxyLower = \"https_proxy\"\n\tenvNoProxy         = \"NO_PROXY\"\n\tenvNoProxyLower    = \"no_proxy\"\n\tenvRequestMethod   = \"REQUEST_METHOD\"\n)\n\n// Allows lazy loading of the proxy.\nvar (\n\tenvProxyOnce      sync.Once\n\tenvProxyFuncValue func(*url.URL) (*url.URL, error)\n)\n\nfunc defaultTransport(namespace string) http.RoundTripper {\n\ttr, ok := http.DefaultTransport.(*http.Transport)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tclone := tr.Clone()\n\tclone.Proxy = proxyFromEnvironment(namespace)\n\n\treturn clone\n}\n\n// Inspired by:\n// - https://pkg.go.dev/net/http#ProxyFromEnvironment\n// - https://pkg.go.dev/golang.org/x/net/http/httpproxy#FromEnvironment\nfunc envProxyFunc(namespace string) func(*url.URL) (*url.URL, error) {\n\tenvProxyOnce.Do(func() {\n\t\tcfg := &httpproxy.Config{\n\t\t\tHTTPProxy:  getEnv(namespace, envHTTPProxy, envHTTPProxyLower),\n\t\t\tHTTPSProxy: getEnv(namespace, envHTTPSProxy, envHTTPSProxyLower),\n\t\t\tNoProxy:    getEnv(namespace, envNoProxy, envNoProxyLower),\n\t\t\tCGI:        env.GetOneWithFallback(namespace+envRequestMethod, \"\", env.ParseString, envRequestMethod) != \"\",\n\t\t}\n\n\t\tenvProxyFuncValue = cfg.ProxyFunc()\n\t})\n\n\treturn envProxyFuncValue\n}\n\n// Inspired by:\n// - https://pkg.go.dev/net/http#ProxyFromEnvironment\n// - https://pkg.go.dev/golang.org/x/net/http/httpproxy#FromEnvironment\nfunc proxyFromEnvironment(namespace string) func(req *http.Request) (*url.URL, error) {\n\treturn func(req *http.Request) (*url.URL, error) {\n\t\treturn envProxyFunc(namespace)(req.URL)\n\t}\n}\n\nfunc getEnv(namespace, baseEnvName, baseEnvNameLower string) string {\n\treturn env.GetOneWithFallback(namespace+baseEnvName, \"\", env.ParseString,\n\t\tstrings.ToLower(namespace)+baseEnvNameLower, baseEnvName, baseEnvNameLower)\n}\n"
  },
  {
    "path": "providers/dns/namecheap/transport_test.go",
    "content": "package namecheap\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_defaultTransport(t *testing.T) {\n\tclient := servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*http.Client, error) {\n\t\t\tcl := server.Client()\n\n\t\t\tt.Setenv(\"NAMECHEAP_HTTP_PROXY\", server.URL)\n\n\t\t\tcl.Transport = defaultTransport(envNamespace)\n\n\t\t\treturn cl, nil\n\t\t}).\n\t\tRoute(\"/\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusTeapot)).\n\t\tBuild(t)\n\n\treq, err := http.NewRequest(http.MethodGet, \"http://example.com\", nil)\n\trequire.NoError(t, err)\n\n\tresp, err := client.Do(req)\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\t_ = resp.Body.Close()\n\t})\n\n\tassert.Equal(t, http.StatusTeapot, resp.StatusCode)\n}\n"
  },
  {
    "path": "providers/dns/namedotcom/namedotcom.go",
    "content": "// Package namedotcom implements a DNS provider for solving the DNS-01 challenge using Name.com's DNS service.\npackage namedotcom\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/namedotcom/go/v4/namecom\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"NAMECOM_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvAPIToken = envNamespace + \"API_TOKEN\"\n\tEnvServer   = envNamespace + \"SERVER\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// according to https://www.name.com/api-docs/DNS#CreateRecord\nconst minTTL = 300\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername           string\n\tAPIToken           string\n\tServer             string\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 15*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 20*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *namecom.NameCom\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for namedotcom.\n// Credentials must be passed in the environment variables:\n// NAMECOM_USERNAME and NAMECOM_API_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"namedotcom: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.APIToken = values[EnvAPIToken]\n\tconfig.Server = env.GetOrFile(EnvServer)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for namedotcom.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"namedotcom: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Username == \"\" {\n\t\treturn nil, errors.New(\"namedotcom: username is required\")\n\t}\n\n\tif config.APIToken == \"\" {\n\t\treturn nil, errors.New(\"namedotcom: API token is required\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"namedotcom: invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\tclient := namecom.New(config.Username, config.APIToken)\n\n\tif config.HTTPClient != nil {\n\t\tclient.Client = config.HTTPClient\n\t}\n\n\tclient.Client = clientdebug.Wrap(client.Client)\n\n\tif config.Server != \"\" {\n\t\tclient.Server = config.Server\n\t}\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tif info.EffectiveFQDN != info.FQDN {\n\t\tdomain = dns01.UnFqdn(info.EffectiveFQDN)\n\t}\n\n\tdomainDetails, err := d.client.GetDomain(&namecom.GetDomainRequest{DomainName: domain})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namedotcom: API call failed: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, domainDetails.DomainName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namedotcom: %w\", err)\n\t}\n\n\trequest := &namecom.Record{\n\t\tDomainName: domain,\n\t\tHost:       subDomain,\n\t\tType:       \"TXT\",\n\t\tTTL:        uint32(d.config.TTL),\n\t\tAnswer:     info.Value,\n\t}\n\n\t_, err = d.client.CreateRecord(request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namedotcom: API call failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tif info.EffectiveFQDN != info.FQDN {\n\t\tdomain = dns01.UnFqdn(info.EffectiveFQDN)\n\t}\n\n\trecords, err := d.getRecords(domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namedotcom: %w\", err)\n\t}\n\n\tfor _, rec := range records {\n\t\tif rec.Fqdn == info.EffectiveFQDN && rec.Type == \"TXT\" {\n\t\t\trequest := &namecom.DeleteRecordRequest{\n\t\t\t\tDomainName: domain,\n\t\t\t\tID:         rec.ID,\n\t\t\t}\n\n\t\t\t_, err := d.client.DeleteRecord(request)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"namedotcom: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) getRecords(domain string) ([]*namecom.Record, error) {\n\trequest := &namecom.ListRecordsRequest{\n\t\tDomainName: domain,\n\t\tPage:       1,\n\t}\n\n\tvar records []*namecom.Record\n\n\tfor request.Page > 0 {\n\t\tresponse, err := d.client.ListRecords(request)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\trecords = append(records, response.Records...)\n\t\trequest.Page = response.NextPage\n\t}\n\n\treturn records, nil\n}\n"
  },
  {
    "path": "providers/dns/namedotcom/namedotcom.toml",
    "content": "Name = \"Name.com\"\nDescription = ''''''\nURL = \"https://www.name.com\"\nCode = \"namedotcom\"\nSince = \"v0.5.0\"\n\nExample = '''\nNAMECOM_USERNAME=foo.bar \\\nNAMECOM_API_TOKEN=a379a6f6eeafb9a55e378c118034e2751e682fab \\\nlego --dns namedotcom -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    NAMECOM_USERNAME = \"Username\"\n    NAMECOM_API_TOKEN = \"API token\"\n  [Configuration.Additional]\n    NAMECOM_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 20)\"\n    NAMECOM_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 900)\"\n    NAMECOM_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    NAMECOM_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://www.name.com/api-docs/DNS\"\n  GoClient = \"https://github.com/namedotcom/go\"\n\n"
  },
  {
    "path": "providers/dns/namedotcom/namedotcom_test.go",
    "content": "package namedotcom\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvUsername,\n\tEnvAPIToken).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"A\",\n\t\t\t\tEnvAPIToken: \"B\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvAPIToken: \"\",\n\t\t\t},\n\t\t\texpected: \"namedotcom: some credentials information are missing: NAMECOM_USERNAME,NAMECOM_API_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvAPIToken: \"B\",\n\t\t\t},\n\t\t\texpected: \"namedotcom: some credentials information are missing: NAMECOM_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"A\",\n\t\t\t\tEnvAPIToken: \"\",\n\t\t\t},\n\t\t\texpected: \"namedotcom: some credentials information are missing: NAMECOM_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiToken string\n\t\tusername string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiToken: \"A\",\n\t\t\tusername: \"B\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"namedotcom: username is required\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API token\",\n\t\t\tapiToken: \"\",\n\t\t\tusername: \"B\",\n\t\t\texpected: \"namedotcom: API token is required\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tapiToken: \"A\",\n\t\t\tusername: \"\",\n\t\t\texpected: \"namedotcom: username is required\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.APIToken = test.apiToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/namesilo/namesilo.go",
    "content": "// Package namesilo implements a DNS provider for solving the DNS-01 challenge using namesilo DNS.\npackage namesilo\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/nrdcg/namesilo\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"NAMESILO_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nconst (\n\tdefaultTTL = 3600\n\tmaxTTL     = 2592000\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, defaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *namesilo.Client\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for namesilo.\n// API_KEY must be passed in the environment variables: NAMESILO_API_KEY.\n//\n// See: https://www.namesilo.com/api_reference.php\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"namesilo: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Namesilo.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"namesilo: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.TTL < defaultTTL || config.TTL > maxTTL {\n\t\treturn nil, fmt.Errorf(\"namesilo: TTL should be in [%d, %d]\", defaultTTL, maxTTL)\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"namesilo: credentials missing\")\n\t}\n\n\tclient := namesilo.NewClient(config.APIKey)\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namesilo: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzoneName := dns01.UnFqdn(zone)\n\n\tsubdomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zoneName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namesilo: %w\", err)\n\t}\n\n\t_, err = d.client.DnsAddRecord(context.Background(), &namesilo.DnsAddRecordParams{\n\t\tDomain: zoneName,\n\t\tType:   \"TXT\",\n\t\tHost:   subdomain,\n\t\tValue:  info.Value,\n\t\tTTL:    d.config.TTL,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namesilo: failed to add record %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namesilo: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzoneName := dns01.UnFqdn(zone)\n\n\tresp, err := d.client.DnsListRecords(ctx, &namesilo.DnsListRecordsParams{Domain: zoneName})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namesilo: %w\", err)\n\t}\n\n\tsubdomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zoneName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namesilo: %w\", err)\n\t}\n\n\tfor _, r := range resp.Reply.ResourceRecord {\n\t\tif r.Type == \"TXT\" && r.Value == info.Value && (r.Host == subdomain || r.Host == dns01.UnFqdn(info.EffectiveFQDN)) {\n\t\t\t_, err := d.client.DnsDeleteRecord(ctx, &namesilo.DnsDeleteRecordParams{Domain: zoneName, ID: r.RecordID})\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"namesilo: %w\", err)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"namesilo: no TXT record to delete for %s (%s)\", info.EffectiveFQDN, info.Value)\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/namesilo/namesilo.toml",
    "content": "Name = \"Namesilo\"\nDescription = ''''''\nURL = \"https://www.namesilo.com/\"\nCode = \"namesilo\"\nSince = \"v2.7.0\"\n\nExample = '''\nNAMESILO_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \\\nlego --dns namesilo -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    NAMESILO_API_KEY = \"Client ID\"\n  [Configuration.Additional]\n    NAMESILO_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    NAMESILO_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60), it is better to set larger than 15 minutes\"\n    NAMESILO_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600), should be in [3600, 2592000]\"\n\n[Links]\n  API = \"https://www.namesilo.com/api_reference.php\"\n  GoClient = \"https://github.com/nrdcg/namesilo\"\n"
  },
  {
    "path": "providers/dns/namesilo/namesilo_test.go",
    "content": "package namesilo\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvTTL,\n\tEnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"A\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"namesilo: some credentials information are missing: NAMESILO_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"unsupported TTL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"A\",\n\t\t\t\tEnvTTL:    \"180\",\n\t\t\t},\n\t\t\texpected: \"namesilo: TTL should be in [3600, 2592000]\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tttl      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"A\",\n\t\t\tttl:    defaultTTL,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\tttl:      defaultTTL,\n\t\t\texpected: \"namesilo: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"unavailable TTL\",\n\t\t\tapiKey:   \"A\",\n\t\t\tttl:      100,\n\t\t\texpected: \"namesilo: TTL should be in [3600, 2592000]\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.TTL = test.ttl\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/namesurfer/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\ntype Client struct {\n\tapiKey    string\n\tapiSecret string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(baseURL, apiKey, apiSecret string) (*Client, error) {\n\tif apiKey == \"\" || apiSecret == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tif baseURL == \"\" {\n\t\treturn nil, errors.New(\"base URL missing\")\n\t}\n\n\tapiEndpoint, err := url.Parse(baseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\tapiKey:    apiKey,\n\t\tapiSecret: apiSecret,\n\t\tBaseURL:   apiEndpoint.JoinPath(\"jsonrpc10\"),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: 5 * time.Second,\n\t\t},\n\t}, nil\n}\n\n// AddDNSRecord adds a DNS record.\n// http://95.128.3.201:8053/API/NSService_10#addDNSRecord\nfunc (d *Client) AddDNSRecord(ctx context.Context, zoneName, viewName string, record DNSNode) error {\n\tdigest := d.computeDigest(\n\t\tzoneName,\n\t\tviewName,\n\t\trecord.Name,\n\t\trecord.Type,\n\t\tstrconv.Itoa(record.TTL),\n\t\trecord.Data,\n\t)\n\n\t// JSON-RPC 1.0 requires positional parameters array\n\tparams := []any{\n\t\tdigest,\n\t\tzoneName,\n\t\tviewName,\n\t\trecord,\n\t}\n\n\tvar ok bool\n\n\terr := d.doRequest(ctx, \"addDNSRecord\", params, &ok)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !ok {\n\t\treturn errors.New(\"addDNSRecord failed\")\n\t}\n\n\treturn nil\n}\n\n// UpdateDNSHost updates a DNS host record.\n// Passing an empty newNode removes the oldNode.\n// http://95.128.3.201:8053/API/NSService_10#updateDNSHost\nfunc (d *Client) UpdateDNSHost(ctx context.Context, zoneName, viewName string, oldNode, newNode DNSNode) error {\n\tdigest := d.computeDigest(zoneName, viewName)\n\n\t// JSON-RPC 1.0 requires positional parameters array\n\tparams := []any{\n\t\tdigest,\n\t\tzoneName,\n\t\tviewName,\n\t\toldNode,\n\t\tnewNode,\n\t}\n\n\tvar ok bool\n\n\terr := d.doRequest(ctx, \"updateDNSHost\", params, &ok)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !ok {\n\t\treturn errors.New(\"updateDNSHost failed\")\n\t}\n\n\treturn nil\n}\n\n// SearchDNSHosts searches for DNS host records.\n// http://95.128.3.201:8053/API/NSService_10#searchDNSHosts\nfunc (d *Client) SearchDNSHosts(ctx context.Context, pattern string) ([]DNSNode, error) {\n\tdigest := d.computeDigest(pattern)\n\n\t// JSON-RPC 1.0 requires positional parameters array\n\tparams := []any{\n\t\tdigest,\n\t\tpattern,\n\t}\n\n\tvar nodes []DNSNode\n\n\terr := d.doRequest(ctx, \"searchDNSHosts\", params, &nodes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn nodes, nil\n}\n\n// ListZones lists DNS zones.\n// http://95.128.3.201:8053/API/NSService_10#listZones\nfunc (d *Client) ListZones(ctx context.Context, mode string) ([]DNSZone, error) {\n\tdigest := d.computeDigest()\n\n\t// JSON-RPC 1.0 requires positional parameters array\n\tparams := []any{\n\t\tdigest,\n\t\tmode,\n\t}\n\n\tvar zones []DNSZone\n\n\terr := d.doRequest(ctx, \"listZones\", params, &zones)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn zones, nil\n}\n\nfunc (d *Client) doRequest(ctx context.Context, method string, params []any, result any) error {\n\tpayload := APIRequest{\n\t\tID:     1,\n\t\tMethod: method,\n\t\tParams: slices.Concat([]any{d.apiKey}, params),\n\t}\n\n\tbuf := new(bytes.Buffer)\n\n\terr := json.NewEncoder(buf).Encode(payload)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, d.BaseURL.String(), buf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tresp, err := d.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\tvar rpcResp APIResponse\n\n\terr = json.Unmarshal(raw, &rpcResp)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tif rpcResp.Error != nil {\n\t\treturn rpcResp.Error\n\t}\n\n\terr = json.Unmarshal(rpcResp.Result, result)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to unmarshal response: %w: %s\", err, rpcResp.Result)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Client) computeDigest(parts ...string) string {\n\tparams := []string{d.apiKey}\n\tparams = append(params, parts...)\n\tparams = append(params, d.apiSecret)\n\n\tmac := hmac.New(sha256.New, []byte(d.apiSecret))\n\tmac.Write([]byte(strings.Join(params, \"&\")))\n\n\treturn hex.EncodeToString(mac.Sum(nil))\n}\n"
  },
  {
    "path": "providers/dns/namesurfer/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(server.URL, \"user\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders(),\n\t)\n}\n\nfunc TestClient_AddDNSRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /jsonrpc10\",\n\t\t\tservermock.ResponseFromFixture(\"addDNSRecord.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"addDNSRecord-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := DNSNode{\n\t\tName: \"_acme-challenge\",\n\t\tType: \"TXT\",\n\t\tData: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:  300,\n\t}\n\n\terr := client.AddDNSRecord(t.Context(), \"example.com\", \"viewA\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddDNSRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /jsonrpc10\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := DNSNode{\n\t\tName: \"_acme-challenge\",\n\t\tType: \"TXT\",\n\t\tData: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:  300,\n\t}\n\n\terr := client.AddDNSRecord(t.Context(), \"example.com\", \"viewA\", record)\n\trequire.EqualError(t, err, \"code: Server.Keyfailure, \"+\n\t\t\"filename: service, line: 13, \"+\n\t\t\"message: Unknown keyname user, \"+\n\t\t`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 `)\n}\n\nfunc TestClient_UpdateDNSHost(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /jsonrpc10\",\n\t\t\tservermock.ResponseFromFixture(\"updateDNSHost.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"updateDNSHost-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := DNSNode{\n\t\tName: \"_acme-challenge\",\n\t\tType: \"TXT\",\n\t\tData: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:  300,\n\t}\n\n\terr := client.UpdateDNSHost(t.Context(), \"example.com\", \"viewA\", record, DNSNode{})\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_SearchDNSHosts(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /jsonrpc10\",\n\t\t\tservermock.ResponseFromFixture(\"searchDNSHosts.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"searchDNSHosts-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\trecords, err := client.SearchDNSHosts(t.Context(), \"value\")\n\trequire.NoError(t, err)\n\n\texpected := []DNSNode{\n\t\t{Name: \"foo\", Type: \"TXT\", Data: \"xxx\", TTL: 300},\n\t\t{Name: \"_acme-challenge\", Type: \"TXT\", Data: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\", TTL: 300},\n\t\t{Name: \"bar\", Type: \"A\", Data: \"yyy\", TTL: 300},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_ListZones(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /jsonrpc10\",\n\t\t\tservermock.ResponseFromFixture(\"listZones.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"listZones-request.json\"),\n\t\t).\n\t\tBuild(t)\n\n\tzones, err := client.ListZones(t.Context(), \"value\")\n\trequire.NoError(t, err)\n\n\texpected := []DNSZone{\n\t\t{Name: \"example.com\", View: \"viewA\"},\n\t\t{Name: \"example.org\", View: \"viewB\"},\n\t\t{Name: \"example.net\", View: \"viewC\"},\n\t}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_computeDigest(t *testing.T) {\n\tclient, err := NewClient(\"https://test.example.com\", \"testkey\", \"testsecret\")\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tparts    []string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"no parts\",\n\t\t\tparts:    []string{},\n\t\t\texpected: \"99b5dcdc19bfc0ce2af3fe848f4bcb6f7beb352e9599e8ba50544d86de567282\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"parts\",\n\t\t\tparts:    []string{\"zone.example.com\", \"default\"},\n\t\t\texpected: \"94efef76383889b1ae620582a25d1c3aa9bd9ba9ac4bdccdf4aefbc3ae6e8329\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tdigest := client.computeDigest(test.parts...)\n\n\t\t\tassert.Equal(t, test.expected, digest)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/namesurfer/internal/fixtures/addDNSRecord-request.json",
    "content": "{\n  \"id\": 1,\n  \"method\": \"addDNSRecord\",\n  \"params\": [\n    \"user\",\n    \"4fcc5fa29531709b0381c8debea127a6a26e71cb9491727876819cf5805c4990\",\n    \"example.com\",\n    \"viewA\",\n    {\n      \"name\": \"_acme-challenge\",\n      \"type\": \"TXT\",\n      \"data\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n      \"ttl\": 300\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/namesurfer/internal/fixtures/addDNSRecord.json",
    "content": "{\n  \"id\": 1,\n  \"result\": true\n}\n"
  },
  {
    "path": "providers/dns/namesurfer/internal/fixtures/error.json",
    "content": "{\n  \"result\": null,\n  \"error\": {\n    \"filename\": \"service\",\n    \"lineno\": 13,\n    \"code\": \"Server.Keyfailure\",\n    \"string\": \"Unknown keyname user\",\n    \"detail\": [\n      \"Traceback (most recent call last):\",\n      \"  File \\\"/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/server/dispatcher.py\\\", line 159, in dispatch_request\",\n      \"    result = self.call_method(method,req_dict,tc,export_dict,log_line)\",\n      \"  File \\\"/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/server/dispatcher.py\\\", line 96, in call_method\",\n      \"    result = getattr(service_class_instance,req_dict['methodname'])(*args)\",\n      \"  File \\\"/usr/local/namesurfer/python/lib/python2.6/site-packages/ladon/ladonizer/decorator.py\\\", line 77, in injector\",\n      \"    res = f(*args,**kw)\",\n      \"  File \\\"/usr/local/namesurfer/webui2/webui/service/service10/NSService_10.py\\\", line 502, in addDNSRecord\",\n      \"    key = validate_key(keyname, digest, [zonename, viewname, record.name, record.type, str(record.ttl), record.data])\",\n      \"  File \\\"/usr/local/namesurfer/webui2/webui/service/base/implementation.py\\\", line 63, in validate_key\",\n      \"    raise ApiFault('Server.Keyfailure', 'Unknown keyname %s' % keyname)\",\n      \"ApiFault: service(13): Unknown keyname user\",\n      \"\"\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/namesurfer/internal/fixtures/listZones-request.json",
    "content": "{\n  \"id\": 1,\n  \"method\": \"listZones\",\n  \"params\": [\n    \"user\",\n    \"2739461ea1a3dc51302993f724f40228409c53b78025d8d7b1d7bba3c1bf2d66\",\n    \"value\"\n  ]\n}\n"
  },
  {
    "path": "providers/dns/namesurfer/internal/fixtures/listZones.json",
    "content": "{\n  \"id\": 1,\n  \"result\": [\n    {\n      \"name\": \"example.com\",\n      \"view\": \"viewA\"\n    },\n    {\n      \"name\": \"example.org\",\n      \"view\": \"viewB\"\n    },\n    {\n      \"name\": \"example.net\",\n      \"view\": \"viewC\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/namesurfer/internal/fixtures/searchDNSHosts-request.json",
    "content": "{\n  \"id\": 1,\n  \"method\": \"searchDNSHosts\",\n  \"params\": [\n    \"user\",\n    \"02cf1a2f6e124507d16738d595f583932185313fc96afc2d8404960acaec29b4\",\n    \"value\"\n  ]\n}\n"
  },
  {
    "path": "providers/dns/namesurfer/internal/fixtures/searchDNSHosts.json",
    "content": "{\n  \"id\": 1,\n  \"result\": [\n    {\n      \"name\": \"foo\",\n      \"type\": \"TXT\",\n      \"data\": \"xxx\",\n      \"ttl\": 300\n    },\n    {\n      \"name\": \"_acme-challenge\",\n      \"type\": \"TXT\",\n      \"data\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n      \"ttl\": 300\n    },\n    {\n      \"name\": \"bar\",\n      \"type\": \"A\",\n      \"data\": \"yyy\",\n      \"ttl\": 300\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/namesurfer/internal/fixtures/updateDNSHost-request.json",
    "content": "{\n  \"id\": 1,\n  \"method\": \"updateDNSHost\",\n  \"params\": [\n    \"user\",\n    \"510e63288ac874c1d5ba313a9411591daa346e5621fb0153263adc278794e378\",\n    \"example.com\",\n    \"viewA\",\n    {\n      \"name\": \"_acme-challenge\",\n      \"type\": \"TXT\",\n      \"data\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n      \"ttl\": 300\n    },\n    {\n      \"name\": \"\",\n      \"type\": \"\",\n      \"data\": \"\",\n      \"ttl\": 0\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/namesurfer/internal/fixtures/updateDNSHost.json",
    "content": "{\n  \"id\": 1,\n  \"result\": true\n}\n"
  },
  {
    "path": "providers/dns/namesurfer/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n)\n\n// DNSNode represents a DNS record.\n// http://95.128.3.201:8053/API/NSService_10#DNSNode\ntype DNSNode struct {\n\tName string `json:\"name\"`\n\tType string `json:\"type\"`\n\tData string `json:\"data\"`\n\tTTL  int    `json:\"ttl\"`\n}\n\n// DNSZone represents a DNS zone.\n// http://95.128.3.201:8053/API/NSService_10#DNSZone\ntype DNSZone struct {\n\tName string `json:\"name,omitempty\"`\n\tView string `json:\"view,omitempty\"`\n}\n\n// APIRequest represents a JSON-RPC request.\n// https://www.jsonrpc.org/specification_v1#a1.1Requestmethodinvocation\ntype APIRequest struct {\n\tID     any    `json:\"id\"` // Can be int or string depending on API\n\tMethod string `json:\"method\"`\n\tParams []any  `json:\"params\"`\n}\n\n// APIResponse represents a JSON-RPC response.\n// https://www.jsonrpc.org/specification_v1#a1.2Response\ntype APIResponse struct {\n\tID     any             `json:\"id\"` // Can be int or string depending on API\n\tResult json.RawMessage `json:\"result\"`\n\tError  *APIError       `json:\"error\"`\n}\n\n// APIError represents an error.\ntype APIError struct {\n\tCode       any      `json:\"code\"` // Can be int or string depending on API\n\tFilename   string   `json:\"filename\"`\n\tLineNumber int      `json:\"lineno\"`\n\tMessage    string   `json:\"string\"`\n\tDetail     []string `json:\"detail\"`\n}\n\nfunc (e *APIError) Error() string {\n\tmsg := new(strings.Builder)\n\n\t_, _ = fmt.Fprintf(msg, \"code: %v\", e.Code)\n\n\tif e.Filename != \"\" {\n\t\t_, _ = fmt.Fprintf(msg, \", filename: %s\", e.Filename)\n\t}\n\n\tif e.LineNumber > 0 {\n\t\t_, _ = fmt.Fprintf(msg, \", line: %d\", e.LineNumber)\n\t}\n\n\tif e.Message != \"\" {\n\t\t_, _ = fmt.Fprintf(msg, \", message: %s\", e.Message)\n\t}\n\n\tif len(e.Detail) > 0 {\n\t\t_, _ = fmt.Fprintf(msg, \", detail: %v\", strings.Join(e.Detail, \" \"))\n\t}\n\n\treturn msg.String()\n}\n"
  },
  {
    "path": "providers/dns/namesurfer/namesurfer.go",
    "content": "// Package namesurfer implements a DNS provider for solving the DNS-01 challenge using FusionLayer NameSurfer API.\npackage namesurfer\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/namesurfer/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"NAMESURFER_\"\n\n\tEnvBaseURL   = envNamespace + \"BASE_URL\"\n\tEnvAPIKey    = envNamespace + \"API_KEY\"\n\tEnvAPISecret = envNamespace + \"API_SECRET\"\n\tEnvView      = envNamespace + \"VIEW\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvInsecureSkipVerify = envNamespace + \"INSECURE_SKIP_VERIFY\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL   string\n\tAPIKey    string\n\tAPISecret string\n\tView      string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\tzones   map[string]string\n\tzonesMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for FusionLayer NameSurfer.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvBaseURL, EnvAPIKey, EnvAPISecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"namesurfer: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.BaseURL = values[EnvBaseURL]\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.APISecret = values[EnvAPISecret]\n\tconfig.View = env.GetOrDefaultString(EnvView, \"\")\n\n\tif env.GetOrDefaultBool(EnvInsecureSkipVerify, false) {\n\t\tconfig.HTTPClient.Transport = &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t}\n\t}\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for FusionLayer NameSurfer.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"namesurfer: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.BaseURL, config.APIKey, config.APISecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"namesurfer: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t\tzones:  make(map[string]string),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.findZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namesurfer: %w\", err)\n\t}\n\n\td.zonesMu.Lock()\n\td.zones[token] = zone\n\td.zonesMu.Unlock()\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namesurfer: %w\", err)\n\t}\n\n\trecord := internal.DNSNode{\n\t\tName: subDomain,\n\t\tType: \"TXT\",\n\t\tTTL:  d.config.TTL,\n\t\tData: info.Value,\n\t}\n\n\terr = d.client.AddDNSRecord(ctx, zone, d.config.View, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namesurfer: add DNS record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.zonesMu.Lock()\n\tzone, ok := d.zones[token]\n\td.zonesMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"namesurfer: unknown zone for '%s'\", info.EffectiveFQDN)\n\t}\n\n\td.zonesMu.Lock()\n\tdelete(d.zones, token)\n\td.zonesMu.Unlock()\n\n\texisting, err := d.client.SearchDNSHosts(ctx, dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"namesurfer: search DNS hosts: %w\", err)\n\t}\n\n\tfor _, node := range existing {\n\t\tif node.Type != \"TXT\" || node.Data != info.Value {\n\t\t\tcontinue\n\t\t}\n\n\t\terr = d.client.UpdateDNSHost(ctx, zone, d.config.View, node, internal.DNSNode{})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"namesurfer: update DNS host: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) findZone(ctx context.Context, fqdn string) (string, error) {\n\tzones, err := d.client.ListZones(ctx, \"forward\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"list zones: %w\", err)\n\t}\n\n\tdomain := dns01.UnFqdn(fqdn)\n\n\tvar zoneName string\n\n\tfor _, zone := range zones {\n\t\tif strings.HasSuffix(domain, zone.Name) && len(zone.Name) > len(zoneName) {\n\t\t\tzoneName = zone.Name\n\t\t}\n\t}\n\n\tif zoneName == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no zone found for %s\", fqdn)\n\t}\n\n\treturn zoneName, nil\n}\n"
  },
  {
    "path": "providers/dns/namesurfer/namesurfer.toml",
    "content": "Name = \"FusionLayer NameSurfer\"\nDescription = ''''''\nURL = \"https://www.fusionlayer.com/\"\nCode = \"namesurfer\"\nSince = \"v4.32.0\"\n\nExample = '''\nNAMESURFER_BASE_URL=https://foo.example.com:8443/API/NSService_10 \\\nNAMESURFER_API_KEY=xxx \\\nNAMESURFER_API_SECRET=yyy \\\nlego --dns namesurfer -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    NAMESURFER_BASE_URL = \"The base URL of NameSurfer API (jsonrpc10) endpoint URL (e.g., https://foo.example.com:8443/API/NSService_10)\"\n    NAMESURFER_API_KEY = \"API key name\"\n    NAMESURFER_API_SECRET = \"API secret\"\n  [Configuration.Additional]\n    NAMESURFER_VIEW = \"DNS view name (optional, default: empty string)\"\n    NAMESURFER_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    NAMESURFER_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    NAMESURFER_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    NAMESURFER_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n    NAMESURFER_INSECURE_SKIP_VERIFY = \"Whether to verify the API certificate\"\n\n[Links]\n  API = \"https://web.archive.org/web/20260213170737/http://95.128.3.201:8053/API/NSService_10\"\n"
  },
  {
    "path": "providers/dns/namesurfer/namesurfer_test.go",
    "content": "package namesurfer\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvBaseURL,\n\tEnvAPIKey,\n\tEnvAPISecret,\n\tEnvView,\n).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvBaseURL:   \"https://example.com\",\n\t\t\t\tEnvAPIKey:    \"user\",\n\t\t\t\tEnvAPISecret: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing base URL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvBaseURL:   \"\",\n\t\t\t\tEnvAPIKey:    \"user\",\n\t\t\t\tEnvAPISecret: \"secret\",\n\t\t\t},\n\t\t\texpected: \"namesurfer: some credentials information are missing: NAMESURFER_BASE_URL\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvBaseURL:   \"https://example.com\",\n\t\t\t\tEnvAPIKey:    \"\",\n\t\t\t\tEnvAPISecret: \"secret\",\n\t\t\t},\n\t\t\texpected: \"namesurfer: some credentials information are missing: NAMESURFER_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvBaseURL:   \"https://example.com\",\n\t\t\t\tEnvAPIKey:    \"user\",\n\t\t\t\tEnvAPISecret: \"\",\n\t\t\t},\n\t\t\texpected: \"namesurfer: some credentials information are missing: NAMESURFER_API_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"namesurfer: some credentials information are missing: NAMESURFER_BASE_URL,NAMESURFER_API_KEY,NAMESURFER_API_SECRET\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tbaseURL   string\n\t\tapiKey    string\n\t\tapiSecret string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tbaseURL:   \"https://example.com\",\n\t\t\tapiKey:    \"user\",\n\t\t\tapiSecret: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing base URL\",\n\t\t\tapiKey:    \"user\",\n\t\t\tapiSecret: \"secret\",\n\t\t\texpected:  \"namesurfer: base URL missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing API key\",\n\t\t\tbaseURL:   \"https://example.com\",\n\t\t\tapiSecret: \"secret\",\n\t\t\texpected:  \"namesurfer: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API secret\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\tapiKey:   \"user\",\n\t\t\texpected: \"namesurfer: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"namesurfer: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.BaseURL = test.baseURL\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.APISecret = test.apiSecret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/nearlyfreespeech/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\nconst apiURL = \"https://api.nearlyfreespeech.net\"\n\nconst authenticationHeader = \"X-NFSN-Authentication\"\n\nconst saltBytes = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"\n\ntype Client struct {\n\tlogin  string\n\tapiKey string\n\n\tsigner *Signer\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(login, apiKey string) *Client {\n\tbaseURL, _ := url.Parse(apiURL)\n\n\treturn &Client{\n\t\tlogin:      login,\n\t\tapiKey:     apiKey,\n\t\tsigner:     NewSigner(),\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}\n}\n\nfunc (c *Client) AddRecord(ctx context.Context, domain string, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"dns\", dns01.UnFqdn(domain), \"addRR\")\n\n\tparams, err := querystring.Values(record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.doRequest(ctx, endpoint, params)\n}\n\nfunc (c *Client) RemoveRecord(ctx context.Context, domain string, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"dns\", dns01.UnFqdn(domain), \"removeRR\")\n\n\tparams, err := querystring.Values(record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.doRequest(ctx, endpoint, params)\n}\n\nfunc (c *Client) doRequest(ctx context.Context, endpoint *url.URL, params url.Values) error {\n\tpayload := params.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(payload))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(authenticationHeader, c.signer.Sign(endpoint.Path, payload, c.login, c.apiKey))\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn parseError(req, resp)\n\t}\n\n\treturn nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\terrAPI := &APIError{}\n\n\terr := json.Unmarshal(raw, errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn errAPI\n}\n\ntype Signer struct {\n\tsaltShaker func() []byte\n\tclock      func() time.Time\n}\n\nfunc NewSigner() *Signer {\n\treturn &Signer{saltShaker: getRandomSalt, clock: time.Now}\n}\n\nfunc (c Signer) Sign(uri, body, login, apiKey string) string {\n\t// Header is \"login;timestamp;salt;hash\".\n\t// hash is SHA1(\"login;timestamp;salt;api-key;request-uri;body-hash\")\n\t// and body-hash is SHA1(body).\n\tbodyHash := sha1.Sum([]byte(body))\n\ttimestamp := strconv.FormatInt(c.clock().Unix(), 10)\n\n\t// Workaround for https://golang.org/issue/58605\n\turi = \"/\" + strings.TrimLeft(uri, \"/\")\n\n\tsalt := c.saltShaker()\n\n\thashInput := fmt.Sprintf(\"%s;%s;%s;%s;%s;%02x\", login, timestamp, salt, apiKey, uri, bodyHash)\n\n\treturn fmt.Sprintf(\"%s;%s;%s;%02x\", login, timestamp, salt, sha1.Sum([]byte(hashInput)))\n}\n\nfunc getRandomSalt() []byte {\n\t// This is the only part of this that needs to be serialized.\n\tsalt := make([]byte, 16)\n\tfor i := range 16 {\n\t\tsalt[i] = saltBytes[rand.Intn(len(saltBytes))]\n\t}\n\n\treturn salt\n}\n"
  },
  {
    "path": "providers/dns/nearlyfreespeech/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"user\", \"secret\")\n\tclient.HTTPClient = server.Client()\n\tclient.baseURL, _ = url.Parse(server.URL)\n\n\tclient.signer.saltShaker = func() []byte { return []byte(\"0123456789ABCDEF\") }\n\tclient.signer.clock = func() time.Time { return time.Unix(1692475113, 0) }\n\n\treturn client, nil\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded().\n\t\t\tWith(authenticationHeader, \"user;1692475113;0123456789ABCDEF;24a32faf74c7bd0525f560ff12a1c1fb6545bafc\"),\n\t).\n\t\tRoute(\"POST /dns/example.com/addRR\", nil, servermock.CheckForm().Strict().\n\t\t\tWith(\"data\", \"txtTXTtxt\").\n\t\t\tWith(\"name\", \"sub\").\n\t\t\tWith(\"type\", \"TXT\").\n\t\t\tWith(\"ttl\", \"30\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName: \"sub\",\n\t\tType: \"TXT\",\n\t\tData: \"txtTXTtxt\",\n\t\tTTL:  30,\n\t}\n\n\terr := client.AddRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded().\n\t\t\tWith(authenticationHeader, \"user;1692475113;0123456789ABCDEF;24a32faf74c7bd0525f560ff12a1c1fb6545bafc\"),\n\t).\n\t\tRoute(\"POST /dns/example.com/addRR\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName: \"sub\",\n\t\tType: \"TXT\",\n\t\tData: \"txtTXTtxt\",\n\t\tTTL:  30,\n\t}\n\n\terr := client.AddRecord(t.Context(), \"example.com\", record)\n\trequire.Error(t, err)\n}\n\nfunc TestClient_RemoveRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded().\n\t\t\tWith(authenticationHeader, \"user;1692475113;0123456789ABCDEF;699f01f077ca487bd66ac370d6dfc5b122c65522\"),\n\t).\n\t\tRoute(\"POST /dns/example.com/removeRR\", nil,\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"data\", \"txtTXTtxt\").\n\t\t\t\tWith(\"name\", \"sub\").\n\t\t\t\tWith(\"type\", \"TXT\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName: \"sub\",\n\t\tType: \"TXT\",\n\t\tData: \"txtTXTtxt\",\n\t}\n\n\terr := client.RemoveRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_RemoveRecord_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded().\n\t\t\tWith(authenticationHeader, \"user;1692475113;0123456789ABCDEF;699f01f077ca487bd66ac370d6dfc5b122c65522\"),\n\t).\n\t\tRoute(\"POST /dns/example.com/removeRR\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName: \"sub\",\n\t\tType: \"TXT\",\n\t\tData: \"txtTXTtxt\",\n\t}\n\n\terr := client.RemoveRecord(t.Context(), \"example.com\", record)\n\trequire.Error(t, err)\n}\n\nfunc TestSigner_Sign(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tpath     string\n\t\tnow      int64\n\t\tsalt     string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"basic\",\n\t\t\tpath:     \"/path\",\n\t\t\tnow:      1692475113,\n\t\t\tsalt:     \"0123456789ABCDEF\",\n\t\t\texpected: \"user;1692475113;0123456789ABCDEF;417a9988c7ad7919b297884dd120b5808d8a1e6f\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"another date\",\n\t\t\tpath:     \"/path\",\n\t\t\tnow:      1692567766,\n\t\t\tsalt:     \"0123456789ABCDEF\",\n\t\t\texpected: \"user;1692567766;0123456789ABCDEF;b5c28286fd2e1a45a7c576dc2a6430116f721502\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"another salt\",\n\t\t\tpath:     \"/path\",\n\t\t\tnow:      1692475113,\n\t\t\tsalt:     \"FEDCBA9876543210\",\n\t\t\texpected: \"user;1692475113;FEDCBA9876543210;0f766822bda4fdc09829be4e1ea5e27ae3ae334e\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"empty path\",\n\t\t\tpath:     \"\",\n\t\t\tnow:      1692475113,\n\t\t\tsalt:     \"0123456789ABCDEF\",\n\t\t\texpected: \"user;1692475113;0123456789ABCDEF;c7c241a4d15d04d92805631d58d4d72ac1c339a1\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"root path\",\n\t\t\tpath:     \"/\",\n\t\t\tnow:      1692475113,\n\t\t\tsalt:     \"0123456789ABCDEF\",\n\t\t\texpected: \"user;1692475113;0123456789ABCDEF;c7c241a4d15d04d92805631d58d4d72ac1c339a1\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tsigner := NewSigner()\n\t\t\tsigner.saltShaker = func() []byte { return []byte(test.salt) }\n\t\t\tsigner.clock = func() time.Time { return time.Unix(test.now, 0) }\n\n\t\t\tsign := signer.Sign(test.path, \"data\", \"user\", \"secret\")\n\n\t\t\tassert.Equal(t, test.expected, sign)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/nearlyfreespeech/internal/fixtures/error.json",
    "content": "{\n  \"error\": \"The API request could not be authenticated.\",\n  \"debug\": \"The X-NFSN-Authentication header is not present.\"\n}\n"
  },
  {
    "path": "providers/dns/nearlyfreespeech/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype Record struct {\n\tName string `url:\"name,omitempty\"`\n\tType string `url:\"type,omitempty\"`\n\tData string `url:\"data,omitempty\"`\n\tTTL  int    `url:\"ttl,omitempty\"`\n}\n\ntype APIError struct {\n\tMessage string `json:\"error\"`\n\tDebug   string `json:\"debug\"`\n}\n\nfunc (a APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", a.Message, a.Debug)\n}\n"
  },
  {
    "path": "providers/dns/nearlyfreespeech/nearlyfreespeech.go",
    "content": "// Package nearlyfreespeech implements a DNS provider for solving the DNS-01 challenge using NearlyFreeSpeech.NET.\npackage nearlyfreespeech\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"NEARLYFREESPEECH_\"\n\n\tEnvLogin  = envNamespace + \"LOGIN\"\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey string\n\tLogin  string\n\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 3600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for NearlyFreeSpeech.NET.\n// Credentials must be passed in the environment variable: NEARLYFREESPEECH_LOGIN, NEARLYFREESPEECH_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey, EnvLogin)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"nearlyfreespeech: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.Login = values[EnvLogin]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for NearlyFreeSpeech.NET.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"nearlyfreespeech: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Login == \"\" || config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"nearlyfreespeech: API credentials are missing\")\n\t}\n\n\tclient := internal.NewClient(config.Login, config.APIKey)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nearlyfreespeech: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecordName, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nearlyfreespeech: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName: recordName,\n\t\tType: \"TXT\",\n\t\tData: info.Value,\n\t\tTTL:  d.config.TTL,\n\t}\n\n\terr = d.client.AddRecord(context.Background(), authZone, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nearlyfreespeech: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nearlyfreespeech: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecordName, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nearlyfreespeech: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName: recordName,\n\t\tType: \"TXT\",\n\t\tData: info.Value,\n\t}\n\n\terr = d.client.RemoveRecord(context.Background(), domain, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nearlyfreespeech: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/nearlyfreespeech/nearlyfreespeech.toml",
    "content": "Name = \"NearlyFreeSpeech.NET\"\nDescription = ''''''\nURL = \"https://nearlyfreespeech.net/\"\nCode = \"nearlyfreespeech\"\nSince = \"v4.8.0\"\n\nExample = '''\nNEARLYFREESPEECH_API_KEY=xxxxxx \\\nNEARLYFREESPEECH_LOGIN=xxxx \\\nlego --dns nearlyfreespeech -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    NEARLYFREESPEECH_API_KEY = \"API Key for API requests\"\n    NEARLYFREESPEECH_LOGIN = \"Username for API requests\"\n  [Configuration.Additional]\n    NEARLYFREESPEECH_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    NEARLYFREESPEECH_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    NEARLYFREESPEECH_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)\"\n    NEARLYFREESPEECH_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n    NEARLYFREESPEECH_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://members.nearlyfreespeech.net/wiki/API/Reference\"\n"
  },
  {
    "path": "providers/dns/nearlyfreespeech/nearlyfreespeech_test.go",
    "content": "package nearlyfreespeech\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey, EnvLogin).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t\tEnvLogin:  \"testuser\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t\tEnvLogin:  \"\",\n\t\t\t},\n\t\t\texpected: \"nearlyfreespeech: some credentials information are missing: NEARLYFREESPEECH_API_KEY,NEARLYFREESPEECH_LOGIN\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t\tEnvLogin:  \"testuser\",\n\t\t\t},\n\t\t\texpected: \"nearlyfreespeech: some credentials information are missing: NEARLYFREESPEECH_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing login\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t\tEnvLogin:  \"\",\n\t\t\t},\n\t\t\texpected: \"nearlyfreespeech: some credentials information are missing: NEARLYFREESPEECH_LOGIN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tlogin    string\n\t\tapikey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tlogin:  \"login\",\n\t\t\tapikey: \"apikey\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"nearlyfreespeech: API credentials are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing login\",\n\t\t\tlogin:    \"\",\n\t\t\tapikey:   \"apikey\",\n\t\t\texpected: \"nearlyfreespeech: API credentials are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing key\",\n\t\t\tlogin:    \"login\",\n\t\t\tapikey:   \"\",\n\t\t\texpected: \"nearlyfreespeech: API credentials are missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apikey\n\t\t\tconfig.Login = test.login\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/neodigit/neodigit.go",
    "content": "// Package neodigit implements a DNS provider for solving the DNS-01 challenge using Neodigit DNS.\npackage neodigit\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/tecnocratica\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"NEODIGIT_\"\n\n\tEnvToken = envNamespace + \"TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultBaseURL = \"https://api.neodigit.net/v1\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config = tecnocratica.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Neodigit.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"neodigit: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Neodigit.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"neodigit: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := tecnocratica.NewDNSProviderConfig(config, defaultBaseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"neodigit: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"neodigit: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"neodigit: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n"
  },
  {
    "path": "providers/dns/neodigit/neodigit.toml",
    "content": "Name = \"Neodigit\"\nDescription = ''''''\nURL = \"https://www.neodigit.net\"\nCode = \"neodigit\"\nSince = \"v4.30.0\"\n\nExample = '''\nNEODIGIT_TOKEN=xxxxxx \\\nlego --dns neodigit -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    NEODIGIT_TOKEN = \"API token\"\n  [Configuration.Additional]\n    NEODIGIT_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    NEODIGIT_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 300)\"\n    NEODIGIT_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    NEODIGIT_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://developers.neodigit.net/#dns\"\n"
  },
  {
    "path": "providers/dns/neodigit/neodigit_test.go",
    "content": "package neodigit\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials: token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"\",\n\t\t\t},\n\t\t\texpected: \"neodigit: some credentials information are missing: NEODIGIT_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing token\",\n\t\t\texpected: \"neodigit: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// defaultBaseURL for reaching the jSON-based API-Endpoint of netcup.\nconst defaultBaseURL = \"https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON\"\n\n// Client netcup DNS client.\ntype Client struct {\n\tcustomerNumber string\n\tapiKey         string\n\tapiPassword    string\n\n\tbaseURL    string\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a netcup DNS client.\nfunc NewClient(customerNumber, apiKey, apiPassword string) (*Client, error) {\n\tif customerNumber == \"\" || apiKey == \"\" || apiPassword == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\treturn &Client{\n\t\tcustomerNumber: customerNumber,\n\t\tapiKey:         apiKey,\n\t\tapiPassword:    apiPassword,\n\t\tbaseURL:        defaultBaseURL,\n\t\tHTTPClient:     &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// UpdateDNSRecord performs an update of the DNSRecords as specified by the netcup WSDL.\n// https://ccp.netcup.net/run/webservice/servers/endpoint.php\nfunc (c *Client) UpdateDNSRecord(ctx context.Context, domainName string, records []DNSRecord) error {\n\tpayload := &Request{\n\t\tAction: \"updateDnsRecords\",\n\t\tParam: UpdateDNSRecordsRequest{\n\t\t\tDomainName:      domainName,\n\t\t\tCustomerNumber:  c.customerNumber,\n\t\t\tAPIKey:          c.apiKey,\n\t\t\tAPISessionID:    getSessionID(ctx),\n\t\t\tClientRequestID: \"\",\n\t\t\tDNSRecordSet:    DNSRecordSet{DNSRecords: records},\n\t\t},\n\t}\n\n\terr := c.doRequest(ctx, payload, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error when sending the request: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// GetDNSRecords retrieves all dns records of an DNS-Zone as specified by the netcup WSDL\n// returns an array of DNSRecords.\n// https://ccp.netcup.net/run/webservice/servers/endpoint.php\nfunc (c *Client) GetDNSRecords(ctx context.Context, hostname string) ([]DNSRecord, error) {\n\tpayload := &Request{\n\t\tAction: \"infoDnsRecords\",\n\t\tParam: InfoDNSRecordsRequest{\n\t\t\tDomainName:      hostname,\n\t\t\tCustomerNumber:  c.customerNumber,\n\t\t\tAPIKey:          c.apiKey,\n\t\t\tAPISessionID:    getSessionID(ctx),\n\t\t\tClientRequestID: \"\",\n\t\t},\n\t}\n\n\tvar responseData InfoDNSRecordsResponse\n\n\terr := c.doRequest(ctx, payload, &responseData)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error when sending the request: %w\", err)\n\t}\n\n\treturn responseData.DNSRecords, nil\n}\n\n// doRequest marshals given body to JSON, send the request to netcup API\n// and returns body of response.\nfunc (c *Client) doRequest(ctx context.Context, payload, result any) error {\n\treq, err := newJSONRequest(ctx, http.MethodPost, c.baseURL, payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Close = true\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode >= http.StatusMultipleChoices {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\trespMsg, err := unmarshalResponseMsg(req, resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif respMsg.Status != success {\n\t\treturn respMsg\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\terr = json.Unmarshal(respMsg.ResponseData, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, respMsg.ResponseData, err)\n\t}\n\n\treturn nil\n}\n\n// GetDNSRecordIdx searches a given array of DNSRecords for a given DNSRecord\n// equivalence is determined by Destination and RecortType attributes\n// returns index of given DNSRecord in given array of DNSRecords.\nfunc GetDNSRecordIdx(records []DNSRecord, record DNSRecord) (int, error) {\n\tfor index, element := range records {\n\t\tif record.Destination == element.Destination && record.RecordType == element.RecordType {\n\t\t\treturn index, nil\n\t\t}\n\t}\n\n\treturn -1, errors.New(\"no DNS Record found\")\n}\n\nfunc newJSONRequest(ctx context.Context, method, endpoint string, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint, buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc unmarshalResponseMsg(req *http.Request, resp *http.Response) (*ResponseMsg, error) {\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar respMsg ResponseMsg\n\n\terr = json.Unmarshal(raw, &respMsg)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn &respMsg, nil\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/client_live_test.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar envTest = tester.NewEnvTest(\n\t\"NETCUP_CUSTOMER_NUMBER\",\n\t\"NETCUP_API_KEY\",\n\t\"NETCUP_API_PASSWORD\").\n\tWithDomain(\"NETCUP_DOMAIN\")\n\nfunc TestClient_GetDNSRecords_Live(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\t// Setup\n\tenvTest.RestoreEnv()\n\n\tclient, err := NewClient(\n\t\tenvTest.GetValue(\"NETCUP_CUSTOMER_NUMBER\"),\n\t\tenvTest.GetValue(\"NETCUP_API_KEY\"),\n\t\tenvTest.GetValue(\"NETCUP_API_PASSWORD\"))\n\trequire.NoError(t, err)\n\n\tctx, err := client.CreateSessionContext(t.Context())\n\trequire.NoError(t, err)\n\n\tinfo := dns01.GetChallengeInfo(envTest.GetDomain(), \"123d==\")\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\trequire.NoError(t, err)\n\n\tzone = dns01.UnFqdn(zone)\n\n\t// TestMethod\n\t_, err = client.GetDNSRecords(ctx, zone)\n\trequire.NoError(t, err)\n\n\t// Tear down\n\terr = client.Logout(ctx)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_UpdateDNSRecord_Live(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\t// Setup\n\tenvTest.RestoreEnv()\n\n\tclient, err := NewClient(\n\t\tenvTest.GetValue(\"NETCUP_CUSTOMER_NUMBER\"),\n\t\tenvTest.GetValue(\"NETCUP_API_KEY\"),\n\t\tenvTest.GetValue(\"NETCUP_API_PASSWORD\"))\n\trequire.NoError(t, err)\n\n\tctx, err := client.CreateSessionContext(t.Context())\n\trequire.NoError(t, err)\n\n\tinfo := dns01.GetChallengeInfo(envTest.GetDomain(), \"123d==\")\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\trequire.NotErrorIs(t, err, fmt.Errorf(\"error finding DNSZone, %w\", err))\n\n\thostname := strings.Replace(info.EffectiveFQDN, \".\"+zone, \"\", 1)\n\n\trecord := DNSRecord{\n\t\tHostname:     hostname,\n\t\tRecordType:   \"TXT\",\n\t\tDestination:  \"asdf5678\",\n\t\tDeleteRecord: false,\n\t}\n\n\t// test\n\tzone = dns01.UnFqdn(zone)\n\n\terr = client.UpdateDNSRecord(ctx, zone, []DNSRecord{record})\n\trequire.NoError(t, err)\n\n\trecords, err := client.GetDNSRecords(ctx, zone)\n\trequire.NoError(t, err)\n\n\trecordIdx, err := GetDNSRecordIdx(records, record)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, record.Hostname, records[recordIdx].Hostname)\n\tassert.Equal(t, record.RecordType, records[recordIdx].RecordType)\n\tassert.Equal(t, record.Destination, records[recordIdx].Destination)\n\tassert.Equal(t, record.DeleteRecord, records[recordIdx].DeleteRecord)\n\n\trecords[recordIdx].DeleteRecord = true\n\n\t// Tear down\n\terr = client.UpdateDNSRecord(ctx, envTest.GetDomain(), []DNSRecord{records[recordIdx]})\n\trequire.NoError(t, err)\n\n\terr = client.Logout(ctx)\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveClientAuth(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\t// Setup\n\tenvTest.RestoreEnv()\n\n\tclient, err := NewClient(\n\t\tenvTest.GetValue(\"NETCUP_CUSTOMER_NUMBER\"),\n\t\tenvTest.GetValue(\"NETCUP_API_KEY\"),\n\t\tenvTest.GetValue(\"NETCUP_API_PASSWORD\"))\n\trequire.NoError(t, err)\n\n\tfor i := range 4 {\n\t\tt.Run(\"Test_\"+strconv.Itoa(i+1), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tctx, err := client.CreateSessionContext(t.Context())\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = client.Logout(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"a\", \"b\", \"c\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.baseURL = server.URL\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t)\n}\n\nfunc TestGetDNSRecordIdx(t *testing.T) {\n\trecords := []DNSRecord{\n\t\t{\n\t\t\tID:           12345,\n\t\t\tHostname:     \"asdf\",\n\t\t\tRecordType:   \"TXT\",\n\t\t\tPriority:     \"0\",\n\t\t\tDestination:  \"randomtext\",\n\t\t\tDeleteRecord: false,\n\t\t\tState:        \"yes\",\n\t\t},\n\t\t{\n\t\t\tID:           23456,\n\t\t\tHostname:     \"@\",\n\t\t\tRecordType:   \"A\",\n\t\t\tPriority:     \"0\",\n\t\t\tDestination:  \"127.0.0.1\",\n\t\t\tDeleteRecord: false,\n\t\t\tState:        \"yes\",\n\t\t},\n\t\t{\n\t\t\tID:           34567,\n\t\t\tHostname:     \"dfgh\",\n\t\t\tRecordType:   \"CNAME\",\n\t\t\tPriority:     \"0\",\n\t\t\tDestination:  \"example.com\",\n\t\t\tDeleteRecord: false,\n\t\t\tState:        \"yes\",\n\t\t},\n\t\t{\n\t\t\tID:           45678,\n\t\t\tHostname:     \"fghj\",\n\t\t\tRecordType:   \"MX\",\n\t\t\tPriority:     \"10\",\n\t\t\tDestination:  \"mail.example.com\",\n\t\t\tDeleteRecord: false,\n\t\t\tState:        \"yes\",\n\t\t},\n\t}\n\n\ttestCases := []struct {\n\t\tdesc        string\n\t\trecord      DNSRecord\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tdesc: \"simple\",\n\t\t\trecord: DNSRecord{\n\t\t\t\tID:           12345,\n\t\t\t\tHostname:     \"asdf\",\n\t\t\t\tRecordType:   \"TXT\",\n\t\t\t\tPriority:     \"0\",\n\t\t\t\tDestination:  \"randomtext\",\n\t\t\t\tDeleteRecord: false,\n\t\t\t\tState:        \"yes\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"wrong Destination\",\n\t\t\trecord: DNSRecord{\n\t\t\t\tID:           12345,\n\t\t\t\tHostname:     \"asdf\",\n\t\t\t\tRecordType:   \"TXT\",\n\t\t\t\tPriority:     \"0\",\n\t\t\t\tDestination:  \"wrong\",\n\t\t\t\tDeleteRecord: false,\n\t\t\t\tState:        \"yes\",\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"record type CNAME\",\n\t\t\trecord: DNSRecord{\n\t\t\t\tID:           12345,\n\t\t\t\tHostname:     \"asdf\",\n\t\t\t\tRecordType:   \"CNAME\",\n\t\t\t\tPriority:     \"0\",\n\t\t\t\tDestination:  \"randomtext\",\n\t\t\t\tDeleteRecord: false,\n\t\t\t\tState:        \"yes\",\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tidx, err := GetDNSRecordIdx(records, test.record)\n\t\t\tif test.expectError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Equal(t, -1, idx)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, records[idx], test.record)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_GetDNSRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"get_dns_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"get_dns_records-request.json\")).\n\t\tBuild(t)\n\n\texpected := []DNSRecord{{\n\t\tID:           1,\n\t\tHostname:     \"example.com\",\n\t\tRecordType:   \"TXT\",\n\t\tPriority:     \"1\",\n\t\tDestination:  \"bGVnbzE=\",\n\t\tDeleteRecord: false,\n\t\tState:        \"yes\",\n\t}, {\n\t\tID:           2,\n\t\tHostname:     \"example2.com\",\n\t\tRecordType:   \"TXT\",\n\t\tPriority:     \"1\",\n\t\tDestination:  \"bGVnbw==\",\n\t\tDeleteRecord: false,\n\t\tState:        \"yes\",\n\t}}\n\n\trecords, err := client.GetDNSRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetDNSRecords_errors(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\thandler  http.Handler\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"HTTP error\",\n\t\t\thandler:  servermock.Noop().WithStatusCode(http.StatusInternalServerError),\n\t\t\texpected: `error when sending the request: unexpected status code: [status code: 500] body: `,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"API error\",\n\t\t\thandler:  servermock.ResponseFromFixture(\"get_dns_records_error.json\"),\n\t\t\texpected: `error when sending the request: an error occurred during the action infoDnsRecords: [Status=error, StatusCode=4013, ShortMessage=Validation Error., LongMessage=Message is empty.]`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"responsedata marshaling error\",\n\t\t\thandler:  servermock.ResponseFromFixture(\"get_dns_records_error_unmarshal.json\"),\n\t\t\texpected: `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`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(\"POST /\", test.handler).\n\t\t\t\tBuild(t)\n\n\t\t\trecords, err := client.GetDNSRecords(t.Context(), \"example.com\")\n\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\tassert.Empty(t, records)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/fixtures/get_dns_records-request.json",
    "content": "{\n  \"action\": \"infoDnsRecords\",\n  \"param\": {\n    \"domainname\": \"example.com\",\n    \"customernumber\": \"a\",\n    \"apikey\": \"b\",\n    \"apisessionid\": \"\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/fixtures/get_dns_records.json",
    "content": "{\n  \"serverrequestid\": \"srv-request-id\",\n  \"clientrequestid\": \"\",\n  \"action\": \"infoDnsRecords\",\n  \"status\": \"success\",\n  \"statuscode\": 2000,\n  \"shortmessage\": \"Login successful\",\n  \"longmessage\": \"Session has been created successful.\",\n  \"responsedata\": {\n    \"apisessionid\": \"api-session-id\",\n    \"dnsrecords\": [\n      {\n        \"id\": \"1\",\n        \"hostname\": \"example.com\",\n        \"type\": \"TXT\",\n        \"priority\": \"1\",\n        \"destination\": \"bGVnbzE=\",\n        \"state\": \"yes\",\n        \"ttl\": 300\n      },\n      {\n        \"id\": \"2\",\n        \"hostname\": \"example2.com\",\n        \"type\": \"TXT\",\n        \"priority\": \"1\",\n        \"destination\": \"bGVnbw==\",\n        \"state\": \"yes\",\n        \"ttl\": 300\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/fixtures/get_dns_records_error.json",
    "content": "{\n  \"serverrequestid\":\"YxTr4EzdbJ101T211zR4yzUEMVE\",\n  \"clientrequestid\":\"\",\n  \"action\":\"infoDnsRecords\",\n  \"status\":\"error\",\n  \"statuscode\":4013,\n  \"shortmessage\":\"Validation Error.\",\n  \"longmessage\":\"Message is empty.\",\n  \"responsedata\":\"\"\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/fixtures/get_dns_records_error_unmarshal.json",
    "content": "{\n  \"serverrequestid\":\"srv-request-id\",\n  \"clientrequestid\":\"\",\n  \"action\":\"infoDnsRecords\",\n  \"status\":\"success\",\n  \"statuscode\":2000,\n  \"shortmessage\":\"Login successful\",\n  \"longmessage\":\"Session has been created successful.\",\n  \"responsedata\":\"\"\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/fixtures/login-request.json",
    "content": "{\n  \"action\": \"login\",\n  \"param\": {\n    \"customernumber\": \"a\",\n    \"apikey\": \"b\",\n    \"apipassword\": \"c\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/fixtures/login.json",
    "content": "{\n  \"serverrequestid\": \"srv-request-id\",\n  \"clientrequestid\": \"\",\n  \"action\": \"login\",\n  \"status\": \"success\",\n  \"statuscode\": 2000,\n  \"shortmessage\": \"Login successful\",\n  \"longmessage\": \"Session has been created successful.\",\n  \"responsedata\": {\n    \"apisessionid\": \"api-session-id\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/fixtures/login_error.json",
    "content": "{\n  \"serverrequestid\":\"YxTr4EzdbJ101T211zR4yzUEMVE\",\n  \"clientrequestid\":\"\",\n  \"action\":\"login\",\n  \"status\":\"error\",\n  \"statuscode\":4013,\n  \"shortmessage\":\"Validation Error.\",\n  \"longmessage\":\"Message is empty.\",\n  \"responsedata\":\"\"\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/fixtures/login_error_unmarshal.json",
    "content": "{\n  \"serverrequestid\": \"srv-request-id\",\n  \"clientrequestid\": \"\",\n  \"action\": \"login\",\n  \"status\": \"success\",\n  \"statuscode\": 2000,\n  \"shortmessage\": \"Login successful\",\n  \"longmessage\": \"Session has been created successful.\",\n  \"responsedata\": \"\"\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/fixtures/logout-request.json",
    "content": "{\n  \"action\": \"logout\",\n  \"param\": {\n    \"customernumber\": \"a\",\n    \"apikey\": \"b\",\n    \"apisessionid\": \"session-id\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/fixtures/logout.json",
    "content": "{\n  \"serverrequestid\": \"request-id\",\n  \"clientrequestid\": \"\",\n  \"action\": \"logout\",\n  \"status\": \"success\",\n  \"statuscode\": 2000,\n  \"shortmessage\": \"Logout successful\",\n  \"longmessage\": \"Session has been terminated successful.\",\n  \"responsedata\": \"\"\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/fixtures/logout_error.json",
    "content": "{\n  \"serverrequestid\":\"YxTr4EzdbJ101T211zR4yzUEMVE\",\n  \"clientrequestid\":\"\",\n  \"action\":\"logout\",\n  \"status\":\"error\",\n  \"statuscode\":4013,\n  \"shortmessage\":\"Validation Error.\",\n  \"longmessage\":\"Message is empty.\",\n  \"responsedata\":\"\"\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/session.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\ntype sessionKey string\n\nconst sessionIDKey sessionKey = \"sessionID\"\n\n// login performs the login as specified by the netcup WSDL\n// returns sessionID needed to perform remaining actions.\n// https://ccp.netcup.net/run/webservice/servers/endpoint.php\nfunc (c *Client) login(ctx context.Context) (string, error) {\n\tpayload := &Request{\n\t\tAction: \"login\",\n\t\tParam: &LoginRequest{\n\t\t\tCustomerNumber:  c.customerNumber,\n\t\t\tAPIKey:          c.apiKey,\n\t\t\tAPIPassword:     c.apiPassword,\n\t\t\tClientRequestID: \"\",\n\t\t},\n\t}\n\n\tvar responseData LoginResponse\n\n\terr := c.doRequest(ctx, payload, &responseData)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"loging error: %w\", err)\n\t}\n\n\treturn responseData.APISessionID, nil\n}\n\n// Logout performs the logout with the supplied sessionID as specified by the netcup WSDL.\n// https://ccp.netcup.net/run/webservice/servers/endpoint.php\nfunc (c *Client) Logout(ctx context.Context) error {\n\tpayload := &Request{\n\t\tAction: \"logout\",\n\t\tParam: &LogoutRequest{\n\t\t\tCustomerNumber:  c.customerNumber,\n\t\t\tAPIKey:          c.apiKey,\n\t\t\tAPISessionID:    getSessionID(ctx),\n\t\t\tClientRequestID: \"\",\n\t\t},\n\t}\n\n\terr := c.doRequest(ctx, payload, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"logout error: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) CreateSessionContext(ctx context.Context) (context.Context, error) {\n\tsessID, err := c.login(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn context.WithValue(ctx, sessionIDKey, sessID), nil\n}\n\nfunc getSessionID(ctx context.Context) string {\n\tsessID, ok := ctx.Value(sessionIDKey).(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn sessID\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/session_test.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockContext(t *testing.T) context.Context {\n\tt.Helper()\n\n\treturn context.WithValue(t.Context(), sessionIDKey, \"session-id\")\n}\n\nfunc TestClient_Login(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"login.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"login-request.json\")).\n\t\tBuild(t)\n\n\tsessionID, err := client.login(t.Context())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"api-session-id\", sessionID)\n}\n\nfunc TestClient_Login_errors(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\thandler  http.Handler\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"HTTP error\",\n\t\t\thandler:  servermock.Noop().WithStatusCode(http.StatusInternalServerError),\n\t\t\texpected: `loging error: unexpected status code: [status code: 500] body: `,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"API error\",\n\t\t\thandler:  servermock.ResponseFromFixture(\"login_error.json\"),\n\t\t\texpected: `loging error: an error occurred during the action login: [Status=error, StatusCode=4013, ShortMessage=Validation Error., LongMessage=Message is empty.]`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"responsedata marshaling error\",\n\t\t\thandler:  servermock.ResponseFromFixture(\"login_error_unmarshal.json\"),\n\t\t\texpected: `loging error: unable to unmarshal response: [status code: 200] body: \"\" error: json: cannot unmarshal string into Go value of type internal.LoginResponse`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(\"POST /\", test.handler).\n\t\t\t\tBuild(t)\n\n\t\t\tsessionID, err := client.login(t.Context())\n\t\t\tassert.EqualError(t, err, test.expected)\n\t\t\tassert.Empty(t, sessionID)\n\t\t})\n\t}\n}\n\nfunc TestClient_Logout(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"logout.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"logout-request.json\")).\n\t\tBuild(t)\n\n\terr := client.Logout(mockContext(t))\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_Logout_errors(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\thandler  http.Handler\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:    \"HTTP error\",\n\t\t\thandler: servermock.Noop().WithStatusCode(http.StatusInternalServerError),\n\t\t},\n\t\t{\n\t\t\tdesc:    \"API error\",\n\t\t\thandler: servermock.ResponseFromFixture(\"login_error.json\"),\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(\"POST /\", test.handler).\n\t\t\t\tBuild(t)\n\n\t\t\terr := client.Logout(t.Context())\n\t\t\trequire.Error(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/netcup/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\n// success response status.\nconst success = \"success\"\n\n// Request wrapper as specified in netcup wiki\n// needed for every request to netcup API around *Msg.\n// https://www.netcup-wiki.de/wiki/CCP_API#Anmerkungen_zu_JSON-Requests\ntype Request struct {\n\tAction string `json:\"action\"`\n\tParam  any    `json:\"param\"`\n}\n\n// LoginRequest as specified in netcup WSDL.\n// https://ccp.netcup.net/run/webservice/servers/endpoint.php#login\ntype LoginRequest struct {\n\tCustomerNumber  string `json:\"customernumber\"`\n\tAPIKey          string `json:\"apikey\"`\n\tAPIPassword     string `json:\"apipassword\"`\n\tClientRequestID string `json:\"clientrequestid,omitempty\"`\n}\n\n// LogoutRequest as specified in netcup WSDL.\n// https://ccp.netcup.net/run/webservice/servers/endpoint.php#logout\ntype LogoutRequest struct {\n\tCustomerNumber  string `json:\"customernumber\"`\n\tAPIKey          string `json:\"apikey\"`\n\tAPISessionID    string `json:\"apisessionid\"`\n\tClientRequestID string `json:\"clientrequestid,omitempty\"`\n}\n\n// UpdateDNSRecordsRequest as specified in netcup WSDL.\n// https://ccp.netcup.net/run/webservice/servers/endpoint.php#updateDnsRecords\ntype UpdateDNSRecordsRequest struct {\n\tDomainName      string       `json:\"domainname\"`\n\tCustomerNumber  string       `json:\"customernumber\"`\n\tAPIKey          string       `json:\"apikey\"`\n\tAPISessionID    string       `json:\"apisessionid\"`\n\tClientRequestID string       `json:\"clientrequestid,omitempty\"`\n\tDNSRecordSet    DNSRecordSet `json:\"dnsrecordset\"`\n}\n\n// DNSRecordSet as specified in netcup WSDL.\n// needed in UpdateDNSRecordsRequest.\n// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Dnsrecordset\ntype DNSRecordSet struct {\n\tDNSRecords []DNSRecord `json:\"dnsrecords\"`\n}\n\n// InfoDNSRecordsRequest as specified in netcup WSDL.\n// https://ccp.netcup.net/run/webservice/servers/endpoint.php#infoDnsRecords\ntype InfoDNSRecordsRequest struct {\n\tDomainName      string `json:\"domainname\"`\n\tCustomerNumber  string `json:\"customernumber\"`\n\tAPIKey          string `json:\"apikey\"`\n\tAPISessionID    string `json:\"apisessionid\"`\n\tClientRequestID string `json:\"clientrequestid,omitempty\"`\n}\n\n// DNSRecord as specified in netcup WSDL.\n// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Dnsrecord\ntype DNSRecord struct {\n\tID           int    `json:\"id,string,omitempty\"`\n\tHostname     string `json:\"hostname\"`\n\tRecordType   string `json:\"type\"`\n\tPriority     string `json:\"priority,omitempty\"`\n\tDestination  string `json:\"destination\"`\n\tDeleteRecord bool   `json:\"deleterecord,omitempty\"`\n\tState        string `json:\"state,omitempty\"`\n}\n\n// ResponseMsg as specified in netcup WSDL.\n// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Responsemessage\ntype ResponseMsg struct {\n\tServerRequestID string          `json:\"serverrequestid\"`\n\tClientRequestID string          `json:\"clientrequestid,omitempty\"`\n\tAction          string          `json:\"action\"`\n\tStatus          string          `json:\"status\"`\n\tStatusCode      int             `json:\"statuscode\"`\n\tShortMessage    string          `json:\"shortmessage\"`\n\tLongMessage     string          `json:\"longmessage\"`\n\tResponseData    json.RawMessage `json:\"responsedata,omitempty\"`\n}\n\nfunc (r *ResponseMsg) Error() string {\n\treturn fmt.Sprintf(\"an error occurred during the action %s: [Status=%s, StatusCode=%d, ShortMessage=%s, LongMessage=%s]\",\n\t\tr.Action, r.Status, r.StatusCode, r.ShortMessage, r.LongMessage)\n}\n\n// LoginResponse response to login action.\ntype LoginResponse struct {\n\tAPISessionID string `json:\"apisessionid\"`\n}\n\n// InfoDNSRecordsResponse response to infoDnsRecords action.\ntype InfoDNSRecordsResponse struct {\n\tAPISessionID string      `json:\"apisessionid\"`\n\tDNSRecords   []DNSRecord `json:\"dnsrecords,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/netcup/netcup.go",
    "content": "// Package netcup implements a DNS Provider for solving the DNS-01 challenge using the netcup DNS API.\npackage netcup\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/netcup/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"NETCUP_\"\n\n\tEnvCustomerNumber = envNamespace + \"CUSTOMER_NUMBER\"\n\tEnvAPIKey         = envNamespace + \"API_KEY\"\n\tEnvAPIPassword    = envNamespace + \"API_PASSWORD\"\n\n\t// Deprecated: the TTL is not configurable on record.\n\tEnvTTL = envNamespace + \"TTL\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tKey                string\n\tPassword           string\n\tCustomer           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n\n\t// Deprecated: the TTL is not configurable on record.\n\tTTL int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 15*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 30*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *internal.Client\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for netcup.\n// Credentials must be passed in the environment variables:\n// NETCUP_CUSTOMER_NUMBER, NETCUP_API_KEY, NETCUP_API_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvCustomerNumber, EnvAPIKey, EnvAPIPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"netcup: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Customer = values[EnvCustomerNumber]\n\tconfig.Key = values[EnvAPIKey]\n\tconfig.Password = values[EnvAPIPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for netcup.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"netcup: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.Customer, config.Key, config.Password)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"netcup: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"netcup: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx, err := d.client.CreateSessionContext(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"netcup: %w\", err)\n\t}\n\n\tdefer func() {\n\t\terr = d.client.Logout(ctx)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"netcup: %v\", err)\n\t\t}\n\t}()\n\n\thostname := strings.Replace(info.EffectiveFQDN, \".\"+zone, \"\", 1)\n\trecord := internal.DNSRecord{\n\t\tHostname:    hostname,\n\t\tRecordType:  \"TXT\",\n\t\tDestination: info.Value,\n\t}\n\n\tzone = dns01.UnFqdn(zone)\n\n\trecords, err := d.client.GetDNSRecords(ctx, zone)\n\tif err != nil {\n\t\t// skip no existing records\n\t\tlog.Infof(\"no existing records, error ignored: %v\", err)\n\t}\n\n\trecords = append(records, record)\n\n\terr = d.client.UpdateDNSRecord(ctx, zone, records)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"netcup: failed to add TXT-Record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"netcup: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx, err := d.client.CreateSessionContext(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"netcup: %w\", err)\n\t}\n\n\tdefer func() {\n\t\terr = d.client.Logout(ctx)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"netcup: %v\", err)\n\t\t}\n\t}()\n\n\thostname := strings.Replace(info.EffectiveFQDN, \".\"+zone, \"\", 1)\n\n\tzone = dns01.UnFqdn(zone)\n\n\trecords, err := d.client.GetDNSRecords(ctx, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"netcup: %w\", err)\n\t}\n\n\trecord := internal.DNSRecord{\n\t\tHostname:    hostname,\n\t\tRecordType:  \"TXT\",\n\t\tDestination: info.Value,\n\t}\n\n\tidx, err := internal.GetDNSRecordIdx(records, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"netcup: %w\", err)\n\t}\n\n\trecords[idx].DeleteRecord = true\n\n\terr = d.client.UpdateDNSRecord(ctx, zone, []internal.DNSRecord{records[idx]})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"netcup: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/netcup/netcup.toml",
    "content": "Name = \"Netcup\"\nDescription = ''''''\nURL = \"https://www.netcup.eu/\"\nCode = \"netcup\"\nSince = \"v1.1.0\"\n\nExample = '''\nNETCUP_CUSTOMER_NUMBER=xxxx \\\nNETCUP_API_KEY=yyyy \\\nNETCUP_API_PASSWORD=zzzz \\\nlego --dns netcup -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    NETCUP_CUSTOMER_NUMBER = \"Customer number\"\n    NETCUP_API_KEY = \"API key\"\n    NETCUP_API_PASSWORD = \"API password\"\n  [Configuration.Additional]\n    NETCUP_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 30)\"\n    NETCUP_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 900)\"\n    NETCUP_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://www.netcup-wiki.de/wiki/DNS_API\"\n"
  },
  {
    "path": "providers/dns/netcup/netcup_test.go",
    "content": "package netcup\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvCustomerNumber,\n\tEnvAPIKey,\n\tEnvAPIPassword).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCustomerNumber: \"A\",\n\t\t\t\tEnvAPIKey:         \"B\",\n\t\t\t\tEnvAPIPassword:    \"C\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCustomerNumber: \"\",\n\t\t\t\tEnvAPIKey:         \"\",\n\t\t\t\tEnvAPIPassword:    \"\",\n\t\t\t},\n\t\t\texpected: \"netcup: some credentials information are missing: NETCUP_CUSTOMER_NUMBER,NETCUP_API_KEY,NETCUP_API_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing customer number\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCustomerNumber: \"\",\n\t\t\t\tEnvAPIKey:         \"B\",\n\t\t\t\tEnvAPIPassword:    \"C\",\n\t\t\t},\n\t\t\texpected: \"netcup: some credentials information are missing: NETCUP_CUSTOMER_NUMBER\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCustomerNumber: \"A\",\n\t\t\t\tEnvAPIKey:         \"\",\n\t\t\t\tEnvAPIPassword:    \"C\",\n\t\t\t},\n\t\t\texpected: \"netcup: some credentials information are missing: NETCUP_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCustomerNumber: \"A\",\n\t\t\t\tEnvAPIKey:         \"B\",\n\t\t\t\tEnvAPIPassword:    \"\",\n\t\t\t},\n\t\t\texpected: \"netcup: some credentials information are missing: NETCUP_API_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tcustomer string\n\t\tkey      string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tcustomer: \"A\",\n\t\t\tkey:      \"B\",\n\t\t\tpassword: \"C\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"netcup: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing customer\",\n\t\t\tcustomer: \"\",\n\t\t\tkey:      \"B\",\n\t\t\tpassword: \"C\",\n\t\t\texpected: \"netcup: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing key\",\n\t\t\tcustomer: \"A\",\n\t\t\tkey:      \"\",\n\t\t\tpassword: \"C\",\n\t\t\texpected: \"netcup: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tcustomer: \"A\",\n\t\t\tkey:      \"B\",\n\t\t\tpassword: \"\",\n\t\t\texpected: \"netcup: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Customer = test.customer\n\t\t\tconfig.Key = test.key\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresentAndCleanup(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tp, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\tinfo := dns01.GetChallengeInfo(envTest.GetDomain(), \"123d==\")\n\n\tzone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\trequire.NoError(t, err)\n\n\tzone = dns01.UnFqdn(zone)\n\n\ttestCases := []string{\n\t\tzone,\n\t\t\"sub.\" + zone,\n\t\t\"*.\" + zone,\n\t\t\"*.sub.\" + zone,\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(fmt.Sprintf(\"domain(%s)\", test), func(t *testing.T) {\n\t\t\terr = p.Present(test, \"987d\", \"123d==\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = p.CleanUp(test, \"987d\", \"123d==\")\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/netlify/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"golang.org/x/oauth2\"\n)\n\nconst defaultBaseURL = \"https://api.netlify.com/api/v1\"\n\n// Client Netlify API client.\ntype Client struct {\n\tbaseURL    *url.URL\n\thttpClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(hc *http.Client) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\tif hc == nil {\n\t\thc = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\treturn &Client{baseURL: baseURL, httpClient: hc}\n}\n\n// GetRecords gets a DNS records.\nfunc (c *Client) GetRecords(ctx context.Context, zoneID string) ([]DNSRecord, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns_zones\", zoneID, \"dns_records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar records []DNSRecord\n\n\terr = json.Unmarshal(raw, &records)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn records, nil\n}\n\n// CreateRecord creates a DNS records.\nfunc (c *Client) CreateRecord(ctx context.Context, zoneID string, record DNSRecord) (*DNSRecord, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns_zones\", zoneID, \"dns_records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusCreated {\n\t\treturn nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar recordResp DNSRecord\n\n\terr = json.Unmarshal(raw, &recordResp)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn &recordResp, nil\n}\n\n// RemoveRecord removes a DNS records.\nfunc (c *Client) RemoveRecord(ctx context.Context, zoneID, recordID string) error {\n\tendpoint := c.baseURL.JoinPath(\"dns_zones\", zoneID, \"dns_records\", recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusNoContent {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\t}\n\n\treturn req, nil\n}\n\nfunc OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {\n\tif client == nil {\n\t\tclient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tclient.Transport = &oauth2.Transport{\n\t\tSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),\n\t\tBase:   client.Transport,\n\t}\n\n\treturn client\n}\n"
  },
  {
    "path": "providers/dns/netlify/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(token string) func(server *httptest.Server) (*Client, error) {\n\treturn func(server *httptest.Server) (*Client, error) {\n\t\tclient := NewClient(OAuthStaticAccessToken(server.Client(), token))\n\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\treturn client, nil\n\t}\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient(\"tokenA\"),\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer tokenA\"),\n\t).\n\t\tRoute(\"GET /dns_zones/zoneID/dns_records\",\n\t\t\tservermock.ResponseFromFixture(\"get_records.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"zoneID\")\n\trequire.NoError(t, err)\n\n\texpected := []DNSRecord{\n\t\t{ID: \"u6b433c15a27a2d79c6616d6\", Hostname: \"example.org\", TTL: 3600, Type: \"A\", Value: \"10.10.10.10\"},\n\t\t{ID: \"u6b4764216f272872ac0ff71\", Hostname: \"test.example.org\", TTL: 300, Type: \"TXT\", Value: \"txtxtxtxtxtxt\"},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient(\"tokenB\"),\n\t\tservermock.CheckHeader().\n\t\t\tWithAccept(\"application/json\").\n\t\t\tWithContentType(\"application/json; charset=utf-8\").\n\t\t\tWithAuthorization(\"Bearer tokenB\"),\n\t).\n\t\tRoute(\"POST /dns_zones/zoneID/dns_records\",\n\t\t\tservermock.ResponseFromFixture(\"create_record.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated)).\n\t\tBuild(t)\n\n\trecord := DNSRecord{\n\t\tHostname: \"_acme-challenge.example.com\",\n\t\tTTL:      300,\n\t\tType:     \"TXT\",\n\t\tValue:    \"txtxtxtxtxtxt\",\n\t}\n\n\tresult, err := client.CreateRecord(t.Context(), \"zoneID\", record)\n\trequire.NoError(t, err)\n\n\texpected := &DNSRecord{\n\t\tID:       \"u6b4764216f272872ac0ff71\",\n\t\tHostname: \"test.example.org\",\n\t\tTTL:      300,\n\t\tType:     \"TXT\",\n\t\tValue:    \"txtxtxtxtxtxt\",\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_RemoveRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient(\"tokenC\"),\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer tokenC\"),\n\t).\n\t\tRoute(\"DELETE /dns_zones/zoneID/dns_records/recordID\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\terr := client.RemoveRecord(t.Context(), \"zoneID\", \"recordID\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/netlify/internal/fixtures/create_record.json",
    "content": "{\n  \"hostname\": \"test.example.org\",\n  \"type\": \"TXT\",\n  \"ttl\": 300,\n  \"priority\": null,\n  \"weight\": null,\n  \"port\": null,\n  \"flag\": null,\n  \"tag\": null,\n  \"id\": \"u6b4764216f272872ac0ff71\",\n  \"site_id\": null,\n  \"dns_zone_id\": \"u6b4336178f002e0a06bb0b6\",\n  \"errors\": [],\n  \"managed\": false,\n  \"value\": \"txtxtxtxtxtxt\"\n}"
  },
  {
    "path": "providers/dns/netlify/internal/fixtures/get_records.json",
    "content": "[\n  {\n    \"hostname\": \"example.org\",\n    \"type\": \"A\",\n    \"ttl\": 3600,\n    \"priority\": null,\n    \"weight\": null,\n    \"port\": null,\n    \"flag\": null,\n    \"tag\": null,\n    \"id\": \"u6b433c15a27a2d79c6616d6\",\n    \"site_id\": null,\n    \"dns_zone_id\": \"u6b4336178f002e0a06bb0b6\",\n    \"errors\": [],\n    \"managed\": false,\n    \"value\": \"10.10.10.10\"\n  },\n  {\n    \"hostname\": \"test.example.org\",\n    \"type\": \"TXT\",\n    \"ttl\": 300,\n    \"priority\": null,\n    \"weight\": null,\n    \"port\": null,\n    \"flag\": null,\n    \"tag\": null,\n    \"id\": \"u6b4764216f272872ac0ff71\",\n    \"site_id\": null,\n    \"dns_zone_id\": \"u6b4336178f002e0a06bb0b6\",\n    \"errors\": [],\n    \"managed\": false,\n    \"value\": \"txtxtxtxtxtxt\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/netlify/internal/types.go",
    "content": "package internal\n\n// DNSRecord DNS record representation.\ntype DNSRecord struct {\n\tID       string `json:\"id,omitempty\"`\n\tHostname string `json:\"hostname,omitempty\"`\n\tTTL      int    `json:\"ttl,omitempty\"`\n\tType     string `json:\"type,omitempty\"`\n\tValue    string `json:\"value,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/netlify/netlify.go",
    "content": "// Package netlify implements a DNS provider for solving the DNS-01 challenge using Netlify.\npackage netlify\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/netlify/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"NETLIFY_\"\n\n\tEnvToken = envNamespace + \"TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken              string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Netlify.\n// Credentials must be passed in the environment variable: NETLIFY_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"netlify: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Netlify.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"netlify: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"netlify: incomplete credentials, missing token\")\n\t}\n\n\tclient := internal.NewClient(\n\t\tclientdebug.Wrap(\n\t\t\tinternal.OAuthStaticAccessToken(config.HTTPClient, config.Token),\n\t\t),\n\t)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"netlify: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\trecord := internal.DNSRecord{\n\t\tHostname: dns01.UnFqdn(info.EffectiveFQDN),\n\t\tTTL:      d.config.TTL,\n\t\tType:     \"TXT\",\n\t\tValue:    info.Value,\n\t}\n\n\tresp, err := d.client.CreateRecord(context.Background(), strings.ReplaceAll(authZone, \".\", \"_\"), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"netlify: failed to create TXT records: fqdn=%s, authZone=%s: %w\", info.EffectiveFQDN, authZone, err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = resp.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"netlify: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\t// gets the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"netlify: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\terr = d.client.RemoveRecord(context.Background(), strings.ReplaceAll(authZone, \".\", \"_\"), recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"netlify: failed to delete TXT records: fqdn=%s, authZone=%s, recordID=%s: %w\", info.EffectiveFQDN, authZone, recordID, err)\n\t}\n\n\t// deletes record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/netlify/netlify.toml",
    "content": "Name = \"Netlify\"\nDescription = ''''''\nURL = \"https://www.netlify.com\"\nCode = \"netlify\"\nSince = \"v3.7.0\"\n\nExample = '''\nNETLIFY_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns netlify -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    NETLIFY_TOKEN = \"Token\"\n  [Configuration.Additional]\n    NETLIFY_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    NETLIFY_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    NETLIFY_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    NETLIFY_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://open-api.netlify.com/\"\n"
  },
  {
    "path": "providers/dns/netlify/netlify_test.go",
    "content": "package netlify\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"\",\n\t\t\t},\n\t\t\texpected: \"netlify: some credentials information are missing: NETLIFY_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\texpected string\n\t\ttoken    string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"api_key\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"netlify: incomplete credentials, missing token\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/nicmanager/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/pquerna/otp/totp\"\n)\n\nconst (\n\tdefaultBaseURL  = \"https://api.nicmanager.com/v1\"\n\theaderTOTPToken = \"X-Auth-Token\"\n)\n\n// Modes.\nconst (\n\tModeAnycast = \"anycast\"\n\tModeZone    = \"zones\"\n)\n\n// Options the Client options.\ntype Options struct {\n\tLogin    string\n\tUsername string\n\n\tEmail string\n\n\tPassword string\n\tOTP      string\n\n\tMode string\n}\n\n// Client a nicmanager DNS client.\ntype Client struct {\n\tusername string\n\tpassword string\n\totp      string\n\n\tmode string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient create a new Client.\nfunc NewClient(opts Options) *Client {\n\tc := &Client{\n\t\tmode:       ModeAnycast,\n\t\tusername:   opts.Email,\n\t\tpassword:   opts.Password,\n\t\totp:        opts.OTP,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}\n\n\tc.baseURL, _ = url.Parse(defaultBaseURL)\n\n\tif opts.Mode != \"\" {\n\t\tc.mode = opts.Mode\n\t}\n\n\tif opts.Login != \"\" && opts.Username != \"\" {\n\t\tc.username = fmt.Sprintf(\"%s.%s\", opts.Login, opts.Username)\n\t}\n\n\treturn c\n}\n\nfunc (c *Client) GetZone(ctx context.Context, name string) (*Zone, error) {\n\tendpoint := c.baseURL.JoinPath(c.mode, name)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar zone Zone\n\n\terr = c.do(req, http.StatusOK, &zone)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &zone, nil\n}\n\nfunc (c *Client) AddRecord(ctx context.Context, zone string, payload RecordCreateUpdate) error {\n\tendpoint := c.baseURL.JoinPath(c.mode, zone, \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, http.StatusAccepted, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, zone string, record int) error {\n\tendpoint := c.baseURL.JoinPath(c.mode, zone, \"records\", strconv.Itoa(record))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, http.StatusAccepted, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) do(req *http.Request, expectedStatusCode int, result any) error {\n\treq.SetBasicAuth(c.username, c.password)\n\n\tif c.otp != \"\" {\n\t\ttan, err := totp.GenerateCode(c.otp, time.Now())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treq.Header.Set(headerTOTPToken, tan)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != expectedStatusCode {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn err\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\terrAPI := APIError{StatusCode: resp.StatusCode}\n\tif err := json.Unmarshal(raw, &errAPI); err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn errAPI\n}\n"
  },
  {
    "path": "providers/dns/nicmanager/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\topts := Options{\n\t\t\t\tLogin:    \"l\",\n\t\t\t\tUsername: \"u\",\n\t\t\t\tPassword: \"p\",\n\t\t\t\tOTP:      \"2hsn\",\n\t\t\t}\n\n\t\t\tclient := NewClient(opts)\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithBasicAuth(\"l.u\", \"p\").\n\t\t\tWithRegexp(headerTOTPToken, `\\d{6}`))\n}\n\nfunc TestClient_GetZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /anycast/nicmanager-anycastdns4.net\",\n\t\t\tservermock.ResponseFromFixture(\"zone.json\")).\n\t\tBuild(t)\n\n\tzone, err := client.GetZone(t.Context(), \"nicmanager-anycastdns4.net\")\n\trequire.NoError(t, err)\n\n\texpected := &Zone{\n\t\tName:   \"nicmanager-anycastdns4.net\",\n\t\tActive: true,\n\t\tRecords: []Record{\n\t\t\t{\n\t\t\t\tID:      186,\n\t\t\t\tName:    \"nicmanager-anycastdns4.net\",\n\t\t\t\tType:    \"A\",\n\t\t\t\tContent: \"123.123.123.123\",\n\t\t\t\tTTL:     3600,\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zone)\n}\n\nfunc TestClient_GetZone_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /anycast/foo\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusNotFound)).\n\t\tBuild(t)\n\n\t_, err := client.GetZone(t.Context(), \"foo\")\n\trequire.EqualError(t, err, \"404: Not Found\")\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /anycast/zonedomain.tld/records\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusAccepted)).\n\t\tBuild(t)\n\n\trecord := RecordCreateUpdate{\n\t\tType:  \"TXT\",\n\t\tName:  \"lego\",\n\t\tValue: \"content\",\n\t\tTTL:   3600,\n\t}\n\n\terr := client.AddRecord(t.Context(), \"zonedomain.tld\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /anycast/zonedomain.tld/records\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\trecord := RecordCreateUpdate{\n\t\tType:  \"TXT\",\n\t\tName:  \"zonedomain.tld\",\n\t\tValue: \"content\",\n\t\tTTL:   3600,\n\t}\n\n\terr := client.AddRecord(t.Context(), \"zonedomain.tld\", record)\n\trequire.EqualError(t, err, \"401: Not Found\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /anycast/zonedomain.tld/records/6\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusAccepted)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"zonedomain.tld\", 6)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /anycast/zonedomain.tld/records/6\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusNotFound)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"zonedomain.tld\", 6)\n\trequire.EqualError(t, err, \"404: Not Found\")\n}\n"
  },
  {
    "path": "providers/dns/nicmanager/internal/fixtures/error.json",
    "content": "{\n  \"message\": \"Not Found\"\n}\n"
  },
  {
    "path": "providers/dns/nicmanager/internal/fixtures/zone.json",
    "content": "{\n  \"order_id\": 9053,\n  \"name\": \"nicmanager-anycastdns4.net\",\n  \"order_status\": \"active\",\n  \"event_status\": \"done\",\n  \"active\": true,\n  \"dnssec\": \"inactive\",\n  \"master1\": null,\n  \"master2\": null,\n  \"soa\": {\n    \"primary\": \"ns1.nic53.net\",\n    \"mail\": \"hostmaster.nicmanager.de\",\n    \"serial\": 1481109046,\n    \"refresh\": 14400,\n    \"retry\": 1800,\n    \"expire\": 1209600,\n    \"default\": 3600,\n    \"ttl\": 86400\n  },\n  \"updated_datetime\": \"2016-09-02T13:52:18Z\",\n  \"order_datetime\": \"2016-09-02T13:52:18Z\",\n  \"records\": [\n    {\n      \"id\": 186,\n      \"name\": \"nicmanager-anycastdns4.net\",\n      \"type\": \"A\",\n      \"content\": \"123.123.123.123\",\n      \"ttl\": 3600,\n      \"priority\": 0,\n      \"active\": true,\n      \"updated_datetime\": \"2016-09-02T13:52:18Z\"\n    }\n  ],\n  \"redirects\": [\n      {\n        \"id\": 10,\n        \"name\": \"test.nicmanager-anycastdns4.net\",\n        \"target\": \"https:\\/\\/www.nicmanager.com\\/\",\n        \"type\": \"frame\",\n        \"updated_datetime\": \"2016-12-05T14:40:47Z\",\n        \"request_uri\": true,\n        \"ssl\": false,\n        \"meta\": {\n          \"title\": \"My frame\",\n          \"keywords\": \"foo,bar\",\n          \"description\": \"Just a Test\"\n        },\n        \"subdomain\": \"test\"\n      }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/nicmanager/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype Record struct {\n\tID   int    `json:\"id\"`\n\tName string `json:\"name\"`\n\n\tType    string `json:\"type\"`\n\tContent string `json:\"content\"`\n\tTTL     int    `json:\"ttl\"`\n}\n\ntype Zone struct {\n\tName    string   `json:\"name\"`\n\tActive  bool     `json:\"active\"`\n\tRecords []Record `json:\"records\"`\n}\n\ntype RecordCreateUpdate struct {\n\tName  string `json:\"name\"`\n\tValue string `json:\"value\"`\n\tTTL   int    `json:\"ttl\"`\n\tType  string `json:\"type\"`\n}\n\ntype APIError struct {\n\tMessage    string `json:\"message\"`\n\tStatusCode int    `json:\"-\"`\n}\n\nfunc (a APIError) Error() string {\n\treturn fmt.Sprintf(\"%d: %s\", a.StatusCode, a.Message)\n}\n"
  },
  {
    "path": "providers/dns/nicmanager/nicmanager.go",
    "content": "// Package nicmanager implements a DNS provider for solving the DNS-01 challenge using nicmanager DNS.\npackage nicmanager\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/nicmanager/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"NICMANAGER_\"\n\n\tEnvLogin    = envNamespace + \"API_LOGIN\"\n\tEnvUsername = envNamespace + \"API_USERNAME\"\n\tEnvEmail    = envNamespace + \"API_EMAIL\"\n\tEnvPassword = envNamespace + \"API_PASSWORD\"\n\tEnvOTP      = envNamespace + \"API_OTP\"\n\tEnvMode     = envNamespace + \"API_MODE\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst minTTL = 900\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tLogin     string\n\tUsername  string\n\tEmail     string\n\tPassword  string\n\tOTPSecret string\n\tMode      string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *internal.Client\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for nicmanager.\n// Credentials must be passed in the environment variables:\n// NICMANAGER_API_LOGIN, NICMANAGER_API_USERNAME\n// NICMANAGER_API_EMAIL\n// NICMANAGER_API_PASSWORD\n// NICMANAGER_API_OTP\n// NICMANAGER_API_MODE.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"nicmanager: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Password = values[EnvPassword]\n\n\tconfig.Mode = env.GetOneWithFallback(EnvMode, internal.ModeAnycast, env.ParseString, envNamespace+\"MODE\")\n\tconfig.Username = env.GetOrFile(EnvUsername)\n\tconfig.Login = env.GetOrFile(EnvLogin)\n\tconfig.Email = env.GetOrFile(EnvEmail)\n\tconfig.OTPSecret = env.GetOrFile(EnvOTP)\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"TTL must be higher than %d: %d\", minTTL, config.TTL)\n\t}\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for nicmanager.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"nicmanager: the configuration of the DNS provider is nil\")\n\t}\n\n\topts := internal.Options{\n\t\tPassword: config.Password,\n\t\tOTP:      config.OTPSecret,\n\t\tMode:     config.Mode,\n\t}\n\n\tswitch {\n\tcase config.Password == \"\":\n\t\treturn nil, errors.New(\"nicmanager: credentials missing\")\n\tcase config.Email != \"\":\n\t\topts.Email = config.Email\n\tcase config.Login != \"\" && config.Username != \"\":\n\t\topts.Login = config.Login\n\t\topts.Username = config.Username\n\tdefault:\n\t\treturn nil, errors.New(\"nicmanager: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(opts)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trootDomain, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicmanager: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tzone, err := d.client.GetZone(ctx, dns01.UnFqdn(rootDomain))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicmanager: failed to get zone %q: %w\", rootDomain, err)\n\t}\n\n\t// The way nic manager deals with record with multiple values is that they are completely different records with unique ids\n\t// Hence we don't check for an existing record here, but rather just create one\n\trecord := internal.RecordCreateUpdate{\n\t\tName:  info.EffectiveFQDN,\n\t\tType:  \"TXT\",\n\t\tTTL:   d.config.TTL,\n\t\tValue: info.Value,\n\t}\n\n\terr = d.client.AddRecord(ctx, zone.Name, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicmanager: failed to create record [zone: %q, fqdn: %q]: %w\", zone.Name, info.EffectiveFQDN, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trootDomain, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicmanager: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tzone, err := d.client.GetZone(ctx, dns01.UnFqdn(rootDomain))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicmanager: failed to get zone %q: %w\", rootDomain, err)\n\t}\n\n\tname := dns01.UnFqdn(info.EffectiveFQDN)\n\n\tvar (\n\t\texistingRecord      internal.Record\n\t\texistingRecordFound bool\n\t)\n\n\tfor _, record := range zone.Records {\n\t\tif strings.EqualFold(record.Type, \"TXT\") && strings.EqualFold(record.Name, name) && record.Content == info.Value {\n\t\t\texistingRecord = record\n\t\t\texistingRecordFound = true\n\t\t}\n\t}\n\n\tif existingRecordFound {\n\t\terr = d.client.DeleteRecord(ctx, zone.Name, existingRecord.ID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"nicmanager: failed to delete record [zone: %q, domain: %q]: %w\", zone.Name, name, err)\n\t\t}\n\t}\n\n\treturn errors.New(\"nicmanager: no record found to clean up\")\n}\n"
  },
  {
    "path": "providers/dns/nicmanager/nicmanager.toml",
    "content": "Name = \"Nicmanager\"\nDescription = ''''''\nURL = \"https://www.nicmanager.com/\"\nCode = \"nicmanager\"\nSince = \"v4.5.0\"\n\nExample = '''\n## Login using email\n\nNICMANAGER_API_EMAIL = \"you@example.com\" \\\nNICMANAGER_API_PASSWORD = \"password\" \\\n\n# Optionally, if your account has TOTP enabled, set the secret here\nNICMANAGER_API_OTP = \"long-secret\" \\\n\nlego --dns nicmanager -d '*.example.com' -d example.com run\n\n## Login using account name + username\n\nNICMANAGER_API_LOGIN = \"myaccount\" \\\nNICMANAGER_API_USERNAME = \"myuser\" \\\nNICMANAGER_API_PASSWORD = \"password\" \\\n\n# Optionally, if your account has TOTP enabled, set the secret here\nNICMANAGER_API_OTP = \"long-secret\" \\\n\nlego --dns nicmanager -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Description\n\nYou can log in using your account name + username or using your email address.\nOptionally, if TOTP is configured for your account, set `NICMANAGER_API_OTP`.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    NICMANAGER_API_LOGIN = \"Login, used for Username-based login\"\n    NICMANAGER_API_USERNAME = \"Username, used for Username-based login\"\n    NICMANAGER_API_EMAIL = \"Email-based login\"\n    NICMANAGER_API_PASSWORD = \"Password, always required\"\n  [Configuration.Additional]\n    NICMANAGER_API_OTP = \"TOTP Secret (optional)\"\n    NICMANAGER_API_MODE = \"mode: 'anycast' or 'zones' (for FreeDNS) (default: 'anycast')\"\n    NICMANAGER_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    NICMANAGER_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 300)\"\n    NICMANAGER_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 900)\"\n    NICMANAGER_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://api.nicmanager.com/docs/v1/\"\n"
  },
  {
    "path": "providers/dns/nicmanager/nicmanager_test.go",
    "content": "package nicmanager\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvUsername, EnvLogin, EnvEmail, EnvPassword, EnvOTP).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success (email)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEmail:    \"foo@example.com\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success (login.username)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvLogin:    \"foo\",\n\t\t\t\tEnvUsername: \"bar\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"nicmanager: some credentials information are missing: NICMANAGER_API_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEmail: \"foo@example.com\",\n\t\t\t},\n\t\t\texpected: \"nicmanager: some credentials information are missing: NICMANAGER_API_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvLogin:    \"foo\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"nicmanager: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing login\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"bar\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"nicmanager: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tlogin     string\n\t\tusername  string\n\t\temail     string\n\t\tpassword  string\n\t\totpSecret string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success (email)\",\n\t\t\temail:    \"foo@example.com\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"success (login.username)\",\n\t\t\tlogin:    \"john\",\n\t\t\tusername: \"doe\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"nicmanager: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\temail:    \"foo@example.com\",\n\t\t\texpected: \"nicmanager: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing login\",\n\t\t\tlogin:    \"\",\n\t\t\tusername: \"doe\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"nicmanager: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tlogin:    \"john\",\n\t\t\tusername: \"\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"nicmanager: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Login = test.login\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Email = test.email\n\t\t\tconfig.Password = test.password\n\t\t\tconfig.OTPSecret = test.otpSecret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/nicru/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst (\n\tapiBaseURL = \"https://api.nic.ru/dns-master\"\n\ttokenURL   = \"https://api.nic.ru/oauth/token\"\n)\n\nconst successStatus = \"success\"\n\n// Trimmer trim all XML fields.\ntype Trimmer struct {\n\tdecoder *xml.Decoder\n}\n\nfunc (tr Trimmer) Token() (xml.Token, error) {\n\tt, err := tr.decoder.Token()\n\tif cd, ok := t.(xml.CharData); ok {\n\t\tt = xml.CharData(bytes.TrimSpace(cd))\n\t}\n\n\treturn t, err\n}\n\ntype Client struct {\n\tbaseURL    *url.URL\n\thttpClient *http.Client\n}\n\nfunc NewClient(httpClient *http.Client) (*Client, error) {\n\tif httpClient == nil {\n\t\thttpClient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tbaseURL, _ := url.Parse(apiBaseURL)\n\n\treturn &Client{\n\t\tbaseURL:    baseURL,\n\t\thttpClient: httpClient,\n\t}, nil\n}\n\nfunc (c *Client) GetServices(ctx context.Context) ([]Service, error) {\n\tendpoint := c.baseURL.JoinPath(\"services\")\n\n\treq, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tapiResponse, err := c.do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif apiResponse.Data == nil {\n\t\treturn nil, nil\n\t}\n\n\treturn apiResponse.Data.Service, nil\n}\n\nfunc (c *Client) ListZones(ctx context.Context) ([]Zone, error) {\n\tendpoint := c.baseURL.JoinPath(\"zones\")\n\n\treq, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tapiResponse, err := c.do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif apiResponse.Data == nil {\n\t\treturn nil, nil\n\t}\n\n\treturn apiResponse.Data.Zone, nil\n}\n\nfunc (c *Client) GetZonesByService(ctx context.Context, serviceName string) ([]Zone, error) {\n\tendpoint := c.baseURL.JoinPath(\"services\", serviceName, \"zones\")\n\n\treq, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tapiResponse, err := c.do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif apiResponse.Data == nil {\n\t\treturn nil, nil\n\t}\n\n\treturn apiResponse.Data.Zone, nil\n}\n\nfunc (c *Client) GetRecords(ctx context.Context, serviceName, zoneName string) ([]RR, error) {\n\tendpoint := c.baseURL.JoinPath(\"services\", serviceName, \"zones\", zoneName, \"records\")\n\n\treq, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tapiResponse, err := c.do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif apiResponse.Data == nil {\n\t\treturn nil, nil\n\t}\n\n\tvar records []RR\n\tfor _, zone := range apiResponse.Data.Zone {\n\t\trecords = append(records, zone.RR...)\n\t}\n\n\treturn records, nil\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, serviceName, zoneName, id string) error {\n\tendpoint := c.baseURL.JoinPath(\"services\", serviceName, \"zones\", zoneName, \"records\", id)\n\n\treq, err := newXMLRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = c.do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) CommitZone(ctx context.Context, serviceName, zoneName string) error {\n\tendpoint := c.baseURL.JoinPath(\"services\", serviceName, \"zones\", zoneName, \"commit\")\n\n\treq, err := newXMLRequest(ctx, http.MethodPost, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = c.do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) AddRecords(ctx context.Context, serviceName, zoneName string, rrs []RR) ([]Zone, error) {\n\tendpoint := c.baseURL.JoinPath(\"services\", serviceName, \"zones\", zoneName, \"records\")\n\n\tpayload := &Request{RRList: &RRList{RR: rrs}}\n\n\treq, err := newXMLRequest(ctx, http.MethodPut, endpoint, payload)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tapiResponse, err := c.do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif apiResponse.Data == nil {\n\t\treturn nil, nil\n\t}\n\n\treturn apiResponse.Data.Zone, nil\n}\n\nfunc (c *Client) do(req *http.Request) (*Response, error) {\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tapiResponse := &Response{}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tdecoder := xml.NewTokenDecoder(Trimmer{decoder: xml.NewDecoder(bytes.NewReader(raw))})\n\n\terr = decoder.Decode(apiResponse)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"[status code=%d] decode XML response: %s\", resp.StatusCode, string(raw))\n\t}\n\n\tif apiResponse.Status != successStatus {\n\t\treturn nil, fmt.Errorf(\"[status code=%d] %s: %w\", resp.StatusCode, apiResponse.Status, apiResponse.Errors.Error)\n\t}\n\n\treturn apiResponse, nil\n}\n\nfunc newXMLRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbody := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\tbody.WriteString(xml.Header)\n\n\t\tencoder := xml.NewEncoder(body)\n\t\tencoder.Indent(\"\", \"  \")\n\n\t\terr := encoder.Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"text/xml\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"text/xml\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/nicru/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(server.Client())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithAccept(\"text/xml\"),\n\t)\n}\n\nfunc TestClient_GetServices(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /services\", servermock.ResponseFromFixture(\"services_GET.xml\")).\n\t\tBuild(t)\n\n\tzones, err := client.GetServices(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []Service{\n\t\t{\n\t\t\tAdmin:        \"123/NIC-REG\",\n\t\t\tDomainsLimit: \"12\",\n\t\t\tDomainsNum:   \"5\",\n\t\t\tEnable:       \"true\",\n\t\t\tHasPrimary:   \"false\",\n\t\t\tName:         \"testservice\",\n\t\t\tPayer:        \"123/NIC-REG\",\n\t\t\tTariff:       \"Secondary L\",\n\t\t},\n\t\t{\n\t\t\tAdmin:        \"123/NIC-REG\",\n\t\t\tDomainsLimit: \"150\",\n\t\t\tDomainsNum:   \"10\",\n\t\t\tEnable:       \"true\",\n\t\t\tHasPrimary:   \"true\",\n\t\t\tName:         \"myservice\",\n\t\t\tPayer:        \"123/NIC-REG\",\n\t\t\tTariff:       \"DNS-master XXL\",\n\t\t\tRRLimit:      \"7500\",\n\t\t\tRRNum:        \"1000\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_ListZones(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones\", servermock.ResponseFromFixture(\"zones_all_GET.xml\")).\n\t\tBuild(t)\n\n\tzones, err := client.ListZones(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []Zone{\n\t\t{\n\t\t\tAdmin:      \"123/NIC-REG\",\n\t\t\tEnable:     \"true\",\n\t\t\tHasChanges: \"false\",\n\t\t\tHasPrimary: \"true\",\n\t\t\tID:         \"227645\",\n\t\t\tIDNName:    \"тест.рф\",\n\t\t\tName:       \"xn—e1aybc.xn--p1ai\",\n\t\t\tPayer:      \"123/NIC-REG\",\n\t\t\tService:    \"myservice\",\n\t\t},\n\t\t{\n\t\t\tAdmin:      \"123/NIC-REG\",\n\t\t\tEnable:     \"true\",\n\t\t\tHasChanges: \"false\",\n\t\t\tHasPrimary: \"true\",\n\t\t\tID:         \"227642\",\n\t\t\tIDNName:    \"example.ru\",\n\t\t\tName:       \"example.ru\",\n\t\t\tPayer:      \"123/NIC-REG\",\n\t\t\tService:    \"myservice\",\n\t\t},\n\t\t{\n\t\t\tAdmin:      \"123/NIC-REG\",\n\t\t\tEnable:     \"true\",\n\t\t\tHasChanges: \"false\",\n\t\t\tHasPrimary: \"true\",\n\t\t\tID:         \"227643\",\n\t\t\tIDNName:    \"test.su\",\n\t\t\tName:       \"test.su\",\n\t\t\tPayer:      \"123/NIC-REG\",\n\t\t\tService:    \"myservice\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_ListZones_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones\", servermock.ResponseFromFixture(\"errors.xml\")).\n\t\tBuild(t)\n\n\t_, err := client.ListZones(t.Context())\n\trequire.ErrorIs(t, err, Error{\n\t\tText: \"Access token expired or not found\",\n\t\tCode: \"4097\",\n\t})\n}\n\nfunc TestClient_GetZonesByService(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /services/test/zones\",\n\t\t\tservermock.ResponseFromFixture(\"zones_GET.xml\")).\n\t\tBuild(t)\n\n\tzones, err := client.GetZonesByService(t.Context(), \"test\")\n\trequire.NoError(t, err)\n\n\texpected := []Zone{\n\t\t{\n\t\t\tAdmin:      \"123/NIC-REG\",\n\t\t\tEnable:     \"true\",\n\t\t\tHasChanges: \"false\",\n\t\t\tHasPrimary: \"true\",\n\t\t\tID:         \"227645\",\n\t\t\tIDNName:    \"тест.рф\",\n\t\t\tName:       \"xn—e1aybc.xn--p1ai\",\n\t\t\tPayer:      \"123/NIC-REG\",\n\t\t\tService:    \"myservice\",\n\t\t},\n\t\t{\n\t\t\tAdmin:      \"123/NIC-REG\",\n\t\t\tEnable:     \"true\",\n\t\t\tHasChanges: \"false\",\n\t\t\tHasPrimary: \"true\",\n\t\t\tID:         \"227642\",\n\t\t\tIDNName:    \"example.ru\",\n\t\t\tName:       \"example.ru\",\n\t\t\tPayer:      \"123/NIC-REG\",\n\t\t\tService:    \"myservice\",\n\t\t},\n\t\t{\n\t\t\tAdmin:      \"123/NIC-REG\",\n\t\t\tEnable:     \"true\",\n\t\t\tHasChanges: \"false\",\n\t\t\tHasPrimary: \"true\",\n\t\t\tID:         \"227643\",\n\t\t\tIDNName:    \"test.su\",\n\t\t\tName:       \"test.su\",\n\t\t\tPayer:      \"123/NIC-REG\",\n\t\t\tService:    \"myservice\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zones)\n}\n\nfunc TestClient_GetZonesByService_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /services/test/zones\",\n\t\t\tservermock.ResponseFromFixture(\"errors.xml\")).\n\t\tBuild(t)\n\n\t_, err := client.GetZonesByService(t.Context(), \"test\")\n\trequire.ErrorIs(t, err, Error{\n\t\tText: \"Access token expired or not found\",\n\t\tCode: \"4097\",\n\t})\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /services/test/zones/example.com./records\",\n\t\t\tservermock.ResponseFromFixture(\"records_GET.xml\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"test\", \"example.com.\")\n\trequire.NoError(t, err)\n\n\texpected := []RR{\n\t\t{\n\t\t\tID:      \"210074\",\n\t\t\tName:    \"@\",\n\t\t\tIDNName: \"@\",\n\t\t\tTTL:     \"\",\n\t\t\tType:    \"SOA\",\n\t\t\tSOA: &SOA{\n\t\t\t\tMName: &MName{\n\t\t\t\t\tName:    \"ns3-l2.nic.ru.\",\n\t\t\t\t\tIDNName: \"ns3-l2.nic.ru.\",\n\t\t\t\t},\n\t\t\t\tRName: &RName{\n\t\t\t\t\tName:    \"dns.nic.ru.\",\n\t\t\t\t\tIDNName: \"dns.nic.ru.\",\n\t\t\t\t},\n\t\t\t\tSerial:  \"2011112002\",\n\t\t\t\tRefresh: \"1440\",\n\t\t\t\tRetry:   \"3600\",\n\t\t\t\tExpire:  \"2592000\",\n\t\t\t\tMinimum: \"600\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tID:      \"210075\",\n\t\t\tName:    \"@\",\n\t\t\tIDNName: \"@\",\n\t\t\tType:    \"NS\",\n\t\t\tNS: &NS{\n\t\t\t\tName:    \"ns3-l2.nic.ru.\",\n\t\t\t\tIDNName: \"ns3- l2.nic.ru.\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tID:      \"210076\",\n\t\t\tName:    \"@\",\n\t\t\tIDNName: \"@\",\n\t\t\tType:    \"NS\",\n\t\t\tNS: &NS{\n\t\t\t\tName:    \"ns4-l2.nic.ru.\",\n\t\t\t\tIDNName: \"ns4-l2.nic.ru.\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tID:      \"210077\",\n\t\t\tName:    \"@\",\n\t\t\tIDNName: \"@\",\n\t\t\tType:    \"NS\",\n\t\t\tNS: &NS{\n\t\t\t\tName:    \"ns8-l2.nic.ru.\",\n\t\t\t\tIDNName: \"ns8- l2.nic.ru.\",\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /services/test/zones/example.com./records\",\n\t\t\tservermock.ResponseFromFixture(\"errors.xml\")).\n\t\tBuild(t)\n\n\t_, err := client.GetRecords(t.Context(), \"test\", \"example.com.\")\n\trequire.ErrorIs(t, err, Error{\n\t\tText: \"Access token expired or not found\",\n\t\tCode: \"4097\",\n\t})\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /services/test/zones/example.com./records\",\n\t\t\tservermock.ResponseFromFixture(\"records_PUT.xml\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentType(\"text/xml\")).\n\t\tBuild(t)\n\n\trrs := []RR{\n\t\t{\n\t\t\tName: \"@\",\n\t\t\tType: \"NS\",\n\t\t\tNS:   &NS{Name: \"ns4-l2.nic.ru.\"},\n\t\t},\n\t\t{\n\t\t\tName: \"@\",\n\t\t\tType: \"NS\",\n\t\t\tNS:   &NS{Name: \"ns8-l2.nic.ru.\"},\n\t\t},\n\t}\n\n\tresponse, err := client.AddRecords(t.Context(), \"test\", \"example.com.\", rrs)\n\trequire.NoError(t, err)\n\n\texpected := []Zone{\n\t\t{\n\t\t\tAdmin:      \"123/NIC-REG\",\n\t\t\tHasChanges: \"true\",\n\t\t\tID:         \"228095\",\n\t\t\tIDNName:    \"test.ru\",\n\t\t\tName:       \"test.ru\",\n\t\t\tService:    \"testservice\",\n\t\t\tRR: []RR{\n\t\t\t\t{\n\t\t\t\t\tID:      \"210076\",\n\t\t\t\t\tName:    \"@\",\n\t\t\t\t\tIDNName: \"@\",\n\t\t\t\t\tType:    \"NS\",\n\t\t\t\t\tNS: &NS{\n\t\t\t\t\t\tName:    \"ns4-l2.nic.ru.\",\n\t\t\t\t\t\tIDNName: \"ns4-l2.nic.ru.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:      \"210077\",\n\t\t\t\t\tName:    \"@\",\n\t\t\t\t\tIDNName: \"@\",\n\t\t\t\t\tType:    \"NS\",\n\t\t\t\t\tNS: &NS{\n\t\t\t\t\t\tName:    \"ns8-l2.nic.ru.\",\n\t\t\t\t\t\tIDNName: \"ns8-l2.nic.ru.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, response)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /services/test/zones/example.com./records\",\n\t\t\tservermock.ResponseFromFixture(\"errors.xml\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentType(\"text/xml\")).\n\t\tBuild(t)\n\n\trrs := []RR{\n\t\t{\n\t\t\tName: \"@\",\n\t\t\tType: \"NS\",\n\t\t\tNS:   &NS{Name: \"ns4-l2.nic.ru.\"},\n\t\t},\n\t\t{\n\t\t\tName: \"@\",\n\t\t\tType: \"NS\",\n\t\t\tNS:   &NS{Name: \"ns8-l2.nic.ru.\"},\n\t\t},\n\t}\n\n\t_, err := client.AddRecords(t.Context(), \"test\", \"example.com.\", rrs)\n\trequire.ErrorIs(t, err, Error{\n\t\tText: \"Access token expired or not found\",\n\t\tCode: \"4097\",\n\t})\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /services/test/zones/example.com./records/123\",\n\t\t\tservermock.ResponseFromFixture(\"record_DELETE.xml\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"test\", \"example.com.\", \"123\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /services/test/zones/example.com./records/123\",\n\t\t\tservermock.ResponseFromFixture(\"errors.xml\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"test\", \"example.com.\", \"123\")\n\trequire.ErrorIs(t, err, Error{\n\t\tText: \"Access token expired or not found\",\n\t\tCode: \"4097\",\n\t})\n}\n\nfunc TestClient_CommitZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /services/test/zones/example.com./commit\",\n\t\t\tservermock.ResponseFromFixture(\"commit_POST.xml\")).\n\t\tBuild(t)\n\n\terr := client.CommitZone(t.Context(), \"test\", \"example.com.\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_CommitZone_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /services/test/zones/example.com./commit\",\n\t\t\tservermock.ResponseFromFixture(\"errors.xml\")).\n\t\tBuild(t)\n\n\terr := client.CommitZone(t.Context(), \"test\", \"example.com.\")\n\trequire.ErrorIs(t, err, Error{\n\t\tText: \"Access token expired or not found\",\n\t\tCode: \"4097\",\n\t})\n}\n"
  },
  {
    "path": "providers/dns/nicru/internal/fixtures/commit_POST.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<response>\n    <status>success</status>\n</response>\n"
  },
  {
    "path": "providers/dns/nicru/internal/fixtures/errors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<response>\n    <status>fail</status>\n    <errors>\n        <error code=\"4097\">Access token expired or not found</error>\n    </errors>\n</response>\n"
  },
  {
    "path": "providers/dns/nicru/internal/fixtures/record_DELETE.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<response>\n    <status>success</status>\n</response>\n"
  },
  {
    "path": "providers/dns/nicru/internal/fixtures/records_GET.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<response>\n    <status>success</status>\n    <data>\n        <zone admin=\"123/NIC-REG\" has-changes=\"true\" id=\"228095\" idn-name=\"test.ru\" name=\"test.ru\" service=\"myservice\">\n            <rr id=\"210074\">\n                <name>@</name>\n                <idn-name>@</idn-name>\n                <type>SOA</type>\n                <soa>\n                    <mname>\n                        <name>ns3-l2.nic.ru.</name>\n                        <idn-name>ns3-l2.nic.ru.</idn-name>\n                    </mname>\n                    <rname>\n                        <name>dns.nic.ru.</name>\n                        <idn-name>dns.nic.ru.</idn-name>\n                    </rname>\n                    <serial>2011112002</serial>\n                    <refresh>1440</refresh>\n                    <retry>3600</retry>\n                    <expire>2592000</expire>\n                    <minimum>600</minimum>\n                </soa>\n            </rr>\n            <rr id=\"210075\">\n                <name>@</name>\n                <idn-name>@</idn-name>\n                <type>NS</type>\n                <ns>\n                    <name>ns3-l2.nic.ru.</name>\n                    <idn-name>ns3- l2.nic.ru.</idn-name>\n                </ns>\n            </rr>\n            <rr id=\"210076\">\n                <name>@</name>\n                <idn-name>@</idn-name>\n                <type>NS</type>\n                <ns>\n                    <name>ns4-l2.nic.ru.</name>\n                    <idn-name>ns4-l2.nic.ru.</idn-name>\n                </ns>\n            </rr>\n            <rr id=\"210077\">\n                <name>@</name>\n                <idn-name>@</idn-name>\n                <type>NS</type>\n                <ns>\n                    <name>ns8-l2.nic.ru.</name>\n                    <idn-name>ns8- l2.nic.ru.</idn-name>\n                </ns>\n            </rr>\n        </zone>\n    </data>\n</response>\n"
  },
  {
    "path": "providers/dns/nicru/internal/fixtures/records_PUT.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<response>\n    <status>success</status>\n    <data>\n        <zone admin=\"123/NIC-REG\" has-changes=\"true\" id=\"228095\" idn-name=\"test.ru\" name=\"test.ru\" service=\"testservice\">\n            <rr id=\"210076\"><name>@</name><idn-name>@</idn-name><type>NS</type><ns><name>ns4-l2.nic.ru.</name><idn-name>ns4-l2.nic.ru.</idn-name></ns></rr>\n            <rr id=\"210077\"><name>@</name><idn-name>@</idn-name><type>NS</type><ns><name>ns8-l2.nic.ru.</name><idn-name>ns8-l2.nic.ru.</idn-name></ns></rr>\n        </zone>\n    </data>\n</response>\n"
  },
  {
    "path": "providers/dns/nicru/internal/fixtures/services_GET.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<response>\n    <status>success</status>\n    <data>\n        <service admin=\"123/NIC-REG\" domains-limit=\"12\" domains-num=\"5\" enable=\"true\" has-primary=\"false\"\n                 name=\"testservice\"\n                 payer=\"123/NIC-REG\" tariff=\"Secondary L\"/>\n        <service admin=\"123/NIC-REG\" domains-limit=\"150\" domains-num=\"10\" enable=\"true\" has-primary=\"true\"\n                 name=\"myservice\"\n                 payer=\"123/NIC-REG\" rr-limit=\"7500\" rr-num=\"1000\" tariff=\"DNS-master XXL\"/>\n    </data>\n</response>\n"
  },
  {
    "path": "providers/dns/nicru/internal/fixtures/zones_GET.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<response>\n    <status>success</status>\n    <data>\n        <zone admin=\"123/NIC-REG\" enable=\"true\" has-changes=\"false\" has-primary=\"true\" id=\"227645\" idn-name=\"тест.рф\"\n              name=\"xn—e1aybc.xn--p1ai\" payer=\"123/NIC-REG\" service=\"myservice\"/>\n        <zone admin=\"123/NIC-REG\" enable=\"true\" has-changes=\"false\" has-primary=\"true\" id=\"227642\" idn-name=\"example.ru\"\n              name=\"example.ru\" payer=\"123/NIC-REG\" service=\"myservice\"/>\n        <zone admin=\"123/NIC-REG\" enable=\"true\" has-changes=\"false\" has-primary=\"true\" id=\"227643\" idn-name=\"test.su\"\n              name=\"test.su\" payer=\"123/NIC-REG\" service=\"myservice\"/>\n    </data>\n</response>\n"
  },
  {
    "path": "providers/dns/nicru/internal/fixtures/zones_all_GET.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<response>\n    <status>success</status>\n    <data>\n        <zone admin=\"123/NIC-REG\" enable=\"true\" has-changes=\"false\" has-primary=\"true\" id=\"227645\" idn-name=\"тест.рф\"\n              name=\"xn—e1aybc.xn--p1ai\" payer=\"123/NIC-REG\" service=\"myservice\"/>\n        <zone admin=\"123/NIC-REG\" enable=\"true\" has-changes=\"false\" has-primary=\"true\" id=\"227642\" idn-name=\"example.ru\"\n              name=\"example.ru\" payer=\"123/NIC-REG\" service=\"myservice\"/>\n        <zone admin=\"123/NIC-REG\" enable=\"true\" has-changes=\"false\" has-primary=\"true\" id=\"227643\" idn-name=\"test.su\"\n              name=\"test.su\" payer=\"123/NIC-REG\" service=\"myservice\"/>\n    </data>\n</response>\n"
  },
  {
    "path": "providers/dns/nicru/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"golang.org/x/oauth2\"\n)\n\n// OauthConfiguration credentials.\ntype OauthConfiguration struct {\n\tOAuth2ClientID string\n\tOAuth2SecretID string\n\tUsername       string\n\tPassword       string\n}\n\nfunc (config *OauthConfiguration) Validate() error {\n\tmsg := \" is missing in credentials information\"\n\n\tif config.Username == \"\" {\n\t\treturn errors.New(\"username\" + msg)\n\t}\n\n\tif config.Password == \"\" {\n\t\treturn errors.New(\"password\" + msg)\n\t}\n\n\tif config.OAuth2ClientID == \"\" {\n\t\treturn errors.New(\"serviceID\" + msg)\n\t}\n\n\tif config.OAuth2SecretID == \"\" {\n\t\treturn errors.New(\"secret\" + msg)\n\t}\n\n\treturn nil\n}\n\nfunc NewOauthClient(ctx context.Context, config *OauthConfiguration) (*http.Client, error) {\n\terr := config.Validate()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toauth2Config := oauth2.Config{\n\t\tClientID:     config.OAuth2ClientID,\n\t\tClientSecret: config.OAuth2SecretID,\n\t\tEndpoint: oauth2.Endpoint{\n\t\t\tTokenURL:  tokenURL,\n\t\t\tAuthStyle: oauth2.AuthStyleInParams,\n\t\t},\n\t\tScopes: []string{\".+:/dns-master/.+\"},\n\t}\n\n\toauth2Token, err := oauth2Config.PasswordCredentialsToken(ctx, config.Username, config.Password)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create oauth2 token: %w\", err)\n\t}\n\n\treturn oauth2Config.Client(ctx, oauth2Token), nil\n}\n"
  },
  {
    "path": "providers/dns/nicru/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"encoding/xml\"\n\t\"fmt\"\n)\n\ntype Request struct {\n\tXMLName xml.Name `xml:\"request\"`\n\tText    string   `xml:\",chardata\"`\n\tRRList  *RRList  `xml:\"rr-list\"`\n}\n\ntype RRList struct {\n\tText string `xml:\",chardata\"`\n\tRR   []RR   `xml:\"rr\"`\n}\n\ntype RR struct {\n\tText    string `xml:\",chardata\"`\n\tID      string `xml:\"id,attr,omitempty\"`\n\tName    string `xml:\"name\"`\n\tIDNName string `xml:\"idn-name\"`\n\tTTL     string `xml:\"ttl\"`\n\tType    string `xml:\"type\"`\n\tSOA     *SOA   `xml:\"soa,omitempty\"`\n\tA       string `xml:\"a,omitempty\"`\n\tAAAA    string `xml:\"aaaa,omitempty\"`\n\tCName   *CName `xml:\"cname,omitempty\"`\n\tNS      *NS    `xml:\"ns,omitempty\"`\n\tMX      *MX    `xml:\"mx,omitempty\"`\n\tSRV     *SRV   `xml:\"srv,omitempty\"`\n\tPTR     *PTR   `xml:\"ptr,omitempty\"`\n\tTXT     *TXT   `xml:\"txt,omitempty\"`\n\tDName   *DName `xml:\"dname,omitempty\"`\n\tHInfo   *HInfo `xml:\"hinfo,omitempty\"`\n\tNAPTR   *NAPTR `xml:\"naptr,omitempty\"`\n\tRP      *RP    `xml:\"rp,omitempty\"`\n}\n\ntype SOA struct {\n\tText    string `xml:\",chardata\"`\n\tMName   *MName `xml:\"mname\"`\n\tRName   *RName `xml:\"rname\"`\n\tSerial  string `xml:\"serial\"`\n\tRefresh string `xml:\"refresh\"`\n\tRetry   string `xml:\"retry\"`\n\tExpire  string `xml:\"expire\"`\n\tMinimum string `xml:\"minimum\"`\n}\n\ntype MName struct {\n\tText    string `xml:\",chardata\"`\n\tName    string `xml:\"name\"`\n\tIDNName string `xml:\"idn-name,omitempty\"`\n}\n\ntype RName struct {\n\tText    string `xml:\",chardata\"`\n\tName    string `xml:\"name\"`\n\tIDNName string `xml:\"idn-name,omitempty\"`\n}\n\ntype NS struct {\n\tText    string `xml:\",chardata\"`\n\tName    string `xml:\"name\"`\n\tIDNName string `xml:\"idn-name,omitempty\"`\n}\n\ntype MX struct {\n\tText       string    `xml:\",chardata\"`\n\tPreference string    `xml:\"preference\"`\n\tExchange   *Exchange `xml:\"exchange\"`\n}\n\ntype Exchange struct {\n\tName string `xml:\"name\"`\n}\n\ntype SRV struct {\n\tText     string  `xml:\",chardata\"`\n\tPriority string  `xml:\"priority\"`\n\tWeight   string  `xml:\"weight\"`\n\tPort     string  `xml:\"port\"`\n\tTarget   *Target `xml:\"target\"`\n}\n\ntype Target struct {\n\tText string `xml:\",chardata\"`\n\tName string `xml:\"name\"`\n}\n\ntype PTR struct {\n\tText string `xml:\",chardata\"`\n\tName string `xml:\"name\"`\n}\n\ntype HInfo struct {\n\tText     string `xml:\",chardata\"`\n\tHardware string `xml:\"hardware\"`\n\tOS       string `xml:\"os\"`\n}\n\ntype NAPTR struct {\n\tText        string       `xml:\",chardata\"`\n\tOrder       string       `xml:\"order\"`\n\tPreference  string       `xml:\"preference\"`\n\tFlags       string       `xml:\"flags\"`\n\tService     string       `xml:\"service\"`\n\tRegexp      string       `xml:\"regexp\"`\n\tReplacement *Replacement `xml:\"replacement\"`\n}\n\ntype Replacement struct {\n\tText string `xml:\",chardata\"`\n\tName string `xml:\"name\"`\n}\n\ntype RP struct {\n\tText      string     `xml:\",chardata\"`\n\tMboxDName *MboxDName `xml:\"mbox-dname\"`\n\tTxtDName  *TxtDName  `xml:\"txt-dname\"`\n}\n\ntype MboxDName struct {\n\tText string `xml:\",chardata\"`\n\tName string `xml:\"name\"`\n}\n\ntype TxtDName struct {\n\tText string `xml:\",chardata\"`\n\tName string `xml:\"name\"`\n}\n\ntype CName struct {\n\tText    string `xml:\",chardata\"`\n\tName    string `xml:\"name\"`\n\tIDNName string `xml:\"idn-name,omitempty\"`\n}\n\ntype DName struct {\n\tText string `xml:\",chardata\"`\n\tName string `xml:\"name\"`\n}\n\ntype TXT struct {\n\tText   string `xml:\",chardata\"`\n\tString string `xml:\"string\"`\n}\n\ntype Response struct {\n\tXMLName xml.Name `xml:\"response\"`\n\tText    string   `xml:\",chardata\"`\n\tStatus  string   `xml:\"status\"`\n\tData    *Data    `xml:\"data\"`\n\tErrors  Errors   `xml:\"errors\"`\n}\n\ntype Data struct {\n\tText     string     `xml:\",chardata\"`\n\tService  []Service  `xml:\"service\"`\n\tZone     []Zone     `xml:\"zone\"`\n\tAddress  []string   `xml:\"address\"`\n\tRevision []Revision `xml:\"revision\"`\n}\n\ntype Errors struct {\n\tText  string `xml:\",chardata\"`\n\tError Error  `xml:\"error\"`\n}\n\ntype Error struct {\n\tText string `xml:\",chardata\"`\n\tCode string `xml:\"code,attr\"`\n}\n\nfunc (e Error) Error() string {\n\treturn fmt.Sprintf(\"%s (code %s)\", e.Text, e.Code)\n}\n\ntype Service struct {\n\tText         string `xml:\",chardata\"`\n\tAdmin        string `xml:\"admin,attr\"`\n\tDomainsLimit string `xml:\"domains-limit,attr\"`\n\tDomainsNum   string `xml:\"domains-num,attr\"`\n\tEnable       string `xml:\"enable,attr\"`\n\tHasPrimary   string `xml:\"has-primary,attr\"`\n\tName         string `xml:\"name,attr\"`\n\tPayer        string `xml:\"payer,attr\"`\n\tTariff       string `xml:\"tariff,attr\"`\n\tRRLimit      string `xml:\"rr-limit,attr\"`\n\tRRNum        string `xml:\"rr-num,attr\"`\n}\n\ntype Zone struct {\n\tText       string `xml:\",chardata\"`\n\tAdmin      string `xml:\"admin,attr\"`\n\tEnable     string `xml:\"enable,attr\"`\n\tHasChanges string `xml:\"has-changes,attr\"`\n\tHasPrimary string `xml:\"has-primary,attr\"`\n\tID         string `xml:\"id,attr\"`\n\tIDNName    string `xml:\"idn-name,attr\"`\n\tName       string `xml:\"name,attr\"`\n\tPayer      string `xml:\"payer,attr\"`\n\tService    string `xml:\"service,attr\"`\n\tRR         []RR   `xml:\"rr\"`\n}\n\ntype Revision struct {\n\tText   string `xml:\",chardata\"`\n\tDate   string `xml:\"date,attr\"`\n\tIP     string `xml:\"ip,attr\"`\n\tNumber string `xml:\"number,attr\"`\n}\n"
  },
  {
    "path": "providers/dns/nicru/nicru.go",
    "content": "// Package nicru implements a DNS provider for solving the DNS-01 challenge using RU Center.\npackage nicru\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/nicru/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"NICRU_\"\n\n\tEnvUsername  = envNamespace + \"USER\"\n\tEnvPassword  = envNamespace + \"PASSWORD\"\n\tEnvServiceID = envNamespace + \"SERVICE_ID\"\n\tEnvSecret    = envNamespace + \"SECRET\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tTTL                int\n\tUsername           string\n\tPassword           string\n\tServiceID          string\n\tSecret             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 30),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 1*time.Minute),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *internal.Client\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for RU Center.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword, EnvServiceID, EnvSecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"nicru: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\tconfig.ServiceID = values[EnvServiceID]\n\tconfig.Secret = values[EnvSecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for RU Center.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"nicru: the configuration of the DNS provider is nil\")\n\t}\n\n\tclientCfg := &internal.OauthConfiguration{\n\t\tOAuth2ClientID: config.ServiceID,\n\t\tOAuth2SecretID: config.Secret,\n\t\tUsername:       config.Username,\n\t\tPassword:       config.Password,\n\t}\n\n\toauthClient, err := internal.NewOauthClient(context.Background(), clientCfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"nicru: %w\", err)\n\t}\n\n\tclient, err := internal.NewClient(clientdebug.Wrap(oauthClient))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"nicru: unable to build API client: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tclient: client,\n\t\tconfig: config,\n\t}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, _, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicru: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tzone, err := d.findZone(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicru: find zone: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicru: %w\", err)\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, zone.Service, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicru: get records: %w\", err)\n\t}\n\n\tfor _, record := range records {\n\t\tif record.TXT == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif record.TXT.Text == subDomain && record.TXT.String == info.Value {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\trrs := []internal.RR{{\n\t\tName: subDomain,\n\t\tTTL:  strconv.Itoa(d.config.TTL),\n\t\tType: \"TXT\",\n\t\tTXT:  &internal.TXT{String: info.Value},\n\t}}\n\n\t_, err = d.client.AddRecords(ctx, zone.Service, authZone, rrs)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicru: add records: %w\", err)\n\t}\n\n\terr = d.client.CommitZone(ctx, zone.Service, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicru: commit zone: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicru: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tzone, err := d.findZone(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicru: find zone: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicru: %w\", err)\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, zone.Service, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicru: get records: %w\", err)\n\t}\n\n\tsubDomain = dns01.UnFqdn(subDomain)\n\n\tfor _, record := range records {\n\t\tif record.TXT == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif record.Name != subDomain || record.TXT.String != info.Value {\n\t\t\tcontinue\n\t\t}\n\n\t\terr = d.client.DeleteRecord(ctx, zone.Service, authZone, record.ID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"nicru: delete record: %w\", err)\n\t\t}\n\t}\n\n\terr = d.client.CommitZone(ctx, zone.Service, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nicru: commit zone: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) findZone(ctx context.Context, authZone string) (*internal.Zone, error) {\n\tzones, err := d.client.ListZones(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to fetch dns zones: %w\", err)\n\t}\n\n\tif len(zones) == 0 {\n\t\treturn nil, errors.New(\"no zones found\")\n\t}\n\n\tfor _, zone := range zones {\n\t\tif zone.Name == authZone {\n\t\t\treturn &zone, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"zone not found for %s\", authZone)\n}\n"
  },
  {
    "path": "providers/dns/nicru/nicru.toml",
    "content": "Name = \"RU CENTER\"\nDescription = ''''''\nURL = \"https://nic.ru/\"\nCode = \"nicru\"\nSince = \"v4.24.0\"\n\nExample = '''\nNICRU_USER=\"<your_user>\" \\\nNICRU_PASSWORD=\"<your_password>\" \\\nNICRU_SERVICE_ID=\"<service_id>\" \\\nNICRU_SECRET=\"<service_secret>\" \\\nlego --dns nicru -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Credential information\n\nYou can find information about service ID and secret https://www.nic.ru/manager/oauth.cgi?step=oauth.app_list\n\n| ENV Variable        | Parameter from page            | Example           |\n|---------------------|--------------------------------|-------------------|\n| NICRU_USER          | Username (Number of agreement) | NNNNNNN/NIC-D     |\n| NICRU_PASSWORD      | Password account               |                   |\n| NICRU_SERVICE_ID    | Application ID                 | hex-based, len 32 |\n| NICRU_SECRET        | Identity endpoint              | string len 91     |\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    NICRU_USER = \"Agreement for an account in RU CENTER\"\n    NICRU_PASSWORD = \"Password for an account in RU CENTER\"\n    NICRU_SERVICE_ID = \"Service ID for application in DNS-hosting RU CENTER\"\n    NICRU_SECRET = \"Secret for application in DNS-hosting RU CENTER\"\n    NICRU_SERVICE_NAME = \"Service Name for DNS-hosting RU CENTER\"\n  [Configuration.Additional]\n    NICRU_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 60)\"\n    NICRU_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 600)\"\n    NICRU_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.nic.ru/help/api-dns-hostinga_3643.html\"\n"
  },
  {
    "path": "providers/dns/nicru/nicru_test.go",
    "content": "package nicru\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tfakeServiceID = \"2519234972459cdfa23423adf143324f\"\n\tfakeSecret    = \"oo5ahrie0aiPho3Vee4siupoPhahdahCh1thiesohru\"\n\tfakeUsername  = \"1234567/NIC-D\"\n\tfakePassword  = \"einge8Goo2eBaiXievuj\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvUsername, EnvPassword, EnvServiceID, EnvSecret).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServiceID: fakeServiceID,\n\t\t\t\tEnvSecret:    fakeSecret,\n\t\t\t\tEnvUsername:  fakeUsername,\n\t\t\t\tEnvPassword:  fakePassword,\n\t\t\t},\n\t\t\texpected: \"nicru: failed to create oauth2 token: oauth2: \\\"unauthorized_client\\\"\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing serviceID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecret:   fakeSecret,\n\t\t\t\tEnvUsername: fakeUsername,\n\t\t\t\tEnvPassword: fakePassword,\n\t\t\t},\n\t\t\texpected: \"nicru: some credentials information are missing: NICRU_SERVICE_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServiceID: fakeServiceID,\n\t\t\t\tEnvUsername:  fakeUsername,\n\t\t\t\tEnvPassword:  fakePassword,\n\t\t\t},\n\t\t\texpected: \"nicru: some credentials information are missing: NICRU_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServiceID: fakeServiceID,\n\t\t\t\tEnvSecret:    fakeSecret,\n\t\t\t\tEnvPassword:  fakePassword,\n\t\t\t},\n\t\t\texpected: \"nicru: some credentials information are missing: NICRU_USER\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServiceID: fakeServiceID,\n\t\t\t\tEnvSecret:    fakeSecret,\n\t\t\t\tEnvUsername:  fakeUsername,\n\t\t\t},\n\t\t\texpected: \"nicru: some credentials information are missing: NICRU_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tconfig   *Config\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tconfig: &Config{\n\t\t\t\tServiceID: fakeServiceID,\n\t\t\t\tSecret:    fakeSecret,\n\t\t\t\tUsername:  fakeUsername,\n\t\t\t\tPassword:  fakePassword,\n\t\t\t},\n\t\t\texpected: \"nicru: failed to create oauth2 token: oauth2: \\\"unauthorized_client\\\"\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"nil config\",\n\t\t\tconfig:   nil,\n\t\t\texpected: \"nicru: the configuration of the DNS provider is nil\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tconfig: &Config{\n\t\t\t\tServiceID: fakeServiceID,\n\t\t\t\tPassword:  fakePassword,\n\t\t\t},\n\t\t\texpected: \"nicru: username is missing in credentials information\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tconfig: &Config{\n\t\t\t\tServiceID: fakeServiceID,\n\t\t\t\tSecret:    fakeSecret,\n\t\t\t\tUsername:  fakeUsername,\n\t\t\t},\n\t\t\texpected: \"nicru: password is missing in credentials information\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret\",\n\t\t\tconfig: &Config{\n\t\t\t\tServiceID: fakeServiceID,\n\t\t\t\tUsername:  fakeUsername,\n\t\t\t\tPassword:  fakePassword,\n\t\t\t},\n\t\t\texpected: \"nicru: secret is missing in credentials information\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing serviceID\",\n\t\t\tconfig: &Config{\n\t\t\t\tSecret:   fakeSecret,\n\t\t\t\tUsername: fakeUsername,\n\t\t\t\tPassword: fakePassword,\n\t\t\t},\n\t\t\texpected: \"nicru: serviceID is missing in credentials information\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tp, err := NewDNSProviderConfig(test.config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/nifcloud/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst (\n\tdefaultBaseURL = \"https://dns.api.nifcloud.com\"\n\tapiVersion     = \"2012-12-12N2013-12-16\"\n\t// XMLNs XML NS of Route53.\n\tXMLNs = \"https://route53.amazonaws.com/doc/2012-12-12/\"\n)\n\n// Client the API client for NIFCLOUD DNS.\ntype Client struct {\n\taccessKey string\n\tsecretKey string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new client of NIFCLOUD DNS.\nfunc NewClient(accessKey, secretKey string) (*Client, error) {\n\tif accessKey == \"\" || secretKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\taccessKey:  accessKey,\n\t\tsecretKey:  secretKey,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// ChangeResourceRecordSets Call ChangeResourceRecordSets API and return response.\nfunc (c *Client) ChangeResourceRecordSets(ctx context.Context, hostedZoneID string, input ChangeResourceRecordSetsRequest) (*ChangeResourceRecordSetsResponse, error) {\n\tendpoint := c.BaseURL.JoinPath(apiVersion, \"hostedzone\", hostedZoneID, \"rrset\")\n\n\treq, err := newXMLRequest(ctx, http.MethodPost, endpoint, input)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toutput := &ChangeResourceRecordSetsResponse{}\n\n\terr = c.do(req, output)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn output, nil\n}\n\n// GetChange Call GetChange API and return response.\nfunc (c *Client) GetChange(ctx context.Context, statusID string) (*GetChangeResponse, error) {\n\tendpoint := c.BaseURL.JoinPath(apiVersion, \"change\", statusID)\n\n\treq, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toutput := &GetChangeResponse{}\n\n\terr = c.do(req, output)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn output, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\terr := c.sign(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"an error occurred during the creation of the signature: %w\", err)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = xml.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) sign(req *http.Request) error {\n\tif req.Header.Get(\"Date\") == \"\" {\n\t\treq.Header.Set(\"Date\", time.Now().UTC().Format(http.TimeFormat))\n\t}\n\n\tif req.URL.Path == \"\" {\n\t\treq.URL.Path += \"/\"\n\t}\n\n\tmac := hmac.New(sha1.New, []byte(c.secretKey))\n\n\t_, err := mac.Write([]byte(req.Header.Get(\"Date\")))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thashed := mac.Sum(nil)\n\tsignature := base64.StdEncoding.EncodeToString(hashed)\n\n\tauth := fmt.Sprintf(\"NIFTY3-HTTPS NiftyAccessKeyId=%s,Algorithm=HmacSHA1,Signature=%s\", c.accessKey, signature)\n\treq.Header.Set(\"X-Nifty-Authorization\", auth)\n\n\treturn nil\n}\n\nfunc newXMLRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbody := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\tbody.WriteString(xml.Header)\n\n\t\terr := xml.NewEncoder(body).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request XML body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"text/xml; charset=utf-8\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\terrResp := &ErrorResponse{}\n\n\terr := xml.Unmarshal(raw, errResp)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn errResp.Error\n}\n"
  },
  {
    "path": "providers/dns/nifcloud/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"A\", \"B\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithRegexp(\"X-Nifty-Authorization\", \"NIFTY3-HTTPS NiftyAccessKeyId=A,Algorithm=HmacSHA1,Signature=.+\"),\n\t)\n}\n\nfunc TestClient_ChangeResourceRecordSets(t *testing.T) {\n\tresponseBody := `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ChangeResourceRecordSetsResponse xmlns=\"https://route53.amazonaws.com/doc/2012-12-12/\">\n  <ChangeInfo>\n    <Id>xxxxx</Id>\n    <Status>INSYNC</Status>\n    <SubmittedAt>2015-08-05T00:00:00.000Z</SubmittedAt>\n  </ChangeInfo>\n</ChangeResourceRecordSetsResponse>\n`\n\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\", servermock.RawStringResponse(responseBody),\n\t\t\tservermock.CheckHeader().WithContentType(\"text/xml; charset=utf-8\")).\n\t\tBuild(t)\n\n\tres, err := client.ChangeResourceRecordSets(t.Context(), \"example.com\", ChangeResourceRecordSetsRequest{})\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"xxxxx\", res.ChangeInfo.ID)\n\tassert.Equal(t, \"INSYNC\", res.ChangeInfo.Status)\n\tassert.Equal(t, \"2015-08-05T00:00:00.000Z\", res.ChangeInfo.SubmittedAt)\n}\n\nfunc TestClient_ChangeResourceRecordSets_errors(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc         string\n\t\tresponseBody string\n\t\tstatusCode   int\n\t\texpected     string\n\t}{\n\t\t{\n\t\t\tdesc: \"API error\",\n\t\t\tresponseBody: `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ErrorResponse>\n  <Error>\n    <Type>Sender</Type>\n    <Code>AuthFailed</Code>\n    <Message>The request signature we calculated does not match the signature you provided.</Message>\n  </Error>\n</ErrorResponse>\n`,\n\t\t\tstatusCode: http.StatusUnauthorized,\n\t\t\texpected:   \"Sender(AuthFailed): The request signature we calculated does not match the signature you provided.\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"response body error\",\n\t\t\tresponseBody: \"foo\",\n\t\t\tstatusCode:   http.StatusOK,\n\t\t\texpected:     \"unable to unmarshal response: [status code: 200] body: foo error: EOF\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"error message error\",\n\t\t\tresponseBody: \"foo\",\n\t\t\tstatusCode:   http.StatusInternalServerError,\n\t\t\texpected:     \"unexpected status code: [status code: 500] body: foo\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(\"POST /\",\n\t\t\t\t\tservermock.RawStringResponse(test.responseBody).\n\t\t\t\t\t\tWithStatusCode(test.statusCode),\n\t\t\t\t\tservermock.CheckHeader().\n\t\t\t\t\t\tWithContentType(\"text/xml; charset=utf-8\")).\n\t\t\t\tBuild(t)\n\n\t\t\tres, err := client.ChangeResourceRecordSets(t.Context(), \"example.com\", ChangeResourceRecordSetsRequest{})\n\t\t\tassert.Nil(t, res)\n\t\t\tassert.EqualError(t, err, test.expected)\n\t\t})\n\t}\n}\n\nfunc TestClient_GetChange(t *testing.T) {\n\tresponseBody := `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<GetChangeResponse xmlns=\"https://route53.amazonaws.com/doc/2012-12-12/\">\n  <ChangeInfo>\n    <Id>xxxxx</Id>\n    <Status>INSYNC</Status>\n    <SubmittedAt>2015-08-05T00:00:00.000Z</SubmittedAt>\n  </ChangeInfo>\n</GetChangeResponse>\n`\n\n\tclient := mockBuilder().\n\t\tRoute(\"GET /\", servermock.RawStringResponse(responseBody)).\n\t\tBuild(t)\n\n\tres, err := client.GetChange(t.Context(), \"12345\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"xxxxx\", res.ChangeInfo.ID)\n\tassert.Equal(t, \"INSYNC\", res.ChangeInfo.Status)\n\tassert.Equal(t, \"2015-08-05T00:00:00.000Z\", res.ChangeInfo.SubmittedAt)\n}\n\nfunc TestClient_GetChange_errors(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc         string\n\t\tresponseBody string\n\t\tstatusCode   int\n\t\texpected     string\n\t}{\n\t\t{\n\t\t\tdesc: \"API error\",\n\t\t\tresponseBody: `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ErrorResponse>\n  <Error>\n    <Type>Sender</Type>\n    <Code>AuthFailed</Code>\n    <Message>The request signature we calculated does not match the signature you provided.</Message>\n  </Error>\n</ErrorResponse>\n`,\n\t\t\tstatusCode: http.StatusUnauthorized,\n\t\t\texpected:   \"Sender(AuthFailed): The request signature we calculated does not match the signature you provided.\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"response body error\",\n\t\t\tresponseBody: \"foo\",\n\t\t\tstatusCode:   http.StatusOK,\n\t\t\texpected:     \"unable to unmarshal response: [status code: 200] body: foo error: EOF\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"error message error\",\n\t\t\tresponseBody: \"foo\",\n\t\t\tstatusCode:   http.StatusInternalServerError,\n\t\t\texpected:     \"unexpected status code: [status code: 500] body: foo\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(\"GET /\",\n\t\t\t\t\tservermock.RawStringResponse(test.responseBody).WithStatusCode(test.statusCode)).\n\t\t\t\tBuild(t)\n\n\t\t\tres, err := client.GetChange(t.Context(), \"12345\")\n\t\t\tassert.Nil(t, res)\n\t\t\tassert.EqualError(t, err, test.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/nifcloud/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\n// ChangeResourceRecordSetsRequest is a complex type that contains change information for the resource record set.\ntype ChangeResourceRecordSetsRequest struct {\n\tXMLNs       string      `xml:\"xmlns,attr\"`\n\tChangeBatch ChangeBatch `xml:\"ChangeBatch\"`\n}\n\n// ChangeResourceRecordSetsResponse is a complex type containing the response for the request.\ntype ChangeResourceRecordSetsResponse struct {\n\tChangeInfo ChangeInfo `xml:\"ChangeInfo\"`\n}\n\n// GetChangeResponse is a complex type that contains the ChangeInfo element.\ntype GetChangeResponse struct {\n\tChangeInfo ChangeInfo `xml:\"ChangeInfo\"`\n}\n\ntype Error struct {\n\tType    string `xml:\"Type\"`\n\tMessage string `xml:\"Message\"`\n\tCode    string `xml:\"Code\"`\n}\n\nfunc (e Error) Error() string {\n\treturn fmt.Sprintf(\"%s(%s): %s\", e.Type, e.Code, e.Message)\n}\n\n// ErrorResponse is the information for any errors.\ntype ErrorResponse struct {\n\tError     Error  `xml:\"Error\"`\n\tRequestID string `xml:\"RequestId\"`\n}\n\n// ChangeBatch is the information for a change request.\ntype ChangeBatch struct {\n\tChanges Changes `xml:\"Changes\"`\n\tComment string  `xml:\"Comment\"`\n}\n\n// Changes is array of Change.\ntype Changes struct {\n\tChange []Change `xml:\"Change\"`\n}\n\n// Change is the information for each resource record set that you want to change.\ntype Change struct {\n\tAction            string            `xml:\"Action\"`\n\tResourceRecordSet ResourceRecordSet `xml:\"ResourceRecordSet\"`\n}\n\n// ResourceRecordSet is the information about the resource record set to create or delete.\ntype ResourceRecordSet struct {\n\tName            string          `xml:\"Name\"`\n\tType            string          `xml:\"Type\"`\n\tTTL             int             `xml:\"TTL\"`\n\tResourceRecords ResourceRecords `xml:\"ResourceRecords\"`\n}\n\n// ResourceRecords is array of ResourceRecord.\ntype ResourceRecords struct {\n\tResourceRecord []ResourceRecord `xml:\"ResourceRecord\"`\n}\n\n// ResourceRecord is the information specific to the resource record.\ntype ResourceRecord struct {\n\tValue string `xml:\"Value\"`\n}\n\n// ChangeInfo is A complex type that describes change information about changes made to your hosted zone.\ntype ChangeInfo struct {\n\tID          string `xml:\"Id\"`\n\tStatus      string `xml:\"Status\"`\n\tSubmittedAt string `xml:\"SubmittedAt\"`\n}\n"
  },
  {
    "path": "providers/dns/nifcloud/nifcloud.go",
    "content": "// Package nifcloud implements a DNS provider for solving the DNS-01 challenge using NIFCLOUD DNS.\npackage nifcloud\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/nifcloud/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"NIFCLOUD_\"\n\n\tEnvAccessKeyID     = envNamespace + \"ACCESS_KEY_ID\"\n\tEnvSecretAccessKey = envNamespace + \"SECRET_ACCESS_KEY\"\n\tEnvDNSEndpoint     = envNamespace + \"DNS_ENDPOINT\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL            string\n\tAccessKey          string\n\tSecretKey          string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *internal.Client\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for the NIFCLOUD DNS service.\n// Credentials must be passed in the environment variables:\n// NIFCLOUD_ACCESS_KEY_ID and NIFCLOUD_SECRET_ACCESS_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"nifcloud: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.BaseURL = env.GetOrFile(EnvDNSEndpoint)\n\tconfig.AccessKey = values[EnvAccessKeyID]\n\tconfig.SecretKey = values[EnvSecretAccessKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for NIFCLOUD.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"nifcloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.AccessKey, config.SecretKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"nifcloud: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\tif config.BaseURL != \"\" {\n\t\tbaseURL, err := url.Parse(config.BaseURL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"nifcloud: %w\", err)\n\t\t}\n\n\t\tclient.BaseURL = baseURL\n\t}\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.changeRecord(ctx, \"CREATE\", info.EffectiveFQDN, info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nifcloud: %w\", err)\n\t}\n\n\treturn err\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.changeRecord(ctx, \"DELETE\", info.EffectiveFQDN, info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nifcloud: %w\", err)\n\t}\n\n\treturn err\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) changeRecord(ctx context.Context, action, fqdn, value string, ttl int) error {\n\tauthZone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tname := dns01.UnFqdn(fqdn)\n\tif authZone == fqdn {\n\t\tname = \"@\"\n\t}\n\n\treqParams := internal.ChangeResourceRecordSetsRequest{\n\t\tXMLNs: internal.XMLNs,\n\t\tChangeBatch: internal.ChangeBatch{\n\t\t\tComment: \"Managed by Lego\",\n\t\t\tChanges: internal.Changes{\n\t\t\t\tChange: []internal.Change{\n\t\t\t\t\t{\n\t\t\t\t\t\tAction: action,\n\t\t\t\t\t\tResourceRecordSet: internal.ResourceRecordSet{\n\t\t\t\t\t\t\tName: name,\n\t\t\t\t\t\t\tType: \"TXT\",\n\t\t\t\t\t\t\tTTL:  ttl,\n\t\t\t\t\t\t\tResourceRecords: internal.ResourceRecords{\n\t\t\t\t\t\t\t\tResourceRecord: []internal.ResourceRecord{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tValue: value,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tresp, err := d.client.ChangeResourceRecordSets(ctx, dns01.UnFqdn(authZone), reqParams)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to change record set: %w\", err)\n\t}\n\n\tstatusID := resp.ChangeInfo.ID\n\n\treturn wait.Retry(ctx,\n\t\tfunc() error {\n\t\t\tresp, err := d.client.GetChange(ctx, statusID)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"get change: %w\", err)\n\t\t\t}\n\n\t\t\tif resp.ChangeInfo.Status != \"INSYNC\" {\n\t\t\t\treturn fmt.Errorf(\"change status: %s\", resp.ChangeInfo.Status)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t\tbackoff.WithBackOff(backoff.NewConstantBackOff(4*time.Second)),\n\t\tbackoff.WithMaxElapsedTime(120*time.Second),\n\t)\n}\n"
  },
  {
    "path": "providers/dns/nifcloud/nifcloud.toml",
    "content": "Name = \"NIFCloud\"\nDescription = ''''''\nURL = \"https://www.nifcloud.com/\"\nCode = \"nifcloud\"\nSince = \"v1.1.0\"\n\nExample = '''\nNIFCLOUD_ACCESS_KEY_ID=xxxx \\\nNIFCLOUD_SECRET_ACCESS_KEY=yyyy \\\nlego --dns nifcloud -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    NIFCLOUD_ACCESS_KEY_ID = \"Access key\"\n    NIFCLOUD_SECRET_ACCESS_KEY = \"Secret access key\"\n  [Configuration.Additional]\n    NIFCLOUD_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    NIFCLOUD_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    NIFCLOUD_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    NIFCLOUD_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://mbaas.nifcloud.com/doc/current/rest/common/format.html\"\n"
  },
  {
    "path": "providers/dns/nifcloud/nifcloud_test.go",
    "content": "package nifcloud\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAccessKeyID,\n\tEnvSecretAccessKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKeyID:     \"123\",\n\t\t\t\tEnvSecretAccessKey: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKeyID:     \"\",\n\t\t\t\tEnvSecretAccessKey: \"\",\n\t\t\t},\n\t\t\texpected: \"nifcloud: some credentials information are missing: NIFCLOUD_ACCESS_KEY_ID,NIFCLOUD_SECRET_ACCESS_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKeyID:     \"\",\n\t\t\t\tEnvSecretAccessKey: \"456\",\n\t\t\t},\n\t\t\texpected: \"nifcloud: some credentials information are missing: NIFCLOUD_ACCESS_KEY_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKeyID:     \"123\",\n\t\t\t\tEnvSecretAccessKey: \"\",\n\t\t\t},\n\t\t\texpected: \"nifcloud: some credentials information are missing: NIFCLOUD_SECRET_ACCESS_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\taccessKey string\n\t\tsecretKey string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\taccessKey: \"123\",\n\t\t\tsecretKey: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"nifcloud: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing api key\",\n\t\t\tsecretKey: \"456\",\n\t\t\texpected:  \"nifcloud: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing secret key\",\n\t\t\taccessKey: \"123\",\n\t\t\texpected:  \"nifcloud: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AccessKey = test.accessKey\n\t\t\tconfig.SecretKey = test.secretKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/njalla/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst apiEndpoint = \"https://njal.la/api/1/\"\n\nconst authorizationHeader = \"Authorization\"\n\n// Client is a Njalla API client.\ntype Client struct {\n\ttoken string\n\n\tapiEndpoint string\n\tHTTPClient  *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(token string) *Client {\n\treturn &Client{\n\t\ttoken:       token,\n\t\tapiEndpoint: apiEndpoint,\n\t\tHTTPClient:  &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// AddRecord adds a record.\nfunc (c *Client) AddRecord(ctx context.Context, record Record) (*Record, error) {\n\tdata := APIRequest{\n\t\tMethod: \"add-record\",\n\t\tParams: record,\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, c.apiEndpoint, data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result APIResponse[*Record]\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Result, nil\n}\n\n// RemoveRecord removes a record.\nfunc (c *Client) RemoveRecord(ctx context.Context, id, domain string) error {\n\tdata := APIRequest{\n\t\tMethod: \"remove-record\",\n\t\tParams: Record{\n\t\t\tID:     id,\n\t\t\tDomain: domain,\n\t\t},\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, c.apiEndpoint, data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, &APIResponse[json.RawMessage]{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// ListRecords list the records for one domain.\nfunc (c *Client) ListRecords(ctx context.Context, domain string) ([]Record, error) {\n\tdata := APIRequest{\n\t\tMethod: \"list-records\",\n\t\tParams: Record{\n\t\t\tDomain: domain,\n\t\t},\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, c.apiEndpoint, data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result APIResponse[Records]\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Result.Records, nil\n}\n\nfunc (c *Client) do(req *http.Request, result Response) error {\n\treq.Header.Set(authorizationHeader, \"Njalla \"+c.token)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn result.GetError()\n}\n\nfunc newJSONRequest(ctx context.Context, method, endpoint string, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint, buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/njalla/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"secret\")\n\tclient.apiEndpoint = server.URL\n\tclient.HTTPClient = server.Client()\n\n\treturn client, nil\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Njalla secret\"),\n\t).\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"add_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"add_record-request.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tContent: \"foobar\",\n\t\tDomain:  \"test\",\n\t\tName:    \"example.com\",\n\t\tTTL:     300,\n\t\tType:    \"TXT\",\n\t}\n\n\tresult, err := client.AddRecord(t.Context(), record)\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tID:      \"123\",\n\t\tContent: \"foobar\",\n\t\tDomain:  \"test\",\n\t\tName:    \"example.com\",\n\t\tTTL:     300,\n\t\tType:    \"TXT\",\n\t}\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Njalla invalid\"),\n\t).\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"auth_error.json\")).\n\t\tBuild(t)\n\n\tclient.token = \"invalid\"\n\n\trecord := Record{\n\t\tContent: \"test\",\n\t\tDomain:  \"test01\",\n\t\tName:    \"example.com\",\n\t\tTTL:     300,\n\t\tType:    \"TXT\",\n\t}\n\n\tresult, err := client.AddRecord(t.Context(), record)\n\trequire.EqualError(t, err, \"code: 403, message: Invalid token.\")\n\n\tassert.Nil(t, result)\n}\n\nfunc TestClient_ListRecords(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Njalla secret\"),\n\t).\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(\"list_records.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"list_records-request.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.ListRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tID:      \"1\",\n\t\t\tDomain:  \"example.com\",\n\t\t\tContent: \"test\",\n\t\t\tName:    \"test01\",\n\t\t\tTTL:     300,\n\t\t\tType:    \"TXT\",\n\t\t},\n\t\t{\n\t\t\tID:      \"2\",\n\t\t\tDomain:  \"example.com\",\n\t\t\tContent: \"txtTxt\",\n\t\t\tName:    \"test02\",\n\t\t\tTTL:     120,\n\t\t\tType:    \"TXT\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_ListRecords_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Njalla invalid\"),\n\t).\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"auth_error.json\")).\n\t\tBuild(t)\n\n\tclient.token = \"invalid\"\n\n\trecords, err := client.ListRecords(t.Context(), \"example.com\")\n\trequire.EqualError(t, err, \"code: 403, message: Invalid token.\")\n\n\tassert.Empty(t, records)\n}\n\nfunc TestClient_RemoveRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Njalla secret\"),\n\t).\n\t\tRoute(\"POST /\",\n\t\t\tservermock.RawStringResponse(`{\"jsonrpc\":\"2.0\"}`),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"remove_record-request.json\")).\n\t\tBuild(t)\n\n\terr := client.RemoveRecord(t.Context(), \"123\", \"example.com\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_RemoveRecord_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient,\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Njalla secret\"),\n\t).\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"remove_record_error_missing_domain.json\")).\n\t\tBuild(t)\n\n\terr := client.RemoveRecord(t.Context(), \"123\", \"example.com\")\n\trequire.EqualError(t, err, \"code: 400, message: missing domain\")\n}\n"
  },
  {
    "path": "providers/dns/njalla/internal/fixtures/add_record-request.json",
    "content": "{\n  \"method\": \"add-record\",\n  \"params\": {\n    \"content\": \"foobar\",\n    \"domain\": \"test\",\n    \"name\": \"example.com\",\n    \"ttl\": 300,\n    \"type\": \"TXT\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/njalla/internal/fixtures/add_record.json",
    "content": "{\n  \"id\": \"897\",\n  \"jsonrpc\": \"2.0\",\n  \"result\": {\n    \"id\": \"123\",\n    \"content\": \"foobar\",\n    \"domain\": \"test\",\n    \"name\": \"example.com\",\n    \"ttl\": 300,\n    \"type\": \"TXT\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/njalla/internal/fixtures/auth_error.json",
    "content": "{\n  \"jsonrpc\": \"2.0\",\n  \"Error\": {\n    \"code\": 403,\n    \"message\": \"Invalid token.\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/njalla/internal/fixtures/list_records-request.json",
    "content": "{\n  \"method\": \"list-records\",\n  \"params\": {\n    \"domain\": \"example.com\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/njalla/internal/fixtures/list_records.json",
    "content": "{\n  \"id\": \"897\",\n  \"jsonrpc\": \"2.0\",\n  \"result\": {\n    \"records\": [\n      {\n        \"id\": \"1\",\n        \"content\": \"test\",\n        \"domain\": \"example.com\",\n        \"name\": \"test01\",\n        \"ttl\": 300,\n        \"type\": \"TXT\"\n      },\n      {\n        \"id\": \"2\",\n        \"content\": \"txtTxt\",\n        \"domain\": \"example.com\",\n        \"name\": \"test02\",\n        \"ttl\": 120,\n        \"type\": \"TXT\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/njalla/internal/fixtures/remove_record-request.json",
    "content": "{\n  \"method\": \"remove-record\",\n  \"params\": {\n    \"id\": \"123\",\n    \"domain\": \"example.com\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/njalla/internal/fixtures/remove_record_error_missing_domain.json",
    "content": "{\n  \"jsonrpc\": \"2.0\",\n  \"Error\": {\n    \"code\": 400,\n    \"message\": \"missing domain\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/njalla/internal/fixtures/remove_record_error_missing_id.json",
    "content": "{\n  \"jsonrpc\": \"2.0\",\n  \"Error\": {\n    \"code\": 400,\n    \"message\": \"missing ID\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/njalla/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n)\n\n// APIRequest represents an API request body.\ntype APIRequest struct {\n\tMethod string `json:\"method\"`\n\tParams any    `json:\"params\"`\n}\n\ntype Response interface {\n\tGetError() error\n}\n\n// APIResponse represents an API response body.\ntype APIResponse[T any] struct {\n\tID     string    `json:\"id\"`\n\tRPC    string    `json:\"jsonrpc\"`\n\tError  *APIError `json:\"error,omitempty\"`\n\tResult T         `json:\"result,omitempty\"`\n}\n\nfunc (a APIResponse[T]) GetError() error {\n\tif a.Error == (*APIError)(nil) {\n\t\treturn nil\n\t}\n\n\treturn a.Error\n}\n\n// APIError is an API error.\ntype APIError struct {\n\tCode    int\n\tMessage string\n}\n\nfunc (a APIError) Error() string {\n\treturn fmt.Sprintf(\"code: %d, message: %s\", a.Code, a.Message)\n}\n\n// Record is a DNS record.\ntype Record struct {\n\tID      string `json:\"id,omitempty\"`\n\tContent string `json:\"content,omitempty\"`\n\tDomain  string `json:\"domain,omitempty\"`\n\tName    string `json:\"name,omitempty\"`\n\tTTL     int    `json:\"ttl,omitempty\"`\n\tType    string `json:\"type,omitempty\"`\n}\n\n// Records is a list of DNS records.\ntype Records struct {\n\tRecords []Record `json:\"records,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/njalla/njalla.go",
    "content": "// Package njalla implements a DNS provider for solving the DNS-01 challenge using Njalla.\npackage njalla\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/njalla/internal\"\n\t\"github.com/miekg/dns\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"NJALLA_\"\n\n\tEnvToken = envNamespace + \"TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken              string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Njalla.\n// Credentials must be passed in the environment variable: NJALLA_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"njalla: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Njalla.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"njalla: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"njalla: missing credentials\")\n\t}\n\n\tclient := internal.NewClient(config.Token)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trootDomain, subDomain, err := splitDomain(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"njalla: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName:    subDomain,                // TODO need to be tested\n\t\tDomain:  dns01.UnFqdn(rootDomain), // TODO need to be tested\n\t\tContent: info.Value,\n\t\tTTL:     d.config.TTL,\n\t\tType:    \"TXT\",\n\t}\n\n\tresp, err := d.client.AddRecord(context.Background(), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"njalla: failed to add record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = resp.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trootDomain, _, err := splitDomain(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"njalla: %w\", err)\n\t}\n\n\t// gets the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"njalla: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\terr = d.client.RemoveRecord(context.Background(), recordID, dns01.UnFqdn(rootDomain))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"njalla: failed to delete TXT records: fqdn=%s, recordID=%s: %w\", info.EffectiveFQDN, recordID, err)\n\t}\n\n\t// deletes record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc splitDomain(full string) (string, string, error) {\n\tsplit := dns.Split(full)\n\tif len(split) < 2 {\n\t\treturn \"\", \"\", fmt.Errorf(\"unsupported domain: %s\", full)\n\t}\n\n\tif len(split) == 2 {\n\t\treturn full, \"\", nil\n\t}\n\n\tdomain := full[split[len(split)-2]:]\n\tsubDomain := full[:split[len(split)-2]-1]\n\n\treturn domain, subDomain, nil\n}\n"
  },
  {
    "path": "providers/dns/njalla/njalla.toml",
    "content": "Name = \"Njalla\"\nDescription = ''''''\nURL = \"https://njal.la\"\nCode = \"njalla\"\nSince = \"v4.3.0\"\n\nExample = '''\nNJALLA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns njalla -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    NJALLA_TOKEN = \"API token\"\n  [Configuration.Additional]\n    NJALLA_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    NJALLA_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    NJALLA_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    NJALLA_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://njal.la/api/\"\n"
  },
  {
    "path": "providers/dns/njalla/njalla_test.go",
    "content": "package njalla\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"\",\n\t\t\t},\n\t\t\texpected: \"njalla: some credentials information are missing: NJALLA_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"njalla: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/nodion/nodion.go",
    "content": "// Package nodion implements a DNS provider for solving the DNS-01 challenge using Nodion DNS.\npackage nodion\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/nrdcg/nodion\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"NODION_\"\n\n\tEnvAPIToken = envNamespace + \"API_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *nodion.Client\n\n\tzoneIDs   map[string]string\n\tzoneIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Nodion.\n// Credentials must be passed in the environment variable: NODION_API_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"nodion: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIToken = values[EnvAPIToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Nodion.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"nodion: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIToken == \"\" {\n\t\treturn nil, errors.New(\"nodion: incomplete credentials, missing API token\")\n\t}\n\n\tclient, err := nodion.NewClient(config.APIToken)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:  config,\n\t\tclient:  client,\n\t\tzoneIDs: map[string]string{},\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nodion: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nodion: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\tzones, err := d.client.GetZones(ctx, &nodion.ZonesFilter{Name: dns01.UnFqdn(authZone)})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nodion: %w\", err)\n\t}\n\n\tif len(zones) == 0 {\n\t\treturn fmt.Errorf(\"nodion: zone not found: %s\", authZone)\n\t}\n\n\tif len(zones) > 1 {\n\t\treturn fmt.Errorf(\"nodion: too many possible zones for the domain %s: %v\", authZone, zones)\n\t}\n\n\tzoneID := zones[0].ID\n\n\trecord := nodion.Record{\n\t\tRecordType: nodion.TypeTXT,\n\t\tName:       subDomain,\n\t\tContent:    info.Value,\n\t\tTTL:        d.config.TTL,\n\t}\n\n\t_, err = d.client.CreateRecord(ctx, zoneID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nodion: failed to create TXT records [domain: %s, sub domain: %s]: %w\",\n\t\t\tdns01.UnFqdn(authZone), subDomain, err)\n\t}\n\n\td.zoneIDsMu.Lock()\n\td.zoneIDs[token] = zoneID\n\td.zoneIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nodion: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\td.zoneIDsMu.Lock()\n\tzoneID, ok := d.zoneIDs[token]\n\td.zoneIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"nodion: unknown zone ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nodion: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\tfilter := &nodion.RecordsFilter{\n\t\tName:       subDomain,\n\t\tRecordType: nodion.TypeTXT,\n\t\tContent:    info.Value,\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, zoneID, filter)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nodion: %w\", err)\n\t}\n\n\tif len(records) == 0 {\n\t\treturn fmt.Errorf(\"nodion: record not found: %s\", authZone)\n\t}\n\n\tif len(records) > 1 {\n\t\treturn fmt.Errorf(\"nodion: too many possible records for the domain %s: %v\", info.EffectiveFQDN, records)\n\t}\n\n\t_, err = d.client.DeleteRecord(ctx, zoneID, records[0].ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"regru: failed to remove TXT records [domain: %s]: %w\", dns01.UnFqdn(authZone), err)\n\t}\n\n\td.zoneIDsMu.Lock()\n\tdelete(d.zoneIDs, token)\n\td.zoneIDsMu.Unlock()\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/nodion/nodion.toml",
    "content": "Name = \"Nodion\"\nDescription = ''''''\nURL = \"https://www.nodion.com\"\nCode = \"nodion\"\nSince = \"v4.11.0\"\n\nExample = '''\nNODION_API_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns nodion -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    NODION_API_TOKEN = \"The API token\"\n  [Configuration.Additional]\n    NODION_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    NODION_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    NODION_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    NODION_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.nodion.com/en/docs/dns/api/\"\n"
  },
  {
    "path": "providers/dns/nodion/nodion_test.go",
    "content": "package nodion\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"nodion: some credentials information are missing: NODION_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiToken string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiToken: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"nodion: incomplete credentials, missing API token\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = test.apiToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/ns1/ns1.go",
    "content": "// Package ns1 implements a DNS provider for solving the DNS-01 challenge using NS1 DNS.\npackage ns1\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"gopkg.in/ns1/ns1-go.v2/rest\"\n\t\"gopkg.in/ns1/ns1-go.v2/rest/model/dns\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"NS1_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *rest.Client\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for NS1.\n// Credentials must be passed in the environment variables: NS1_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ns1: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for NS1.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"ns1: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"ns1: credentials missing\")\n\t}\n\n\tif config.HTTPClient == nil {\n\t\t// Because the rest.NewClient uses the http.DefaultClient.\n\t\tconfig.HTTPClient = &http.Client{Timeout: 10 * time.Second}\n\t}\n\n\tclient := rest.NewClient(clientdebug.Wrap(config.HTTPClient), rest.SetAPIKey(config.APIKey))\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getHostedZone(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ns1: %w\", err)\n\t}\n\n\trecord, _, err := d.client.Records.Get(zone.Zone, dns01.UnFqdn(info.EffectiveFQDN), \"TXT\")\n\n\t// Create a new record\n\tif errors.Is(err, rest.ErrRecordMissing) || record == nil {\n\t\tlog.Infof(\"Create a new record for [zone: %s, fqdn: %s, domain: %s]\", zone.Zone, info.EffectiveFQDN, domain)\n\n\t\t// Work through a bug in the NS1 API library that causes 400 Input validation failed (Value None for field '<obj>.filters' is not of type ...)\n\t\t// So the `tags` and `blockedTags` parameters should be initialized to empty.\n\t\trecord = dns.NewRecord(zone.Zone, dns01.UnFqdn(info.EffectiveFQDN), \"TXT\", make(map[string]string), make([]string, 0))\n\t\trecord.TTL = d.config.TTL\n\t\trecord.Answers = []*dns.Answer{{Rdata: []string{info.Value}}}\n\n\t\t_, err = d.client.Records.Create(record)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"ns1: failed to create record [zone: %q, fqdn: %q]: %w\", zone.Zone, info.EffectiveFQDN, err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ns1: failed to get the existing record: %w\", err)\n\t}\n\n\t// Update the existing records\n\trecord.Answers = append(record.Answers, &dns.Answer{Rdata: []string{info.Value}})\n\n\tlog.Infof(\"Update an existing record for [zone: %s, fqdn: %s, domain: %s]\", zone.Zone, info.EffectiveFQDN, domain)\n\n\t_, err = d.client.Records.Update(record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ns1: failed to update record [zone: %q, fqdn: %q]: %w\", zone.Zone, info.EffectiveFQDN, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getHostedZone(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ns1: %w\", err)\n\t}\n\n\tname := dns01.UnFqdn(info.EffectiveFQDN)\n\n\t_, err = d.client.Records.Delete(zone.Zone, name, \"TXT\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ns1: failed to delete record [zone: %q, domain: %q]: %w\", zone.Zone, name, err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) getHostedZone(fqdn string) (*dns.Zone, error) {\n\tauthZone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tzone, _, err := d.client.Zones.Get(authZone, false)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get zone [authZone: %q, fqdn: %q]: %w\", authZone, fqdn, err)\n\t}\n\n\treturn zone, nil\n}\n"
  },
  {
    "path": "providers/dns/ns1/ns1.toml",
    "content": "Name = \"NS1\"\nDescription = ''''''\nURL = \"https://ns1.com\"\nCode = \"ns1\"\nSince = \"v0.4.0\"\n\nExample = '''\nNS1_API_KEY=xxxx \\\nlego --dns ns1 -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    NS1_API_KEY = \"API key\"\n  [Configuration.Additional]\n    NS1_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    NS1_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    NS1_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    NS1_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://ns1.com/api\"\n  GoClient = \"https://github.com/ns1/ns1-go\"\n"
  },
  {
    "path": "providers/dns/ns1/ns1_test.go",
    "content": "package ns1\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"ns1: some credentials information are missing: NS1_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"ns1: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/octenium/fixtures/add_dns_record.json",
    "content": "{\n  \"api-status\": \"success\",\n  \"api-response\": {\n    \"record\": {\n      \"type\": \"TXT\",\n      \"name\": \"_acme-challenge.example.com.\",\n      \"ttl\": 120,\n      \"value\": \"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\",\n      \"raw\": {\n        \"txtdata\": \"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/octenium/fixtures/delete_dns_record.json",
    "content": "{\n  \"api-status\": \"success\",\n  \"api-response\": {\n    \"deleted\": {\n      \"count\": 1,\n      \"lines\": [\n        123\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/octenium/fixtures/list_dns_records.json",
    "content": "{\n  \"api-status\": \"success\",\n  \"api-response\": {\n    \"records\": [\n      {\n        \"line\": 31,\n        \"type\": \"TXT\",\n        \"name\": \"_dmarc.example.com.\",\n        \"ttl\": 300,\n        \"value\": \"xxx\",\n        \"raw\": {\n          \"txtdata\": \"xxx\"\n        }\n      },\n      {\n        \"line\": 123,\n        \"type\": \"TXT\",\n        \"name\": \"_acme-challenge.example.com.\",\n        \"ttl\": 300,\n        \"value\": \"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\",\n        \"raw\": {\n          \"txtdata\": \"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\"\n        }\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/octenium/fixtures/list_domains.json",
    "content": "{\n  \"api-status\": \"success\",\n  \"api-response\": {\n    \"domains\": {\n      \"2976\": {\n        \"domain-name\": \"example.com\",\n        \"registration-date\": \"21\\/08\\/2025\",\n        \"expiration-date\": \"-\",\n        \"status\": \"active\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/octenium/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\nconst defaultBaseURL = \"https://api.panel.octenium.com/\"\n\nconst statusSuccess = \"success\"\n\n// Client the Octenium API client.\ntype Client struct {\n\tapiKey string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiKey string) (*Client, error) {\n\tif apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// ListDomains retrieves a list of domains.\n// https://octenium.com/api#tag/Domains/operation/listdomains\nfunc (c *Client) ListDomains(ctx context.Context, domain string) (map[string]Domain, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domains\")\n\n\tdata := endpoint.Query()\n\tdata.Set(\"domain-name\", domain)\n\tendpoint.RawQuery = data.Encode()\n\n\treq, err := newRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &DomainsResponse{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Domains, nil\n}\n\n// ListDNSRecords retrieves a list of DNS records.\n// https://octenium.com/api#tag/Domains-DNS/operation/domains-dns-records-list\nfunc (c *Client) ListDNSRecords(ctx context.Context, orderID, recordType string) ([]Record, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", \"dns-records\", \"list\")\n\n\tdata := make(url.Values)\n\tdata.Set(\"order-id\", orderID)\n\tdata.Set(\"types[]\", recordType)\n\n\treq, err := newRequest(ctx, http.MethodPost, endpoint, data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &ListRecordsResponse{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Records, nil\n}\n\n// AddDNSRecord adds a DNS record.\n// https://octenium.com/api#tag/Domains-DNS/operation/domains-dns-records-add\nfunc (c *Client) AddDNSRecord(ctx context.Context, orderID string, record Record) (*Record, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", \"dns-records\", \"add\")\n\n\tdata, err := querystring.Values(record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata.Set(\"order-id\", orderID)\n\n\treq, err := newRequest(ctx, http.MethodPost, endpoint, data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &AddRecordResponse{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Record, nil\n}\n\n// DeleteDNSRecord deletes a DNS record.\n// https://octenium.com/api#tag/Domains-DNS/operation/domains-dns-records-delete\nfunc (c *Client) DeleteDNSRecord(ctx context.Context, orderID string, recordID int) (*DeletedRecordInfo, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", \"dns-records\", \"delete\")\n\n\tdata := make(url.Values)\n\tdata.Set(\"order-id\", orderID)\n\tdata.Set(\"line\", strconv.Itoa(recordID))\n\n\treq, err := newRequest(ctx, http.MethodPost, endpoint, data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := &DeleteRecordResponse{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Deleted, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(\"X-Api-Key\", c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\traw, _ := io.ReadAll(resp.Body)\n\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar response APIResponse\n\n\terr = json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tif response.Status != statusSuccess {\n\t\treturn fmt.Errorf(\"unexpected status: %s: %s\", response.Status, response.Error)\n\t}\n\n\terr = json.Unmarshal(response.Response, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, response.Response, err)\n\t}\n\n\treturn nil\n}\n\nfunc newRequest(ctx context.Context, method string, endpoint *url.URL, payload url.Values) (*http.Request, error) {\n\tvar body io.Reader = http.NoBody\n\n\tif method == http.MethodPost && payload != nil {\n\t\tbody = strings.NewReader(payload.Encode())\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif method == http.MethodPost && payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/octenium/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithAccept(\"application/json\").\n\t\t\tWith(\"X-Api-Key\", \"secret\"),\n\t)\n}\n\nfunc TestClient_ListDomains(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains\",\n\t\t\tservermock.ResponseFromFixture(\"list_domains.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"domain-name\", \"example.com\")).\n\t\tBuild(t)\n\n\tdomains, err := client.ListDomains(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := map[string]Domain{\n\t\t\"2976\": {DomainName: \"example.com\", RegistrationDate: \"12/09/2021\", ExpirationDate: \"12/09/2024\", Status: \"active\"},\n\t\t\"2977\": {DomainName: \"example.org\", RegistrationDate: \"01/10/2021\", ExpirationDate: \"01/10/2024\", Status: \"active\"},\n\t\t\"2978\": {DomainName: \"example.net\", RegistrationDate: \"21/08/2025\", ExpirationDate: \"-\", Status: \"active\"},\n\t}\n\n\tassert.Equal(t, expected, domains)\n}\n\nfunc TestClient_ListDomains_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\t_, err := client.ListDomains(t.Context(), \"example.com\")\n\trequire.EqualError(t, err, \"unexpected status code: [status code: 400] body: \")\n}\n\nfunc TestClient_ListDomains_api_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\t_, err := client.ListDomains(t.Context(), \"example.com\")\n\trequire.EqualError(t, err, \"unexpected status: error: missing required fields (type, name, ttl)\")\n}\n\nfunc TestClient_ListDNSRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/dns-records/list\",\n\t\t\tservermock.ResponseFromFixture(\"list_dns_records.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentType(\"application/x-www-form-urlencoded\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"order-id\", \"abc\").\n\t\t\t\tWith(\"types[]\", \"TXT\")).\n\t\tBuild(t)\n\n\trecords, err := client.ListDNSRecords(t.Context(), \"abc\", \"TXT\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{ID: 15, Type: \"A\", Name: \"example.com.\", TTL: 14400, Value: \"203.0.113.10\"},\n\t\t{ID: 22, Type: \"MX\", Name: \"example.com.\", TTL: 14400, Value: \"10 mail.example.com.\"},\n\t\t{ID: 31, Type: \"TXT\", Name: \"_dmarc.example.com.\", TTL: 300, Value: \"v=DMARC1; p=none; rua=mailto:dmarc@example.com\"},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_ListDNSRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/dns-records/list\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\t_, err := client.ListDNSRecords(t.Context(), \"abc\", \"TXT\")\n\trequire.EqualError(t, err, \"unexpected status code: [status code: 400] body: \")\n}\n\nfunc TestClient_ListDNSRecords_api_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/dns-records/list\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\t_, err := client.ListDNSRecords(t.Context(), \"abc\", \"TXT\")\n\trequire.EqualError(t, err, \"unexpected status: error: missing required fields (type, name, ttl)\")\n}\n\nfunc TestClient_AddDNSRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/dns-records/add\",\n\t\t\tservermock.ResponseFromFixture(\"add_dns_record.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentType(\"application/x-www-form-urlencoded\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"order-id\", \"abc\").\n\t\t\t\tWith(\"name\", \"example.com.\").\n\t\t\t\tWith(\"ttl\", \"120\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"value\", \"txtTXTtxt\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:  \"TXT\",\n\t\tName:  \"example.com.\",\n\t\tTTL:   120,\n\t\tValue: \"txtTXTtxt\",\n\t}\n\n\tresult, err := client.AddDNSRecord(t.Context(), \"abc\", record)\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tType:  \"A\",\n\t\tName:  \"example.com.\",\n\t\tTTL:   14400,\n\t\tValue: \"203.0.113.10\",\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_AddDNSRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/dns-records/add\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:  \"TXT\",\n\t\tName:  \"example.com.\",\n\t\tTTL:   120,\n\t\tValue: \"txtTXTtxt\",\n\t}\n\n\t_, err := client.AddDNSRecord(t.Context(), \"abc\", record)\n\trequire.EqualError(t, err, \"unexpected status code: [status code: 400] body: \")\n}\n\nfunc TestClient_AddDNSRecord_api_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/dns-records/add\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:  \"TXT\",\n\t\tName:  \"example.com.\",\n\t\tTTL:   120,\n\t\tValue: \"txtTXTtxt\",\n\t}\n\n\t_, err := client.AddDNSRecord(t.Context(), \"abc\", record)\n\trequire.EqualError(t, err, \"unexpected status: error: missing required fields (type, name, ttl)\")\n}\n\nfunc TestClient_DeleteDNSRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/dns-records/delete\",\n\t\t\tservermock.ResponseFromFixture(\"delete_dns_record.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentType(\"application/x-www-form-urlencoded\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"order-id\", \"abc\").\n\t\t\t\tWith(\"line\", \"123\")).\n\t\tBuild(t)\n\n\tresult, err := client.DeleteDNSRecord(t.Context(), \"abc\", 123)\n\trequire.NoError(t, err)\n\n\texpected := &DeletedRecordInfo{\n\t\tCount: 1,\n\t\tLines: []int{15},\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_DeleteDNSRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/dns-records/delete\",\n\t\t\tservermock.Noop().WithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\t_, err := client.DeleteDNSRecord(t.Context(), \"abc\", 123)\n\trequire.EqualError(t, err, \"unexpected status code: [status code: 400] body: \")\n}\n\nfunc TestClient_DeleteDNSRecord_api_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/dns-records/delete\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\t_, err := client.DeleteDNSRecord(t.Context(), \"abc\", 123)\n\trequire.EqualError(t, err, \"unexpected status: error: missing required fields (type, name, ttl)\")\n}\n"
  },
  {
    "path": "providers/dns/octenium/internal/fixtures/add_dns_record.json",
    "content": "{\n  \"api-status\": \"success\",\n  \"api-response\": {\n    \"record\": {\n      \"type\": \"A\",\n      \"name\": \"example.com.\",\n      \"ttl\": 14400,\n      \"value\": \"203.0.113.10\",\n      \"raw\": {\n        \"address\": \"203.0.113.10\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/octenium/internal/fixtures/delete_dns_record.json",
    "content": "{\n  \"api-status\": \"success\",\n  \"api-response\": {\n    \"deleted\": {\n      \"count\": 1,\n      \"lines\": [\n        15\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/octenium/internal/fixtures/error.json",
    "content": "{\n  \"api-status\": \"error\",\n  \"api-response\": [],\n  \"api-error\": \"missing required fields (type, name, ttl)\"\n}\n"
  },
  {
    "path": "providers/dns/octenium/internal/fixtures/list_dns_records.json",
    "content": "{\n  \"api-status\": \"success\",\n  \"api-response\": {\n    \"records\": [\n      {\n        \"line\": 15,\n        \"type\": \"A\",\n        \"name\": \"example.com.\",\n        \"ttl\": 14400,\n        \"value\": \"203.0.113.10\",\n        \"raw\": {\n          \"address\": \"203.0.113.10\"\n        }\n      },\n      {\n        \"line\": 22,\n        \"type\": \"MX\",\n        \"name\": \"example.com.\",\n        \"ttl\": 14400,\n        \"value\": \"10 mail.example.com.\",\n        \"raw\": {\n          \"preference\": 10,\n          \"exchange\": \"mail.example.com.\"\n        }\n      },\n      {\n        \"line\": 31,\n        \"type\": \"TXT\",\n        \"name\": \"_dmarc.example.com.\",\n        \"ttl\": 300,\n        \"value\": \"v=DMARC1; p=none; rua=mailto:dmarc@example.com\",\n        \"raw\": {\n          \"txtdata\": \"v=DMARC1; p=none; rua=mailto:dmarc@example.com\"\n        }\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/octenium/internal/fixtures/list_domains.json",
    "content": "{\n  \"api-status\": \"success\",\n  \"api-response\": {\n    \"domains\": {\n      \"2976\": {\n        \"domain-name\": \"example.com\",\n        \"registration-date\": \"12/09/2021\",\n        \"expiration-date\": \"12/09/2024\",\n        \"status\": \"active\"\n      },\n      \"2977\": {\n        \"domain-name\": \"example.org\",\n        \"registration-date\": \"01/10/2021\",\n        \"expiration-date\": \"01/10/2024\",\n        \"status\": \"active\"\n      },\n      \"2978\": {\n        \"domain-name\": \"example.net\",\n        \"registration-date\": \"21\\/08\\/2025\",\n        \"expiration-date\": \"-\",\n        \"status\": \"active\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/octenium/internal/types.go",
    "content": "package internal\n\nimport \"encoding/json\"\n\ntype APIResponse struct {\n\tStatus   string          `json:\"api-status,omitempty\"`\n\tResponse json.RawMessage `json:\"api-response,omitempty\"`\n\tError    string          `json:\"api-error,omitempty\"`\n}\n\ntype Domain struct {\n\tDomainName       string `json:\"domain-name,omitempty\"`\n\tRegistrationDate string `json:\"registration-date,omitempty\"`\n\tExpirationDate   string `json:\"expiration-date,omitempty\"`\n\tStatus           string `json:\"status,omitempty\"`\n}\n\ntype Record struct {\n\tID    int    `json:\"line,omitempty\" url:\"-\"`\n\tType  string `json:\"type,omitempty\" url:\"type,omitempty\"`\n\tName  string `json:\"name,omitempty\" url:\"name,omitempty\"`\n\tTTL   int    `json:\"ttl,omitempty\" url:\"ttl,omitempty\"`\n\tValue string `json:\"value,omitempty\" url:\"value,omitempty\"`\n}\n\ntype DomainsResponse struct {\n\tDomains map[string]Domain `json:\"domains,omitempty\"`\n}\n\ntype AddRecordResponse struct {\n\tRecord *Record `json:\"record,omitempty\"`\n}\n\ntype ListRecordsResponse struct {\n\tRecords []Record `json:\"records,omitempty\"`\n}\n\ntype DeleteRecordResponse struct {\n\tDeleted *DeletedRecordInfo `json:\"deleted,omitempty\"`\n}\n\ntype DeletedRecordInfo struct {\n\tCount int   `json:\"count,omitempty\"`\n\tLines []int `json:\"lines,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/octenium/octenium.go",
    "content": "// Package octenium implements a DNS provider for solving the DNS-01 challenge using Octenium.\npackage octenium\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/octenium/internal\"\n\t\"github.com/hashicorp/go-retryablehttp\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"OCTENIUM_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\tdomainIDs   map[string]string\n\tdomainIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Octenium.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"octenium: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Octenium.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"octenium: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.APIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"octenium: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tretryClient := retryablehttp.NewClient()\n\tretryClient.RetryMax = 5\n\tretryClient.HTTPClient = client.HTTPClient\n\tretryClient.Logger = log.Logger\n\n\tclient.HTTPClient = clientdebug.Wrap(retryClient.StandardClient())\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\tdomainIDs: make(map[string]string),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"octenium: could not find zone for domain '%s': %w\", domain, err)\n\t}\n\n\tdomainID, err := d.getDomainID(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"octenium: get domain ID: %w\", err)\n\t}\n\n\td.domainIDsMu.Lock()\n\td.domainIDs[token] = domainID\n\td.domainIDsMu.Unlock()\n\n\trecord := internal.Record{\n\t\tType:  \"TXT\",\n\t\tName:  info.EffectiveFQDN,\n\t\tTTL:   d.config.TTL,\n\t\tValue: info.Value,\n\t}\n\n\t_, err = d.client.AddDNSRecord(ctx, domainID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"octenium: add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.domainIDsMu.Lock()\n\tdomainID, ok := d.domainIDs[token]\n\td.domainIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"octenium: unknown domain ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\trecords, err := d.client.ListDNSRecords(ctx, domainID, \"TXT\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"octenium: list records: %w\", err)\n\t}\n\n\tfor _, record := range records {\n\t\tif record.Type != \"TXT\" || record.Name != info.EffectiveFQDN || record.Value != info.Value {\n\t\t\tcontinue\n\t\t}\n\n\t\t_, err = d.client.DeleteDNSRecord(ctx, domainID, record.ID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"octenium: delete record: %w\", err)\n\t\t}\n\n\t\tbreak\n\t}\n\n\td.domainIDsMu.Lock()\n\tdelete(d.domainIDs, token)\n\td.domainIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) getDomainID(ctx context.Context, authZone string) (string, error) {\n\tdomains, err := d.client.ListDomains(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"list domains: %w\", err)\n\t}\n\n\tif len(domains) == 0 {\n\t\treturn \"\", errors.New(\"domain not found\")\n\t}\n\n\tif len(domains) > 1 {\n\t\treturn \"\", errors.New(\"multiple domains found\")\n\t}\n\n\tfor id := range domains {\n\t\treturn id, nil\n\t}\n\n\treturn \"\", errors.New(\"domain ID not found\")\n}\n"
  },
  {
    "path": "providers/dns/octenium/octenium.toml",
    "content": "Name = \"Octenium\"\nDescription = ''''''\nURL = \"https://octenium.com/\"\nCode = \"octenium\"\nSince = \"v4.27.0\"\n\nExample = '''\nOCTENIUM_API_KEY=\"xxx\" \\\nlego --dns octenium -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    OCTENIUM_API_KEY = \"API key\"\n  [Configuration.Additional]\n    OCTENIUM_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    OCTENIUM_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    OCTENIUM_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    OCTENIUM_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://octenium.com/api#tag/Domains-DNS\"\n"
  },
  {
    "path": "providers/dns/octenium/octenium_test.go",
    "content": "package octenium\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"octenium: some credentials information are missing: OCTENIUM_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"octenium: some credentials information are missing: OCTENIUM_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\texpected: \"octenium: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"octenium: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithAccept(\"application/json\").\n\t\t\tWith(\"X-Api-Key\", \"secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /domains\",\n\t\t\tservermock.ResponseFromFixture(\"list_domains.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"domain-name\", \"example.com\")).\n\t\tRoute(\"POST /domains/dns-records/add\",\n\t\t\tservermock.ResponseFromFixture(\"add_dns_record.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentType(\"application/x-www-form-urlencoded\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"order-id\", \"2976\").\n\t\t\t\tWith(\"name\", \"_acme-challenge.example.com.\").\n\t\t\t\tWith(\"ttl\", \"120\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"value\", \"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"\", \"foobar\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /domains/dns-records/list\",\n\t\t\tservermock.ResponseFromFixture(\"list_dns_records.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentType(\"application/x-www-form-urlencoded\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"order-id\", \"2976\").\n\t\t\t\tWith(\"types[]\", \"TXT\")).\n\t\tRoute(\"POST /domains/dns-records/delete\",\n\t\t\tservermock.ResponseFromFixture(\"delete_dns_record.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentType(\"application/x-www-form-urlencoded\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"order-id\", \"2976\").\n\t\t\t\tWith(\"line\", \"123\")).\n\t\tBuild(t)\n\n\tprovider.domainIDs[\"token\"] = \"2976\"\n\n\terr := provider.CleanUp(\"example.com\", \"token\", \"foobar\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/oraclecloud/configurationprovider.go",
    "content": "package oraclecloud\n\nimport (\n\t\"crypto/rsa\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/nrdcg/oci-go-sdk/common/v1065\"\n)\n\ntype environmentConfigurationProvider struct {\n\tvalues map[string]string\n}\n\nfunc newEnvironmentConfigurationProvider() (*environmentConfigurationProvider, error) {\n\tvalues, err := env.GetWithFallback(\n\t\t[]string{EnvRegion, altEnvTFVarRegion},\n\t\t[]string{EnvUserOCID, altEnvTFVarUserOCID},\n\t\t[]string{EnvTenancyOCID, altEnvTFVarTenancyOCID},\n\t\t[]string{EnvPubKeyFingerprint, altEnvFingerprint, altEnvTFVarFingerprint},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &environmentConfigurationProvider{\n\t\tvalues: values,\n\t}, nil\n}\n\nfunc (p *environmentConfigurationProvider) PrivateRSAKey() (*rsa.PrivateKey, error) {\n\tprivateKey, err := getPrivateKey()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn common.PrivateKeyFromBytesWithPassword(privateKey, []byte(p.privateKeyPassword()))\n}\n\nfunc (p *environmentConfigurationProvider) KeyID() (string, error) {\n\ttenancy, err := p.TenancyOCID()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tuser, err := p.UserOCID()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfingerprint, err := p.KeyFingerprint()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn fmt.Sprintf(\"%s/%s/%s\", tenancy, user, fingerprint), nil\n}\n\nfunc (p *environmentConfigurationProvider) TenancyOCID() (string, error) {\n\treturn p.values[EnvTenancyOCID], nil\n}\n\nfunc (p *environmentConfigurationProvider) UserOCID() (string, error) {\n\treturn p.values[EnvUserOCID], nil\n}\n\nfunc (p *environmentConfigurationProvider) KeyFingerprint() (string, error) {\n\treturn p.values[EnvPubKeyFingerprint], nil\n}\n\nfunc (p *environmentConfigurationProvider) Region() (string, error) {\n\treturn p.values[EnvRegion], nil\n}\n\nfunc (p *environmentConfigurationProvider) AuthType() (common.AuthConfig, error) {\n\t// Inspired by https://github.com/oracle/oci-go-sdk/blob/e7635c292e60d0a9dcdd3a1e7de180d7c99b1eee/common/configuration.go#L231-L234\n\treturn common.AuthConfig{AuthType: common.UnknownAuthenticationType}, errors.New(\"unsupported, keep the interface\")\n}\n\nfunc (p *environmentConfigurationProvider) privateKeyPassword() string {\n\treturn env.GetOneWithFallback(EnvPrivKeyPass, \"\", env.ParseString, altEnvPrivateKeyPassword, altEnvTFVarPrivateKeyPassword)\n}\n\nfunc getPrivateKey() ([]byte, error) {\n\tbase64EnvKeys := []string{envPrivKey, altEnvPrivateKey}\n\n\tenvVarValue := getEnvWithStrictFallback(base64EnvKeys...)\n\tif envVarValue != \"\" {\n\t\tbytes, err := base64.StdEncoding.DecodeString(envVarValue)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to read base64 value %s (defined by env vars %s): %w\", envVarValue,\n\t\t\t\tstrings.Join(base64EnvKeys, \" or \"), err)\n\t\t}\n\n\t\treturn bytes, nil\n\t}\n\n\tfileEnvKeys := []string{EnvPrivKeyFile, altEnvPrivateKeyPath, altEnvTFVarPrivateKeyPath}\n\n\tfileVarValue := getEnvFileWithStrictFallback(fileEnvKeys...)\n\tif len(fileVarValue) == 0 {\n\t\treturn nil, fmt.Errorf(\"no value provided for: %s\",\n\t\t\tstrings.Join(slices.Concat(base64EnvKeys, fileEnvKeys), \" or \"),\n\t\t)\n\t}\n\n\treturn fileVarValue, nil\n}\n\nfunc getEnvWithStrictFallback(keys ...string) string {\n\tfor _, key := range keys {\n\t\tenvVarValue := os.Getenv(key)\n\t\tif envVarValue != \"\" {\n\t\t\treturn envVarValue\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\nfunc getEnvFileWithStrictFallback(keys ...string) []byte {\n\tfor _, key := range keys {\n\t\tfileVarValue := os.Getenv(key)\n\t\tif fileVarValue == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tfileContents, err := os.ReadFile(fileVarValue)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Failed to read the file %s (defined by env var %s): %s\", fileVarValue, key, err)\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fileContents\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/oraclecloud/fixtures/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDHzCCAgegAwIBAgIQKIExaCLIXtXecrT1dWGLszANBgkqhkiG9w0BAQsFADAS\nMRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw\nMDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAwM4wEPHOGAu8tZNNWx3cH6AMuqKwAmB2RwbA3OK034MzhydOjnDm\nigw93eUc4nd3dnICyNpb2rbP9FgGlAuMlJ8raHQkG4DSXF1Bf14neOhLpfBItaX9\n+EB3oO0NupKZhaHrsTKzLGD7bauAPX6PDXuAPp3u5mgGGuZjpLZoKqg3//WImb/2\nxEMVsmvPKTb5FxS/tAMtywjGSUtCTCrudUEh4Gnj6IboVdwYmt539ETDK/Rerxf3\n/GsmEbuOkDUdBixQwLo0U+UAoMOw4zoyQDrrtyUmvffDxI50RAdZDFyFtqZ0ZQa8\nlQqrMdQdf+x1Wb7BKozSktAw4igRP/mknQIDAQABo28wbTAOBgNVHQ8BAf8EBAMC\nAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E\nFgQUcetTliVbYxxutNS8JRkotRY4DRkwFgYDVR0RBA8wDYILZXhhbXBsZS5vcmcw\nDQYJKoZIhvcNAQELBQADggEBAEJP74/XB+12aGQ+EMERIX2Pn6YaaBLt6rTLqV7A\nzFxI9YGIc4xlGa0qkpDhpz6RSypTQG6HN5aZ5b8dz3foMleUVP2cXd8zduc8GQCb\np4/8PpEhSl6dQb5+mg/qyHGUAaDl40VAbTLXHtn98dhacaJc+TKuXVJAgYRU3Sm3\nwFJxULZSnx+aGdE9s2brOGhvz1fVWnhvWzDvJSM+8xDURz8UiEnimTpV6m3CKItz\n2GatNjM8ADKC7MHQI4I5v4fEwronN/g3NfPfFSmnOKk+lPSAW42WEvhFol+2VvdX\n3p5X2QracSLCIj/DUBebZP9110C8Lj/YfFtOjFokqtQ9Fh4=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "providers/dns/oraclecloud/fixtures/key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDAzjAQ8c4YC7y1\nk01bHdwfoAy6orACYHZHBsDc4rTfgzOHJ06OcOaKDD3d5Rzid3d2cgLI2lvats/0\nWAaUC4yUnytodCQbgNJcXUF/Xid46Eul8Ei1pf34QHeg7Q26kpmFoeuxMrMsYPtt\nq4A9fo8Ne4A+ne7maAYa5mOktmgqqDf/9YiZv/bEQxWya88pNvkXFL+0Ay3LCMZJ\nS0JMKu51QSHgaePohuhV3Bia3nf0RMMr9F6vF/f8ayYRu46QNR0GLFDAujRT5QCg\nw7DjOjJAOuu3JSa998PEjnREB1kMXIW2pnRlBryVCqsx1B1/7HVZvsEqjNKS0DDi\nKBE/+aSdAgMBAAECggEAWl2pWJ/ErS9/HIl0NbMKk0YEAUuz/AEzHnoTVdPp22KW\neY+aOZe/7c7sBj7WqWw98SVhmbsCV0HcuNSzDJtXIedyRGw+6icYMVNCGgzKqlgR\n8K3snjq1DLBGgYXpq9r/Got4ON6e7LttzIqXufrB2JtcUbzbFmGGDwCRjkcyDl9l\nM8ufwD/Xgcd2L8jainU43d2pVxvxUIpRlRdoupCCSlkRYPsXiWlqav7YO4F/Txos\nz3gJyzkXzc3WwfNZdQtEMYwBwozO+Dp2p4TUBr0Ta3MbfrKfDoTs4XT/Ce9IwJJS\n/h6E9cxZD8t5oMT50quFjwhHBKodMiUqIlh2YQEAbwKBgQDIULzo/tgDgTwveyEn\nL9n8yVbEh/SfrE9QtXcjkDB5+tYmIsIaz16NRWlAqnJVGZvcanrCq7ZTxgUcs/hW\nAg+sfWkeg7lmfeJAkiZ6kmi1h2qJjXMOBri+Cm6MTOsE6qdIc3eT4PnYkNpV7o6S\n70hWNncVadXLV4Thm9BLAbMbQwKBgQD2ZwKe/2zRQcbuBe1loF0HWIsJPxcKQ3LH\nhVf7f0YLQlIuzOhK8TQXgM0G4hxLlk1XeLjgf3z4Ju7hfh2JQLor1QYPRGUj66SX\nKTE5eDwE0yEX1c9m5PW6M+f8vkOU4LQ/OtPw5OrKyYxpLf9dp42nmDYY/8IvUk96\niKZNY1sSnwKBgQC27tS2SxVmjf0yt1WdfdurOQueSzKhJzD/2djFh4Zdvy8WgKOW\n7E3C4eKvBXmIMezeq/cUFNBbTPmaLtjZYuSBd74p+c20xb17jnzJby9kqBgpKh4q\nbwUDuG8gfZYbVVgTmC9ZwxkoJ5Dc7RETKqZ65R53VcHDA1f82Nitxw2UFQKBgBDl\nc2qPvViEGC4OPf8wBfERA0e5Cc1sXpyL6kKWsajn/Va0OmGZNKc/788/Bg2w2tDa\nuGK8m0cw9ESGL2RQCfQjgWzelcjmybyL2JJGSmdSSvylbrlxjeAc2xWbvmqhFfsX\n/5yPNgJ926ECxHYZnT8W0u7X6urvy/9tC2pXG9GlAoGBAKOAfij4fMbHY+Z1m825\nVhY110FDnePYFJWmExP8GAVqOzhCs0mzyCnYh6nvS/OY8moH2LOuwPUlDfF3IzyT\nhTUuXnykWT3w40eYQXXIaXEGhue+guL8ch16vEEJy5ltwEdIPNMTErbqAAk2W6Ps\nNB46HzETzEIWnzoamX6iQVWj\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "providers/dns/oraclecloud/oraclecloud.go",
    "content": "// Package oraclecloud implements a DNS provider for solving the DNS-01 challenge using Oracle Cloud DNS.\npackage oraclecloud\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/nrdcg/oci-go-sdk/common/v1065\"\n\t\"github.com/nrdcg/oci-go-sdk/common/v1065/auth\"\n\t\"github.com/nrdcg/oci-go-sdk/dns/v1065\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"OCI_\"\n\n\tEnvAuthType = envNamespace + \"AUTH_TYPE\"\n\n\tEnvCompartmentOCID = envNamespace + \"COMPARTMENT_OCID\"\n\tEnvRegion          = envNamespace + \"REGION\"\n\n\tenvPrivKey           = envNamespace + \"PRIVKEY\"\n\tEnvPrivKeyFile       = envPrivKey + \"_FILE\"\n\tEnvPrivKeyPass       = envPrivKey + \"_PASS\"\n\tEnvTenancyOCID       = envNamespace + \"TENANCY_OCID\"\n\tEnvUserOCID          = envNamespace + \"USER_OCID\"\n\tEnvPubKeyFingerprint = envNamespace + \"PUBKEY_FINGERPRINT\"\n\n\taltEnvPrivateKey         = envNamespace + \"PRIVATE_KEY\"   // alias on OCI_PRIVKEY\n\taltEnvPrivateKeyPath     = altEnvPrivateKey + \"_PATH\"     // alias on OCI_PRIVKEY_FILE\n\taltEnvPrivateKeyPassword = altEnvPrivateKey + \"_PASSWORD\" // alias on OCI_PRIVKEY_PASS\n\taltEnvFingerprint        = envNamespace + \"FINGERPRINT\"   // alias on OCI_PUBKEY_FINGERPRINT\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// https://github.com/oracle/oci-go-sdk/blob/7f425f74c74fd0c6a5acb74466c85eb5346e0092/common/client.go#L350\n// https://github.com/oracle/oci-go-sdk/blob/7f425f74c74fd0c6a5acb74466c85eb5346e0092/common/configuration.go#L174-L175\nconst (\n\taltEnvTFVarNamespace          = \"TF_VAR_\"\n\taltEnvTFVarRegion             = altEnvTFVarNamespace + \"region\"               // alias on OCI_REGION\n\taltEnvTFVarFingerprint        = altEnvTFVarNamespace + \"fingerprint\"          // alias on OCI_PUBKEY_FINGERPRINT\n\taltEnvTFVarUserOCID           = altEnvTFVarNamespace + \"user_ocid\"            // alias on OCI_USER_OCID\n\taltEnvTFVarTenancyOCID        = altEnvTFVarNamespace + \"tenancy_ocid\"         // alias on OCI_TENANCY_OCID\n\taltEnvTFVarPrivateKeyPath     = altEnvTFVarNamespace + \"private_key_path\"     // alias on OCI_PRIVKEY_FILE\n\taltEnvTFVarPrivateKeyPassword = altEnvTFVarNamespace + \"private_key_password\" // alias on OCI_PRIVKEY_PASS\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tCompartmentID     string\n\tOCIConfigProvider common.ConfigurationProvider\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, time.Minute),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *dns.DnsClient\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for OracleCloud.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tconfig := NewDefaultConfig()\n\n\tswitch env.GetOrFile(EnvAuthType) {\n\tcase string(common.InstancePrincipal):\n\t\tvalues, err := env.Get(EnvCompartmentOCID)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"oraclecloud: %w\", err)\n\t\t}\n\n\t\tconfig.CompartmentID = values[EnvCompartmentOCID]\n\n\t\tregion := env.GetOneWithFallback(EnvRegion, \"\", env.ParseString, altEnvTFVarRegion)\n\n\t\tconfigurationProvider, err := auth.InstancePrincipalConfigurationProviderForRegion(common.Region(region))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"oraclecloud: %w\", err)\n\t\t}\n\n\t\tconfig.OCIConfigProvider = configurationProvider\n\n\tdefault:\n\t\tvalues, err := env.Get(EnvCompartmentOCID)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"oraclecloud: %w\", err)\n\t\t}\n\n\t\tconfig.CompartmentID = values[EnvCompartmentOCID]\n\n\t\tecp, err := newEnvironmentConfigurationProvider()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"oraclecloud: %w\", err)\n\t\t}\n\n\t\tconfig.OCIConfigProvider = ecp\n\t}\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for OracleCloud.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"oraclecloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.CompartmentID == \"\" {\n\t\treturn nil, errors.New(\"oraclecloud: CompartmentID is missing\")\n\t}\n\n\tif config.OCIConfigProvider == nil {\n\t\treturn nil, errors.New(\"oraclecloud: OCIConfigProvider is missing\")\n\t}\n\n\tclient, err := dns.NewDnsClientWithConfigurationProvider(config.OCIConfigProvider)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"oraclecloud: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = clientdebug.Wrap(config.HTTPClient)\n\t}\n\n\treturn &DNSProvider{client: &client, config: config}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneNameOrID, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"oraclecloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// generate request to dns.PatchDomainRecordsRequest\n\trecordOperation := dns.RecordOperation{\n\t\tDomain:      common.String(dns01.UnFqdn(info.EffectiveFQDN)),\n\t\tRdata:       common.String(info.Value),\n\t\tRtype:       common.String(\"TXT\"),\n\t\tTtl:         common.Int(d.config.TTL),\n\t\tIsProtected: common.Bool(false),\n\t}\n\n\trequest := dns.PatchDomainRecordsRequest{\n\t\tCompartmentId: common.String(d.config.CompartmentID),\n\t\tZoneNameOrId:  common.String(zoneNameOrID),\n\t\tDomain:        common.String(dns01.UnFqdn(info.EffectiveFQDN)),\n\t\tPatchDomainRecordsDetails: dns.PatchDomainRecordsDetails{\n\t\t\tItems: []dns.RecordOperation{recordOperation},\n\t\t},\n\t}\n\n\t_, err = d.client.PatchDomainRecords(context.Background(), request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"oraclecloud: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneNameOrID, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"oraclecloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// search to TXT record's hash to delete\n\tgetRequest := dns.GetDomainRecordsRequest{\n\t\tZoneNameOrId:  common.String(zoneNameOrID),\n\t\tDomain:        common.String(dns01.UnFqdn(info.EffectiveFQDN)),\n\t\tCompartmentId: common.String(d.config.CompartmentID),\n\t\tRtype:         common.String(\"TXT\"),\n\t}\n\n\tctx := context.Background()\n\n\tdomainRecords, err := d.client.GetDomainRecords(ctx, getRequest)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"oraclecloud: %w\", err)\n\t}\n\n\tif *domainRecords.OpcTotalItems == 0 {\n\t\treturn errors.New(\"oraclecloud: no record to clean up\")\n\t}\n\n\tvar deleteHash *string\n\n\tfor _, record := range domainRecords.Items {\n\t\tif record.Rdata != nil && *record.Rdata == `\"`+info.Value+`\"` {\n\t\t\tdeleteHash = record.RecordHash\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif deleteHash == nil {\n\t\treturn errors.New(\"oraclecloud: no record to clean up\")\n\t}\n\n\trecordOperation := dns.RecordOperation{\n\t\tRecordHash: deleteHash,\n\t\tOperation:  dns.RecordOperationOperationRemove,\n\t}\n\n\tpatchRequest := dns.PatchDomainRecordsRequest{\n\t\tZoneNameOrId: common.String(zoneNameOrID),\n\t\tDomain:       common.String(dns01.UnFqdn(info.EffectiveFQDN)),\n\t\tPatchDomainRecordsDetails: dns.PatchDomainRecordsDetails{\n\t\t\tItems: []dns.RecordOperation{recordOperation},\n\t\t},\n\t\tCompartmentId: common.String(d.config.CompartmentID),\n\t}\n\n\t_, err = d.client.PatchDomainRecords(ctx, patchRequest)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"oraclecloud: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/oraclecloud/oraclecloud.toml",
    "content": "Name = \"Oracle Cloud\"\nDescription = ''''''\nURL = \"https://cloud.oracle.com/home\"\nCode = \"oraclecloud\"\nSince = \"v2.3.0\"\n\nExample = '''\n# Using API Key authentication:\nOCI_PRIVATE_KEY_PATH=\"~/.oci/oci_api_key.pem\" \\\nOCI_PRIVATE_KEY_PASSWORD=\"secret\" \\\nOCI_TENANCY_OCID=\"ocid1.tenancy.oc1..secret\" \\\nOCI_USER_OCID=\"ocid1.user.oc1..secret\" \\\nOCI_FINGERPRINT=\"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00\" \\\nOCI_REGION=\"us-phoenix-1\" \\\nOCI_COMPARTMENT_OCID=\"ocid1.tenancy.oc1..secret\" \\\nlego --dns oraclecloud -d '*.example.com' -d example.com run\n\n# Using Instance Principal authentication (when running on OCI compute instances):\n# https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm\nOCI_AUTH_TYPE=\"instance_principal\" \\\nOCI_COMPARTMENT_OCID=\"ocid1.tenancy.oc1..secret\" \\\nlego --dns oraclecloud -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    OCI_COMPARTMENT_OCID = \"Compartment OCID\"\n    OCI_REGION = \"Region (it can be empty if `OCI_AUTH_TYPE=instance_principal`).\"\n    OCI_PRIVATE_KEY_PATH = \"Private key file (ignored if `OCI_AUTH_TYPE=instance_principal`)\"\n    OCI_PRIVATE_KEY_PASSWORD = \"Private key password (ignored if `OCI_AUTH_TYPE=instance_principal`)\"\n    OCI_TENANCY_OCID = \"Tenancy OCID (ignored if `OCI_AUTH_TYPE=instance_principal`)\"\n    OCI_USER_OCID = \"User OCID (ignored if `OCI_AUTH_TYPE=instance_principal`)\"\n    OCI_FINGERPRINT = \"Public key fingerprint (ignored if `OCI_AUTH_TYPE=instance_principal`)\"\n  [Configuration.Additional]\n    OCI_AUTH_TYPE = \"Authorization type. Possible values: 'instance_principal', ''  (Default: '')\"\n    TF_VAR_region = \"Alias on `OCI_REGION`\"\n    TF_VAR_fingerprint = \"Alias on `OCI_FINGERPRINT`\"\n    TF_VAR_user_ocid = \"Alias on `OCI_USER_OCID`\"\n    TF_VAR_tenancy_ocid = \"Alias on `OCI_TENANCY_OCID`\"\n    TF_VAR_private_key_path = \"Alias on `OCI_PRIVATE_KEY_PATH`\"\n    OCI_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    OCI_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    OCI_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    OCI_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 60)\"\n\n[Links]\n  API = \"https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm\"\n  GoClient = \"https://github.com/oracle/oci-go-sdk\"\n\n"
  },
  {
    "path": "providers/dns/oraclecloud/oraclecloud_test.go",
    "content": "package oraclecloud\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/pem\"\n\t\"maps\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/nrdcg/oci-go-sdk/common/v1065\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\n// Used by Instance Principal authentication.\nconst (\n\tenvMetadataBaseURL        = \"OCI_METADATA_BASE_URL\"\n\tenvSDKAuthClientRegionURL = \"OCI_SDK_AUTH_CLIENT_REGION_URL\"\n)\n\nvar envTest = tester.NewEnvTest(\n\tenvPrivKey,\n\tEnvAuthType,\n\tenvMetadataBaseURL,\n\tenvSDKAuthClientRegionURL,\n\tEnvPrivKeyFile,\n\tEnvPrivKeyPass,\n\tEnvTenancyOCID,\n\tEnvUserOCID,\n\tEnvPubKeyFingerprint,\n\tEnvRegion,\n\tEnvCompartmentOCID).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvPrivKey:           mustGeneratePrivateKey(\"secret1\"),\n\t\t\t\tEnvPrivKeyPass:       \"secret1\",\n\t\t\t\tEnvTenancyOCID:       \"ocid1.tenancy.oc1..secret\",\n\t\t\t\tEnvUserOCID:          \"ocid1.user.oc1..secret\",\n\t\t\t\tEnvPubKeyFingerprint: \"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00\",\n\t\t\t\tEnvRegion:            \"us-phoenix-1\",\n\t\t\t\tEnvCompartmentOCID:   \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success file\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPrivKeyFile:       mustGeneratePrivateKeyFile(t, \"secret1\"),\n\t\t\t\tEnvPrivKeyPass:       \"secret1\",\n\t\t\t\tEnvTenancyOCID:       \"ocid1.tenancy.oc1..secret\",\n\t\t\t\tEnvUserOCID:          \"ocid1.user.oc1..secret\",\n\t\t\t\tEnvPubKeyFingerprint: \"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00\",\n\t\t\t\tEnvRegion:            \"us-phoenix-1\",\n\t\t\t\tEnvCompartmentOCID:   \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"oraclecloud: some credentials information are missing: OCI_COMPARTMENT_OCID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing CompartmentID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvPrivKey:           mustGeneratePrivateKey(\"secret\"),\n\t\t\t\tEnvPrivKeyPass:       \"secret\",\n\t\t\t\tEnvTenancyOCID:       \"ocid1.tenancy.oc1..secret\",\n\t\t\t\tEnvUserOCID:          \"ocid1.user.oc1..secret\",\n\t\t\t\tEnvPubKeyFingerprint: \"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00\",\n\t\t\t\tEnvRegion:            \"us-phoenix-1\",\n\t\t\t\tEnvCompartmentOCID:   \"\",\n\t\t\t},\n\t\t\texpected: \"oraclecloud: some credentials information are missing: OCI_COMPARTMENT_OCID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing OCI_PRIVKEY\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvPrivKey:           \"\",\n\t\t\t\tEnvPrivKeyPass:       \"secret\",\n\t\t\t\tEnvTenancyOCID:       \"ocid1.tenancy.oc1..secret\",\n\t\t\t\tEnvUserOCID:          \"ocid1.user.oc1..secret\",\n\t\t\t\tEnvPubKeyFingerprint: \"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00\",\n\t\t\t\tEnvRegion:            \"us-phoenix-1\",\n\t\t\t\tEnvCompartmentOCID:   \"123\",\n\t\t\t},\n\t\t\texpected: \"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\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing OCI_PRIVKEY_PASS\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvPrivKey:           mustGeneratePrivateKey(\"secret\"),\n\t\t\t\tEnvPrivKeyPass:       \"\",\n\t\t\t\tEnvTenancyOCID:       \"ocid1.tenancy.oc1..secret\",\n\t\t\t\tEnvUserOCID:          \"ocid1.user.oc1..secret\",\n\t\t\t\tEnvPubKeyFingerprint: \"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00\",\n\t\t\t\tEnvRegion:            \"us-phoenix-1\",\n\t\t\t\tEnvCompartmentOCID:   \"123\",\n\t\t\t},\n\t\t\texpected: \"oraclecloud: can not create client, bad configuration: \",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing OCI_TENANCY_OCID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvPrivKey:           mustGeneratePrivateKey(\"secret\"),\n\t\t\t\tEnvPrivKeyPass:       \"secret\",\n\t\t\t\tEnvTenancyOCID:       \"\",\n\t\t\t\tEnvUserOCID:          \"ocid1.user.oc1..secret\",\n\t\t\t\tEnvPubKeyFingerprint: \"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00\",\n\t\t\t\tEnvRegion:            \"us-phoenix-1\",\n\t\t\t\tEnvCompartmentOCID:   \"123\",\n\t\t\t},\n\t\t\texpected: \"oraclecloud: some credentials information are missing: OCI_TENANCY_OCID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing OCI_USER_OCID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvPrivKey:           mustGeneratePrivateKey(\"secret\"),\n\t\t\t\tEnvPrivKeyPass:       \"secret\",\n\t\t\t\tEnvTenancyOCID:       \"ocid1.tenancy.oc1..secret\",\n\t\t\t\tEnvUserOCID:          \"\",\n\t\t\t\tEnvPubKeyFingerprint: \"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00\",\n\t\t\t\tEnvRegion:            \"us-phoenix-1\",\n\t\t\t\tEnvCompartmentOCID:   \"123\",\n\t\t\t},\n\t\t\texpected: \"oraclecloud: some credentials information are missing: OCI_USER_OCID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing OCI_PUBKEY_FINGERPRINT\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvPrivKey:           mustGeneratePrivateKey(\"secret\"),\n\t\t\t\tEnvPrivKeyPass:       \"secret\",\n\t\t\t\tEnvTenancyOCID:       \"ocid1.tenancy.oc1..secret\",\n\t\t\t\tEnvUserOCID:          \"ocid1.user.oc1..secret\",\n\t\t\t\tEnvPubKeyFingerprint: \"\",\n\t\t\t\tEnvRegion:            \"us-phoenix-1\",\n\t\t\t\tEnvCompartmentOCID:   \"123\",\n\t\t\t},\n\t\t\texpected: \"oraclecloud: some credentials information are missing: OCI_PUBKEY_FINGERPRINT\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing OCI_REGION\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvPrivKey:           mustGeneratePrivateKey(\"secret\"),\n\t\t\t\tEnvPrivKeyPass:       \"secret\",\n\t\t\t\tEnvTenancyOCID:       \"ocid1.tenancy.oc1..secret\",\n\t\t\t\tEnvUserOCID:          \"ocid1.user.oc1..secret\",\n\t\t\t\tEnvPubKeyFingerprint: \"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00\",\n\t\t\t\tEnvRegion:            \"\",\n\t\t\t\tEnvCompartmentOCID:   \"123\",\n\t\t\t},\n\t\t\texpected: \"oraclecloud: some credentials information are missing: OCI_REGION\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing OCI_REGION\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tenvPrivKey:           mustGeneratePrivateKey(\"secret\"),\n\t\t\t\tEnvPrivKeyPass:       \"secret\",\n\t\t\t\tEnvTenancyOCID:       \"ocid1.tenancy.oc1..secret\",\n\t\t\t\tEnvUserOCID:          \"ocid1.user.oc1..secret\",\n\t\t\t\tEnvPubKeyFingerprint: \"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00\",\n\t\t\t\tEnvRegion:            \"\",\n\t\t\t\tEnvCompartmentOCID:   \"123\",\n\t\t\t},\n\t\t\texpected: \"oraclecloud: some credentials information are missing: OCI_REGION\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer func() {\n\t\t\t\tprivKeyFile := os.Getenv(EnvPrivKeyFile)\n\t\t\t\tif privKeyFile != \"\" {\n\t\t\t\t\t_ = os.Remove(privKeyFile)\n\t\t\t\t}\n\n\t\t\t\tenvTest.RestoreEnv()\n\t\t\t}()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProvider_instance_principal(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthType:        \"instance_principal\",\n\t\t\t\tEnvCompartmentOCID: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing CompartmentID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthType: \"instance_principal\",\n\t\t\t},\n\t\t\texpected: \"oraclecloud: some credentials information are missing: OCI_COMPARTMENT_OCID\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer func() {\n\t\t\t\tenvTest.RestoreEnv()\n\t\t\t}()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tserverURL := servermock.NewBuilder(\n\t\t\t\tfunc(server *httptest.Server) (string, error) {\n\t\t\t\t\treturn server.URL, nil\n\t\t\t\t}).\n\t\t\t\tRoute(\"GET /instance/region\", servermock.RawStringResponse(\"oc1\")).\n\t\t\t\t// To generate fake certificates:\n\t\t\t\t//     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\n\t\t\t\tRoute(\"GET /identity/cert.pem\", servermock.ResponseFromFixture(\"cert.pem\")).\n\t\t\t\tRoute(\"GET /identity/key.pem\", servermock.ResponseFromFixture(\"key.pem\")).\n\t\t\t\tRoute(\"GET /identity/intermediate.pem\", servermock.ResponseFromFixture(\"cert.pem\")).\n\t\t\t\t// https://github.com/oracle/oci-go-sdk/blob/413a2f277f95c5eb76e26a0e0833c396a518bf50/common/auth/jwt_test.go#L12\n\t\t\t\tRoute(\"POST /v1/x509\", servermock.RawStringResponse(`{\"token\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6ImFzdyIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJvcGMub3JhY2xlLmNvbSIsImV4cCI6MTUxMTgzODc5MywiaWF0IjoxNTExODE3MTkzLCJpc3MiOiJhdXRoU2VydmljZS5vcmFjbGUuY29tIiwib3BjLWNlcnR0eXBlIjoiaW5zdGFuY2UiLCJvcGMtY29tcGFydG1lbnQiOiJvY2lkMS5jb21wYXJ0bWVudC5vYzEuLmJsdWhibHVoYmx1aCIsIm9wYy1pbnN0YW5jZSI6Im9jaWQxLmluc3RhbmNlLm9jMS5waHguYmx1aGJsdWhibHVoIiwib3BjLXRlbmFudCI6Im9jaWR2MTp0ZW5hbmN5Om9jMTpwaHg6MTIzNDU2Nzg5MDpibHVoYmx1aGJsdWgiLCJwdHlwZSI6Imluc3RhbmNlIiwic3ViIjoib2NpZDEuaW5zdGFuY2Uub2MxLnBoeC5ibHVoYmx1aGJsdWgiLCJ0ZW5hbnQiOiJvY2lkdjE6dGVuYW5jeTpvYzE6cGh4OjEyMzQ1Njc4OTA6Ymx1aGJsdWhibHVoIiwidHR5cGUiOiJ4NTA5In0.zen7q2yJSpMjzH4ym_H7VEwZA0-vTT4Wcild-HRfLxX6A1ej4tlpACa7A24j5JoZYI4mHooZVJ8e7ZezFenK0zZx5j8RbIjsqJKwroYXExOiBXLCUwMWOLXIndEsUzzGLqnPfKHXd80vrhMLmtkVTCJqBMzvPUSYkH_ciWgmjP9m0YETdQ9ifghkADhZGt9IlnOswg0s3Bx9ASwxFZEtom0BmU9GwEuITTTZfKvndk785BlNeZMOjhovaD97-LYpv5B_PiWEz8zialK5zxjijLCw06zyA8CQRQqmVCagNUPilfz_BcPyImzvFDuzQcPyDkTcsB7weX35tafHmA_Ul\"}`)).\n\t\t\t\tBuild(t)\n\n\t\t\tenvVars := map[string]string{\n\t\t\t\tenvMetadataBaseURL:        serverURL,\n\t\t\t\tenvSDKAuthClientRegionURL: serverURL,\n\t\t\t}\n\n\t\t\tmaps.Copy(envVars, test.envVars)\n\n\t\t\tenvTest.Apply(envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\tenvTest.ClearEnv()\n\tdefer envTest.RestoreEnv()\n\n\ttestCases := []struct {\n\t\tdesc                  string\n\t\tcompartmentID         string\n\t\tconfigurationProvider common.ConfigurationProvider\n\t\texpected              string\n\t}{\n\t\t{\n\t\t\tdesc:                  \"configuration provider error\",\n\t\t\tconfigurationProvider: mockConfigurationProvider(\"wrong-secret\"),\n\t\t\tcompartmentID:         \"123\",\n\t\t\texpected:              \"oraclecloud: can not create client, bad configuration: x509: decryption password incorrect\",\n\t\t},\n\t\t{\n\t\t\tdesc:          \"OCIConfigProvider is missing\",\n\t\t\tcompartmentID: \"123\",\n\t\t\texpected:      \"oraclecloud: OCIConfigProvider is missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:                  \"missing CompartmentID\",\n\t\t\tconfigurationProvider: mockConfigurationProvider(\"secret\"),\n\t\t\texpected:              \"oraclecloud: CompartmentID is missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.CompartmentID = test.compartmentID\n\t\t\tconfig.OCIConfigProvider = test.configurationProvider\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockConfigurationProvider(keyPassphrase string) *environmentConfigurationProvider {\n\tenvTest.Apply(map[string]string{\n\t\tenvPrivKey: mustGeneratePrivateKey(\"secret\"),\n\t})\n\n\treturn &environmentConfigurationProvider{\n\t\tvalues: map[string]string{\n\t\t\tEnvCompartmentOCID:   \"test\",\n\t\t\tEnvPrivKeyPass:       keyPassphrase,\n\t\t\tEnvTenancyOCID:       \"test\",\n\t\t\tEnvUserOCID:          \"test\",\n\t\t\tEnvPubKeyFingerprint: \"test\",\n\t\t\tEnvRegion:            \"test\",\n\t\t},\n\t}\n}\n\nfunc mustGeneratePrivateKey(pwd string) string {\n\tblock, err := generatePrivateKey(pwd)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn base64.StdEncoding.EncodeToString(pem.EncodeToMemory(block))\n}\n\nfunc mustGeneratePrivateKeyFile(t *testing.T, pwd string) string {\n\tt.Helper()\n\n\tblock, err := generatePrivateKey(pwd)\n\trequire.NoError(t, err)\n\n\tfile, err := os.CreateTemp(t.TempDir(), \"lego_oci_*.pem\")\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\t_ = file.Close()\n\t}()\n\n\terr = pem.Encode(file, block)\n\trequire.NoError(t, err)\n\n\treturn file.Name()\n}\n\nfunc generatePrivateKey(pwd string) (*pem.Block, error) {\n\tkey, err := rsa.GenerateKey(rand.Reader, 1024)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tblock := &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(key),\n\t}\n\n\tif pwd != \"\" {\n\t\tblock, err = x509.EncryptPEMBlock(rand.Reader, block.Type, block.Bytes, []byte(pwd), x509.PEMCipherAES256)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn block, nil\n}\n"
  },
  {
    "path": "providers/dns/otc/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\ntype Client struct {\n\tusername    string\n\tpassword    string\n\tdomainName  string\n\tprojectName string\n\n\tIdentityEndpoint string\n\ttoken            string\n\tmuToken          sync.Mutex\n\n\tbaseURL   *url.URL\n\tmuBaseURL sync.Mutex\n\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(username, password, domainName, projectName string) *Client {\n\treturn &Client{\n\t\tusername:         username,\n\t\tpassword:         password,\n\t\tdomainName:       domainName,\n\t\tprojectName:      projectName,\n\t\tIdentityEndpoint: DefaultIdentityEndpoint,\n\t\tHTTPClient:       &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\nfunc (c *Client) GetZoneID(ctx context.Context, zone string, privateZone bool) (string, error) {\n\tzonesResp, err := c.getZones(ctx, zone, privateZone)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(zonesResp.Zones) < 1 {\n\t\treturn \"\", fmt.Errorf(\"zone %s not found\", zone)\n\t}\n\n\tfor _, z := range zonesResp.Zones {\n\t\tif z.Name == zone {\n\t\t\treturn z.ID, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"zone %s not found\", zone)\n}\n\n// https://docs.otc.t-systems.com/domain-name-service/api-ref/apis/public_zone_management/querying_public_zones.html\nfunc (c *Client) getZones(ctx context.Context, zone string, privateZone bool) (*ZonesResponse, error) {\n\tc.muBaseURL.Lock()\n\tendpoint := c.baseURL.JoinPath(\"zones\")\n\tc.muBaseURL.Unlock()\n\n\tquery := endpoint.Query()\n\tquery.Set(\"name\", zone)\n\n\tif privateZone {\n\t\tquery.Set(\"type\", \"private\")\n\t}\n\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar zones ZonesResponse\n\n\terr = c.do(req, &zones)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &zones, nil\n}\n\nfunc (c *Client) GetRecordSetID(ctx context.Context, zoneID, fqdn string) (string, error) {\n\trecordSetsRes, err := c.getRecordSet(ctx, zoneID, fqdn)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(recordSetsRes.RecordSets) < 1 {\n\t\treturn \"\", errors.New(\"record not found\")\n\t}\n\n\tif len(recordSetsRes.RecordSets) > 1 {\n\t\treturn \"\", errors.New(\"to many records found\")\n\t}\n\n\tif recordSetsRes.RecordSets[0].ID == \"\" {\n\t\treturn \"\", errors.New(\"id not found\")\n\t}\n\n\treturn recordSetsRes.RecordSets[0].ID, nil\n}\n\n// https://docs.otc.t-systems.com/domain-name-service/api-ref/apis/record_set_management/querying_all_record_sets.html\nfunc (c *Client) getRecordSet(ctx context.Context, zoneID, fqdn string) (*RecordSetsResponse, error) {\n\tc.muBaseURL.Lock()\n\tendpoint := c.baseURL.JoinPath(\"zones\", zoneID, \"recordsets\")\n\tc.muBaseURL.Unlock()\n\n\tquery := endpoint.Query()\n\tquery.Set(\"type\", \"TXT\")\n\tquery.Set(\"name\", fqdn)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar recordSetsRes RecordSetsResponse\n\n\terr = c.do(req, &recordSetsRes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &recordSetsRes, nil\n}\n\n// CreateRecordSet creates a record.\n// https://docs.otc.t-systems.com/domain-name-service/api-ref/apis/record_set_management/creating_a_record_set.html\nfunc (c *Client) CreateRecordSet(ctx context.Context, zoneID string, record RecordSets) error {\n\tc.muBaseURL.Lock()\n\tendpoint := c.baseURL.JoinPath(\"zones\", zoneID, \"recordsets\")\n\tc.muBaseURL.Unlock()\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\n// DeleteRecordSet delete a record set.\n// https://docs.otc.t-systems.com/domain-name-service/api-ref/apis/record_set_management/deleting_a_record_set.html\nfunc (c *Client) DeleteRecordSet(ctx context.Context, zoneID, recordID string) error {\n\tc.muBaseURL.Lock()\n\tendpoint := c.baseURL.JoinPath(\"zones\", zoneID, \"recordsets\", recordID)\n\tc.muBaseURL.Unlock()\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tc.muToken.Lock()\n\n\tif c.token != \"\" {\n\t\treq.Header.Set(\"X-Auth-Token\", c.token)\n\t}\n\n\tc.muToken.Unlock()\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode >= http.StatusBadRequest {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest[T string | *url.URL](ctx context.Context, method string, endpoint T, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, fmt.Sprintf(\"%s\", endpoint), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/otc/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\", \"example.com\", \"test\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t)\n}\n\nfunc TestClient_GetZoneID(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones\",\n\t\t\tservermock.ResponseFromFixture(\"zones_GET.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com.\")).\n\t\tBuild(t)\n\n\tzoneID, err := client.GetZoneID(context.Background(), \"example.com.\", false)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"123123\", zoneID)\n}\n\nfunc TestClient_GetZoneID_private(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones\",\n\t\t\tservermock.ResponseFromFixture(\"zones_GET.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com.\").\n\t\t\t\tWith(\"type\", \"private\")).\n\t\tBuild(t)\n\n\tzoneID, err := client.GetZoneID(context.Background(), \"example.com.\", true)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"123123\", zoneID)\n}\n\nfunc TestClient_GetZoneID_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones\",\n\t\t\tservermock.ResponseFromFixture(\"zones_GET_empty.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com.\")).\n\t\tBuild(t)\n\n\t_, err := client.GetZoneID(context.Background(), \"example.com.\", false)\n\trequire.EqualError(t, err, \"zone example.com. not found\")\n}\n\nfunc TestClient_GetRecordSetID(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones/123123/recordsets\",\n\t\t\tservermock.ResponseFromFixture(\"zones-recordsets_GET.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com.\").\n\t\t\t\tWith(\"type\", \"TXT\"),\n\t\t).\n\t\tBuild(t)\n\n\trecordSetID, err := client.GetRecordSetID(context.Background(), \"123123\", \"example.com.\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"321321\", recordSetID)\n}\n\nfunc TestClient_GetRecordSetID_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /zones/123123/recordsets\",\n\t\t\tservermock.ResponseFromFixture(\"zones-recordsets_GET_empty.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com.\").\n\t\t\t\tWith(\"type\", \"TXT\"),\n\t\t).\n\t\tBuild(t)\n\n\t_, err := client.GetRecordSetID(context.Background(), \"123123\", \"example.com.\")\n\trequire.EqualError(t, err, \"record not found\")\n}\n\nfunc TestClient_CreateRecordSet(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zones/123123/recordsets\",\n\t\t\tservermock.ResponseFromFixture(\"zones-recordsets_POST.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"zones-recordsets_POST-request.json\")).\n\t\tBuild(t)\n\n\trs := RecordSets{\n\t\tName:        \"_acme-challenge.example.com.\",\n\t\tDescription: \"Added TXT record for ACME dns-01 challenge using lego client\",\n\t\tType:        \"TXT\",\n\t\tTTL:         300,\n\t\tRecords:     []string{strconv.Quote(\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\")},\n\t}\n\terr := client.CreateRecordSet(context.Background(), \"123123\", rs)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecordSet(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /zones/123123/recordsets/321321\",\n\t\t\tservermock.ResponseFromFixture(\"zones-recordsets_DELETE.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecordSet(context.Background(), \"123123\", \"321321\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/otc/internal/fixtures/zones-recordsets_DELETE.json",
    "content": "{\n  \"id\": \"2c9eb155587228570158722b6ac30007\",\n  \"name\": \"www.example.com.\",\n  \"description\": \"This is an example record set.\",\n  \"type\": \"A\",\n  \"ttl\": 300,\n  \"status\": \"PENDING_DELETE\",\n  \"links\": {\n    \"self\": \"https://Endpoint/v2/zones/2c9eb155587194ec01587224c9f90149/recordsets/2c9eb155587228570158722b6ac30007\"\n  },\n  \"zone_id\": \"2c9eb155587194ec01587224c9f90149\",\n  \"zone_name\": \"example.com.\",\n  \"create_at\": \"2016-11-17T12:03:17.827\",\n  \"update_at\": \"2016-11-17T12:56:03.827\",\n  \"default\": false,\n  \"project_id\": \"e55c6f3dc4e34c9f86353b664ae0e70c\"\n}\n"
  },
  {
    "path": "providers/dns/otc/internal/fixtures/zones-recordsets_GET.json",
    "content": "{\n  \"links\": {\n    \"self\": \"https://Endpoint/v2/recordsets\",\n    \"next\": \"https://Endpoint/v2/recordsets?id=&limit=11&marker=2c9eb155587194ec01587224c9f9014a\"\n  },\n  \"recordsets\": [\n    {\n      \"id\": \"321321\",\n      \"name\": \"_acme-challenge.example.com\",\n      \"type\": \"TXT\",\n      \"ttl\": 300,\n      \"records\": [\n        \"ns1.hotrot.de. xx.example.com. (1 7200 900 1209600 300)\"\n      ],\n      \"status\": \"ACTIVE\",\n      \"links\": {\n        \"self\": \"https://Endpoint/v2/zones/2c9eb155587194ec01587224c9f90149/recordsets/2c9eb155587194ec01587224c9f9014a\"\n      },\n      \"zone_id\": \"2c9eb155587194ec01587224c9f90149\",\n      \"zone_name\": \"example.com.\",\n      \"create_at\": \"2016-11-17T11:56:03.439\",\n      \"update_at\": \"2016-11-17T11:56:03.827\",\n      \"default\": true,\n      \"project_id\": \"e55c6f3dc4e34c9f86353b664ae0e70c\"\n    }\n  ],\n  \"metadata\": {\n    \"total_count\": 1\n  }\n}\n"
  },
  {
    "path": "providers/dns/otc/internal/fixtures/zones-recordsets_GET_empty.json",
    "content": "{\n  \"recordsets\": []\n}\n"
  },
  {
    "path": "providers/dns/otc/internal/fixtures/zones-recordsets_POST-request.json",
    "content": "{\n  \"name\": \"_acme-challenge.example.com.\",\n  \"description\": \"Added TXT record for ACME dns-01 challenge using lego client\",\n  \"type\": \"TXT\",\n  \"ttl\": 300,\n  \"records\": [\n    \"\\\"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\\\"\"\n  ]\n}\n"
  },
  {
    "path": "providers/dns/otc/internal/fixtures/zones-recordsets_POST.json",
    "content": "{\n  \"id\": \"2c9eb155587228570158722b6ac30007\",\n  \"name\": \"www.example.com.\",\n  \"description\": \"This is an example record set.\",\n  \"type\": \"A\",\n  \"ttl\": 300,\n  \"records\": [\n    \"192.168.10.1\",\n    \"192.168.10.2\"\n  ],\n  \"status\": \"PENDING_CREATE\",\n  \"links\": {\n    \"self\": \"https://Endpoint/v2/zones/2c9eb155587194ec01587224c9f90149/recordsets/2c9eb155587228570158722b6ac30007\"\n  },\n  \"zone_id\": \"2c9eb155587194ec01587224c9f90149\",\n  \"zone_name\": \"example.com.\",\n  \"create_at\": \"2016-11-17T12:03:17.827\",\n  \"update_at\": null,\n  \"default\": false,\n  \"project_id\": \"e55c6f3dc4e34c9f86353b664ae0e70c\"\n}\n"
  },
  {
    "path": "providers/dns/otc/internal/fixtures/zones_GET.json",
    "content": "{\n  \"links\": {\n    \"self\": \"https://Endpoint/v2/zones?type=public&limit=11\",\n    \"next\": \"https://Endpoint/v2/zones?type=public&limit=11&marker=2c9eb155587194ec01587224c9f90149\"\n  },\n  \"zones\": [\n    {\n      \"id\": \"123123\",\n      \"name\": \"example.com.\",\n      \"description\": \"This is an example zone.\",\n      \"email\": \"xx@example.com\",\n      \"ttl\": 300,\n      \"serial\": 0,\n      \"masters\": [],\n      \"status\": \"ACTIVE\",\n      \"links\": {\n        \"self\": \"https://Endpoint/v2/zones/2c9eb155587194ec01587224c9f90149\"\n      },\n      \"pool_id\": \"00000000570e54ee01570e9939b20019\",\n      \"project_id\": \"e55c6f3dc4e34c9f86353b664ae0e70c\",\n      \"zone_type\": \"public\",\n      \"created_at\": \"2016-11-17T11:56:03.439\",\n      \"updated_at\": \"2016-11-17T11:56:05.528\",\n      \"record_num\": 2\n    },\n    {\n      \"id\": \"2c9eb155587228570158722996c50001\",\n      \"name\": \"example.org.\",\n      \"description\": \"This is an example zone.\",\n      \"email\": \"xx@example.org\",\n      \"ttl\": 300,\n      \"serial\": 0,\n      \"masters\": [],\n      \"status\": \"PENDING_CREATE\",\n      \"links\": {\n        \"self\": \"https://Endpoint/v2/zones/2c9eb155587228570158722996c50001\"\n      },\n      \"pool_id\": \"00000000570e54ee01570e9939b20019\",\n      \"project_id\": \"e55c6f3dc4e34c9f86353b664ae0e70c\",\n      \"zone_type\": \"public\",\n      \"created_at\": \"2016-11-17T12:01:17.996\",\n      \"updated_at\": \"2016-11-17T12:01:18.528\",\n      \"record_num\": 2\n    }\n  ],\n  \"metadata\": {\n    \"total_count\": 2\n  }\n}\n"
  },
  {
    "path": "providers/dns/otc/internal/fixtures/zones_GET_empty.json",
    "content": "{\n  \"zones\": []\n}\n"
  },
  {
    "path": "providers/dns/otc/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// DefaultIdentityEndpoint the default API identity endpoint.\nconst DefaultIdentityEndpoint = \"https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens\"\n\n// Login Starts a new OTC API Session. Authenticates using userName, password\n// and receives a token to be used in for subsequent requests.\nfunc (c *Client) Login(ctx context.Context) error {\n\tpayload := LoginRequest{\n\t\tAuth: Auth{\n\t\t\tIdentity: Identity{\n\t\t\t\tMethods: []string{\"password\"},\n\t\t\t\tPassword: Password{\n\t\t\t\t\tUser: User{\n\t\t\t\t\t\tName:     c.username,\n\t\t\t\t\t\tPassword: c.password,\n\t\t\t\t\t\tDomain: Domain{\n\t\t\t\t\t\t\tName: c.domainName,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tScope: Scope{\n\t\t\t\tProject: Project{\n\t\t\t\t\tName: c.projectName,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\ttokenResp, token, err := c.obtainUserToken(ctx, payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.muToken.Lock()\n\tdefer c.muToken.Unlock()\n\n\tc.token = token\n\n\tif c.token == \"\" {\n\t\treturn errors.New(\"unable to get auth token\")\n\t}\n\n\tbaseURL, err := getBaseURL(tokenResp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.muBaseURL.Lock()\n\tc.baseURL = baseURL\n\tc.muBaseURL.Unlock()\n\n\treturn nil\n}\n\n// https://docs.otc.t-systems.com/identity-access-management/api-ref/apis/token_management/obtaining_a_user_token.html\nfunc (c *Client) obtainUserToken(ctx context.Context, payload LoginRequest) (*TokenResponse, string, error) {\n\treq, err := newJSONRequest(ctx, http.MethodPost, c.IdentityEndpoint, payload)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\tclient := &http.Client{Timeout: c.HTTPClient.Timeout}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn nil, \"\", errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\ttoken := resp.Header.Get(\"X-Subject-Token\")\n\n\tif token == \"\" {\n\t\treturn nil, \"\", errors.New(\"unable to get auth token\")\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, \"\", errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar newToken TokenResponse\n\n\terr = json.Unmarshal(raw, &newToken)\n\tif err != nil {\n\t\treturn nil, \"\", errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn &newToken, token, nil\n}\n\nfunc getBaseURL(tokenResp *TokenResponse) (*url.URL, error) {\n\tvar endpoints []Endpoint\n\n\tfor _, v := range tokenResp.Token.Catalog {\n\t\tif v.Type == \"dns\" {\n\t\t\tendpoints = append(endpoints, v.Endpoints...)\n\t\t}\n\t}\n\n\tif len(endpoints) == 0 {\n\t\treturn nil, errors.New(\"unable to get dns endpoint\")\n\t}\n\n\tbaseURL, err := url.JoinPath(endpoints[0].URL, \"v2\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn url.Parse(baseURL)\n}\n"
  },
  {
    "path": "providers/dns/otc/internal/identity_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestClient_Login(t *testing.T) {\n\tvar serverURL *url.URL\n\n\tclient := servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\", \"example.com\", \"test\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.IdentityEndpoint = server.URL + \"/v3/auth/token\"\n\n\t\t\tserverURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t).\n\t\tRoute(\"POST /v3/auth/token\", IdentityHandlerMock()).\n\t\tBuild(t)\n\n\terr := client.Login(t.Context())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, serverURL.JoinPath(\"v2\").String(), client.baseURL.String())\n\tassert.Equal(t, fakeOTCToken, client.token)\n}\n"
  },
  {
    "path": "providers/dns/otc/internal/mock.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\nconst fakeOTCToken = \"62244bc21da68d03ebac94e6636ff01f\"\n\nfunc IdentityHandlerMock() http.HandlerFunc {\n\treturn func(w http.ResponseWriter, req *http.Request) {\n\t\tw.Header().Set(\"X-Subject-Token\", fakeOTCToken)\n\n\t\t_, _ = fmt.Fprintf(w, `{\n\t\t  \"token\": {\n\t\t    \"catalog\": [\n\t\t      {\n\t\t\t\"type\": \"dns\",\n\t\t\t\"id\": \"56cd81db1f8445d98652479afe07c5ba\",\n\t\t\t\"name\": \"\",\n\t\t\t\"endpoints\": [\n\t\t\t  {\n\t\t\t    \"url\": \"http://%s\",\n\t\t\t    \"region\": \"eu-de\",\n\t\t\t    \"region_id\": \"eu-de\",\n\t\t\t    \"interface\": \"public\",\n\t\t\t    \"id\": \"0047a06690484d86afe04877074efddf\"\n\t\t\t  }\n\t\t\t]\n\t\t      }\n\t\t    ]\n\t\t  }}`, req.Context().Value(http.LocalAddrContextKey))\n\t}\n}\n"
  },
  {
    "path": "providers/dns/otc/internal/types.go",
    "content": "package internal\n\n// LoginRequest\n\ntype LoginRequest struct {\n\tAuth Auth `json:\"auth\"`\n}\n\ntype Auth struct {\n\tIdentity Identity `json:\"identity\"`\n\tScope    Scope    `json:\"scope\"`\n}\n\ntype Identity struct {\n\tMethods  []string `json:\"methods\"`\n\tPassword Password `json:\"password\"`\n}\n\ntype Password struct {\n\tUser User `json:\"user\"`\n}\n\ntype User struct {\n\tName     string `json:\"name\"`\n\tPassword string `json:\"password\"`\n\tDomain   Domain `json:\"domain\"`\n}\n\ntype Scope struct {\n\tProject Project `json:\"project\"`\n}\n\ntype Project struct {\n\tName string `json:\"name\"`\n}\n\n// TokenResponse\n\ntype TokenResponse struct {\n\tToken Token `json:\"token\"`\n}\n\ntype Token struct {\n\tUser      UserR     `json:\"user\"`\n\tDomain    Domain    `json:\"domain\"`\n\tCatalog   []Catalog `json:\"catalog,omitempty\"`\n\tMethods   []string  `json:\"methods,omitempty\"`\n\tRoles     []Role    `json:\"roles,omitempty\"`\n\tExpiresAt string    `json:\"expires_at,omitempty\"`\n\tIssuedAt  string    `json:\"issued_at,omitempty\"`\n}\n\ntype Catalog struct {\n\tID        string     `json:\"id,omitempty\"`\n\tType      string     `json:\"type,omitempty\"`\n\tName      string     `json:\"name,omitempty\"`\n\tEndpoints []Endpoint `json:\"endpoints,omitempty\"`\n}\n\ntype UserR struct {\n\tID                string `json:\"id,omitempty\"`\n\tDomain            Domain `json:\"domain\"`\n\tName              string `json:\"name,omitempty\"`\n\tPasswordExpiresAt string `json:\"password_expires_at,omitempty\"`\n}\n\ntype Endpoint struct {\n\tID        string `json:\"id,omitempty\"`\n\tURL       string `json:\"url,omitempty\"`\n\tRegion    string `json:\"region,omitempty\"`\n\tRegionID  string `json:\"region_id,omitempty\"`\n\tInterface string `json:\"interface,omitempty\"`\n}\n\ntype Role struct {\n\tID   string `json:\"id,omitempty\"`\n\tName string `json:\"name,omitempty\"`\n}\n\n// RecordSetsResponse\n\ntype RecordSetsResponse struct {\n\tLinks      Links        `json:\"links\"`\n\tRecordSets []RecordSets `json:\"recordsets\"`\n\tMetadata   Metadata     `json:\"metadata\"`\n}\n\ntype RecordSets struct {\n\tID          string   `json:\"id,omitempty\"`\n\tName        string   `json:\"name,omitempty\"`\n\tDescription string   `json:\"description,omitempty\"`\n\tType        string   `json:\"type,omitempty\"`\n\tTTL         int      `json:\"ttl,omitempty\"`\n\tRecords     []string `json:\"records,omitempty\"`\n\n\tStatus    string `json:\"status,omitempty\"`\n\tLinks     *Links `json:\"links,omitempty\"`\n\tZoneID    string `json:\"zone_id,omitempty\"`\n\tZoneName  string `json:\"zone_name,omitempty\"`\n\tCreateAt  string `json:\"create_at,omitempty\"`\n\tUpdateAt  string `json:\"update_at,omitempty\"`\n\tDefault   bool   `json:\"default,omitempty\"`\n\tProjectID string `json:\"project_id,omitempty\"`\n}\n\n// ZonesResponse\n\ntype ZonesResponse struct {\n\tLinks    Links    `json:\"links\"`\n\tZones    []Zone   `json:\"zones\"`\n\tMetadata Metadata `json:\"metadata\"`\n}\n\ntype Zone struct {\n\tID          string `json:\"id,omitempty\"`\n\tName        string `json:\"name,omitempty\"`\n\tDescription string `json:\"description,omitempty\"`\n\tEmail       string `json:\"email,omitempty\"`\n\tTTL         int    `json:\"ttl,omitempty\"`\n\tSerial      int    `json:\"serial,omitempty\"`\n\tStatus      string `json:\"status,omitempty\"`\n\tLinks       *Links `json:\"links,omitempty\"`\n\tPoolID      string `json:\"pool_id,omitempty\"`\n\tProjectID   string `json:\"project_id,omitempty\"`\n\tZoneType    string `json:\"zone_type,omitempty\"`\n\tCreatedAt   string `json:\"created_at,omitempty\"`\n\tUpdatedAt   string `json:\"updated_at,omitempty\"`\n\tRecordNum   int    `json:\"record_num,omitempty\"`\n}\n\n// Response\n\ntype Links struct {\n\tSelf string `json:\"self,omitempty\"`\n\tNext string `json:\"next,omitempty\"`\n}\n\ntype Metadata struct {\n\tTotalCount int `json:\"total_count,omitempty\"`\n}\n\n// Shared\n\ntype Domain struct {\n\tID   string `json:\"id,omitempty\"`\n\tName string `json:\"name,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/otc/otc.go",
    "content": "// Package otc implements a DNS provider for solving the DNS-01 challenge using Open Telekom Cloud Managed DNS.\npackage otc\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/otc/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"OTC_\"\n\n\tEnvDomainName       = envNamespace + \"DOMAIN_NAME\"\n\tEnvUserName         = envNamespace + \"USER_NAME\"\n\tEnvPassword         = envNamespace + \"PASSWORD\"\n\tEnvProjectName      = envNamespace + \"PROJECT_NAME\"\n\tEnvIdentityEndpoint = envNamespace + \"IDENTITY_ENDPOINT\"\n\tEnvPrivateZone      = envNamespace + \"PRIVATE_ZONE\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n)\n\nconst defaultIdentityEndpoint = \"https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens\"\n\n// minTTL 300 is otc minimum value for TTL.\nconst minTTL = 300\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tDomainName       string\n\tProjectName      string\n\tUserName         string\n\tPassword         string\n\tIdentityEndpoint string\n\tPrivateZone      bool\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\ttr := &http.Transport{}\n\n\tdefaultTransport, ok := http.DefaultTransport.(*http.Transport)\n\tif ok {\n\t\ttr = defaultTransport.Clone()\n\t}\n\n\t// Workaround for keep alive bug in otc api\n\ttr.DisableKeepAlives = true\n\n\treturn &Config{\n\t\tPrivateZone:      env.GetOrDefaultBool(EnvPrivateZone, false),\n\t\tIdentityEndpoint: env.GetOrDefaultString(EnvIdentityEndpoint, defaultIdentityEndpoint),\n\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout:   env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t\tTransport: tr,\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for OTC DNS.\n// Credentials must be passed in the environment variables: OTC_USER_NAME,\n// OTC_DOMAIN_NAME, OTC_PASSWORD OTC_PROJECT_NAME and OTC_IDENTITY_ENDPOINT.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvDomainName, EnvUserName, EnvPassword, EnvProjectName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"otc: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.DomainName = values[EnvDomainName]\n\tconfig.UserName = values[EnvUserName]\n\tconfig.Password = values[EnvPassword]\n\tconfig.ProjectName = values[EnvProjectName]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for OTC DNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"otc: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.DomainName == \"\" || config.UserName == \"\" || config.Password == \"\" || config.ProjectName == \"\" {\n\t\treturn nil, errors.New(\"otc: credentials missing\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"otc: invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\tclient := internal.NewClient(config.UserName, config.Password, config.DomainName, config.ProjectName)\n\n\tif config.IdentityEndpoint != \"\" {\n\t\tclient.IdentityEndpoint = config.IdentityEndpoint\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"otc: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\terr = d.client.Login(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"otc: %w\", err)\n\t}\n\n\tzoneID, err := d.client.GetZoneID(ctx, authZone, d.config.PrivateZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"otc: unable to get zone: %w\", err)\n\t}\n\n\trecord := internal.RecordSets{\n\t\tName:        info.EffectiveFQDN,\n\t\tDescription: \"Added TXT record for ACME dns-01 challenge using lego client\",\n\t\tType:        \"TXT\",\n\t\tTTL:         d.config.TTL,\n\t\tRecords:     []string{fmt.Sprintf(\"%q\", info.Value)},\n\t}\n\n\terr = d.client.CreateRecordSet(ctx, zoneID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"otc: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"otc: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\terr = d.client.Login(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"otc: %w\", err)\n\t}\n\n\tzoneID, err := d.client.GetZoneID(ctx, authZone, d.config.PrivateZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"otc: %w\", err)\n\t}\n\n\trecordID, err := d.client.GetRecordSetID(ctx, zoneID, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"otc: unable to get record %s for zone %s: %w\", info.EffectiveFQDN, domain, err)\n\t}\n\n\terr = d.client.DeleteRecordSet(ctx, zoneID, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"otc: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n"
  },
  {
    "path": "providers/dns/otc/otc.toml",
    "content": "Name = \"Open Telekom Cloud\"\nDescription = ''''''\nURL = \"https://cloud.telekom.de/en\"\nCode = \"otc\"\nSince = \"v0.4.1\"\n\nExample = '''\nOTC_DOMAIN_NAME=domain_name \\\nOTC_USER_NAME=user_name \\\nOTC_PASSWORD=password \\\nOTC_PROJECT_NAME=project_name \\\nlego --dns otc -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    OTC_USER_NAME = \"User name\"\n    OTC_PASSWORD = \"Password\"\n    OTC_PROJECT_NAME = \"Project name\"\n    OTC_DOMAIN_NAME = \"Domain name\"\n  [Configuration.Additional]\n    OTC_IDENTITY_ENDPOINT = \"Identity endpoint URL (default: https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens)\"\n    OTC_PRIVATE_ZONE = \"Set to true to use private zones only (default: use public zones only)\"\n    OTC_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    OTC_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    OTC_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n    OTC_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    OTC_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://docs.otc.t-systems.com/domain-name-service/api-ref/index.html\"\n\n"
  },
  {
    "path": "providers/dns/otc/otc_test.go",
    "content": "package otc\n\nimport (\n\t\"fmt\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-acme/lego/v4/providers/dns/otc/internal\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvDomainName,\n\tEnvUserName,\n\tEnvPassword,\n\tEnvPrivateZone,\n\tEnvProjectName,\n\tEnvIdentityEndpoint).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvDomainName:  \"example.com\",\n\t\t\t\tEnvUserName:    \"user\",\n\t\t\t\tEnvPassword:    \"secret\",\n\t\t\t\tEnvProjectName: \"test\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvDomainName:  \"\",\n\t\t\t\tEnvUserName:    \"\",\n\t\t\t\tEnvPassword:    \"\",\n\t\t\t\tEnvProjectName: \"\",\n\t\t\t},\n\t\t\texpected: \"otc: some credentials information are missing: OTC_DOMAIN_NAME,OTC_USER_NAME,OTC_PASSWORD,OTC_PROJECT_NAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing domain name\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvDomainName:  \"\",\n\t\t\t\tEnvUserName:    \"user\",\n\t\t\t\tEnvPassword:    \"secret\",\n\t\t\t\tEnvProjectName: \"test\",\n\t\t\t},\n\t\t\texpected: \"otc: some credentials information are missing: OTC_DOMAIN_NAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvDomainName:  \"example.com\",\n\t\t\t\tEnvUserName:    \"\",\n\t\t\t\tEnvPassword:    \"secret\",\n\t\t\t\tEnvProjectName: \"test\",\n\t\t\t},\n\t\t\texpected: \"otc: some credentials information are missing: OTC_USER_NAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvDomainName:  \"example.com\",\n\t\t\t\tEnvUserName:    \"user\",\n\t\t\t\tEnvPassword:    \"\",\n\t\t\t\tEnvProjectName: \"test\",\n\t\t\t},\n\t\t\texpected: \"otc: some credentials information are missing: OTC_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing project name\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvDomainName:  \"example.com\",\n\t\t\t\tEnvUserName:    \"user\",\n\t\t\t\tEnvPassword:    \"secret\",\n\t\t\t\tEnvProjectName: \"\",\n\t\t\t},\n\t\t\texpected: \"otc: some credentials information are missing: OTC_PROJECT_NAME\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tdomainName  string\n\t\tprojectName string\n\t\tusername    string\n\t\tpassword    string\n\t\texpected    string\n\t}{\n\t\t{\n\t\t\tdesc:        \"success\",\n\t\t\tdomainName:  \"example.com\",\n\t\t\tprojectName: \"test\",\n\t\t\tusername:    \"user\",\n\t\t\tpassword:    \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"otc: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:        \"missing domain name\",\n\t\t\tdomainName:  \"\",\n\t\t\tprojectName: \"test\",\n\t\t\tusername:    \"user\",\n\t\t\tpassword:    \"secret\",\n\t\t\texpected:    \"otc: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:        \"missing project name\",\n\t\t\tdomainName:  \"example.com\",\n\t\t\tprojectName: \"\",\n\t\t\tusername:    \"user\",\n\t\t\tpassword:    \"secret\",\n\t\t\texpected:    \"otc: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:        \"missing username\",\n\t\t\tdomainName:  \"example.com\",\n\t\t\tprojectName: \"test\",\n\t\t\tusername:    \"\",\n\t\t\tpassword:    \"secret\",\n\t\t\texpected:    \"otc: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:        \"missing password \",\n\t\t\tdomainName:  \"example.com\",\n\t\t\tprojectName: \"test\",\n\t\t\tusername:    \"user\",\n\t\t\tpassword:    \"\",\n\t\t\texpected:    \"otc: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.DomainName = test.domainName\n\t\t\tconfig.ProjectName = test.projectName\n\t\t\tconfig.UserName = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder(false).\n\t\tRoute(\"GET /v2/zones\",\n\t\t\tservermock.ResponseFromInternal(\"zones_GET.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com.\")).\n\t\tRoute(\"POST /v2/zones/123123/recordsets\",\n\t\t\tservermock.Noop(),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"zones-recordsets_POST-request.json\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_Present_private(t *testing.T) {\n\tprovider := mockBuilder(true).\n\t\tRoute(\"GET /v2/zones\",\n\t\t\tservermock.ResponseFromInternal(\"zones_GET.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com.\").\n\t\t\t\tWith(\"type\", \"private\")).\n\t\tRoute(\"POST /v2/zones/123123/recordsets\",\n\t\t\tservermock.Noop(),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"zones-recordsets_POST-request.json\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_Present_emptyZone(t *testing.T) {\n\tprovider := mockBuilder(false).\n\t\tRoute(\"GET /v2/zones\",\n\t\t\tservermock.ResponseFromInternal(\"zones_GET_empty.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com.\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"\", \"123d==\")\n\trequire.EqualError(t, err, \"otc: unable to get zone: zone example.com. not found\")\n}\n\nfunc TestDNSProvider_Cleanup(t *testing.T) {\n\tprovider := mockBuilder(false).\n\t\tRoute(\"GET /v2/zones\",\n\t\t\tservermock.ResponseFromInternal(\"zones_GET.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com.\")).\n\t\tRoute(\"GET /v2/zones/123123/recordsets\",\n\t\t\tservermock.ResponseFromInternal(\"zones-recordsets_GET.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"_acme-challenge.example.com.\").\n\t\t\t\tWith(\"type\", \"TXT\")).\n\t\tRoute(\"DELETE /v2/zones/123123/recordsets/321321\",\n\t\t\tservermock.ResponseFromInternal(\"zones-recordsets_DELETE.json\")).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_Cleanup_private(t *testing.T) {\n\tprovider := mockBuilder(true).\n\t\tRoute(\"GET /v2/zones\",\n\t\t\tservermock.ResponseFromInternal(\"zones_GET.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com.\").\n\t\t\t\tWith(\"type\", \"private\")).\n\t\tRoute(\"GET /v2/zones/123123/recordsets\",\n\t\t\tservermock.ResponseFromInternal(\"zones-recordsets_GET.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"_acme-challenge.example.com.\").\n\t\t\t\tWith(\"type\", \"TXT\")).\n\t\tRoute(\"DELETE /v2/zones/123123/recordsets/321321\",\n\t\t\tservermock.ResponseFromInternal(\"zones-recordsets_DELETE.json\")).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_Cleanup_emptyRecordset(t *testing.T) {\n\tprovider := mockBuilder(false).\n\t\tRoute(\"GET /v2/zones\",\n\t\t\tservermock.ResponseFromInternal(\"zones_GET.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com.\")).\n\t\tRoute(\"GET /v2/zones/123123/recordsets\",\n\t\t\tservermock.ResponseFromInternal(\"zones-recordsets_GET_empty.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"_acme-challenge.example.com.\").\n\t\t\t\tWith(\"type\", \"TXT\")).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"\", \"123d==\")\n\trequire.EqualError(t, err, \"otc: unable to get record _acme-challenge.example.com. for zone example.com: record not found\")\n}\n\nfunc mockBuilder(private bool) *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.HTTPClient = server.Client()\n\t\t\tconfig.UserName = \"user\"\n\t\t\tconfig.Password = \"secret\"\n\t\t\tconfig.DomainName = \"example.com\"\n\t\t\tconfig.ProjectName = \"test\"\n\t\t\tconfig.IdentityEndpoint = fmt.Sprintf(\"%s/v3/auth/token\", server.URL)\n\t\t\tconfig.PrivateZone = private\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t).\n\t\tRoute(\"POST /v3/auth/token\", internal.IdentityHandlerMock())\n}\n"
  },
  {
    "path": "providers/dns/ovh/ovh.go",
    "content": "// Package ovh implements a DNS provider for solving the DNS-01 challenge using OVH DNS.\npackage ovh\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\t\"github.com/ovh/go-ovh/ovh\"\n)\n\n// OVH API reference:       https://eu.api.ovh.com/\n// Create a Token:          https://eu.api.ovh.com/createToken/\n// Create a OAuth2 client:   https://eu.api.ovh.com/console/?section=%2Fme&branch=v1#post-/me/api/oauth2/client\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"OVH_\"\n\n\tEnvEndpoint = envNamespace + \"ENDPOINT\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Authenticate using application key.\nconst (\n\tEnvApplicationKey    = envNamespace + \"APPLICATION_KEY\"\n\tEnvApplicationSecret = envNamespace + \"APPLICATION_SECRET\"\n\tEnvConsumerKey       = envNamespace + \"CONSUMER_KEY\"\n)\n\n// Authenticate using OAuth2 client.\nconst (\n\tEnvClientID     = envNamespace + \"CLIENT_ID\"\n\tEnvClientSecret = envNamespace + \"CLIENT_SECRET\"\n)\n\n// EnvAccessToken Authenticate using Access Token client.\nconst EnvAccessToken = envNamespace + \"ACCESS_TOKEN\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Record a DNS record.\ntype Record struct {\n\tID        int64  `json:\"id,omitempty\"`\n\tFieldType string `json:\"fieldType,omitempty\"`\n\tSubDomain string `json:\"subDomain,omitempty\"`\n\tTarget    string `json:\"target,omitempty\"`\n\tTTL       int    `json:\"ttl,omitempty\"`\n\tZone      string `json:\"zone,omitempty\"`\n}\n\n// OAuth2Config the OAuth2 specific configuration.\ntype OAuth2Config struct {\n\tClientID     string\n\tClientSecret string\n}\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIEndpoint string\n\n\tApplicationKey    string\n\tApplicationSecret string\n\tConsumerKey       string\n\n\tOAuth2Config *OAuth2Config\n\n\tAccessToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, ovh.DefaultTimeout),\n\t\t},\n\t}\n}\n\nfunc (c *Config) hasAppKeyAuth() bool {\n\treturn c.ApplicationKey != \"\" || c.ApplicationSecret != \"\" || c.ConsumerKey != \"\"\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *ovh.Client\n\n\trecordIDs   map[string]int64\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for OVH\n// Credentials must be passed in the environment variables:\n// OVH_ENDPOINT (must be either \"ovh-eu\" or \"ovh-ca\"), OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tconfig := NewDefaultConfig()\n\n\t// https://github.com/ovh/go-ovh/blob/6817886d12a8c5650794b28da635af9fcdfd1162/ovh/configuration.go#L105\n\tconfig.APIEndpoint = env.GetOrDefaultString(EnvEndpoint, \"ovh-eu\")\n\n\tconfig.ApplicationKey = env.GetOrFile(EnvApplicationKey)\n\tconfig.ApplicationSecret = env.GetOrFile(EnvApplicationSecret)\n\tconfig.ConsumerKey = env.GetOrFile(EnvConsumerKey)\n\n\tconfig.AccessToken = env.GetOrFile(EnvAccessToken)\n\n\tclientID := env.GetOrFile(EnvClientID)\n\tclientSecret := env.GetOrFile(EnvClientSecret)\n\n\tif clientID != \"\" || clientSecret != \"\" {\n\t\tconfig.OAuth2Config = &OAuth2Config{\n\t\t\tClientID:     clientID,\n\t\t\tClientSecret: clientSecret,\n\t\t}\n\t}\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for OVH.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"ovh: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.OAuth2Config != nil && config.hasAppKeyAuth() && config.AccessToken != \"\" {\n\t\treturn nil, errors.New(\"ovh: can't use multiple authentication systems (ApplicationKey, OAuth2, Access Token)\")\n\t}\n\n\tif config.OAuth2Config != nil && config.AccessToken != \"\" {\n\t\treturn nil, errors.New(\"ovh: can't use multiple authentication systems (OAuth2, Access Token)\")\n\t}\n\n\tif config.OAuth2Config != nil && config.hasAppKeyAuth() {\n\t\treturn nil, errors.New(\"ovh: can't use multiple authentication systems (ApplicationKey, OAuth2)\")\n\t}\n\n\tif config.hasAppKeyAuth() && config.AccessToken != \"\" {\n\t\treturn nil, errors.New(\"ovh: can't use multiple authentication systems (ApplicationKey, Access Token)\")\n\t}\n\n\tclient, err := newClient(config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ovh: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]int64),\n\t}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ovh: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ovh: %w\", err)\n\t}\n\n\treqURL := fmt.Sprintf(\"/domain/zone/%s/record\", authZone)\n\treqData := Record{FieldType: \"TXT\", SubDomain: subDomain, Target: info.Value, TTL: d.config.TTL}\n\n\t// Create TXT record\n\tvar respData Record\n\n\terr = d.client.Post(reqURL, reqData, &respData)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ovh: error when call api to add record (%s): %w\", reqURL, err)\n\t}\n\n\t// Apply the change\n\treqURL = fmt.Sprintf(\"/domain/zone/%s/refresh\", authZone)\n\n\terr = d.client.Post(reqURL, nil, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ovh: error when call api to refresh zone (%s): %w\", reqURL, err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = respData.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// get the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"ovh: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ovh: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\treqURL := fmt.Sprintf(\"/domain/zone/%s/record/%d\", authZone, recordID)\n\n\terr = d.client.Delete(reqURL, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ovh: error when call OVH api to delete challenge record (%s): %w\", reqURL, err)\n\t}\n\n\t// Apply the change\n\treqURL = fmt.Sprintf(\"/domain/zone/%s/refresh\", authZone)\n\n\terr = d.client.Post(reqURL, nil, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ovh: error when call api to refresh zone (%s): %w\", reqURL, err)\n\t}\n\n\t// Delete record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc newClient(config *Config) (*ovh.Client, error) {\n\tvar (\n\t\tclient *ovh.Client\n\t\terr    error\n\t)\n\n\tswitch {\n\tcase config.hasAppKeyAuth():\n\t\tclient, err = ovh.NewClient(config.APIEndpoint, config.ApplicationKey, config.ApplicationSecret, config.ConsumerKey)\n\tcase config.OAuth2Config != nil:\n\t\tclient, err = ovh.NewOAuth2Client(config.APIEndpoint, config.OAuth2Config.ClientID, config.OAuth2Config.ClientSecret)\n\tcase config.AccessToken != \"\":\n\t\tclient, err = ovh.NewAccessTokenClient(config.APIEndpoint, config.AccessToken)\n\tdefault:\n\t\tclient, err = ovh.NewDefaultClient()\n\t}\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"new client: %w\", err)\n\t}\n\n\tclient.UserAgent = useragent.Get()\n\n\tif config.HTTPClient != nil {\n\t\tclient.Client = config.HTTPClient\n\t}\n\n\tclient.Client = clientdebug.Wrap(client.Client)\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "providers/dns/ovh/ovh.toml",
    "content": "Name = \"OVH\"\nDescription = ''''''\nURL = \"https://www.ovh.com/\"\nCode = \"ovh\"\nSince = \"v0.4.0\"\n\nExample = '''\n# Application Key authentication:\n\nOVH_APPLICATION_KEY=1234567898765432 \\\nOVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \\\nOVH_CONSUMER_KEY=256vfsd347245sdfg \\\nOVH_ENDPOINT=ovh-eu \\\nlego --dns ovh -d '*.example.com' -d example.com run\n\n# Or Access Token:\n\nOVH_ACCESS_TOKEN=xxx \\\nOVH_ENDPOINT=ovh-eu \\\nlego --dns ovh -d '*.example.com' -d example.com run\n\n# Or OAuth2:\n\nOVH_CLIENT_ID=yyy \\\nOVH_CLIENT_SECRET=xxx \\\nOVH_ENDPOINT=ovh-eu \\\nlego --dns ovh -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Application Key and Secret\n\nApplication key and secret can be created by following the [OVH guide](https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/).\n\nWhen requesting the consumer key, the following configuration can be used to define access rights:\n\n```json\n{\n  \"accessRules\": [\n    {\n      \"method\": \"POST\",\n      \"path\": \"/domain/zone/*\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/domain/zone/*\"\n    }\n  ]\n}\n```\n\n## OAuth2 Client Credentials\n\nAnother method for authentication is by using OAuth2 client credentials.\n\nAn IAM policy and service account can be created by following the [OVH guide](https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343).\n\nFollowing IAM policies need to be authorized for the affected domain:\n\n* dnsZone:apiovh:record/create\n* dnsZone:apiovh:record/delete\n* dnsZone:apiovh:refresh\n\n## Important Note\n\nBoth authentication methods cannot be used at the same time.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    OVH_ENDPOINT = \"Endpoint URL (ovh-eu or ovh-ca)\"\n    OVH_APPLICATION_KEY = \"Application key (Application Key authentication)\"\n    OVH_APPLICATION_SECRET = \"Application secret (Application Key authentication)\"\n    OVH_CONSUMER_KEY = \"Consumer key (Application Key authentication)\"\n    OVH_CLIENT_ID = \"Client ID (OAuth2)\"\n    OVH_CLIENT_SECRET = \"Client secret (OAuth2)\"\n    OVH_ACCESS_TOKEN = \"Access token\"\n  [Configuration.Additional]\n    OVH_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    OVH_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    OVH_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    OVH_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 180)\"\n\n[Links]\n  API = \"https://eu.api.ovh.com/\"\n  GoClient = \"https://github.com/ovh/go-ovh\"\n"
  },
  {
    "path": "providers/dns/ovh/ovh_test.go",
    "content": "package ovh\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvEndpoint,\n\tEnvApplicationKey,\n\tEnvApplicationSecret,\n\tEnvConsumerKey,\n\tEnvClientID,\n\tEnvClientSecret,\n\tEnvAccessToken).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"application key: success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint:          \"ovh-eu\",\n\t\t\t\tEnvApplicationKey:    \"B\",\n\t\t\t\tEnvApplicationSecret: \"C\",\n\t\t\t\tEnvConsumerKey:       \"D\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"application key: missing invalid endpoint\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint:          \"foobar\",\n\t\t\t\tEnvApplicationKey:    \"B\",\n\t\t\t\tEnvApplicationSecret: \"C\",\n\t\t\t\tEnvConsumerKey:       \"D\",\n\t\t\t},\n\t\t\texpected: \"ovh: new client: unknown endpoint 'foobar', consider checking 'Endpoints' list or using an URL\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"application key: missing application key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint:          \"ovh-eu\",\n\t\t\t\tEnvApplicationKey:    \"\",\n\t\t\t\tEnvApplicationSecret: \"C\",\n\t\t\t\tEnvConsumerKey:       \"D\",\n\t\t\t},\n\t\t\texpected: \"ovh: new client: invalid authentication config, both application_key and application_secret must be given\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"application key: missing application secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint:          \"ovh-eu\",\n\t\t\t\tEnvApplicationKey:    \"B\",\n\t\t\t\tEnvApplicationSecret: \"\",\n\t\t\t\tEnvConsumerKey:       \"D\",\n\t\t\t},\n\t\t\texpected: \"ovh: new client: invalid authentication config, both application_key and application_secret must be given\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"oauth2: success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint:     \"ovh-eu\",\n\t\t\t\tEnvClientID:     \"E\",\n\t\t\t\tEnvClientSecret: \"F\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"access token: success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint:    \"ovh-eu\",\n\t\t\t\tEnvAccessToken: \"G\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"oauth2: missing client secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint:     \"ovh-eu\",\n\t\t\t\tEnvClientID:     \"E\",\n\t\t\t\tEnvClientSecret: \"\",\n\t\t\t},\n\t\t\texpected: \"ovh: new client: invalid oauth2 config, both client_id and client_secret must be given\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"oauth2: missing client ID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint:     \"ovh-eu\",\n\t\t\t\tEnvClientID:     \"\",\n\t\t\t\tEnvClientSecret: \"F\",\n\t\t\t},\n\t\t\texpected: \"ovh: new client: invalid oauth2 config, both client_id and client_secret must be given\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint:          \"\",\n\t\t\t\tEnvApplicationKey:    \"\",\n\t\t\t\tEnvApplicationSecret: \"\",\n\t\t\t\tEnvConsumerKey:       \"\",\n\t\t\t\tEnvClientID:          \"\",\n\t\t\t\tEnvClientSecret:      \"\",\n\t\t\t\tEnvAccessToken:       \"\",\n\t\t\t},\n\t\t\texpected: \"ovh: new client: missing authentication information, you need to provide one of the following: application_key/application_secret, client_id/client_secret, or access_token\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"mixed auth (all)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint:          \"ovh-eu\",\n\t\t\t\tEnvApplicationKey:    \"B\",\n\t\t\t\tEnvApplicationSecret: \"C\",\n\t\t\t\tEnvConsumerKey:       \"D\",\n\t\t\t\tEnvClientID:          \"E\",\n\t\t\t\tEnvClientSecret:      \"F\",\n\t\t\t\tEnvAccessToken:       \"G\",\n\t\t\t},\n\t\t\texpected: \"ovh: can't use multiple authentication systems (ApplicationKey, OAuth2, Access Token)\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"mixed auth (ApplicationKey, OAuth2)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint:          \"ovh-eu\",\n\t\t\t\tEnvApplicationKey:    \"B\",\n\t\t\t\tEnvApplicationSecret: \"C\",\n\t\t\t\tEnvConsumerKey:       \"D\",\n\t\t\t\tEnvClientID:          \"E\",\n\t\t\t\tEnvClientSecret:      \"F\",\n\t\t\t},\n\t\t\texpected: \"ovh: can't use multiple authentication systems (ApplicationKey, OAuth2)\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"mixed auth (ApplicationKey, Access Token)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint:          \"ovh-eu\",\n\t\t\t\tEnvApplicationKey:    \"B\",\n\t\t\t\tEnvApplicationSecret: \"C\",\n\t\t\t\tEnvConsumerKey:       \"D\",\n\t\t\t\tEnvAccessToken:       \"G\",\n\t\t\t},\n\t\t\texpected: \"ovh: can't use multiple authentication systems (ApplicationKey, Access Token)\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"mixed auth (OAuth2, Access Token)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint:     \"ovh-eu\",\n\t\t\t\tEnvClientID:     \"E\",\n\t\t\t\tEnvClientSecret: \"F\",\n\t\t\t\tEnvAccessToken:  \"G\",\n\t\t\t},\n\t\t\texpected: \"ovh: can't use multiple authentication systems (OAuth2, Access Token)\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc              string\n\t\tapiEndpoint       string\n\t\tapplicationKey    string\n\t\tapplicationSecret string\n\t\tconsumerKey       string\n\t\tclientID          string\n\t\tclientSecret      string\n\t\taccessToken       string\n\t\texpected          string\n\t}{\n\t\t{\n\t\t\tdesc:              \"application key: success\",\n\t\t\tapiEndpoint:       \"ovh-eu\",\n\t\t\tapplicationKey:    \"B\",\n\t\t\tapplicationSecret: \"C\",\n\t\t\tconsumerKey:       \"D\",\n\t\t},\n\t\t{\n\t\t\tdesc:              \"application key: default api endpoint\",\n\t\t\tapiEndpoint:       \"\",\n\t\t\tapplicationKey:    \"B\",\n\t\t\tapplicationSecret: \"C\",\n\t\t\tconsumerKey:       \"D\",\n\t\t},\n\t\t{\n\t\t\tdesc:              \"application key: invalid api endpoint\",\n\t\t\tapiEndpoint:       \"foobar\",\n\t\t\tapplicationKey:    \"B\",\n\t\t\tapplicationSecret: \"C\",\n\t\t\tconsumerKey:       \"D\",\n\t\t\texpected:          \"ovh: new client: unknown endpoint 'foobar', consider checking 'Endpoints' list or using an URL\",\n\t\t},\n\t\t{\n\t\t\tdesc:              \"application key: missing application key\",\n\t\t\tapiEndpoint:       \"ovh-eu\",\n\t\t\tapplicationKey:    \"\",\n\t\t\tapplicationSecret: \"C\",\n\t\t\tconsumerKey:       \"D\",\n\t\t\texpected:          \"ovh: new client: invalid authentication config, both application_key and application_secret must be given\",\n\t\t},\n\t\t{\n\t\t\tdesc:              \"application key: missing application secret\",\n\t\t\tapiEndpoint:       \"ovh-eu\",\n\t\t\tapplicationKey:    \"B\",\n\t\t\tapplicationSecret: \"\",\n\t\t\tconsumerKey:       \"D\",\n\t\t\texpected:          \"ovh: new client: invalid authentication config, both application_key and application_secret must be given\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"oauth2: success\",\n\t\t\tapiEndpoint:  \"ovh-eu\",\n\t\t\tclientID:     \"B\",\n\t\t\tclientSecret: \"C\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"oauth2: default api endpoint\",\n\t\t\tapiEndpoint:  \"\",\n\t\t\tclientID:     \"B\",\n\t\t\tclientSecret: \"C\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"oauth2: invalid api endpoint\",\n\t\t\tapiEndpoint:  \"foobar\",\n\t\t\tclientID:     \"B\",\n\t\t\tclientSecret: \"C\",\n\t\t\texpected:     \"ovh: new client: unknown endpoint 'foobar', consider checking 'Endpoints' list or using an URL\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"oauth2: missing client id\",\n\t\t\tapiEndpoint:  \"ovh-eu\",\n\t\t\tclientID:     \"\",\n\t\t\tclientSecret: \"C\",\n\t\t\texpected:     \"ovh: new client: invalid oauth2 config, both client_id and client_secret must be given\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"oauth2: missing client secret\",\n\t\t\tapiEndpoint:  \"ovh-eu\",\n\t\t\tclientID:     \"B\",\n\t\t\tclientSecret: \"\",\n\t\t\texpected:     \"ovh: new client: invalid oauth2 config, both client_id and client_secret must be given\",\n\t\t},\n\t\t{\n\t\t\tdesc:        \"access token: success\",\n\t\t\tapiEndpoint: \"ovh-eu\",\n\t\t\taccessToken: \"G\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"ovh: new client: missing authentication information, you need to provide one of the following: application_key/application_secret, client_id/client_secret, or access_token\",\n\t\t},\n\t\t{\n\t\t\tdesc:              \"mixed auth (all)\",\n\t\t\tapiEndpoint:       \"ovh-eu\",\n\t\t\tapplicationKey:    \"B\",\n\t\t\tapplicationSecret: \"C\",\n\t\t\tconsumerKey:       \"D\",\n\t\t\tclientID:          \"B\",\n\t\t\tclientSecret:      \"C\",\n\t\t\taccessToken:       \"G\",\n\t\t\texpected:          \"ovh: can't use multiple authentication systems (ApplicationKey, OAuth2, Access Token)\",\n\t\t},\n\t\t{\n\t\t\tdesc:              \"mixed auth (ApplicationKey, OAuth2)\",\n\t\t\tapiEndpoint:       \"ovh-eu\",\n\t\t\tapplicationKey:    \"B\",\n\t\t\tapplicationSecret: \"C\",\n\t\t\tconsumerKey:       \"D\",\n\t\t\tclientID:          \"B\",\n\t\t\tclientSecret:      \"C\",\n\t\t\texpected:          \"ovh: can't use multiple authentication systems (ApplicationKey, OAuth2)\",\n\t\t},\n\t\t{\n\t\t\tdesc:              \"mixed auth (ApplicationKey, Access Token)\",\n\t\t\tapiEndpoint:       \"ovh-eu\",\n\t\t\tapplicationKey:    \"B\",\n\t\t\tapplicationSecret: \"C\",\n\t\t\tconsumerKey:       \"D\",\n\t\t\taccessToken:       \"G\",\n\t\t\texpected:          \"ovh: can't use multiple authentication systems (ApplicationKey, Access Token)\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"mixed auth (OAuth2, Access Token)\",\n\t\t\tapiEndpoint:  \"ovh-eu\",\n\t\t\tclientID:     \"B\",\n\t\t\tclientSecret: \"C\",\n\t\t\taccessToken:  \"G\",\n\t\t\texpected:     \"ovh: can't use multiple authentication systems (OAuth2, Access Token)\",\n\t\t},\n\t}\n\n\t// The OVH client use the same env vars than lego, so it requires to clean them.\n\tdefer envTest.RestoreEnv()\n\n\tenvTest.ClearEnv()\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIEndpoint = test.apiEndpoint\n\t\t\tconfig.ApplicationKey = test.applicationKey\n\t\t\tconfig.ApplicationSecret = test.applicationSecret\n\t\t\tconfig.ConsumerKey = test.consumerKey\n\t\t\tconfig.AccessToken = test.accessToken\n\n\t\t\tif test.clientID != \"\" || test.clientSecret != \"\" {\n\t\t\t\tconfig.OAuth2Config = &OAuth2Config{\n\t\t\t\t\tClientID:     test.clientID,\n\t\t\t\t\tClientSecret: test.clientSecret,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/pdns/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/miekg/dns\"\n)\n\n// APIKeyHeader API key header.\nconst APIKeyHeader = \"X-Api-Key\"\n\n// Client the PowerDNS API client.\ntype Client struct {\n\tserverName string\n\tapiKey     string\n\n\tapiVersion int\n\n\tHost       *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(host *url.URL, serverName string, apiVersion int, apiKey string) *Client {\n\treturn &Client{\n\t\tserverName: serverName,\n\t\tapiKey:     apiKey,\n\t\tapiVersion: apiVersion,\n\t\tHost:       host,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\nfunc (c *Client) APIVersion() int {\n\treturn c.apiVersion\n}\n\nfunc (c *Client) SetAPIVersion(ctx context.Context) error {\n\tvar err error\n\n\tc.apiVersion, err = c.getAPIVersion(ctx)\n\n\treturn err\n}\n\nfunc (c *Client) getAPIVersion(ctx context.Context) (int, error) {\n\tendpoint := c.joinPath(\"/\", \"api\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tresult, err := c.do(req)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvar versions []apiVersion\n\n\terr = json.Unmarshal(result, &versions)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tlatestVersion := 0\n\tfor _, v := range versions {\n\t\tif v.Version > latestVersion {\n\t\t\tlatestVersion = v.Version\n\t\t}\n\t}\n\n\treturn latestVersion, err\n}\n\nfunc (c *Client) GetHostedZone(ctx context.Context, authZone string) (*HostedZone, error) {\n\tendpoint := c.joinPath(\"/\", \"servers\", c.serverName, \"zones\", dns.Fqdn(authZone))\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult, err := c.do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar zone HostedZone\n\n\terr = json.Unmarshal(result, &zone)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// convert pre-v1 API result\n\tif len(zone.Records) > 0 {\n\t\tzone.RRSets = []RRSet{}\n\t\tfor _, record := range zone.Records {\n\t\t\tset := RRSet{\n\t\t\t\tName:    record.Name,\n\t\t\t\tType:    record.Type,\n\t\t\t\tRecords: []Record{record},\n\t\t\t}\n\t\t\tzone.RRSets = append(zone.RRSets, set)\n\t\t}\n\t}\n\n\treturn &zone, nil\n}\n\nfunc (c *Client) UpdateRecords(ctx context.Context, zone *HostedZone, sets RRSets) error {\n\tendpoint := c.joinPath(\"/\", \"servers\", c.serverName, \"zones\", zone.ID)\n\n\treq, err := newJSONRequest(ctx, http.MethodPatch, endpoint, sets)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = c.do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) Notify(ctx context.Context, zone *HostedZone) error {\n\tif c.apiVersion < 1 || zone.Kind != \"Master\" && zone.Kind != \"Slave\" {\n\t\treturn nil\n\t}\n\n\tendpoint := c.joinPath(\"/\", \"servers\", c.serverName, \"zones\", zone.ID, \"notify\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = c.do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) joinPath(elem ...string) *url.URL {\n\tp := path.Join(elem...)\n\n\tif p != \"/api\" && c.apiVersion > 0 && !strings.HasPrefix(p, \"/api/v\") {\n\t\tp = path.Join(\"/api\", \"v\"+strconv.Itoa(c.apiVersion), p)\n\t}\n\n\treturn c.Host.JoinPath(p)\n}\n\nfunc (c *Client) do(req *http.Request) (json.RawMessage, error) {\n\treq.Header.Set(APIKeyHeader, c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusUnprocessableEntity && (resp.StatusCode < 200 || resp.StatusCode >= 300) {\n\t\treturn nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tvar msg json.RawMessage\n\n\terr = json.NewDecoder(resp.Body).Decode(&msg)\n\tif err != nil {\n\t\tif errors.Is(err, io.EOF) {\n\t\t\t// empty body\n\t\t\treturn nil, nil\n\t\t}\n\t\t// other error\n\t\treturn nil, err\n\t}\n\n\t// check for PowerDNS error message\n\tif len(msg) > 0 && msg[0] == '{' {\n\t\tvar errInfo apiError\n\n\t\terr = json.Unmarshal(msg, &errInfo)\n\t\tif err != nil {\n\t\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, msg, err)\n\t\t}\n\n\t\tif errInfo.ShortMsg != \"\" {\n\t\t\treturn nil, fmt.Errorf(\"error talking to PDNS API: %w\", errInfo)\n\t\t}\n\t}\n\n\treturn msg, nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, strings.TrimSuffix(endpoint.String(), \"/\"), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\t// PowerDNS doesn't follow HTTP convention about the \"Content-Type\" header.\n\tif method != http.MethodGet && method != http.MethodDelete {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/pdns/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tserverURL, _ := url.Parse(server.URL)\n\n\t\t\tclient := NewClient(serverURL, \"server\", 0, \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().With(APIKeyHeader, \"secret\"))\n}\n\nfunc TestClient_joinPath(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc       string\n\t\tapiVersion int\n\t\tbaseURL    string\n\t\turi        string\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tdesc:       \"host with path\",\n\t\t\tapiVersion: 1,\n\t\t\tbaseURL:    \"https://example.com/test\",\n\t\t\turi:        \"/foo\",\n\t\t\texpected:   \"https://example.com/test/api/v1/foo\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"host with path + trailing slash\",\n\t\t\tapiVersion: 1,\n\t\t\tbaseURL:    \"https://example.com/test/\",\n\t\t\turi:        \"/foo\",\n\t\t\texpected:   \"https://example.com/test/api/v1/foo\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"no URI\",\n\t\t\tapiVersion: 1,\n\t\t\tbaseURL:    \"https://example.com/test\",\n\t\t\turi:        \"\",\n\t\t\texpected:   \"https://example.com/test/api/v1\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"host without path\",\n\t\t\tapiVersion: 1,\n\t\t\tbaseURL:    \"https://example.com\",\n\t\t\turi:        \"/foo\",\n\t\t\texpected:   \"https://example.com/api/v1/foo\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"api\",\n\t\t\tapiVersion: 1,\n\t\t\tbaseURL:    \"https://example.com\",\n\t\t\turi:        \"/api\",\n\t\t\texpected:   \"https://example.com/api\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"API version 0, host with path\",\n\t\t\tapiVersion: 0,\n\t\t\tbaseURL:    \"https://example.com/test\",\n\t\t\turi:        \"/foo\",\n\t\t\texpected:   \"https://example.com/test/foo\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"API version 0, host with path + trailing slash\",\n\t\t\tapiVersion: 0,\n\t\t\tbaseURL:    \"https://example.com/test/\",\n\t\t\turi:        \"/foo\",\n\t\t\texpected:   \"https://example.com/test/foo\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"API version 0, no URI\",\n\t\t\tapiVersion: 0,\n\t\t\tbaseURL:    \"https://example.com/test\",\n\t\t\turi:        \"\",\n\t\t\texpected:   \"https://example.com/test\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"API version 0, host without path\",\n\t\t\tapiVersion: 0,\n\t\t\tbaseURL:    \"https://example.com\",\n\t\t\turi:        \"/foo\",\n\t\t\texpected:   \"https://example.com/foo\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"API version 0, api\",\n\t\t\tapiVersion: 0,\n\t\t\tbaseURL:    \"https://example.com\",\n\t\t\turi:        \"/api\",\n\t\t\texpected:   \"https://example.com/api\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\thost, err := url.Parse(test.baseURL)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tclient := NewClient(host, \"test\", test.apiVersion, \"secret\")\n\n\t\t\tendpoint := client.joinPath(test.uri)\n\n\t\t\tassert.Equal(t, test.expected, endpoint.String())\n\t\t})\n\t}\n}\n\nfunc TestClient_GetHostedZone(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/v1/servers/server/zones/example.org.\",\n\t\t\tservermock.ResponseFromFixture(\"zone.json\")).\n\t\tBuild(t)\n\n\tclient.apiVersion = 1\n\n\tzone, err := client.GetHostedZone(t.Context(), \"example.org.\")\n\trequire.NoError(t, err)\n\n\texpected := &HostedZone{\n\t\tID:   \"example.org.\",\n\t\tName: \"example.org.\",\n\t\tURL:  \"api/v1/servers/localhost/zones/example.org.\",\n\t\tKind: \"Master\",\n\t\tRRSets: []RRSet{\n\t\t\t{\n\t\t\t\tName:    \"example.org.\",\n\t\t\t\tType:    \"NS\",\n\t\t\t\tRecords: []Record{{Content: \"ns2.example.org.\"}, {Content: \"ns1.example.org.\"}},\n\t\t\t\tTTL:     86400,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:    \"example.org.\",\n\t\t\t\tType:    \"SOA\",\n\t\t\t\tRecords: []Record{{Content: \"ns1.example.org. hostmaster.example.org. 2015120401 10800 15 604800 10800\"}},\n\t\t\t\tTTL:     86400,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:    \"ns1.example.org.\",\n\t\t\t\tType:    \"A\",\n\t\t\t\tRecords: []Record{{Content: \"192.168.0.1\"}},\n\t\t\t\tTTL:     86400,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:    \"www.example.org.\",\n\t\t\t\tType:    \"A\",\n\t\t\t\tRecords: []Record{{Content: \"192.168.0.2\"}},\n\t\t\t\tTTL:     86400,\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zone)\n}\n\nfunc TestClient_GetHostedZone_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/v1/servers/server/zones/example.org.\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnprocessableEntity)).\n\t\tBuild(t)\n\n\tclient.apiVersion = 1\n\n\t_, err := client.GetHostedZone(t.Context(), \"example.org.\")\n\trequire.ErrorAs(t, err, &apiError{})\n}\n\nfunc TestClient_GetHostedZone_v0(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /servers/server/zones/example.org.\",\n\t\t\tservermock.ResponseFromFixture(\"zone.json\")).\n\t\tBuild(t)\n\n\tclient.apiVersion = 0\n\n\tzone, err := client.GetHostedZone(t.Context(), \"example.org.\")\n\trequire.NoError(t, err)\n\n\texpected := &HostedZone{\n\t\tID:   \"example.org.\",\n\t\tName: \"example.org.\",\n\t\tURL:  \"api/v1/servers/localhost/zones/example.org.\",\n\t\tKind: \"Master\",\n\t\tRRSets: []RRSet{\n\t\t\t{\n\t\t\t\tName:    \"example.org.\",\n\t\t\t\tType:    \"NS\",\n\t\t\t\tRecords: []Record{{Content: \"ns2.example.org.\"}, {Content: \"ns1.example.org.\"}},\n\t\t\t\tTTL:     86400,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:    \"example.org.\",\n\t\t\t\tType:    \"SOA\",\n\t\t\t\tRecords: []Record{{Content: \"ns1.example.org. hostmaster.example.org. 2015120401 10800 15 604800 10800\"}},\n\t\t\t\tTTL:     86400,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:    \"ns1.example.org.\",\n\t\t\t\tType:    \"A\",\n\t\t\t\tRecords: []Record{{Content: \"192.168.0.1\"}},\n\t\t\t\tTTL:     86400,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:    \"www.example.org.\",\n\t\t\t\tType:    \"A\",\n\t\t\t\tRecords: []Record{{Content: \"192.168.0.2\"}},\n\t\t\t\tTTL:     86400,\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, zone)\n}\n\nfunc TestClient_UpdateRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PATCH /api/v1/servers/localhost/zones/example.org.\",\n\t\t\tservermock.ResponseFromFixture(\"zone.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"zone-request.json\")).\n\t\tBuild(t)\n\n\tclient.apiVersion = 1\n\tclient.serverName = \"localhost\"\n\n\tzone := &HostedZone{\n\t\tID:   \"example.org.\",\n\t\tName: \"example.org.\",\n\t\tURL:  \"api/v1/servers/localhost/zones/example.org.\",\n\t\tKind: \"Master\",\n\t}\n\n\trrSets := RRSets{\n\t\tRRSets: []RRSet{{\n\t\t\tName:       \"example.org.\",\n\t\t\tType:       \"NS\",\n\t\t\tChangeType: \"REPLACE\",\n\t\t\tRecords: []Record{{\n\t\t\t\tContent: \"192.0.2.5\",\n\t\t\t\tName:    \"ns1.example.org.\",\n\t\t\t\tTTL:     86400,\n\t\t\t\tType:    \"A\",\n\t\t\t}},\n\t\t}},\n\t}\n\n\terr := client.UpdateRecords(t.Context(), zone, rrSets)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_UpdateRecords_NonRootApi(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PATCH /some/path/api/v1/servers/localhost/zones/example.org.\",\n\t\t\tservermock.ResponseFromFixture(\"zone.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"zone-request.json\")).\n\t\tBuild(t)\n\n\tclient.Host = client.Host.JoinPath(\"some\", \"path\")\n\tclient.apiVersion = 1\n\tclient.serverName = \"localhost\"\n\n\tzone := &HostedZone{\n\t\tID:   \"example.org.\",\n\t\tName: \"example.org.\",\n\t\tURL:  \"some/path/api/v1/servers/server/zones/example.org.\",\n\t\tKind: \"Master\",\n\t}\n\n\trrSets := RRSets{\n\t\tRRSets: []RRSet{{\n\t\t\tName:       \"example.org.\",\n\t\t\tType:       \"NS\",\n\t\t\tChangeType: \"REPLACE\",\n\t\t\tRecords: []Record{{\n\t\t\t\tContent: \"192.0.2.5\",\n\t\t\t\tName:    \"ns1.example.org.\",\n\t\t\t\tTTL:     86400,\n\t\t\t\tType:    \"A\",\n\t\t\t}},\n\t\t}},\n\t}\n\n\terr := client.UpdateRecords(t.Context(), zone, rrSets)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_UpdateRecords_v0(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PATCH /servers/localhost/zones/example.org.\",\n\t\t\tservermock.ResponseFromFixture(\"zone.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"zone-request.json\")).\n\t\tBuild(t)\n\n\tclient.apiVersion = 0\n\tclient.serverName = \"localhost\"\n\n\tzone := &HostedZone{\n\t\tID:   \"example.org.\",\n\t\tName: \"example.org.\",\n\t\tURL:  \"servers/localhost/zones/example.org.\",\n\t\tKind: \"Master\",\n\t}\n\n\trrSets := RRSets{\n\t\tRRSets: []RRSet{{\n\t\t\tName:       \"example.org.\",\n\t\t\tType:       \"NS\",\n\t\t\tChangeType: \"REPLACE\",\n\t\t\tRecords: []Record{{\n\t\t\t\tContent: \"192.0.2.5\",\n\t\t\t\tName:    \"ns1.example.org.\",\n\t\t\t\tTTL:     86400,\n\t\t\t\tType:    \"A\",\n\t\t\t}},\n\t\t}},\n\t}\n\n\terr := client.UpdateRecords(t.Context(), zone, rrSets)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_Notify(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /api/v1/servers/localhost/zones/example.org./notify\", nil).\n\t\tBuild(t)\n\n\tclient.apiVersion = 1\n\tclient.serverName = \"localhost\"\n\n\tzone := &HostedZone{\n\t\tID:   \"example.org.\",\n\t\tName: \"example.org.\",\n\t\tURL:  \"api/v1/servers/localhost/zones/example.org.\",\n\t\tKind: \"Master\",\n\t}\n\n\terr := client.Notify(t.Context(), zone)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_Notify_NonRootApi(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /some/path/api/v1/servers/localhost/zones/example.org./notify\", nil).\n\t\tBuild(t)\n\n\tclient.Host = client.Host.JoinPath(\"some\", \"path\")\n\tclient.apiVersion = 1\n\tclient.serverName = \"localhost\"\n\n\tzone := &HostedZone{\n\t\tID:   \"example.org.\",\n\t\tName: \"example.org.\",\n\t\tURL:  \"/some/path/api/v1/servers/server/zones/example.org.\",\n\t\tKind: \"Master\",\n\t}\n\n\terr := client.Notify(t.Context(), zone)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_Notify_v0(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /some/path/api/v1/servers/localhost/zones/example.org./notify\", nil).\n\t\tBuild(t)\n\n\tclient.apiVersion = 0\n\n\tzone := &HostedZone{\n\t\tID:   \"example.org.\",\n\t\tName: \"example.org.\",\n\t\tURL:  \"servers/localhost/zones/example.org.\",\n\t\tKind: \"Master\",\n\t}\n\n\terr := client.Notify(t.Context(), zone)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_getAPIVersion(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api\",\n\t\t\tservermock.ResponseFromFixture(\"versions.json\")).\n\t\tBuild(t)\n\n\tversion, err := client.getAPIVersion(t.Context())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 4, version)\n}\n"
  },
  {
    "path": "providers/dns/pdns/internal/fixtures/error.json",
    "content": "{\n  \"error\": \"A human readable error message\"\n}\n"
  },
  {
    "path": "providers/dns/pdns/internal/fixtures/versions.json",
    "content": "[\n  {\n    \"url\": \"/fooa\",\n    \"version\": 0\n  },\n  {\n    \"url\": \"/foob\",\n    \"version\": 4\n  },\n  {\n    \"url\": \"/fooc\",\n    \"version\": 2\n  },\n  {\n    \"url\": \"/food\",\n    \"version\": 1\n  }\n]\n"
  },
  {
    "path": "providers/dns/pdns/internal/fixtures/zone-request.json",
    "content": "{\n  \"rrsets\": [\n    {\n      \"name\": \"example.org.\",\n      \"type\": \"NS\",\n      \"kind\": \"\",\n      \"changetype\": \"REPLACE\",\n      \"records\": [\n        {\n          \"content\": \"192.0.2.5\",\n          \"disabled\": false,\n          \"name\": \"ns1.example.org.\",\n          \"type\": \"A\",\n          \"ttl\": 86400\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/pdns/internal/fixtures/zone.json",
    "content": "{\n  \"id\": \"example.org.\",\n  \"url\": \"api/v1/servers/localhost/zones/example.org.\",\n  \"name\": \"example.org.\",\n  \"kind\": \"Master\",\n  \"dnssec\": false,\n  \"account\": \"\",\n  \"masters\": [],\n  \"serial\": 2015120401,\n  \"notified_serial\": 0,\n  \"last_check\": 0,\n  \"soa_edit_api\": \"\",\n  \"soa_edit\": \"\",\n  \"rrsets\": [\n    {\n      \"comments\": [],\n      \"name\": \"example.org.\",\n      \"records\": [\n        {\n          \"content\": \"ns2.example.org.\",\n          \"disabled\": false\n        },\n        {\n          \"content\": \"ns1.example.org.\",\n          \"disabled\": false\n        }\n      ],\n      \"ttl\": 86400,\n      \"type\": \"NS\"\n    },\n    {\n      \"comments\": [],\n      \"name\": \"example.org.\",\n      \"type\": \"SOA\",\n      \"ttl\": 86400,\n      \"records\": [\n        {\n          \"disabled\": false,\n          \"content\": \"ns1.example.org. hostmaster.example.org. 2015120401 10800 15 604800 10800\"\n        }\n      ]\n    },\n    {\n      \"comments\": [],\n      \"name\": \"ns1.example.org.\",\n      \"type\": \"A\",\n      \"ttl\": 86400,\n      \"records\": [\n        {\n          \"content\": \"192.168.0.1\",\n          \"disabled\": false\n        }\n      ]\n    },\n    {\n      \"comments\": [],\n      \"name\": \"www.example.org.\",\n      \"type\": \"A\",\n      \"ttl\": 86400,\n      \"records\": [\n        {\n          \"disabled\": false,\n          \"content\": \"192.168.0.2\"\n        }\n      ]\n    }\n  ]\n}\n\n"
  },
  {
    "path": "providers/dns/pdns/internal/types.go",
    "content": "package internal\n\ntype Record struct {\n\tContent  string `json:\"content\"`\n\tDisabled bool   `json:\"disabled\"`\n\n\t// pre-v1 API\n\tName string `json:\"name\"`\n\tType string `json:\"type\"`\n\tTTL  int    `json:\"ttl,omitempty\"`\n}\n\ntype HostedZone struct {\n\tID     string  `json:\"id\"`\n\tName   string  `json:\"name\"`\n\tURL    string  `json:\"url\"`\n\tKind   string  `json:\"kind\"`\n\tRRSets []RRSet `json:\"rrsets\"`\n\n\t// pre-v1 API\n\tRecords []Record `json:\"records\"`\n}\n\ntype RRSet struct {\n\tName       string   `json:\"name\"`\n\tType       string   `json:\"type\"`\n\tKind       string   `json:\"kind\"`\n\tChangeType string   `json:\"changetype\"`\n\tRecords    []Record `json:\"records,omitempty\"`\n\tTTL        int      `json:\"ttl,omitempty\"`\n}\n\ntype RRSets struct {\n\tRRSets []RRSet `json:\"rrsets\"`\n}\n\ntype apiError struct {\n\tShortMsg string `json:\"error\"`\n}\n\nfunc (a apiError) Error() string {\n\treturn a.ShortMsg\n}\n\ntype apiVersion struct {\n\tURL     string `json:\"url\"`\n\tVersion int    `json:\"version\"`\n}\n"
  },
  {
    "path": "providers/dns/pdns/pdns.go",
    "content": "// Package pdns implements a DNS provider for solving the DNS-01 challenge using PowerDNS nameserver.\npackage pdns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/pdns/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"PDNS_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\tEnvAPIURL = envNamespace + \"API_URL\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvAPIVersion         = envNamespace + \"API_VERSION\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n\tEnvServerName         = envNamespace + \"SERVER_NAME\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tHost               *url.URL\n\tServerName         string\n\tAPIVersion         int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tServerName:         env.GetOrDefaultString(EnvServerName, \"localhost\"),\n\t\tAPIVersion:         env.GetOrDefaultInt(EnvAPIVersion, 0),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for pdns.\n// Credentials must be passed in the environment variable:\n// PDNS_API_URL and PDNS_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey, EnvAPIURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"pdns: %w\", err)\n\t}\n\n\thostURL, err := url.Parse(values[EnvAPIURL])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"pdns: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Host = hostURL\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for pdns.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"pdns: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"pdns: API key missing\")\n\t}\n\n\tif config.Host == nil || config.Host.Host == \"\" {\n\t\treturn nil, errors.New(\"pdns: API URL missing\")\n\t}\n\n\tclient := internal.NewClient(config.Host, config.ServerName, config.APIVersion, config.APIKey)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\tif config.APIVersion <= 0 {\n\t\terr := client.SetAPIVersion(context.Background())\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"pdns: failed to get API version %v\", err)\n\t\t}\n\t}\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pdns: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzone, err := d.client.GetHostedZone(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pdns: get hosted zone for %s: %w\", authZone, err)\n\t}\n\n\tname := info.EffectiveFQDN\n\tif d.client.APIVersion() == 0 {\n\t\t// pre-v1 API wants non-fqdn\n\t\tname = dns01.UnFqdn(info.EffectiveFQDN)\n\t}\n\n\t// Look for existing records.\n\texistingRRSet := findTxtRecord(zone, info.EffectiveFQDN)\n\n\tvar records []internal.Record\n\tif existingRRSet != nil {\n\t\trecords = existingRRSet.Records\n\t}\n\n\trecords = append(records, internal.Record{\n\t\tContent:  strconv.Quote(info.Value),\n\t\tDisabled: false,\n\n\t\t// pre-v1 API\n\t\tType: \"TXT\",\n\t\tName: name,\n\t\tTTL:  d.config.TTL,\n\t})\n\n\trrSets := internal.RRSets{\n\t\tRRSets: []internal.RRSet{{\n\t\t\tName:       name,\n\t\t\tChangeType: \"REPLACE\",\n\t\t\tType:       \"TXT\",\n\t\t\tKind:       \"Master\",\n\t\t\tTTL:        d.config.TTL,\n\t\t\tRecords:    records,\n\t\t}},\n\t}\n\n\terr = d.client.UpdateRecords(ctx, zone, rrSets)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pdns: update records: %w\", err)\n\t}\n\n\terr = d.client.Notify(ctx, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pdns: notify: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pdns: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tzone, err := d.client.GetHostedZone(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pdns: get hosted zone for %s: %w\", authZone, err)\n\t}\n\n\t// Look for existing records.\n\tset := findTxtRecord(zone, info.EffectiveFQDN)\n\tif set == nil {\n\t\treturn fmt.Errorf(\"pdns: no existing record found for %s\", info.EffectiveFQDN)\n\t}\n\n\tvar records []internal.Record\n\n\tfor _, r := range set.Records {\n\t\tif r.Content != strconv.Quote(info.Value) {\n\t\t\trecords = append(records, r)\n\t\t}\n\t}\n\n\trrSet := internal.RRSet{\n\t\tName: set.Name,\n\t\tType: set.Type,\n\t}\n\n\tif len(records) > 0 {\n\t\trrSet.ChangeType = \"REPLACE\"\n\t\trrSet.TTL = d.config.TTL\n\t\trrSet.Records = records\n\t} else {\n\t\trrSet.ChangeType = \"DELETE\"\n\t}\n\n\terr = d.client.UpdateRecords(ctx, zone, internal.RRSets{RRSets: []internal.RRSet{rrSet}})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pdns: update records: %w\", err)\n\t}\n\n\terr = d.client.Notify(ctx, zone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"pdns: notify: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc findTxtRecord(zone *internal.HostedZone, fqdn string) *internal.RRSet {\n\tfor _, set := range zone.RRSets {\n\t\tif set.Type == \"TXT\" && (set.Name == dns01.UnFqdn(fqdn) || set.Name == fqdn) {\n\t\t\treturn &set\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/pdns/pdns.toml",
    "content": "Name = \"PowerDNS\"\nDescription = ''''''\nURL = \"https://www.powerdns.com/\"\nCode = \"pdns\"\nSince = \"v0.4.0\"\n\nExample = '''\nPDNS_API_URL=http://pdns-server:80/ \\\nPDNS_API_KEY=xxxx \\\nlego --dns pdns -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Information\n\nTested and confirmed to work with PowerDNS authoritative server 3.4.8 and 4.0.1. Refer to [PowerDNS documentation](https://doc.powerdns.com/md/httpapi/README/) instructions on how to enable the built-in API interface.\n\nPowerDNS Notes:\n- PowerDNS API does not currently support SSL, therefore you should take care to ensure that traffic between lego and the PowerDNS API is over a trusted network, VPN etc.\n- In order to have the SOA serial automatically increment each time the `_acme-challenge` record is added/modified via the API, set `SOA-EDIT-API` to `INCEPTION-INCREMENT` for the zone in the `domainmetadata` table\n- Some PowerDNS servers doesn't have root API endpoints enabled and API version autodetection will not work. In that case version number can be defined using `PDNS_API_VERSION`.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    PDNS_API_KEY = \"API key\"\n    PDNS_API_URL = \"API URL\"\n  [Configuration.Additional]\n    PDNS_SERVER_NAME = \"Name of the server in the URL, 'localhost' by default\"\n    PDNS_API_VERSION = \"Skip API version autodetection and use the provided version number.\"\n    PDNS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    PDNS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    PDNS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    PDNS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://doc.powerdns.com/md/httpapi/README/\"\n"
  },
  {
    "path": "providers/dns/pdns/pdns_test.go",
    "content": "package pdns\n\nimport (\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIURL,\n\tEnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t\tEnvAPIURL: \"http://example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t\tEnvAPIURL: \"\",\n\t\t\t},\n\t\t\texpected: \"pdns: some credentials information are missing: PDNS_API_KEY,PDNS_API_URL\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t\tEnvAPIURL: \"http://example.com\",\n\t\t\t},\n\t\t\texpected: \"pdns: some credentials information are missing: PDNS_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API URL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t\tEnvAPIURL: \"\",\n\t\t\t},\n\t\t\texpected: \"pdns: some credentials information are missing: PDNS_API_URL\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc             string\n\t\tapiKey           string\n\t\tcustomAPIVersion int\n\t\thost             *url.URL\n\t\texpected         string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"123\",\n\t\t\thost:   mustParse(\"http://example.com\"),\n\t\t},\n\t\t{\n\t\t\tdesc:             \"success custom API version\",\n\t\t\tapiKey:           \"123\",\n\t\t\tcustomAPIVersion: 1,\n\t\t\thost:             mustParse(\"http://example.com\"),\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"pdns: API key missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\tapiKey:   \"\",\n\t\t\thost:     mustParse(\"http://example.com\"),\n\t\t\texpected: \"pdns: API key missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing host\",\n\t\t\tapiKey:   \"123\",\n\t\t\texpected: \"pdns: API URL missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.Host = test.host\n\t\t\tconfig.APIVersion = test.customAPIVersion\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresentAndCleanup(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123e==\")\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123e==\")\n\trequire.NoError(t, err)\n}\n\nfunc mustParse(rawURL string) *url.URL {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn u\n}\n"
  },
  {
    "path": "providers/dns/plesk/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// Client the Plesk API client.\ntype Client struct {\n\tlogin    string\n\tpassword string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient created a new Client.\nfunc NewClient(baseURL *url.URL, login, password string) *Client {\n\treturn &Client{\n\t\tlogin:      login,\n\t\tpassword:   password,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}\n}\n\n// GetSite gets a site.\n// https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference/managing-sites-domains/getting-information-about-sites.66583/\nfunc (c *Client) GetSite(ctx context.Context, domain string) (int, error) {\n\tpayload := RequestPacketType{Site: &SiteTypeRequest{Get: SiteGetRequest{Filter: &SiteFilterType{\n\t\tName: domain,\n\t}}}}\n\n\tresponse, err := c.doRequest(ctx, payload)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif response.System != nil {\n\t\treturn 0, response.System\n\t}\n\n\tif response == nil || response.Site.Get.Result == nil {\n\t\treturn 0, errors.New(\"unexpected empty result\")\n\t}\n\n\tif response.Site.Get.Result.Status != StatusOK {\n\t\treturn 0, response.Site.Get.Result\n\t}\n\n\treturn response.Site.Get.Result.ID, nil\n}\n\n// AddRecord adds a TXT record.\n// https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference/managing-dns/managing-dns-records/adding-dns-record.34798/\nfunc (c *Client) AddRecord(ctx context.Context, siteID int, host, value string) (int, error) {\n\tpayload := RequestPacketType{DNS: &DNSInputType{AddRec: []AddRecRequest{{\n\t\tSiteID: siteID,\n\t\tType:   \"TXT\",\n\t\tHost:   host,\n\t\tValue:  value,\n\t}}}}\n\n\tresponse, err := c.doRequest(ctx, payload)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif response.System != nil {\n\t\treturn 0, response.System\n\t}\n\n\tif len(response.DNS.AddRec) < 1 {\n\t\treturn 0, errors.New(\"unexpected empty result\")\n\t}\n\n\tif response.DNS.AddRec[0].Result.Status != StatusOK {\n\t\treturn 0, response.DNS.AddRec[0].Result\n\t}\n\n\treturn response.DNS.AddRec[0].Result.ID, nil\n}\n\n// DeleteRecord Deletes a TXT record.\n// https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference/managing-dns/managing-dns-records/deleting-dns-records.34864/\nfunc (c *Client) DeleteRecord(ctx context.Context, recordID int) (int, error) {\n\tpayload := RequestPacketType{DNS: &DNSInputType{DelRec: []DelRecRequest{{Filter: DNSSelectionFilterType{\n\t\tID: recordID,\n\t}}}}}\n\n\tresponse, err := c.doRequest(ctx, payload)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif response.System != nil {\n\t\treturn 0, response.System\n\t}\n\n\tif len(response.DNS.DelRec) < 1 {\n\t\treturn 0, errors.New(\"unexpected empty result\")\n\t}\n\n\tif response.DNS.DelRec[0].Result.Status != StatusOK {\n\t\treturn 0, response.DNS.DelRec[0].Result\n\t}\n\n\treturn response.DNS.DelRec[0].Result.ID, nil\n}\n\nfunc (c *Client) doRequest(ctx context.Context, payload RequestPacketType) (*ResponsePacketType, error) {\n\tendpoint := c.baseURL.JoinPath(\"/enterprise/control/agent.php\")\n\n\tbody := new(bytes.Buffer)\n\n\terr := xml.NewEncoder(body).Encode(payload)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"text/xml\")\n\n\treq.Header.Set(\"Http_auth_login\", c.login)\n\treq.Header.Set(\"Http_auth_passwd\", c.password)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar response ResponsePacketType\n\n\terr = xml.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn &response, nil\n}\n"
  },
  {
    "path": "providers/dns/plesk/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tserverURL, _ := url.Parse(server.URL)\n\n\t\t\tclient := NewClient(serverURL, \"user\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithContentType(\"text/xml\").\n\t\t\tWith(\"Http_auth_login\", \"user\").\n\t\t\tWith(\"Http_auth_passwd\", \"secret\"),\n\t)\n}\n\nfunc TestClient_GetSite(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /enterprise/control/agent.php\", servermock.ResponseFromFixture(\"get-site.xml\")).\n\t\tBuild(t)\n\n\tsiteID, err := client.GetSite(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 82, siteID)\n}\n\nfunc TestClient_GetSite_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /enterprise/control/agent.php\", servermock.ResponseFromFixture(\"get-site-error.xml\")).\n\t\tBuild(t)\n\n\tsiteID, err := client.GetSite(t.Context(), \"example.com\")\n\trequire.Error(t, err)\n\n\tassert.Equal(t, 0, siteID)\n}\n\nfunc TestClient_GetSite_system_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /enterprise/control/agent.php\", servermock.ResponseFromFixture(\"global-error.xml\")).\n\t\tBuild(t)\n\n\tsiteID, err := client.GetSite(t.Context(), \"example.com\")\n\trequire.Error(t, err)\n\n\tassert.Equal(t, 0, siteID)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /enterprise/control/agent.php\", servermock.ResponseFromFixture(\"add-record.xml\")).\n\t\tBuild(t)\n\n\trecordID, err := client.AddRecord(t.Context(), 123, \"_acme-challenge.example.com\", \"txtTXTtxt\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 4537, recordID)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /enterprise/control/agent.php\", servermock.ResponseFromFixture(\"add-record-error.xml\")).\n\t\tBuild(t)\n\n\trecordID, err := client.AddRecord(t.Context(), 123, \"_acme-challenge.example.com\", \"txtTXTtxt\")\n\trequire.ErrorAs(t, err, new(RecResult))\n\n\tassert.Equal(t, 0, recordID)\n}\n\nfunc TestClient_AddRecord_system_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /enterprise/control/agent.php\", servermock.ResponseFromFixture(\"global-error.xml\")).\n\t\tBuild(t)\n\n\trecordID, err := client.AddRecord(t.Context(), 123, \"_acme-challenge.example.com\", \"txtTXTtxt\")\n\trequire.ErrorAs(t, err, new(*System))\n\n\tassert.Equal(t, 0, recordID)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /enterprise/control/agent.php\", servermock.ResponseFromFixture(\"delete-record.xml\")).\n\t\tBuild(t)\n\n\trecordID, err := client.DeleteRecord(t.Context(), 4537)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 4537, recordID)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /enterprise/control/agent.php\", servermock.ResponseFromFixture(\"delete-record-error.xml\")).\n\t\tBuild(t)\n\n\trecordID, err := client.DeleteRecord(t.Context(), 4537)\n\trequire.ErrorAs(t, err, new(RecResult))\n\n\tassert.Equal(t, 0, recordID)\n}\n\nfunc TestClient_DeleteRecord_system_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /enterprise/control/agent.php\", servermock.ResponseFromFixture(\"global-error.xml\")).\n\t\tBuild(t)\n\n\trecordID, err := client.DeleteRecord(t.Context(), 4537)\n\trequire.ErrorAs(t, err, new(*System))\n\n\tassert.Equal(t, 0, recordID)\n}\n"
  },
  {
    "path": "providers/dns/plesk/internal/fixtures/add-record-error.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<packet version=\"1.6.9.1\">\n    <dns>\n        <add_rec>\n            <result>\n                <status>error</status>\n                <errcode>1015</errcode>\n                <errtext>Domain does not exist.</errtext>\n            </result>\n        </add_rec>\n    </dns>\n</packet>\n"
  },
  {
    "path": "providers/dns/plesk/internal/fixtures/add-record.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<packet version=\"1.6.9.1\">\n    <dns>\n        <add_rec>\n            <result>\n                <status>ok</status>\n                <id>4537</id>\n            </result>\n        </add_rec>\n    </dns>\n</packet>\n"
  },
  {
    "path": "providers/dns/plesk/internal/fixtures/delete-record-error.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<packet version=\"1.6.9.1\">\n    <dns>\n        <del_rec>\n            <result>\n                <status>error</status>\n                <errcode>1013</errcode>\n                <errtext>Record does not exist</errtext>\n                <id>453899</id>\n            </result>\n        </del_rec>\n    </dns>\n</packet>\n"
  },
  {
    "path": "providers/dns/plesk/internal/fixtures/delete-record.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<packet version=\"1.6.9.1\">\n    <dns>\n        <del_rec>\n            <result>\n                <status>ok</status>\n                <id>4537</id>\n            </result>\n        </del_rec>\n    </dns>\n</packet>\n"
  },
  {
    "path": "providers/dns/plesk/internal/fixtures/get-site-error.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<packet version=\"1.6.9.1\">\n    <site>\n        <get>\n            <result>\n                <status>error</status>\n                <errcode>1013</errcode>\n                <errtext>Site does not exist</errtext>\n                <filter-id>bollox.com</filter-id>\n            </result>\n        </get>\n    </site>\n</packet>\n"
  },
  {
    "path": "providers/dns/plesk/internal/fixtures/get-site.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<packet version=\"1.6.9.1\">\n    <site>\n        <get>\n            <result>\n                <status>ok</status>\n                <filter-id>example.com</filter-id>\n                <id>82</id>\n                <data>\n                    <gen_info>\n                        <cr_date>2022-12-31</cr_date>\n                        <name>example.com</name>\n                        <ascii-name>example.com</ascii-name>\n                        <status>0</status>\n                        <real_size>2717782016</real_size>\n                        <dns_ip_address>217.28.1.1</dns_ip_address>\n                        <htype>vrt_hst</htype>\n                        <guid>e9114a63-e626-4977-ac15-a8e608750a33</guid>\n                        <webspace-guid>e9114a63-e626-4977-ac15-a8e608750a33</webspace-guid>\n                        <sb-site-uuid></sb-site-uuid>\n                        <webspace-id>82</webspace-id>\n                        <description></description>\n                    </gen_info>\n                </data>\n            </result>\n        </get>\n    </site>\n</packet>\n"
  },
  {
    "path": "providers/dns/plesk/internal/fixtures/global-error.xml",
    "content": "<?xml version=\"1.0\"?>\n<packet version=\"1.6.9.1\">\n    <system>\n        <status>error</status>\n        <errcode>1001</errcode>\n        <errtext>You have entered incorrect username or password.</errtext>\n    </system>\n</packet>\n"
  },
  {
    "path": "providers/dns/plesk/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"encoding/xml\"\n\t\"fmt\"\n)\n\n// Response status.\nconst (\n\tStatusOK    = \"ok\"\n\tStatusError = \"error\"\n)\n\n// Request.\n\ntype RequestPacketType struct {\n\tXMLName xml.Name `xml:\"packet\"`\n\tText    string   `xml:\",chardata\"`\n\n\tDNS  *DNSInputType    `xml:\"dns,omitempty\"`\n\tSite *SiteTypeRequest `xml:\"site,omitempty\"`\n}\n\ntype DNSInputType struct {\n\tText string `xml:\",chardata\"`\n\n\tAddRec []AddRecRequest `xml:\"add_rec,omitempty\"`\n\tDelRec []DelRecRequest `xml:\"del_rec,omitempty\"`\n}\n\ntype AddRecRequest struct {\n\tText string `xml:\",chardata\"`\n\n\tSiteID int    `xml:\"site-id,omitempty\"`\n\tType   string `xml:\"type,omitempty\"`\n\tHost   string `xml:\"host,omitempty\"`\n\tValue  string `xml:\"value,omitempty\"`\n}\n\ntype DelRecRequest struct {\n\tText string `xml:\",chardata\"`\n\n\tFilter DNSSelectionFilterType `xml:\"filter\"`\n}\n\ntype DNSSelectionFilterType struct {\n\tText string `xml:\",chardata\"`\n\n\tID int `xml:\"id\"`\n}\n\ntype SiteTypeRequest struct {\n\tText string `xml:\",chardata\"`\n\n\tGet SiteGetRequest `xml:\"get\"`\n}\n\ntype SiteGetRequest struct {\n\tText string `xml:\",chardata\"`\n\n\tFilter  *SiteFilterType `xml:\"filter,omitempty\"`\n\tDataset SiteDatasetType `xml:\"dataset,omitempty\"`\n}\n\ntype SiteFilterType struct {\n\tText string `xml:\",chardata\"`\n\n\tName string `xml:\"name\"`\n}\n\ntype SiteDatasetType struct {\n\tText string `xml:\",chardata\"`\n\n\tGenInfo *SiteGenInfoType `xml:\"gen_info,omitempty\"`\n}\n\ntype SiteGenInfoType struct {\n\tText string `xml:\",chardata\"`\n\n\tCrDate       string `xml:\"cr_date,omitempty\"`\n\tName         string `xml:\"name,omitempty\"`\n\tASCIIName    string `xml:\"ascii-name,omitempty\"`\n\tStatus       string `xml:\"status,omitempty\"`\n\tRealSize     string `xml:\"real_size,omitempty\"`\n\tDNSIPAddress string `xml:\"dns_ip_address,omitempty\"`\n\tHType        string `xml:\"htype,omitempty\"`\n\tGUID         string `xml:\"guid,omitempty\"`\n\tWebspaceGUID string `xml:\"webspace-guid,omitempty\"`\n\tSbSiteUUID   string `xml:\"sb-site-uuid,omitempty\"`\n\tWebspaceID   string `xml:\"webspace-id,omitempty\"`\n\tDescription  string `xml:\"description,omitempty\"`\n}\n\n// Response.\n\ntype ResponsePacketType struct {\n\tXMLName xml.Name `xml:\"packet\"`\n\tText    string   `xml:\",chardata\"`\n\n\tDNS    DNSResponseType  `xml:\"dns,omitempty\"`\n\tSite   SiteResponseType `xml:\"site,omitempty\"`\n\tSystem *System          `xml:\"system,omitempty\"`\n}\n\ntype System struct {\n\tText string `xml:\",chardata\"`\n\n\tStatus  string `xml:\"status\"`\n\tErrCode string `xml:\"errcode\"`\n\tErrText string `xml:\"errtext\"`\n}\n\nfunc (s System) Error() string {\n\treturn fmt.Sprintf(\"%s: %s - %s\", s.Status, s.ErrCode, s.ErrText)\n}\n\ntype DNSResponseType struct {\n\tText string `xml:\",chardata\"`\n\n\tAddRec []AddRecResponse `xml:\"add_rec,omitempty\"`\n\tDelRec []DelRecResponse `xml:\"del_rec,omitempty\"`\n}\n\ntype AddRecResponse struct {\n\tText string `xml:\",chardata\"`\n\n\tResult RecResult `xml:\"result,omitempty\"`\n}\n\ntype DelRecResponse struct {\n\tText string `xml:\",chardata\"`\n\n\tResult RecResult `xml:\"result\"`\n}\n\ntype RecResult struct {\n\tText string `xml:\",chardata\"`\n\n\tID int `xml:\"id\"`\n\n\tStatus  string `xml:\"status\"`\n\tErrCode string `xml:\"errcode\"`\n\tErrText string `xml:\"errtext\"`\n}\n\nfunc (r RecResult) Error() string {\n\treturn fmt.Sprintf(\"%s: %s - %s\", r.Status, r.ErrCode, r.ErrText)\n}\n\ntype SiteResponseType struct {\n\tText string `xml:\",chardata\"`\n\n\tGet SiteGetResponse `xml:\"get\"`\n}\n\ntype SiteGetResponse struct {\n\tText string `xml:\",chardata\"`\n\n\tResult *SiteResult `xml:\"result,omitempty\"`\n}\n\ntype SiteResult struct {\n\tText string `xml:\",chardata\"`\n\n\tID       int    `xml:\"id\"`\n\tFilterID string `xml:\"filter-id\"`\n\n\tStatus  string `xml:\"status\"`\n\tErrCode string `xml:\"errcode\"`\n\tErrText string `xml:\"errtext\"`\n\n\tData *SiteResultData `xml:\"data\"`\n}\n\nfunc (s SiteResult) Error() string {\n\treturn fmt.Sprintf(\"%s: %s - %s\", s.Status, s.ErrCode, s.ErrText)\n}\n\ntype SiteResultData struct {\n\tText string `xml:\",chardata\"`\n\n\tGenInfo *SiteGenInfoType `xml:\"gen_info\"`\n}\n"
  },
  {
    "path": "providers/dns/plesk/plesk.go",
    "content": "// Package plesk implements a DNS provider for solving the DNS-01 challenge using Plesk DNS.\npackage plesk\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/plesk/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"PLESK_\"\n\n\tEnvServerBaseURL = envNamespace + \"SERVER_BASE_URL\"\n\tEnvUsername      = envNamespace + \"USERNAME\"\n\tEnvPassword      = envNamespace + \"PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tbaseURL  string\n\tUsername string\n\tPassword string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]int\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Plesk.\n// Credentials must be passed in the environment variables:\n// PLESK_USERNAME and PLESK_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvServerBaseURL, EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"plesk: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.baseURL = values[EnvServerBaseURL]\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Plesk.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"plesk: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.baseURL == \"\" {\n\t\treturn nil, errors.New(\"plesk: missing server base URL\")\n\t}\n\n\tbaseURL, err := url.Parse(config.baseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"plesk: failed to parse base URL (%s): %w\", config.baseURL, err)\n\t}\n\n\tif config.Username == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"plesk: incomplete credentials, missing username and/or password\")\n\t}\n\n\tclient := internal.NewClient(baseURL, config.Username, config.Password)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: map[string]int{},\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"plesk: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tsiteID, err := d.client.GetSite(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"plesk: failed to get site: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"nodion: %w\", err)\n\t}\n\n\trecordID, err := d.client.AddRecord(ctx, siteID, subDomain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"plesk: failed to add record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = recordID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"plesk: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\t_, err := d.client.DeleteRecord(context.Background(), recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"plesk: failed to delete record (%d): %w\", recordID, err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/plesk/plesk.toml",
    "content": "Name = \"plesk.com\"\nDescription = ''''''\nURL = \"https://www.plesk.com/\"\nCode = \"plesk\"\nSince = \"v4.11.0\"\n\nExample = '''\nPLESK_SERVER_BASE_URL=\"https://plesk.myserver.com:8443\" \\\nPLESK_USERNAME=xxxxxx \\\nPLESK_PASSWORD=yyyyyy \\\nlego --dns plesk -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    PLESK_SERVER_BASE_URL = \"Base URL of the server (ex: https://plesk.myserver.com:8443)\"\n    PLESK_USERNAME = \"API username\"\n    PLESK_PASSWORD = \"API password\"\n  [Configuration.Additional]\n    PLESK_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    PLESK_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    PLESK_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    PLESK_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference.28784/\"\n"
  },
  {
    "path": "providers/dns/plesk/plesk_test.go",
    "content": "package plesk\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvServerBaseURL,\n\tEnvUsername,\n\tEnvPassword).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerBaseURL: \"https//example.com\",\n\t\t\t\tEnvUsername:      \"user\",\n\t\t\t\tEnvPassword:      \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing server base URL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerBaseURL: \"\",\n\t\t\t\tEnvUsername:      \"user\",\n\t\t\t\tEnvPassword:      \"secret\",\n\t\t\t},\n\t\t\texpected: \"plesk: some credentials information are missing: PLESK_SERVER_BASE_URL\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerBaseURL: \"https//example.com\",\n\t\t\t\tEnvUsername:      \"\",\n\t\t\t\tEnvPassword:      \"secret\",\n\t\t\t},\n\t\t\texpected: \"plesk: some credentials information are missing: PLESK_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerBaseURL: \"https//example.com\",\n\t\t\t\tEnvUsername:      \"user\",\n\t\t\t\tEnvPassword:      \"\",\n\t\t\t},\n\t\t\texpected: \"plesk: some credentials information are missing: PLESK_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"plesk: some credentials information are missing: PLESK_SERVER_BASE_URL,PLESK_USERNAME,PLESK_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tbaseURL  string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing base URL\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"plesk: missing server base URL\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"plesk: incomplete credentials, missing username and/or password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\tusername: \"user\",\n\t\t\texpected: \"plesk: incomplete credentials, missing username and/or password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credential\",\n\t\t\tbaseURL:  \"https://example.com\",\n\t\t\texpected: \"plesk: incomplete credentials, missing username and/or password\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.baseURL = test.baseURL\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/porkbun/porkbun.go",
    "content": "// Package porkbun implements a DNS provider for solving the DNS-01 challenge using Porkbun.\npackage porkbun\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/nrdcg/porkbun\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"PORKBUN_\"\n\n\tEnvSecretAPIKey = envNamespace + \"SECRET_API_KEY\"\n\tEnvAPIKey       = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst minTTL = 300\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tSecretAPIKey       string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *porkbun.Client\n\n\trecordIDs   map[string]int\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Porkbun.\n// Credentials must be passed in the environment variables:\n// PORKBUN_SECRET_API_KEY, PORKBUN_PAPI_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvSecretAPIKey, EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"porkbun: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.SecretAPIKey = values[EnvSecretAPIKey]\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Porkbun.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"porkbun: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.SecretAPIKey == \"\" || config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"porkbun: some credentials information are missing\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"porkbun: invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\tclient := porkbun.New(config.SecretAPIKey, config.APIKey)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]int),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzoneName, hostName, err := splitDomain(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"porkbun: %w\", err)\n\t}\n\n\trecord := porkbun.Record{\n\t\tName:    hostName,\n\t\tType:    \"TXT\",\n\t\tContent: info.Value,\n\t\tTTL:     strconv.Itoa(d.config.TTL),\n\t}\n\n\tctx := context.Background()\n\n\trecordID, err := d.client.CreateRecord(ctx, dns01.UnFqdn(zoneName), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"porkbun: failed to create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = recordID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// gets the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"porkbun: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\tzoneName, _, err := splitDomain(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"porkbun: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\terr = d.client.DeleteRecord(ctx, dns01.UnFqdn(zoneName), recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"porkbun: failed to delete record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// splitDomain splits the hostname from the authoritative zone, and returns both parts.\nfunc splitDomain(fqdn string) (string, string, error) {\n\tzone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, zone)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\treturn zone, subDomain, nil\n}\n"
  },
  {
    "path": "providers/dns/porkbun/porkbun.toml",
    "content": "Name = \"Porkbun\"\nDescription = ''''''\n# This URL is NOT the API URL.\nURL = \"https://porkbun.com/\"\nCode = \"porkbun\"\nSince = \"v4.4.0\"\n\nExample = '''\nPORKBUN_SECRET_API_KEY=xxxxxx \\\nPORKBUN_API_KEY=yyyyyy \\\nlego --dns porkbun -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    PORKBUN_SECRET_API_KEY = \"secret API key\"\n    PORKBUN_API_KEY = \"API key\"\n  [Configuration.Additional]\n    PORKBUN_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    PORKBUN_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 600)\"\n    PORKBUN_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    PORKBUN_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://porkbun.com/api/json/v3/documentation\"\n"
  },
  {
    "path": "providers/dns/porkbun/porkbun_test.go",
    "content": "package porkbun\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvSecretAPIKey, EnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretAPIKey: \"secret\",\n\t\t\t\tEnvAPIKey:       \"key\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretAPIKey: \"\",\n\t\t\t\tEnvAPIKey:       \"key\",\n\t\t\t},\n\t\t\texpected: \"porkbun: some credentials information are missing: PORKBUN_SECRET_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretAPIKey: \"secret\",\n\t\t\t\tEnvAPIKey:       \"\",\n\t\t\t},\n\t\t\texpected: \"porkbun: some credentials information are missing: PORKBUN_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing all credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretAPIKey: \"\",\n\t\t\t\tEnvAPIKey:       \"\",\n\t\t\t},\n\t\t\texpected: \"porkbun: some credentials information are missing: PORKBUN_SECRET_API_KEY,PORKBUN_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc         string\n\t\tsecretAPIKey string\n\t\tapiKey       string\n\t\texpected     string\n\t}{\n\t\t{\n\t\t\tdesc:         \"success\",\n\t\t\tsecretAPIKey: \"secret\",\n\t\t\tapiKey:       \"key\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret API key\",\n\t\t\tapiKey:   \"key\",\n\t\t\texpected: \"porkbun: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:         \"missing API key\",\n\t\t\tsecretAPIKey: \"secret\",\n\t\t\texpected:     \"porkbun: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing all credentials\",\n\t\t\texpected: \"porkbun: some credentials information are missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.SecretAPIKey = test.secretAPIKey\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/rackspace/fixtures/delete.json",
    "content": "{\n  \"status\": \"RUNNING\",\n  \"verb\": \"DELETE\",\n  \"jobId\": \"00000000-0000-0000-0000-0000000000\",\n  \"callbackUrl\": \"https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000\",\n  \"requestUrl\": \"https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/recordsid=TXT-654321\"\n}\n"
  },
  {
    "path": "providers/dns/rackspace/fixtures/identity.json",
    "content": "{\n  \"access\": {\n    \"token\": {\n      \"id\": \"testToken\",\n      \"expires\": \"1970-01-01T00:00:00.000Z\",\n      \"tenant\": {\n        \"id\": \"123456\",\n        \"name\": \"123456\"\n      },\n      \"RAX-AUTH:authenticatedBy\": [\n        \"APIKEY\"\n      ]\n    },\n    \"serviceCatalog\": [\n      {\n        \"type\": \"rax:dns\",\n        \"endpoints\": [\n          {\n            \"publicURL\": \"https://dns.api.rackspacecloud.com/v1.0/123456\",\n            \"tenantId\": \"123456\"\n          }\n        ],\n        \"name\": \"cloudDNS\"\n      }\n    ],\n    \"user\": {\n      \"id\": \"fakeUseID\",\n      \"name\": \"testUser\"\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/rackspace/fixtures/record.json",
    "content": "{\n  \"request\": \"{\\\"records\\\":[{\\\"name\\\":\\\"_acme-challenge.example.com\\\",\\\"type\\\":\\\"TXT\\\",\\\"data\\\":\\\"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\\\",\\\"ttl\\\":300}]}\",\n  \"status\": \"RUNNING\",\n  \"verb\": \"POST\",\n  \"jobId\": \"00000000-0000-0000-0000-0000000000\",\n  \"callbackUrl\": \"https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000\",\n  \"requestUrl\": \"https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/records\"\n}\n"
  },
  {
    "path": "providers/dns/rackspace/fixtures/record_details.json",
    "content": "{\n  \"records\": [\n    {\n      \"name\": \"_acme-challenge.example.com\",\n      \"id\": \"TXT-654321\",\n      \"type\": \"TXT\",\n      \"data\": \"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\",\n      \"ttl\": 300,\n      \"updated\": \"1970-01-01T00:00:00.000+0000\",\n      \"created\": \"1970-01-01T00:00:00.000+0000\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/rackspace/fixtures/zone_details.json",
    "content": "{\n  \"domains\": [\n    {\n      \"name\": \"example.com\",\n      \"id\": \"112233\",\n      \"emailAddress\": \"hostmaster@example.com\",\n      \"updated\": \"1970-01-01T00:00:00.000+0000\",\n      \"created\": \"1970-01-01T00:00:00.000+0000\"\n    }\n  ],\n  \"totalEntries\": 1\n}\n"
  },
  {
    "path": "providers/dns/rackspace/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst AuthToken = \"X-Auth-Token\"\n\ntype Client struct {\n\ttoken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(endpoint, token string) (*Client, error) {\n\tbaseURL, err := url.Parse(endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\ttoken:      token,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}, nil\n}\n\n// AddRecord Adds one record to a specified domain.\n// https://docs.rackspace.com/docs/cloud-dns/v1/api-reference/records#add-records\nfunc (c *Client) AddRecord(ctx context.Context, zoneID string, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"domains\", zoneID, \"records\")\n\n\trecords := Records{Records: []Record{record}}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, records)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// DeleteRecord Deletes a record from the domain.\n// https://docs.rackspace.com/docs/cloud-dns/v1/api-reference/records#delete-records\nfunc (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID string) error {\n\tendpoint := c.baseURL.JoinPath(\"domains\", zoneID, \"records\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"id\", recordID)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// GetHostedZoneID performs a lookup to get the DNS zone which needs modifying for a given FQDN.\nfunc (c *Client) GetHostedZoneID(ctx context.Context, fqdn string) (string, error) {\n\tauthZone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tzoneSearchResponse, err := c.listDomainsByName(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// If nothing was returned, or for whatever reason more than 1 was returned (the search uses exact match, so should not occur)\n\tif zoneSearchResponse.TotalEntries != 1 {\n\t\treturn \"\", fmt.Errorf(\"found %d zones for %s in Rackspace for domain %s\", zoneSearchResponse.TotalEntries, authZone, fqdn)\n\t}\n\n\treturn zoneSearchResponse.HostedZones[0].ID, nil\n}\n\n// listDomainsByName Filters domains by domain name.\n// https://docs.rackspace.com/docs/cloud-dns/v1/api-reference/domains#list-domains-by-name\nfunc (c *Client) listDomainsByName(ctx context.Context, domain string) (*ZoneSearchResponse, error) {\n\tendpoint := c.baseURL.JoinPath(\"domains\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"name\", domain)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar zoneSearchResponse ZoneSearchResponse\n\n\terr = c.do(req, &zoneSearchResponse)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &zoneSearchResponse, nil\n}\n\n// FindTxtRecord searches a DNS zone for a TXT record with a specific name.\nfunc (c *Client) FindTxtRecord(ctx context.Context, fqdn, zoneID string) (*Record, error) {\n\trecords, err := c.searchRecords(ctx, zoneID, dns01.UnFqdn(fqdn), \"TXT\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch len(records.Records) {\n\tcase 1:\n\tcase 0:\n\t\treturn nil, fmt.Errorf(\"no TXT record found for %s\", fqdn)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"more than 1 TXT record found for %s\", fqdn)\n\t}\n\n\treturn &records.Records[0], nil\n}\n\n// https://docs.rackspace.com/docs/cloud-dns/v1/api-reference/records#search-records\nfunc (c *Client) searchRecords(ctx context.Context, zoneID, recordName, recordType string) (*Records, error) {\n\tendpoint := c.baseURL.JoinPath(\"domains\", zoneID, \"records\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"type\", recordType)\n\tquery.Set(\"name\", recordName)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar records Records\n\n\terr = c.do(req, &records)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &records, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(AuthToken, c.token)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest[T string | *url.URL](ctx context.Context, method string, endpoint T, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, fmt.Sprintf(\"%s\", endpoint), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/rackspace/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(server.URL, \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(AuthToken, \"secret\"))\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/1234/records\",\n\t\t\tservermock.ResponseFromFixture(\"add-records.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"records\":[{\"name\":\"exmaple.com\",\"type\":\"TXT\",\"data\":\"value1\",\"ttl\":120,\"id\":\"abc\"}]}`)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName: \"exmaple.com\",\n\t\tType: \"TXT\",\n\t\tData: \"value1\",\n\t\tTTL:  120,\n\t\tID:   \"abc\",\n\t}\n\n\terr := client.AddRecord(t.Context(), \"1234\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domains/1234/records\", nil).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"1234\", \"2725233\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_searchRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains/1234/records\", servermock.ResponseFromFixture(\"search-records.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.searchRecords(t.Context(), \"1234\", \"2725233\", \"A\")\n\trequire.NoError(t, err)\n\n\texpected := &Records{\n\t\tTotalEntries: 6,\n\t\tRecords: []Record{\n\t\t\t{Name: \"ftp.example.com\", Type: \"A\", Data: \"192.0.2.8\", TTL: 5771, ID: \"A-6817754\"},\n\t\t\t{Name: \"example.com\", Type: \"A\", Data: \"192.0.2.17\", TTL: 86400, ID: \"A-6822994\"},\n\t\t\t{Name: \"example.com\", Type: \"NS\", Data: \"ns.rackspace.com\", TTL: 3600, ID: \"NS-6251982\"},\n\t\t\t{Name: \"example.com\", Type: \"NS\", Data: \"ns2.rackspace.com\", TTL: 3600, ID: \"NS-6251983\"},\n\t\t\t{Name: \"example.com\", Type: \"MX\", Data: \"mail.example.com\", TTL: 3600, ID: \"MX-3151218\"},\n\t\t\t{Name: \"www.example.com\", Type: \"CNAME\", Data: \"example.com\", TTL: 5400, ID: \"CNAME-9778009\"},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_listDomainsByName(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains\", servermock.ResponseFromFixture(\"list-domains-by-name.json\")).\n\t\tBuild(t)\n\n\tdomains, err := client.listDomainsByName(t.Context(), \"1234\")\n\trequire.NoError(t, err)\n\n\texpected := &ZoneSearchResponse{\n\t\tTotalEntries: 114,\n\t\tHostedZones:  []HostedZone{{ID: \"2725257\", Name: \"sub1.example.com\"}},\n\t}\n\n\tassert.Equal(t, expected, domains)\n}\n"
  },
  {
    "path": "providers/dns/rackspace/internal/fixtures/add-records.json",
    "content": "{\n  \"totalEntries\": 6,\n  \"records\": [\n    {\n      \"name\": \"ftp.example.com\",\n      \"id\": \"A-6817754\",\n      \"type\": \"A\",\n      \"data\": \"192.0.2.8\",\n      \"updated\": \"2011-05-19T13:07:08.000+0000\",\n      \"ttl\": 5771,\n      \"created\": \"2011-05-18T19:53:09.000+0000\"\n    },\n    {\n      \"name\": \"example.com\",\n      \"id\": \"A-6822994\",\n      \"type\": \"A\",\n      \"data\": \"192.0.2.17\",\n      \"updated\": \"2011-06-24T01:12:52.000+0000\",\n      \"ttl\": 86400,\n      \"created\": \"2011-06-24T01:12:52.000+0000\"\n    },\n    {\n      \"name\": \"example.com\",\n      \"id\": \"NS-6251982\",\n      \"type\": \"NS\",\n      \"data\": \"ns.rackspace.com\",\n      \"updated\": \"2011-06-24T01:12:51.000+0000\",\n      \"ttl\": 3600,\n      \"created\": \"2011-06-24T01:12:51.000+0000\"\n    },\n    {\n      \"name\": \"example.com\",\n      \"id\": \"NS-6251983\",\n      \"type\": \"NS\",\n      \"data\": \"ns2.rackspace.com\",\n      \"updated\": \"2011-06-24T01:12:51.000+0000\",\n      \"ttl\": 3600,\n      \"created\": \"2011-06-24T01:12:51.000+0000\"\n    },\n    {\n      \"name\": \"example.com\",\n      \"priority\": 5,\n      \"id\": \"MX-3151218\",\n      \"type\": \"MX\",\n      \"data\": \"mail.example.com\",\n      \"updated\": \"2011-06-24T01:12:53.000+0000\",\n      \"ttl\": 3600,\n      \"created\": \"2011-06-24T01:12:53.000+0000\"\n    },\n    {\n      \"name\": \"www.example.com\",\n      \"id\": \"CNAME-9778009\",\n      \"type\": \"CNAME\",\n      \"comment\": \"This is a comment on the CNAME record\",\n      \"data\": \"example.com\",\n      \"updated\": \"2011-06-24T01:12:54.000+0000\",\n      \"ttl\": 5400,\n      \"created\": \"2011-06-24T01:12:54.000+0000\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/rackspace/internal/fixtures/delete-records_error.json",
    "content": "{\n  \"failedItems\" : {\n    \"faults\" : [ {\n      \"message\" : \"Object not Found.\",\n      \"code\" : 404,\n      \"details\" : \"Domain ID: 2720150; Record ID: 111111111\"\n    }, {\n      \"message\" : \"Object not Found.\",\n      \"code\" : 404,\n      \"details\" : \"Domain ID: 2720150; Record ID: 222222222\"\n    } ]\n  },\n  \"message\" : \"One or more items could not be deleted.\",\n  \"code\" : 500,\n  \"details\" : \"See errors list for details.\"\n}\n"
  },
  {
    "path": "providers/dns/rackspace/internal/fixtures/list-domains-by-name.json",
    "content": "{\n  \"domains\": [\n    {\n      \"name\": \"sub1.example.com\",\n      \"id\": \"2725257\",\n      \"comment\": \"1st sample subdomain\",\n      \"updated\": \"2011-06-23T03:09:34.000+0000\",\n      \"emailAddress\": \"sample@rackspace.com\",\n      \"created\": \"2011-06-23T03:09:33.000+0000\"\n    }\n  ],\n  \"totalEntries\": 114\n}\n"
  },
  {
    "path": "providers/dns/rackspace/internal/fixtures/search-records.json",
    "content": "{\n  \"totalEntries\": 6,\n  \"records\": [\n    {\n      \"name\": \"ftp.example.com\",\n      \"id\": \"A-6817754\",\n      \"type\": \"A\",\n      \"data\": \"192.0.2.8\",\n      \"updated\": \"2011-05-19T13:07:08.000+0000\",\n      \"ttl\": 5771,\n      \"created\": \"2011-05-18T19:53:09.000+0000\"\n    },\n    {\n      \"name\": \"example.com\",\n      \"id\": \"A-6822994\",\n      \"type\": \"A\",\n      \"data\": \"192.0.2.17\",\n      \"updated\": \"2011-06-24T01:12:52.000+0000\",\n      \"ttl\": 86400,\n      \"created\": \"2011-06-24T01:12:52.000+0000\"\n    },\n    {\n      \"name\": \"example.com\",\n      \"id\": \"NS-6251982\",\n      \"type\": \"NS\",\n      \"data\": \"ns.rackspace.com\",\n      \"updated\": \"2011-06-24T01:12:51.000+0000\",\n      \"ttl\": 3600,\n      \"created\": \"2011-06-24T01:12:51.000+0000\"\n    },\n    {\n      \"name\": \"example.com\",\n      \"id\": \"NS-6251983\",\n      \"type\": \"NS\",\n      \"data\": \"ns2.rackspace.com\",\n      \"updated\": \"2011-06-24T01:12:51.000+0000\",\n      \"ttl\": 3600,\n      \"created\": \"2011-06-24T01:12:51.000+0000\"\n    },\n    {\n      \"name\": \"example.com\",\n      \"priority\": 5,\n      \"id\": \"MX-3151218\",\n      \"type\": \"MX\",\n      \"data\": \"mail.example.com\",\n      \"updated\": \"2011-06-24T01:12:53.000+0000\",\n      \"ttl\": 3600,\n      \"created\": \"2011-06-24T01:12:53.000+0000\"\n    },\n    {\n      \"name\": \"www.example.com\",\n      \"id\": \"CNAME-9778009\",\n      \"type\": \"CNAME\",\n      \"comment\": \"This is a comment on the CNAME record\",\n      \"data\": \"example.com\",\n      \"updated\": \"2011-06-24T01:12:54.000+0000\",\n      \"ttl\": 5400,\n      \"created\": \"2011-06-24T01:12:54.000+0000\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/rackspace/internal/fixtures/tokens.json",
    "content": "{\n  \"access\": {\n    \"token\": {\n      \"id\": \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",\n      \"expires\": \"2014-11-24T22:05:39.115Z\",\n      \"tenant\": {\n        \"id\": \"110011\",\n        \"name\": \"110011\"\n      },\n      \"RAX-AUTH:authenticatedBy\": [\n        \"APIKEY\"\n      ]\n    },\n    \"serviceCatalog\": [\n      {\n        \"name\": \"cloudDatabases\",\n        \"endpoints\": [\n          {\n            \"publicURL\": \"https://syd.databases.api.rackspacecloud.com/v1.0/110011\",\n            \"region\": \"SYD\",\n            \"tenantId\": \"110011\"\n          },\n          {\n            \"publicURL\": \"https://dfw.databases.api.rackspacecloud.com/v1.0/110011\",\n            \"region\": \"DFW\",\n            \"tenantId\": \"110011\"\n          },\n          {\n            \"publicURL\": \"https://ord.databases.api.rackspacecloud.com/v1.0/110011\",\n            \"region\": \"ORD\",\n            \"tenantId\": \"110011\"\n          },\n          {\n            \"publicURL\": \"https://iad.databases.api.rackspacecloud.com/v1.0/110011\",\n            \"region\": \"IAD\",\n            \"tenantId\": \"110011\"\n          },\n          {\n            \"publicURL\": \"https://hkg.databases.api.rackspacecloud.com/v1.0/110011\",\n            \"region\": \"HKG\",\n            \"tenantId\": \"110011\"\n          }\n        ],\n        \"type\": \"rax:database\"\n      },\n      {\n        \"name\": \"cloudDNS\",\n        \"endpoints\": [\n          {\n            \"publicURL\": \"https://dns.api.rackspacecloud.com/v1.0/110011\",\n            \"tenantId\": \"110011\"\n          }\n        ],\n        \"type\": \"rax:dns\"\n      },\n      {\n        \"name\": \"rackCDN\",\n        \"endpoints\": [\n          {\n            \"internalURL\": \"https://global.cdn.api.rackspacecloud.com/v1.0/110011\",\n            \"publicURL\": \"https://global.cdn.api.rackspacecloud.com/v1.0/110011\",\n            \"tenantId\": \"110011\"\n          }\n        ],\n        \"type\": \"rax:cdn\"\n      }\n    ],\n    \"user\": {\n      \"id\": \"123456\",\n      \"roles\": [\n        {\n          \"description\": \"A Role that allows a user access to keystone Service methods\",\n          \"id\": \"6\",\n          \"name\": \"compute:default\",\n          \"tenantId\": \"110011\"\n        },\n        {\n          \"description\": \"User Admin Role.\",\n          \"id\": \"3\",\n          \"name\": \"identity:user-admin\"\n        }\n      ],\n      \"name\": \"jsmith\",\n      \"RAX-AUTH:defaultRegion\": \"ORD\"\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/rackspace/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// DefaultIdentityURL represents the Identity API endpoint to call.\nconst DefaultIdentityURL = \"https://identity.api.rackspacecloud.com/v2.0/tokens\"\n\ntype Identifier struct {\n\tbaseURL    string\n\thttpClient *http.Client\n}\n\n// NewIdentifier creates a new Identifier.\nfunc NewIdentifier(httpClient *http.Client, baseURL string) *Identifier {\n\tif httpClient == nil {\n\t\thttpClient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tif baseURL == \"\" {\n\t\tbaseURL = DefaultIdentityURL\n\t}\n\n\treturn &Identifier{baseURL: baseURL, httpClient: httpClient}\n}\n\n// Login sends an authentication request.\n// https://docs.rackspace.com/docs/cloud-dns/v1/getting-started/authenticate\nfunc (a *Identifier) Login(ctx context.Context, apiUser, apiKey string) (*Identity, error) {\n\tauthData := AuthData{\n\t\tAuth: Auth{\n\t\t\tAPIKeyCredentials: APIKeyCredentials{\n\t\t\t\tUsername: apiUser,\n\t\t\t\tAPIKey:   apiKey,\n\t\t\t},\n\t\t},\n\t}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, a.baseURL, authData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := a.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar identity Identity\n\n\terr = json.Unmarshal(raw, &identity)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn &identity, nil\n}\n"
  },
  {
    "path": "providers/dns/rackspace/internal/identity_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupIdentifier(server *httptest.Server) (*Identifier, error) {\n\treturn NewIdentifier(server.Client(), server.URL), nil\n}\n\nfunc TestIdentifier_Login(t *testing.T) {\n\tidentifier := servermock.NewBuilder[*Identifier](setupIdentifier, servermock.CheckHeader().WithJSONHeaders()).\n\t\tRoute(\"POST /\", servermock.ResponseFromFixture(\"tokens.json\")).\n\t\tBuild(t)\n\n\tidentity, err := identifier.Login(t.Context(), \"user\", \"secret\")\n\trequire.NoError(t, err)\n\n\texpected := &Identity{\n\t\tAccess: Access{\n\t\t\tToken: Token{\n\t\t\t\tID:                     \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",\n\t\t\t\tExpires:                \"2014-11-24T22:05:39.115Z\",\n\t\t\t\tTenant:                 Tenant{ID: \"110011\", Name: \"110011\"},\n\t\t\t\tRAXAUTHAuthenticatedBy: []string{\"APIKEY\"},\n\t\t\t},\n\t\t\tServiceCatalog: []ServiceCatalog{\n\t\t\t\t{\n\t\t\t\t\tName: \"cloudDatabases\",\n\t\t\t\t\tType: \"rax:database\",\n\t\t\t\t\tEndpoints: []Endpoint{\n\t\t\t\t\t\t{PublicURL: \"https://syd.databases.api.rackspacecloud.com/v1.0/110011\", Region: \"SYD\", TenantID: \"110011\", InternalURL: \"\"},\n\t\t\t\t\t\t{PublicURL: \"https://dfw.databases.api.rackspacecloud.com/v1.0/110011\", Region: \"DFW\", TenantID: \"110011\", InternalURL: \"\"},\n\t\t\t\t\t\t{PublicURL: \"https://ord.databases.api.rackspacecloud.com/v1.0/110011\", Region: \"ORD\", TenantID: \"110011\", InternalURL: \"\"},\n\t\t\t\t\t\t{PublicURL: \"https://iad.databases.api.rackspacecloud.com/v1.0/110011\", Region: \"IAD\", TenantID: \"110011\", InternalURL: \"\"},\n\t\t\t\t\t\t{PublicURL: \"https://hkg.databases.api.rackspacecloud.com/v1.0/110011\", Region: \"HKG\", TenantID: \"110011\", InternalURL: \"\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:      \"cloudDNS\",\n\t\t\t\t\tType:      \"rax:dns\",\n\t\t\t\t\tEndpoints: []Endpoint{{PublicURL: \"https://dns.api.rackspacecloud.com/v1.0/110011\", Region: \"\", TenantID: \"110011\", InternalURL: \"\"}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:      \"rackCDN\",\n\t\t\t\t\tType:      \"rax:cdn\",\n\t\t\t\t\tEndpoints: []Endpoint{{PublicURL: \"https://global.cdn.api.rackspacecloud.com/v1.0/110011\", Region: \"\", TenantID: \"110011\", InternalURL: \"https://global.cdn.api.rackspacecloud.com/v1.0/110011\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUser: User{\n\t\t\t\tID: \"123456\",\n\t\t\t\tRoles: []Role{\n\t\t\t\t\t{Description: \"A Role that allows a user access to keystone Service methods\", ID: \"6\", Name: \"compute:default\", TenantID: \"110011\"},\n\t\t\t\t\t{Description: \"User Admin Role.\", ID: \"3\", Name: \"identity:user-admin\", TenantID: \"\"},\n\t\t\t\t},\n\t\t\t\tName:                 \"jsmith\",\n\t\t\t\tRAXAUTHDefaultRegion: \"ORD\",\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, identity)\n}\n"
  },
  {
    "path": "providers/dns/rackspace/internal/types.go",
    "content": "package internal\n\n// Authentication response.\n\n// Identity api structure.\ntype Identity struct {\n\tAccess Access `json:\"access\"`\n}\n\n// Access api structure.\ntype Access struct {\n\tToken          Token            `json:\"token\"`\n\tServiceCatalog []ServiceCatalog `json:\"serviceCatalog\"`\n\tUser           User             `json:\"user\"`\n}\n\n// Token api structure.\ntype Token struct {\n\tID                     string   `json:\"id\"`\n\tExpires                string   `json:\"expires\"`\n\tTenant                 Tenant   `json:\"tenant\"`\n\tRAXAUTHAuthenticatedBy []string `json:\"RAX-AUTH:authenticatedBy\"`\n}\n\n// ServiceCatalog service catalog.\ntype ServiceCatalog struct {\n\tName      string     `json:\"name\"`\n\tType      string     `json:\"type\"`\n\tEndpoints []Endpoint `json:\"endpoints\"`\n}\n\ntype Tenant struct {\n\tID   string `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\n// Endpoint api structure.\ntype Endpoint struct {\n\tPublicURL   string `json:\"publicURL\"`\n\tRegion      string `json:\"region,omitempty\"`\n\tTenantID    string `json:\"tenantId\"`\n\tInternalURL string `json:\"internalURL,omitempty\"`\n}\n\ntype Role struct {\n\tDescription string `json:\"description\"`\n\tID          string `json:\"id\"`\n\tName        string `json:\"name\"`\n\tTenantID    string `json:\"tenantId,omitempty\"`\n}\n\ntype User struct {\n\tID                   string `json:\"id\"`\n\tRoles                []Role `json:\"roles\"`\n\tName                 string `json:\"name\"`\n\tRAXAUTHDefaultRegion string `json:\"RAX-AUTH:defaultRegion\"`\n}\n\n// Authentication request.\n\n// AuthData api structure.\ntype AuthData struct {\n\tAuth `json:\"auth\"`\n}\n\n// Auth api structure.\ntype Auth struct {\n\tAPIKeyCredentials `json:\"RAX-KSKEY:apiKeyCredentials\"`\n}\n\n// APIKeyCredentials api structure.\ntype APIKeyCredentials struct {\n\tUsername string `json:\"username\"`\n\tAPIKey   string `json:\"apiKey\"`\n}\n\n// API responses.\n\n// ZoneSearchResponse represents the response when querying Rackspace DNS zones.\ntype ZoneSearchResponse struct {\n\tTotalEntries int          `json:\"totalEntries\"`\n\tHostedZones  []HostedZone `json:\"domains\"`\n}\n\n// HostedZone api structure.\ntype HostedZone struct {\n\tID   string `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\n// Records is the list of records sent/received from the DNS API.\ntype Records struct {\n\tTotalEntries int      `json:\"totalEntries,omitempty\"`\n\tRecords      []Record `json:\"records,omitempty\"`\n}\n\n// Record represents a Rackspace DNS record.\ntype Record struct {\n\tName string `json:\"name\"`\n\tType string `json:\"type\"`\n\tData string `json:\"data\"`\n\tTTL  int    `json:\"ttl,omitempty\"`\n\tID   string `json:\"id,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/rackspace/rackspace.go",
    "content": "// Package rackspace implements a DNS provider for solving the DNS-01 challenge using rackspace DNS.\npackage rackspace\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/rackspace/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"RACKSPACE_\"\n\n\tEnvUser   = envNamespace + \"USER\"\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL            string\n\tAPIUser            string\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tBaseURL:            internal.DefaultIdentityURL,\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\ttoken            string\n\tcloudDNSEndpoint string\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Rackspace.\n// Credentials must be passed in the environment variables:\n// RACKSPACE_USER and RACKSPACE_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUser, EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rackspace: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIUser = values[EnvUser]\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Rackspace.\n// It authenticates against the API, also grabbing the DNS Endpoint.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"rackspace: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIUser == \"\" || config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"rackspace: credentials missing\")\n\t}\n\n\tidentifier := internal.NewIdentifier(config.HTTPClient, config.BaseURL)\n\n\tidentity, err := identifier.Login(context.Background(), config.APIUser, config.APIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rackspace: %w\", err)\n\t}\n\n\t// Iterate through the Service Catalog to get the DNS Endpoint\n\tvar dnsEndpoint string\n\n\tfor _, service := range identity.Access.ServiceCatalog {\n\t\tif service.Name == \"cloudDNS\" {\n\t\t\tdnsEndpoint = service.Endpoints[0].PublicURL\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif dnsEndpoint == \"\" {\n\t\treturn nil, errors.New(\"rackspace: failed to populate DNS endpoint, check Rackspace API for changes\")\n\t}\n\n\tclient, err := internal.NewClient(dnsEndpoint, identity.Access.Token.ID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rackspace: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:           config,\n\t\tclient:           client,\n\t\ttoken:            identity.Access.Token.ID,\n\t\tcloudDNSEndpoint: dnsEndpoint,\n\t}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzoneID, err := d.client.GetHostedZoneID(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rackspace: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName: dns01.UnFqdn(info.EffectiveFQDN),\n\t\tType: \"TXT\",\n\t\tData: info.Value,\n\t\tTTL:  d.config.TTL,\n\t}\n\n\terr = d.client.AddRecord(ctx, zoneID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rackspace: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzoneID, err := d.client.GetHostedZoneID(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rackspace: %w\", err)\n\t}\n\n\trecord, err := d.client.FindTxtRecord(ctx, info.EffectiveFQDN, zoneID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rackspace: %w\", err)\n\t}\n\n\terr = d.client.DeleteRecord(ctx, zoneID, record.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rackspace: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/rackspace/rackspace.toml",
    "content": "Name = \"Rackspace\"\nDescription = ''''''\nURL = \"https://www.rackspace.com/\"\nCode = \"rackspace\"\nSince = \"v0.4.0\"\n\nExample = '''\nRACKSPACE_USER=xxxx \\\nRACKSPACE_API_KEY=yyyy \\\nlego --dns rackspace -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    RACKSPACE_USER = \"API user\"\n    RACKSPACE_API_KEY = \"API key\"\n  [Configuration.Additional]\n    RACKSPACE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 3)\"\n    RACKSPACE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    RACKSPACE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    RACKSPACE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://developer.rackspace.com/docs/cloud-dns/v1/\"\n"
  },
  {
    "path": "providers/dns/rackspace/rackspace_test.go",
    "content": "package rackspace\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvUser,\n\tEnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\tprovider := mockBuilder().Build(t)\n\n\tassert.Equal(t, \"testToken\", provider.token, \"The token should match\")\n}\n\nfunc TestNewDNSProviderConfig_MissingCredErr(t *testing.T) {\n\t_, err := NewDNSProviderConfig(NewDefaultConfig())\n\trequire.EqualError(t, err, \"rackspace: credentials missing\")\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /123456/domains\",\n\t\t\tservermock.ResponseFromFixture(\"zone_details.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com\")).\n\t\tRoute(\"POST /123456/domains/112233/records\",\n\t\t\tservermock.ResponseFromFixture(\"record.json\").\n\t\t\t\tWithStatusCode(http.StatusAccepted),\n\t\t\tservermock.CheckRequestJSONBody(`{\"records\":[{\"name\":\"_acme-challenge.example.com\",\"type\":\"TXT\",\"data\":\"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\",\"ttl\":300}]}`)).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"token\", \"keyAuth\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /123456/domains\",\n\t\t\tservermock.ResponseFromFixture(\"zone_details.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"example.com\")).\n\t\tRoute(\"GET /123456/domains/112233/records\",\n\t\t\tservermock.ResponseFromFixture(\"record_details.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"name\", \"_acme-challenge.example.com\")).\n\t\tRoute(\"DELETE /123456/domains/112233/records\",\n\t\t\tservermock.ResponseFromFixture(\"delete.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"id\", \"TXT-654321\")).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"token\", \"keyAuth\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveNewDNSProvider_ValidEnv(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, provider.cloudDNSEndpoint, \"https://dns.api.rackspacecloud.com/v1.0/\", \"The endpoint URL should contain the base\")\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"112233445566==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(15 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"112233445566==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.HTTPClient = server.Client()\n\t\t\tconfig.APIUser = \"testUser\"\n\t\t\tconfig.APIKey = \"testKey\"\n\t\t\tconfig.HTTPClient = server.Client()\n\t\t\tconfig.BaseURL = server.URL + \"/v2.0/tokens\"\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t).\n\t\tRoute(\"POST /v2.0/tokens\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\tapiURL := fmt.Sprintf(\"http://%s/123456\", req.Context().Value(http.LocalAddrContextKey))\n\n\t\t\t\tresp := strings.Replace(`\n{\n  \"access\": {\n    \"token\": {\n      \"id\": \"testToken\",\n      \"expires\": \"1970-01-01T00:00:00.000Z\",\n      \"tenant\": {\n        \"id\": \"123456\",\n        \"name\": \"123456\"\n      },\n      \"RAX-AUTH:authenticatedBy\": [\n        \"APIKEY\"\n      ]\n    },\n    \"serviceCatalog\": [\n      {\n        \"type\": \"rax:dns\",\n        \"endpoints\": [\n          {\n            \"publicURL\": \"https://dns.api.rackspacecloud.com/v1.0/123456\",\n            \"tenantId\": \"123456\"\n          }\n        ],\n        \"name\": \"cloudDNS\"\n      }\n    ],\n    \"user\": {\n      \"id\": \"fakeUseID\",\n      \"name\": \"testUser\"\n    }\n  }\n}\n`, \"https://dns.api.rackspacecloud.com/v1.0/123456\", apiURL, 1)\n\n\t\t\t\trw.WriteHeader(http.StatusOK)\n\t\t\t\t_, _ = fmt.Fprint(rw, resp)\n\t\t\t}),\n\t\t\tservermock.CheckRequestJSONBody(`{\"auth\":{\"RAX-KSKEY:apiKeyCredentials\":{\"username\":\"testUser\",\"apiKey\":\"testKey\"}}}`))\n}\n"
  },
  {
    "path": "providers/dns/rainyun/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\nconst defaultBaseURL = \"https://api.v2.rainyun.com/product/\"\n\n// Client the Rain Yun API client.\ntype Client struct {\n\tapiKey string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiKey string) (*Client, error) {\n\tif apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) AddRecord(ctx context.Context, domainID int, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"domain\", strconv.Itoa(domainID), \"dns\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int) error {\n\tendpoint := c.baseURL.JoinPath(\"domain\", strconv.Itoa(domainID), \"dns\")\n\n\tvalues, err := querystring.Values(Record{ID: recordID})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tendpoint.RawQuery = values.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) ListRecords(ctx context.Context, domainID int) ([]Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"domain\", strconv.Itoa(domainID), \"dns\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"limit\", \"100\")\n\tquery.Set(\"page_no\", \"1\")\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar recordData APIResponse[Record]\n\n\terr = c.do(req, &recordData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn recordData.Data.Records, nil\n}\n\nfunc (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {\n\tendpoint := c.baseURL.JoinPath(\"domain\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"options\", `{\"columnFilters\":{\"domains.Domain\":\"\"},\"sort\":[],\"page\":1,\"perPage\":100}`)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar domainData APIResponse[Domain]\n\n\terr = c.do(req, &domainData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn domainData.Data.Records, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Add(\"x-api-key\", c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/rainyun/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders())\n}\n\nfunc TestClient_ListDomains(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domain\",\n\t\t\tservermock.ResponseFromFixture(\"domains.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"options\", `{\"columnFilters\":{\"domains.Domain\":\"\"},\"sort\":[],\"page\":1,\"perPage\":100}`)).\n\t\tBuild(t)\n\n\tdomains, err := client.ListDomains(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []Domain{\n\t\t{ID: 1, Domain: \"example.com\"},\n\t\t{ID: 2, Domain: \"example.org\"},\n\t}\n\n\tassert.Equal(t, expected, domains)\n}\n\nfunc TestClient_ListDomains_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domain\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusForbidden)).\n\t\tBuild(t)\n\n\t_, err := client.ListDomains(t.Context())\n\trequire.Error(t, err)\n\n\tassert.EqualError(t, err, \"30039: 密钥认证错误或已失效\")\n}\n\nfunc TestClient_ListRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domain/123/dns\",\n\t\t\tservermock.ResponseFromFixture(\"records.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"limit\", \"100\").\n\t\t\t\tWith(\"page_no\", \"1\")).\n\t\tBuild(t)\n\n\trecords, err := client.ListRecords(t.Context(), 123)\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tID:    1,\n\t\t\tHost:  \"_acme-challenge.foo.example.com\",\n\t\t\tLine:  \"DEFAULT\",\n\t\t\tTTL:   120,\n\t\t\tType:  \"TXT\",\n\t\t\tValue: \"foo\",\n\t\t},\n\t\t{\n\t\t\tID:    2,\n\t\t\tHost:  \"_acme-challenge.bar.example.com\",\n\t\t\tLine:  \"DEFAULT\",\n\t\t\tTTL:   300,\n\t\t\tType:  \"TXT\",\n\t\t\tValue: \"bar\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_ListRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domain/123/dns\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusForbidden)).\n\t\tBuild(t)\n\n\t_, err := client.ListRecords(t.Context(), 123)\n\trequire.Error(t, err)\n\n\tassert.EqualError(t, err, \"30039: 密钥认证错误或已失效\")\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domain/123/dns\", nil).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tHost:  \"_acme-challenge.foo.example.com\",\n\t\tLine:  \"DEFAULT\",\n\t\tTTL:   120,\n\t\tType:  \"TXT\",\n\t\tValue: \"foo\",\n\t}\n\n\terr := client.AddRecord(t.Context(), 123, record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domain/123/dns\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusForbidden)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tHost:  \"_acme-challenge.foo.example.com\",\n\t\tLine:  \"DEFAULT\",\n\t\tTTL:   120,\n\t\tType:  \"TXT\",\n\t\tValue: \"foo\",\n\t}\n\n\terr := client.AddRecord(t.Context(), 123, record)\n\trequire.Error(t, err)\n\n\tassert.EqualError(t, err, \"30039: 密钥认证错误或已失效\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domain/123/dns\", nil).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), 123, 456)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domain/123/dns\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusForbidden)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), 123, 456)\n\trequire.Error(t, err)\n\n\tassert.EqualError(t, err, \"30039: 密钥认证错误或已失效\")\n}\n"
  },
  {
    "path": "providers/dns/rainyun/internal/fixtures/domains.json",
    "content": "{\n  \"code\": 0,\n  \"data\": {\n    \"TotalRecords\": 2,\n    \"Records\": [\n      {\n        \"id\": 1,\n        \"domain\": \"example.com\"\n      },\n      {\n        \"id\": 2,\n        \"domain\": \"example.org\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/rainyun/internal/fixtures/error.json",
    "content": "{\n  \"code\": 30039,\n  \"message\": \"密钥认证错误或已失效\"\n}\n"
  },
  {
    "path": "providers/dns/rainyun/internal/fixtures/records.json",
    "content": "{\n  \"code\": 0,\n  \"data\": {\n    \"TotalRecords\": 2,\n    \"Records\": [\n      {\n        \"record_id\": 1,\n        \"host\": \"_acme-challenge.foo.example.com\",\n        \"type\": \"TXT\",\n        \"TTL\": 120,\n        \"value\": \"foo\",\n        \"line\": \"DEFAULT\"\n      },\n      {\n        \"record_id\": 2,\n        \"host\": \"_acme-challenge.bar.example.com\",\n        \"type\": \"TXT\",\n        \"TTL\": 300,\n        \"value\": \"bar\",\n        \"line\": \"DEFAULT\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/rainyun/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype APIError struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\nfunc (a *APIError) Error() string {\n\treturn fmt.Sprintf(\"%d: %s\", a.Code, a.Message)\n}\n\ntype Record struct {\n\tID       int    `json:\"record_id,omitempty\" url:\"record_id,omitempty\"`\n\tHost     string `json:\"host,omitempty\" url:\"host,omitempty\"`\n\tPriority int    `json:\"level,omitempty\" url:\"level,omitempty\"`\n\tLine     string `json:\"line,omitempty\" url:\"line,omitempty\"`\n\tTTL      int    `json:\"ttl,omitempty\" url:\"ttl,omitempty\"`\n\tType     string `json:\"type,omitempty\" url:\"type,omitempty\"`\n\tValue    string `json:\"value,omitempty\" url:\"value,omitempty\"`\n}\n\ntype Domain struct {\n\tID     int    `json:\"id,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"`\n}\n\ntype APIResponse[T any] struct {\n\tCode int      `json:\"code\"`\n\tData *Data[T] `json:\"data\"`\n}\n\ntype Data[T any] struct {\n\tTotalRecords int `json:\"TotalRecords\"`\n\tRecords      []T `json:\"Records\"`\n}\n"
  },
  {
    "path": "providers/dns/rainyun/rainyun.go",
    "content": "// Package rainyun implements a DNS provider for solving the DNS-01 challenge using Rain Yun.\npackage rainyun\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/rainyun/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"RAINYUN_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Rain Yun.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rainyun: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Rain Yun.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"rainyun: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.APIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rainyun: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rainyun: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rainyun: %w\", err)\n\t}\n\n\tdomainID, err := d.findDomainID(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rainyun: find domain ID: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tHost:     subDomain,\n\t\tPriority: 10,\n\t\tLine:     \"DEFAULT\",\n\t\tTTL:      d.config.TTL,\n\t\tType:     \"TXT\",\n\t\tValue:    info.Value,\n\t}\n\n\terr = d.client.AddRecord(ctx, domainID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rainyun: add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rainyun: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tdomainID, err := d.findDomainID(ctx, dns01.UnFqdn(authZone))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rainyun: find domain ID: %w\", err)\n\t}\n\n\trecordID, err := d.findRecordID(ctx, domainID, info)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rainyun: find record ID: %w\", err)\n\t}\n\n\terr = d.client.DeleteRecord(ctx, domainID, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rainyun: delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) findDomainID(ctx context.Context, domain string) (int, error) {\n\tdomains, err := d.client.ListDomains(ctx)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tfor _, dom := range domains {\n\t\tif dom.Domain == domain {\n\t\t\treturn dom.ID, nil\n\t\t}\n\t}\n\n\treturn 0, fmt.Errorf(\"domain not found: %s\", domain)\n}\n\nfunc (d *DNSProvider) findRecordID(ctx context.Context, domainID int, info dns01.ChallengeInfo) (int, error) {\n\trecords, err := d.client.ListRecords(ctx, domainID)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"list records: %w\", err)\n\t}\n\n\tzone := dns01.UnFqdn(info.EffectiveFQDN)\n\n\tfor _, record := range records {\n\t\tif strings.HasPrefix(zone, record.Host) && record.Value == info.Value {\n\t\t\treturn record.ID, nil\n\t\t}\n\t}\n\n\treturn 0, fmt.Errorf(\"record not found: domainID=%d, fqdn=%s\", domainID, info.EffectiveFQDN)\n}\n"
  },
  {
    "path": "providers/dns/rainyun/rainyun.toml",
    "content": "Name = \"Rain Yun/雨云\"\nDescription = ''''''\nURL = \"https://www.rainyun.com\"\nCode = \"rainyun\"\nSince = \"v4.21.0\"\n\nExample = '''\nRAINYUN_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns rainyun -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    RAINYUN_API_KEY = \"API key\"\n  [Configuration.Additional]\n    RAINYUN_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    RAINYUN_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    RAINYUN_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    RAINYUN_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.apifox.cn/apidoc/shared-a4595cc8-44c5-4678-a2a3-eed7738dab03/api-151416609\"\n"
  },
  {
    "path": "providers/dns/rainyun/rainyun_test.go",
    "content": "package rainyun\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"rainyun: some credentials information are missing: RAINYUN_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"rainyun: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/rcodezero/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/miekg/dns\"\n)\n\nconst defaultBaseURL = \"https://my.rcodezero.at/api\"\n\nconst authorizationHeader = \"Authorization\"\n\n// Client for the RcodeZero API.\ntype Client struct {\n\tapiToken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiToken string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiToken:   apiToken,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\nfunc (c *Client) UpdateRecords(ctx context.Context, authZone string, sets []UpdateRRSet) (*APIResponse, error) {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"acme\", \"zones\", strings.TrimSuffix(dns.Fqdn(authZone), \".\"), \"rrsets\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPatch, endpoint, sets)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn c.do(req)\n}\n\nfunc (c *Client) do(req *http.Request) (*APIResponse, error) {\n\treq.Header.Set(authorizationHeader, \"Bearer \"+c.apiToken)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn nil, parseError(req, resp)\n\t}\n\n\tresult := &APIResponse{}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn result, nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\terrAPI := &APIResponse{}\n\n\terr := json.Unmarshal(raw, errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code: %d] %w\", resp.StatusCode, errAPI)\n}\n"
  },
  {
    "path": "providers/dns/rcodezero/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"secret\")\n\tclient.HTTPClient = server.Client()\n\tclient.baseURL, _ = url.Parse(server.URL)\n\n\treturn client, nil\n}\n\nfunc TestClient_UpdateRecords_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithJSONHeaders()).\n\t\tRoute(\"PATCH /v1/acme/zones/example.org/rrsets\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnprocessableEntity)).\n\t\tBuild(t)\n\n\trrSet := []UpdateRRSet{{\n\t\tName:       \"acme.example.org.\",\n\t\tChangeType: \"add\",\n\t\tType:       \"TXT\",\n\t\tRecords:    []Record{{Content: `\"my-acme-challenge\"`}},\n\t}}\n\n\tresp, err := client.UpdateRecords(t.Context(), \"example.org\", rrSet)\n\trequire.ErrorAs(t, err, new(*APIResponse))\n\tassert.Nil(t, resp)\n}\n\nfunc TestClient_UpdateRecords(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithJSONHeaders()).\n\t\tRoute(\"PATCH /v1/acme/zones/example.org/rrsets\",\n\t\t\tservermock.ResponseFromFixture(\"rrsets-response.json\")).\n\t\tBuild(t)\n\n\trrSet := []UpdateRRSet{{\n\t\tName:       \"acme.example.org.\",\n\t\tChangeType: \"add\",\n\t\tType:       \"TXT\",\n\t\tRecords:    []Record{{Content: `\"my-acme-challenge\"`}},\n\t}}\n\n\tresp, err := client.UpdateRecords(t.Context(), \"example.org\", rrSet)\n\trequire.NoError(t, err)\n\n\texpected := &APIResponse{Status: \"ok\", Message: \"RRsets updated\"}\n\n\tassert.Equal(t, expected, resp)\n}\n"
  },
  {
    "path": "providers/dns/rcodezero/internal/fixtures/error.json",
    "content": "{\n  \"status\": \"failed\",\n  \"message\": \"A human readable error message\"\n}\n"
  },
  {
    "path": "providers/dns/rcodezero/internal/fixtures/rrsets-response.json",
    "content": "{\n  \"status\": \"ok\",\n  \"message\": \"RRsets updated\"\n}\n"
  },
  {
    "path": "providers/dns/rcodezero/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype UpdateRRSet struct {\n\tName       string   `json:\"name\"`\n\tType       string   `json:\"type\"`\n\tChangeType string   `json:\"changetype\"`\n\tRecords    []Record `json:\"records\"`\n\tTTL        int      `json:\"ttl\"`\n}\n\ntype Record struct {\n\tContent  string `json:\"content\"`\n\tDisabled bool   `json:\"disabled\"`\n}\n\ntype APIResponse struct {\n\tStatus  string `json:\"status\"`\n\tMessage string `json:\"message\"`\n}\n\nfunc (a APIResponse) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", a.Status, a.Message)\n}\n"
  },
  {
    "path": "providers/dns/rcodezero/rcodezero.go",
    "content": "// Package rcodezero implements a DNS provider for solving the DNS-01 challenge using RcodeZero Anycast network.\npackage rcodezero\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/rcodezero/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"RCODEZERO_\"\n\n\tEnvAPIToken = envNamespace + \"API_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIToken           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 4*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for RcodeZero.\n// Credentials must be passed in the environment variable:\n// RCODEZERO_API_URL and RCODEZERO_API_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rcodezero: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIToken = values[EnvAPIToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for RcodeZero.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"rcodezero: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIToken == \"\" {\n\t\treturn nil, errors.New(\"rcodezero: API token missing\")\n\t}\n\n\tclient := internal.NewClient(config.APIToken)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rcodezero: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trrSet := []internal.UpdateRRSet{{\n\t\tName:       info.EffectiveFQDN,\n\t\tChangeType: \"update\",\n\t\tType:       \"TXT\",\n\t\tTTL:        d.config.TTL,\n\t\tRecords:    []internal.Record{{Content: `\"` + info.Value + `\"`}},\n\t}}\n\n\t_, err = d.client.UpdateRecords(ctx, authZone, rrSet)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rcodezero: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rcodezero: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trrSet := []internal.UpdateRRSet{{\n\t\tName:       info.EffectiveFQDN,\n\t\tType:       \"TXT\",\n\t\tChangeType: \"delete\",\n\t}}\n\n\t_, err = d.client.UpdateRecords(ctx, authZone, rrSet)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rcodezero: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/rcodezero/rcodezero.toml",
    "content": "Name = \"RcodeZero\"\nDescription = ''''''\nURL = \"https://www.rcodezero.at/\"\nCode = \"rcodezero\"\nSince = \"v4.13\"\n\nExample = '''\nRCODEZERO_API_TOKEN=<mytoken> \\\nlego --dns rcodezero -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Description\n\nGenerate your API Token via https://my.rcodezero.at with the `ACME` permissions.\nThese are special tokens with limited access for ACME requests only.\n\nRcodeZero is an Anycast Network so the distribution of the DNS01-Challenge can take up to 2 minutes.\n\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    RCODEZERO_API_TOKEN = \"API token\"\n  [Configuration.Additional]\n    RCODEZERO_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    RCODEZERO_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 240)\"\n    RCODEZERO_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    RCODEZERO_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  # Note: the API endpoint used inside the client is not documented.\n  API = \"https://my.rcodezero.at/openapi\"\n"
  },
  {
    "path": "providers/dns/rcodezero/rcodezero_test.go",
    "content": "package rcodezero\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAPIToken).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"\",\n\t\t\t},\n\t\t\texpected: \"rcodezero: some credentials information are missing: RCODEZERO_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiToken string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiToken: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"rcodezero: API token missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = test.apiToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresentAndCleanup(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/regfish/regfish.go",
    "content": "// Package regfish implements a DNS provider for solving the DNS-01 challenge using Regfish.\npackage regfish\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\tregfishapi \"github.com/regfish/regfish-dnsapi-go\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"REGFISH_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *regfishapi.Client\n\n\trecordIDs   map[string]int\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Regfish.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"regfish: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Regfish.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"regfish: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"regfish: credentials missing\")\n\t}\n\n\tclient := regfishapi.NewClient(config.APIKey)\n\n\tif config.HTTPClient != nil {\n\t\tclient.Client = config.HTTPClient\n\t} else {\n\t\t// Because the regfishapi.NewClient uses an empty http.Client.\n\t\tclient.Client = &http.Client{Timeout: 30 * time.Second}\n\t}\n\n\tclient.Client = clientdebug.Wrap(client.Client)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]int),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecord := regfishapi.Record{\n\t\tName: info.EffectiveFQDN,\n\t\tType: \"TXT\",\n\t\tData: info.Value,\n\t\tTTL:  d.config.TTL,\n\t}\n\n\tnewRecord, err := d.client.CreateRecord(record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"regfish: create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = newRecord.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// get the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"regfish: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\terr := d.client.DeleteRecord(recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"regfish: delete record: %w\", err)\n\t}\n\n\t// Delete record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/regfish/regfish.toml",
    "content": "Name = \"Regfish\"\nDescription = ''''''\nURL = \"https://regfish.de/\"\nCode = \"regfish\"\nSince = \"v4.20.0\"\n\nExample = '''\nREGFISH_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns regfish -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    REGFISH_API_KEY = \"API key\"\n  [Configuration.Additional]\n    REGFISH_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    REGFISH_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    REGFISH_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    REGFISH_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://regfish.readme.io/\"\n  GoClient = \"https://github.com/regfish/regfish-dnsapi-go\"\n"
  },
  {
    "path": "providers/dns/regfish/regfish_test.go",
    "content": "package regfish\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"regfish: some credentials information are missing: REGFISH_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"regfish: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/regru/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api.reg.ru/api/regru2/\"\n\n// Client the reg.ru client.\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a reg.ru client.\nfunc NewClient(username, password string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// RemoveTxtRecord removes a TXT record.\n// https://www.reg.ru/support/help/api2#zone_remove_record\nfunc (c *Client) RemoveTxtRecord(ctx context.Context, domain, subDomain, content string) error {\n\trequest := RemoveRecordRequest{\n\t\tDomains:           []Domain{{DName: domain}},\n\t\tSubDomain:         subDomain,\n\t\tContent:           content,\n\t\tRecordType:        \"TXT\",\n\t\tOutputContentType: \"plain\",\n\t}\n\n\tresp, err := c.doRequest(ctx, request, \"zone\", \"remove_record\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn resp.HasError()\n}\n\n// AddTXTRecord adds a TXT record.\n// https://www.reg.ru/support/help/api2#zone_add_txt\nfunc (c *Client) AddTXTRecord(ctx context.Context, domain, subDomain, content string) error {\n\trequest := AddTxtRequest{\n\t\tDomains:           []Domain{{DName: domain}},\n\t\tSubDomain:         subDomain,\n\t\tText:              content,\n\t\tOutputContentType: \"plain\",\n\t}\n\n\tresp, err := c.doRequest(ctx, request, \"zone\", \"add_txt\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn resp.HasError()\n}\n\nfunc (c *Client) doRequest(ctx context.Context, request any, fragments ...string) (*APIResponse, error) {\n\tendpoint := c.baseURL.JoinPath(fragments...)\n\n\tinputData, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create input data: %w\", err)\n\t}\n\n\tdata := url.Values{}\n\tdata.Set(\"username\", c.username)\n\tdata.Set(\"password\", c.password)\n\tdata.Set(\"input_data\", string(inputData))\n\tdata.Set(\"input_format\", \"json\")\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(data.Encode()))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Add(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn nil, parseError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar apiResp APIResponse\n\n\terr = json.Unmarshal(raw, &apiResp)\n\tif err != nil {\n\t\treturn nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn &apiResp, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIResponse\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"status code: %d, %w\", resp.StatusCode, errAPI)\n}\n"
  },
  {
    "path": "providers/dns/regru/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded(),\n\t)\n}\n\nfunc TestRemoveRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zone/remove_record\",\n\t\t\tservermock.ResponseFromFixture(\"remove_record.json\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"input_data\", `{\"domains\":[{\"dname\":\"test.ru\"}],\"subdomain\":\"_acme-challenge\",\"content\":\"txttxttxt\",\"record_type\":\"TXT\",\"output_content_type\":\"plain\"}`).\n\t\t\t\tWith(\"username\", \"user\").\n\t\t\t\tWith(\"password\", \"secret\").\n\t\t\t\tWith(\"input_format\", \"json\")).\n\t\tBuild(t)\n\n\terr := client.RemoveTxtRecord(t.Context(), \"test.ru\", \"_acme-challenge\", \"txttxttxt\")\n\trequire.NoError(t, err)\n}\n\nfunc TestRemoveRecord_errors(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tdomain   string\n\t\tresponse string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"authentication failed\",\n\t\t\tdomain:   \"test.ru\",\n\t\t\tresponse: \"remove_record_error_auth.json\",\n\t\t\texpected: \"API error: NO_AUTH: No authorization mechanism selected\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"domain error\",\n\t\t\tdomain:   \"\",\n\t\t\tresponse: \"remove_record_error_domain.json\",\n\t\t\texpected: \"API error: NO_DOMAIN: domain_name not given or empty\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(\"POST /zone/remove_record\", servermock.ResponseFromFixture(test.response)).\n\t\t\t\tBuild(t)\n\n\t\t\terr := client.RemoveTxtRecord(t.Context(), test.domain, \"_acme-challenge\", \"txttxttxt\")\n\t\t\trequire.EqualError(t, err, test.expected)\n\t\t})\n\t}\n}\n\nfunc TestAddTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zone/add_txt\",\n\t\t\tservermock.ResponseFromFixture(\"add_txt_record.json\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"input_data\", `{\"domains\":[{\"dname\":\"test.ru\"}],\"subdomain\":\"_acme-challenge\",\"text\":\"txttxttxt\",\"output_content_type\":\"plain\"}`).\n\t\t\t\tWith(\"username\", \"user\").\n\t\t\t\tWith(\"password\", \"secret\").\n\t\t\t\tWith(\"input_format\", \"json\")).\n\t\tBuild(t)\n\n\terr := client.AddTXTRecord(t.Context(), \"test.ru\", \"_acme-challenge\", \"txttxttxt\")\n\trequire.NoError(t, err)\n}\n\nfunc TestAddTXTRecord_errors(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tdomain   string\n\t\tresponse string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"authentication failed\",\n\t\t\tdomain:   \"test.ru\",\n\t\t\tresponse: \"add_txt_record_error_auth.json\",\n\t\t\texpected: \"API error: NO_AUTH: No authorization mechanism selected\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"domain error\",\n\t\t\tdomain:   \"\",\n\t\t\tresponse: \"add_txt_record_error_domain.json\",\n\t\t\texpected: \"API error: NO_DOMAIN: domain_name not given or empty\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(\"POST /zone/add_txt\", servermock.ResponseFromFixture(test.response)).\n\t\t\t\tBuild(t)\n\n\t\t\terr := client.AddTXTRecord(t.Context(), test.domain, \"_acme-challenge\", \"txttxttxt\")\n\t\t\trequire.EqualError(t, err, test.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/regru/internal/fixtures/add_txt_record.json",
    "content": "{\n  \"answer\": {\n    \"domains\": [\n      {\n        \"dname\": \"test.ru\",\n        \"result\": \"success\",\n        \"service_id\": 12345\n      }\n    ]\n  },\n  \"charset\": \"utf-8\",\n  \"messagestore\": null,\n  \"result\": \"success\"\n}\n"
  },
  {
    "path": "providers/dns/regru/internal/fixtures/add_txt_record_error_auth.json",
    "content": "{\n  \"charset\": \"utf-8\",\n  \"error_code\": \"NO_AUTH\",\n  \"error_params\": {\n    \"command_name\": \"nop/zone/add_txt\"\n  },\n  \"error_text\": \"No authorization mechanism selected\",\n  \"messagestore\": null,\n  \"result\": \"error\"\n}\n"
  },
  {
    "path": "providers/dns/regru/internal/fixtures/add_txt_record_error_domain.json",
    "content": "{\n  \"answer\": {\n    \"domains\": [\n      {\n        \"error_code\": \"NO_DOMAIN\",\n        \"error_text\": \"domain_name not given or empty\",\n        \"result\": \"error\"\n      }\n    ]\n  },\n  \"charset\": \"utf-8\",\n  \"messagestore\": null,\n  \"result\": \"success\"\n}\n"
  },
  {
    "path": "providers/dns/regru/internal/fixtures/remove_record.json",
    "content": "{\n  \"answer\": {\n    \"domains\": [\n      {\n        \"dname\": \"test.ru\",\n        \"result\": \"success\",\n        \"service_id\": 12345\n      }\n    ]\n  },\n  \"charset\": \"utf-8\",\n  \"messagestore\": null,\n  \"result\": \"success\"\n}\n"
  },
  {
    "path": "providers/dns/regru/internal/fixtures/remove_record_error_auth.json",
    "content": "{\n  \"charset\" : \"utf-8\",\n  \"error_code\" : \"NO_AUTH\",\n  \"error_params\" : {\n    \"command_name\" : \"nop/zone/remove_record\"\n  },\n  \"error_text\" : \"No authorization mechanism selected\",\n  \"messagestore\" : null,\n  \"result\" : \"error\"\n}\n"
  },
  {
    "path": "providers/dns/regru/internal/fixtures/remove_record_error_domain.json",
    "content": "{\n  \"answer\" : {\n    \"domains\" : [\n      {\n        \"error_code\" : \"NO_DOMAIN\",\n        \"error_text\" : \"domain_name not given or empty\",\n        \"result\" : \"error\"\n      }\n    ]\n  },\n  \"charset\" : \"utf-8\",\n  \"messagestore\" : null,\n  \"result\" : \"success\"\n}\n"
  },
  {
    "path": "providers/dns/regru/internal/readme.md",
    "content": "Test account (with the default endpoint):\n- user: `test`\n- password: `test`\n\nNoop endpoint:\n- https://api.reg.ru/api/regru2/nop\n"
  },
  {
    "path": "providers/dns/regru/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\nconst successResult = \"success\"\n\n// APIResponse is the representation of an API response.\ntype APIResponse struct {\n\tResult string `json:\"result\"`\n\n\tAnswer *Answer `json:\"answer,omitempty\"`\n\n\tErrorCode string `json:\"error_code,omitempty\"`\n\tErrorText string `json:\"error_text,omitempty\"`\n}\n\nfunc (a APIResponse) Error() string {\n\treturn fmt.Sprintf(\"API %s: %s: %s\", a.Result, a.ErrorCode, a.ErrorText)\n}\n\n// HasError returns an error is the response contains an error.\nfunc (a APIResponse) HasError() error {\n\tif a.Result != successResult {\n\t\treturn a\n\t}\n\n\tif a.Answer != nil {\n\t\tfor _, domResp := range a.Answer.Domains {\n\t\t\tif domResp.Result != successResult {\n\t\t\t\treturn domResp\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Answer is the representation of an API response answer.\ntype Answer struct {\n\tDomains []DomainResponse `json:\"domains,omitempty\"`\n}\n\n// DomainResponse is the representation of an API response answer domain.\ntype DomainResponse struct {\n\tResult string `json:\"result\"`\n\n\tDName string `json:\"dname\"`\n\n\tErrorCode string `json:\"error_code,omitempty\"`\n\tErrorText string `json:\"error_text,omitempty\"`\n}\n\nfunc (d DomainResponse) Error() string {\n\treturn fmt.Sprintf(\"API %s: %s: %s\", d.Result, d.ErrorCode, d.ErrorText)\n}\n\n// AddTxtRequest is the representation of the payload of a request to add a TXT record.\ntype AddTxtRequest struct {\n\tDomains           []Domain `json:\"domains,omitempty\"`\n\tSubDomain         string   `json:\"subdomain,omitempty\"`\n\tText              string   `json:\"text,omitempty\"`\n\tOutputContentType string   `json:\"output_content_type,omitempty\"`\n}\n\n// RemoveRecordRequest is the representation of the payload of a request to remove a record.\ntype RemoveRecordRequest struct {\n\tDomains           []Domain `json:\"domains,omitempty\"`\n\tSubDomain         string   `json:\"subdomain,omitempty\"`\n\tContent           string   `json:\"content,omitempty\"`\n\tRecordType        string   `json:\"record_type,omitempty\"`\n\tOutputContentType string   `json:\"output_content_type,omitempty\"`\n}\n\n// Domain is the representation of a Domain.\ntype Domain struct {\n\tDName string `json:\"dname\"`\n}\n"
  },
  {
    "path": "providers/dns/regru/regru.go",
    "content": "// Package regru implements a DNS provider for solving the DNS-01 challenge using reg.ru DNS.\npackage regru\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/regru/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"REGRU_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\tEnvTLSCert  = envNamespace + \"TLS_CERT\"\n\tEnvTLSKey   = envNamespace + \"TLS_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername string\n\tPassword string\n\tTLSCert  string\n\tTLSKey   string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for reg.ru.\n// Credentials must be passed in the environment variables:\n// REGRU_USERNAME and REGRU_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"regru: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\tconfig.TLSCert = env.GetOrDefaultString(EnvTLSCert, \"\")\n\tconfig.TLSKey = env.GetOrDefaultString(EnvTLSKey, \"\")\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for reg.ru.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"regru: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Username == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"regru: incomplete credentials, missing username and/or password\")\n\t}\n\n\tclient := internal.NewClient(config.Username, config.Password)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\tif config.TLSCert != \"\" || config.TLSKey != \"\" {\n\t\tif config.TLSCert == \"\" {\n\t\t\treturn nil, errors.New(\"regru: TLS certificate is missing\")\n\t\t}\n\n\t\tif config.TLSKey == \"\" {\n\t\t\treturn nil, errors.New(\"regru: TLS key is missing\")\n\t\t}\n\n\t\ttlsCert, err := tls.X509KeyPair([]byte(config.TLSCert), []byte(config.TLSKey))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"regru: %w\", err)\n\t\t}\n\n\t\tclient.HTTPClient.Transport = &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{\n\t\t\t\tCertificates: []tls.Certificate{tlsCert},\n\t\t\t},\n\t\t}\n\t}\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"regru: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"regru: %w\", err)\n\t}\n\n\terr = d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(authZone), subDomain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"regru: failed to create TXT records [domain: %s, sub domain: %s]: %w\",\n\t\t\tdns01.UnFqdn(authZone), subDomain, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"regru: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"regru: %w\", err)\n\t}\n\n\terr = d.client.RemoveTxtRecord(context.Background(), dns01.UnFqdn(authZone), subDomain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"regru: failed to remove TXT records [domain: %s, sub domain: %s]: %w\",\n\t\t\tdns01.UnFqdn(authZone), subDomain, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/regru/regru.toml",
    "content": "Name = \"reg.ru\"\nDescription = ''''''\nURL = \"https://www.reg.ru/\"\nCode = \"regru\"\nSince = \"v3.5.0\"\n\nExample = '''\nREGRU_USERNAME=xxxxxx \\\nREGRU_PASSWORD=yyyyyy \\\nlego --dns regru -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    REGRU_USERNAME = \"API username\"\n    REGRU_PASSWORD = \"API password\"\n  [Configuration.Additional]\n    REGRU_TLS_CERT = \"authentication certificate\"\n    REGRU_TLS_KEY = \"authentication private key\"\n    REGRU_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    REGRU_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    REGRU_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    REGRU_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.reg.ru/support/help/api2\"\n"
  },
  {
    "path": "providers/dns/regru/regru_test.go",
    "content": "package regru\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvUsername,\n\tEnvPassword).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvPassword: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"regru: some credentials information are missing: REGRU_USERNAME,REGRU_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"api_password\",\n\t\t\t},\n\t\t\texpected: \"regru: some credentials information are missing: REGRU_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"api_username\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"regru: some credentials information are missing: REGRU_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"api_username\",\n\t\t\tpassword: \"api_password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"regru: incomplete credentials, missing username and/or password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tusername: \"\",\n\t\t\tpassword: \"api_password\",\n\t\t\texpected: \"regru: incomplete credentials, missing username and/or password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tusername: \"api_username\",\n\t\t\tpassword: \"\",\n\t\t\texpected: \"regru: incomplete credentials, missing username and/or password\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/rfc2136/internal/fixtures/invalid_field.conf",
    "content": "key \"example.com\" {\n\talgorithm;\n\tsecret \"TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg=\";\n};\n"
  },
  {
    "path": "providers/dns/rfc2136/internal/fixtures/invalid_key.conf",
    "content": "key {\n\talgorithm hmac-sha256;\n\tsecret \"TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg=\";\n};\n"
  },
  {
    "path": "providers/dns/rfc2136/internal/fixtures/mising_algo.conf",
    "content": "key \"example.com\" {\n\tsecret \"TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg=\";\n};\n"
  },
  {
    "path": "providers/dns/rfc2136/internal/fixtures/missing_secret.conf",
    "content": "key \"example.com\" {\n\talgorithm hmac-sha256;\n};\n"
  },
  {
    "path": "providers/dns/rfc2136/internal/fixtures/sample.conf",
    "content": "key \"example.com\" {\n\talgorithm hmac-sha256;\n\tsecret \"TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg=\";\n};\n"
  },
  {
    "path": "providers/dns/rfc2136/internal/fixtures/text_after.conf",
    "content": "key \"example.com\" {\n\talgorithm hmac-sha256;\n\tsecret \"TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg=\";\n};\n\nkey \"example.org\" {\n        algorithm hmac-sha512;\n        secret \"v6CkK3gop6HXj4+dcWiLXLGSYKVY5J1cTMjDsdl/Ah9B8aWfTgjwFBoHHyiHWSyvwWPDuEIRs2Pqm8nedca4+g==\";\n};\n"
  },
  {
    "path": "providers/dns/rfc2136/internal/fixtures/text_before.conf",
    "content": "foo {\n    bar example;\n};\n\nkey \"example.com\" {\n\talgorithm hmac-sha256;\n\tsecret \"TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg=\";\n};\n"
  },
  {
    "path": "providers/dns/rfc2136/internal/readme.md",
    "content": "# TSIG Key File\n\nHow to generate example:\n\n```console\n$ docker run --rm -it -v $(pwd):/app -w /app alpine sh\n/app # apk add bind\n/app # tsig-keygen example.com > sample1.conf\n/app # tsig-keygen -a hmac-sha512 example.com > sample2.conf\n```\n"
  },
  {
    "path": "providers/dns/rfc2136/internal/tsigkey.go",
    "content": "package internal\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\ntype Key struct {\n\tName      string\n\tAlgorithm string\n\tSecret    string\n}\n\n// ReadTSIGFile reads TSIG key file generated with `tsig-keygen`.\nfunc ReadTSIGFile(filename string) (*Key, error) {\n\tfile, err := os.Open(filename)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open file: %w\", err)\n\t}\n\n\tdefer func() { _ = file.Close() }()\n\n\tkey := &Key{}\n\n\tvar read bool\n\n\tscanner := bufio.NewScanner(file)\n\tfor scanner.Scan() {\n\t\tline := strings.TrimSpace(strings.TrimSuffix(scanner.Text(), \";\"))\n\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif read && line == \"}\" {\n\t\t\tbreak\n\t\t}\n\n\t\tfields := strings.Fields(line)\n\n\t\tswitch {\n\t\tcase fields[0] == \"key\":\n\t\t\tread = true\n\n\t\t\tif len(fields) != 3 {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid key line: %s\", line)\n\t\t\t}\n\n\t\t\tkey.Name = safeUnquote(fields[1])\n\n\t\tcase !read:\n\t\t\tcontinue\n\n\t\tdefault:\n\t\t\tif len(fields) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tv := safeUnquote(fields[1])\n\n\t\t\tswitch safeUnquote(fields[0]) {\n\t\t\tcase \"algorithm\":\n\t\t\t\tkey.Algorithm = v\n\t\t\tcase \"secret\":\n\t\t\t\tkey.Secret = v\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n\n\treturn key, nil\n}\n\nfunc safeUnquote(v string) string {\n\tif len(v) < 2 {\n\t\t// empty or single character string\n\t\treturn v\n\t}\n\n\tif v[0] == '\"' && v[len(v)-1] == '\"' {\n\t\t// string wrapped in quotes\n\t\treturn v[1 : len(v)-1]\n\t}\n\n\treturn v\n}\n"
  },
  {
    "path": "providers/dns/rfc2136/internal/tsigkey_test.go",
    "content": "package internal\n\nimport (\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestReadTSIGFile(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tfilename string\n\t\texpected *Key\n\t}{\n\t\t{\n\t\t\tdesc:     \"basic\",\n\t\t\tfilename: \"sample.conf\",\n\t\t\texpected: &Key{Name: \"example.com\", Algorithm: \"hmac-sha256\", Secret: \"TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg=\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"data before the key\",\n\t\t\tfilename: \"text_before.conf\",\n\t\t\texpected: &Key{Name: \"example.com\", Algorithm: \"hmac-sha256\", Secret: \"TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg=\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"data after the key\",\n\t\t\tfilename: \"text_after.conf\",\n\t\t\texpected: &Key{Name: \"example.com\", Algorithm: \"hmac-sha256\", Secret: \"TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg=\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"ignore missing secret\",\n\t\t\tfilename: \"missing_secret.conf\",\n\t\t\texpected: &Key{Name: \"example.com\", Algorithm: \"hmac-sha256\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"ignore missing algorithm\",\n\t\t\tfilename: \"mising_algo.conf\",\n\t\t\texpected: &Key{Name: \"example.com\", Secret: \"TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg=\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"ignore invalid field format\",\n\t\t\tfilename: \"invalid_field.conf\",\n\t\t\texpected: &Key{Name: \"example.com\", Secret: \"TCG5A6/lOHUGbW0e/9RYYbzWDFMlj1pIxCvybLBayBg=\"},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tkey, err := ReadTSIGFile(filepath.Join(\"fixtures\", test.filename))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.expected, key)\n\t\t})\n\t}\n}\n\nfunc TestReadTSIGFile_error(t *testing.T) {\n\tif runtime.GOOS != \"linux\" {\n\t\t// Because error messages are different on Windows.\n\t\tt.Skip(\"only for UNIX systems\")\n\t}\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tfilename string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"missing file\",\n\t\t\tfilename: \"missing.conf\",\n\t\t\texpected: \"open file: open fixtures/missing.conf: no such file or directory\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"invalid key format\",\n\t\t\tfilename: \"invalid_key.conf\",\n\t\t\texpected: \"invalid key line: key {\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t_, err := ReadTSIGFile(filepath.Join(\"fixtures\", test.filename))\n\t\t\trequire.Error(t, err)\n\n\t\t\trequire.EqualError(t, err, test.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/rfc2136/rfc2136.go",
    "content": "// Package rfc2136 implements a DNS provider for solving the DNS-01 challenge using the rfc2136 dynamic update.\npackage rfc2136\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/rfc2136/internal\"\n\t\"github.com/miekg/dns\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"RFC2136_\"\n\n\tEnvTSIGFile = envNamespace + \"TSIG_FILE\"\n\n\tEnvTSIGKey       = envNamespace + \"TSIG_KEY\"\n\tEnvTSIGSecret    = envNamespace + \"TSIG_SECRET\"\n\tEnvTSIGAlgorithm = envNamespace + \"TSIG_ALGORITHM\"\n\n\tEnvNameserver = envNamespace + \"NAMESERVER\"\n\tEnvDNSTimeout = envNamespace + \"DNS_TIMEOUT\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tNameserver string\n\n\tTSIGFile string\n\n\tTSIGAlgorithm string\n\tTSIGKey       string\n\tTSIGSecret    string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tSequenceInterval   time.Duration\n\tDNSTimeout         time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTSIGAlgorithm:      env.GetOrDefaultString(EnvTSIGAlgorithm, dns.HmacSHA1),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, env.GetOrDefaultSecond(\"RFC2136_TIMEOUT\", dns01.DefaultPropagationTimeout)),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tDNSTimeout:         env.GetOrDefaultSecond(EnvDNSTimeout, 10*time.Second),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for rfc2136\n// dynamic update. Configured with environment variables:\n// RFC2136_NAMESERVER: Network address in the form \"host\" or \"host:port\".\n// RFC2136_TSIG_ALGORITHM: Defaults to hmac-md5.sig-alg.reg.int. (HMAC-MD5).\n// See https://github.com/miekg/dns/blob/master/tsig.go for supported values.\n// RFC2136_TSIG_KEY: Name of the secret key as defined in DNS server configuration.\n// RFC2136_TSIG_SECRET: Secret key payload.\n// RFC2136_PROPAGATION_TIMEOUT: DNS propagation timeout in time.ParseDuration format. (60s)\n// To disable TSIG authentication, leave the RFC2136_TSIG* variables unset.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvNameserver)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rfc2136: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Nameserver = values[EnvNameserver]\n\n\tconfig.TSIGFile = env.GetOrDefaultString(EnvTSIGFile, \"\")\n\n\tconfig.TSIGKey = env.GetOrFile(EnvTSIGKey)\n\tconfig.TSIGSecret = env.GetOrFile(EnvTSIGSecret)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for rfc2136.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"rfc2136: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Nameserver == \"\" {\n\t\treturn nil, errors.New(\"rfc2136: nameserver missing\")\n\t}\n\n\tif config.TSIGFile != \"\" {\n\t\tkey, err := internal.ReadTSIGFile(config.TSIGFile)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"rfc2136: read TSIG file %s: %w\", config.TSIGFile, err)\n\t\t}\n\n\t\tconfig.TSIGAlgorithm = key.Algorithm\n\t\tconfig.TSIGKey = key.Name\n\t\tconfig.TSIGSecret = key.Secret\n\t}\n\n\t// Append the default DNS port if none is specified.\n\tif _, _, err := net.SplitHostPort(config.Nameserver); err != nil {\n\t\tif strings.Contains(err.Error(), \"missing port\") {\n\t\t\tconfig.Nameserver = net.JoinHostPort(config.Nameserver, \"53\")\n\t\t} else {\n\t\t\treturn nil, fmt.Errorf(\"rfc2136: %w\", err)\n\t\t}\n\t}\n\n\tif config.TSIGKey == \"\" || config.TSIGSecret == \"\" {\n\t\tconfig.TSIGKey = \"\"\n\t\tconfig.TSIGSecret = \"\"\n\t} else {\n\t\t// zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2)\n\t\tconfig.TSIGKey = dns.CanonicalName(config.TSIGKey)\n\t}\n\n\tif config.TSIGAlgorithm == \"\" {\n\t\tconfig.TSIGAlgorithm = dns.HmacSHA1\n\t} else {\n\t\t// To be compatible with https://github.com/miekg/dns/blob/master/tsig.go\n\t\tconfig.TSIGAlgorithm = dns.Fqdn(config.TSIGAlgorithm)\n\t}\n\n\tswitch config.TSIGAlgorithm {\n\tcase dns.HmacSHA1, dns.HmacSHA224, dns.HmacSHA256, dns.HmacSHA384, dns.HmacSHA512:\n\t\t// valid algorithm\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"rfc2136: unsupported TSIG algorithm: %s\", config.TSIGAlgorithm)\n\t}\n\n\treturn &DNSProvider{config: config}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.changeRecord(\"INSERT\", info.EffectiveFQDN, info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rfc2136: failed to insert: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.changeRecord(\"REMOVE\", info.EffectiveFQDN, info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rfc2136: failed to remove: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {\n\t// Find the zone for the given fqdn\n\tzone, err := dns01.FindZoneByFqdnCustom(fqdn, []string{d.config.Nameserver})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Create RR\n\trrs := []dns.RR{&dns.TXT{\n\t\tHdr: dns.RR_Header{Name: fqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(ttl)},\n\t\tTxt: []string{value},\n\t}}\n\n\t// Create dynamic update packet\n\tm := new(dns.Msg).SetUpdate(zone)\n\n\tswitch action {\n\tcase \"INSERT\":\n\t\t// Always remove old challenge left over from who knows what.\n\t\tm.RemoveRRset(rrs)\n\t\tm.Insert(rrs)\n\tcase \"REMOVE\":\n\t\tm.Remove(rrs)\n\tdefault:\n\t\treturn fmt.Errorf(\"unexpected action: %s\", action)\n\t}\n\n\t// Setup client\n\tc := &dns.Client{Timeout: d.config.DNSTimeout}\n\n\t// TSIG authentication / msg signing\n\tif d.config.TSIGKey != \"\" && d.config.TSIGSecret != \"\" {\n\t\tm.SetTsig(d.config.TSIGKey, d.config.TSIGAlgorithm, 300, time.Now().Unix())\n\n\t\t// Secret(s) for TSIG map[<zonename>]<base64 secret>.\n\t\tc.TsigSecret = map[string]string{d.config.TSIGKey: d.config.TSIGSecret}\n\t}\n\n\t// Send the query\n\treply, _, err := c.Exchange(m, d.config.Nameserver)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"DNS update failed: %w\", err)\n\t}\n\n\tif reply != nil && reply.Rcode != dns.RcodeSuccess {\n\t\treturn fmt.Errorf(\"DNS update failed: server replied: %s\", dns.RcodeToString[reply.Rcode])\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/rfc2136/rfc2136.toml",
    "content": "Name = \"RFC2136\"\nDescription = ''''''\nURL = \"https://www.rfc-editor.org/rfc/rfc2136.html\"\nCode = \"rfc2136\"\nSince = \"v0.3.0\"\n\nExample = '''\nRFC2136_NAMESERVER=127.0.0.1 \\\nRFC2136_TSIG_KEY=example.com \\\nRFC2136_TSIG_ALGORITHM=hmac-sha256. \\\nRFC2136_TSIG_SECRET=YWJjZGVmZGdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNDU= \\\nlego --dns rfc2136 -d '*.example.com' -d example.com run\n\n## ---\n\nkeyname=example.com; keyfile=example.com.key; tsig-keygen $keyname > $keyfile\n\nRFC2136_NAMESERVER=127.0.0.1 \\\nRFC2136_TSIG_FILE=\"$keyfile\" \\\nlego --dns rfc2136 -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    RFC2136_TSIG_KEY = \"Name of the secret key as defined in DNS server configuration. To disable TSIG authentication, leave the `RFC2136_TSIG_KEY` variable unset.\"\n    RFC2136_TSIG_SECRET = \"Secret key payload. To disable TSIG authentication, leave the `RFC2136_TSIG_SECRET` variable unset.\"\n    RFC2136_TSIG_ALGORITHM = \"TSIG algorithm. See [miekg/dns#tsig.go](https://github.com/miekg/dns/blob/master/tsig.go) for supported values. To disable TSIG authentication, leave the `RFC2136_TSIG_KEY` or `RFC2136_TSIG_SECRET` variables unset.\"\n    RFC2136_NAMESERVER = 'Network address in the form \"host\" or \"host:port\"'\n  [Configuration.Additional]\n    RFC2136_TSIG_FILE = \"Path to a key file generated by tsig-keygen\"\n    RFC2136_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    RFC2136_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    RFC2136_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    RFC2136_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n    RFC2136_DNS_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://www.rfc-editor.org/rfc/rfc2136.html\"\n"
  },
  {
    "path": "providers/dns/rfc2136/rfc2136_test.go",
    "content": "package rfc2136\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/dnsmock\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tfakeDomain     = \"123456789.www.example.com\"\n\tfakeKeyAuth    = \"123d==\"\n\tfakeValue      = \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\"\n\tfakeFqdn       = \"_acme-challenge.123456789.www.example.com.\"\n\tfakeZone       = \"example.com.\"\n\tfakeTTL        = 120\n\tfakeTsigKey    = \"example.com.\"\n\tfakeTsigSecret = \"IwBTJx9wrDp4Y1RyC3H0gA==\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvTSIGFile,\n\tEnvTSIGKey,\n\tEnvTSIGSecret,\n\tEnvTSIGAlgorithm,\n\tEnvNameserver,\n\tEnvDNSTimeout,\n).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvNameserver: \"example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing nameserver\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvNameserver: \"\",\n\t\t\t},\n\t\t\texpected: \"rfc2136: some credentials information are missing: RFC2136_NAMESERVER\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid algorithm\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvNameserver:    \"example.com\",\n\t\t\t\tEnvTSIGKey:       \"\",\n\t\t\t\tEnvTSIGSecret:    \"\",\n\t\t\t\tEnvTSIGAlgorithm: \"foo\",\n\t\t\t},\n\t\t\texpected: \"rfc2136: unsupported TSIG algorithm: foo.\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"valid TSIG file\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvNameserver: \"example.com\",\n\t\t\t\tEnvTSIGFile:   \"./internal/fixtures/sample.conf\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid TSIG file\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvNameserver: \"example.com\",\n\t\t\t\tEnvTSIGFile:   \"./internal/fixtures/invalid_key.conf\",\n\t\t\t},\n\t\t\texpected: \"rfc2136: read TSIG file ./internal/fixtures/invalid_key.conf: invalid key line: key {\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\texpected      string\n\t\tnameserver    string\n\t\ttsigFile      string\n\t\ttsigAlgorithm string\n\t\ttsigKey       string\n\t\ttsigSecret    string\n\t}{\n\t\t{\n\t\t\tdesc:       \"success\",\n\t\t\tnameserver: \"example.com\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing nameserver\",\n\t\t\texpected: \"rfc2136: nameserver missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:          \"invalid algorithm\",\n\t\t\tnameserver:    \"example.com\",\n\t\t\ttsigAlgorithm: \"foo\",\n\t\t\texpected:      \"rfc2136: unsupported TSIG algorithm: foo.\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"valid TSIG file\",\n\t\t\tnameserver: \"example.com\",\n\t\t\ttsigFile:   \"./internal/fixtures/sample.conf\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"invalid TSIG file\",\n\t\t\tnameserver: \"example.com\",\n\t\t\ttsigFile:   \"./internal/fixtures/invalid_key.conf\",\n\t\t\texpected:   \"rfc2136: read TSIG file ./internal/fixtures/invalid_key.conf: invalid key line: key {\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Nameserver = test.nameserver\n\t\t\tconfig.TSIGFile = test.tsigFile\n\t\t\tconfig.TSIGAlgorithm = test.tsigAlgorithm\n\t\t\tconfig.TSIGKey = test.tsigKey\n\t\t\tconfig.TSIGSecret = test.tsigSecret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_Present_success(t *testing.T) {\n\tdns01.ClearFqdnCache()\n\n\taddr := dnsmock.NewServer().\n\t\tQuery(fakeZone+\" SOA\", dnsmock.SOA(\"\")).\n\t\tUpdate(fakeZone+\" SOA\", dnsmock.Noop).\n\t\tBuild(t)\n\n\tconfig := NewDefaultConfig()\n\tconfig.Nameserver = addr.String()\n\n\tprovider, err := NewDNSProviderConfig(config)\n\trequire.NoError(t, err)\n\n\terr = provider.Present(fakeDomain, \"\", fakeKeyAuth)\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_Present_success_updatePacket(t *testing.T) {\n\tdns01.ClearFqdnCache()\n\n\treqChan := make(chan *dns.Msg, 1)\n\n\taddr := dnsmock.NewServer().\n\t\tQuery(\"_acme-challenge.123456789.www.example.com. SOA\", dnsmock.SOA(fakeZone)).\n\t\tUpdate(fakeZone+\" SOA\", func(w dns.ResponseWriter, req *dns.Msg) {\n\t\t\tdnsmock.Noop(w, req)\n\n\t\t\t// Only talk back when it is not the SOA RR.\n\t\t\treqChan <- req\n\t\t}).\n\t\tBuild(t)\n\n\tconfig := NewDefaultConfig()\n\tconfig.Nameserver = addr.String()\n\n\tprovider, err := NewDNSProviderConfig(config)\n\trequire.NoError(t, err)\n\n\terr = provider.Present(fakeDomain, \"\", fakeKeyAuth)\n\trequire.NoError(t, err)\n\n\tselect {\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timeout waiting for request\")\n\n\tcase rcvMsg := <-reqChan:\n\t\ttxtRR := &dns.TXT{\n\t\t\tHdr: dns.RR_Header{Name: fakeFqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: fakeTTL},\n\t\t\tTxt: []string{fakeValue},\n\t\t}\n\n\t\tm := new(dns.Msg).SetUpdate(fakeZone)\n\n\t\tm.RemoveRRset([]dns.RR{txtRR})\n\t\tm.Insert([]dns.RR{txtRR})\n\n\t\texpected, err := m.Pack()\n\t\trequire.NoError(t, err, \"error packing\")\n\n\t\trcvMsg.Id = m.Id\n\n\t\tactual, err := rcvMsg.Pack()\n\t\trequire.NoError(t, err, \"error packing\")\n\n\t\tif !bytes.Equal(actual, expected) {\n\t\t\ttmp := new(dns.Msg)\n\t\t\trequire.NoError(t, tmp.Unpack(actual))\n\n\t\t\tt.Errorf(\"Expected msg:\\n%s\", m)\n\t\t\tt.Errorf(\"Actual msg:\\n%s\", tmp)\n\t\t}\n\t}\n}\n\nfunc TestDNSProvider_Present_error(t *testing.T) {\n\tdns01.ClearFqdnCache()\n\n\taddr := dnsmock.NewServer().\n\t\tQuery(fakeZone+\" SOA\", dnsmock.Error(dns.RcodeNotZone)).\n\t\tBuild(t)\n\n\tconfig := NewDefaultConfig()\n\tconfig.Nameserver = addr.String()\n\n\tprovider, err := NewDNSProviderConfig(config)\n\trequire.NoError(t, err)\n\n\terr = provider.Present(fakeDomain, \"\", fakeKeyAuth)\n\trequire.Error(t, err)\n\n\tif !strings.Contains(err.Error(), \"NOTZONE\") {\n\t\tt.Errorf(\"Expected Present() to return an error with the 'NOTZONE' rcode string, but it did not: %v\", err)\n\t}\n}\n\nfunc TestDNSProvider_Present_tsig_success(t *testing.T) {\n\tdns01.ClearFqdnCache()\n\n\taddr := dnsmock.NewServer().\n\t\tQuery(fakeZone+\" SOA\", dnsmock.SOA(\"\")).\n\t\tUpdate(fakeZone+\" SOA\", handleTSIG).\n\t\tBuild(t, func(server *dns.Server) error {\n\t\t\tserver.TsigSecret = map[string]string{fakeTsigKey: fakeTsigSecret}\n\n\t\t\treturn nil\n\t\t})\n\n\tconfig := NewDefaultConfig()\n\tconfig.Nameserver = addr.String()\n\tconfig.TSIGKey = fakeTsigKey\n\tconfig.TSIGSecret = fakeTsigSecret\n\n\tprovider, err := NewDNSProviderConfig(config)\n\trequire.NoError(t, err)\n\n\terr = provider.Present(fakeDomain, \"\", fakeKeyAuth)\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_Present_tsig_error(t *testing.T) {\n\tdns01.ClearFqdnCache()\n\n\taddr := dnsmock.NewServer().\n\t\tQuery(fakeZone+\" SOA\", dnsmock.SOA(\"\")).\n\t\tUpdate(fakeZone+\" SOA\", handleTSIG).\n\t\tBuild(t, func(server *dns.Server) error {\n\t\t\tserver.TsigSecret = map[string]string{\"example.org\": fakeTsigSecret}\n\n\t\t\treturn nil\n\t\t})\n\n\tconfig := NewDefaultConfig()\n\tconfig.Nameserver = addr.String()\n\tconfig.TSIGKey = fakeTsigKey\n\tconfig.TSIGSecret = fakeTsigSecret\n\n\tprovider, err := NewDNSProviderConfig(config)\n\trequire.NoError(t, err)\n\n\terr = provider.Present(fakeDomain, \"\", fakeKeyAuth)\n\trequire.Error(t, err)\n\trequire.EqualError(t, err, \"rfc2136: failed to insert: DNS update failed: server replied: NOTZONE\")\n}\n\nfunc handleTSIG(w dns.ResponseWriter, req *dns.Msg) {\n\tm := new(dns.Msg)\n\n\ttsig := req.IsTsig()\n\tif tsig == nil {\n\t\t_ = w.WriteMsg(m.SetRcode(req, dns.RcodeRefused))\n\t\treturn\n\t}\n\n\terr := w.TsigStatus()\n\tif err != nil {\n\t\t_ = w.WriteMsg(m.SetRcode(req, dns.RcodeNotZone))\n\n\t\treturn\n\t}\n\n\t// Validated\n\t_ = w.WriteMsg(m.\n\t\tSetReply(req).\n\t\tSetTsig(tsig.Hdr.Name, tsig.Algorithm, tsig.Fudge, time.Now().Unix()),\n\t)\n}\n"
  },
  {
    "path": "providers/dns/rimuhosting/rimuhosting.go",
    "content": "// Package rimuhosting implements a DNS provider for solving the DNS-01 challenge using RimuHosting DNS.\npackage rimuhosting\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/rimuhosting\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"RIMUHOSTING_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config = rimuhosting.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, rimuhosting.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for RimuHosting.\n// Credentials must be passed in the environment variables.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rimuhosting: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for RimuHosting.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"rimuhosting: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := rimuhosting.NewDNSProviderConfig(config, \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rimuhosting: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rimuhosting: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"rimuhosting: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n"
  },
  {
    "path": "providers/dns/rimuhosting/rimuhosting.toml",
    "content": "Name = \"RimuHosting\"\nDescription = ''''''\nURL = \"https://rimuhosting.com\"\nCode = \"rimuhosting\"\nSince = \"v0.3.5\"\n\nExample = '''\nRIMUHOSTING_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns rimuhosting -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    RIMUHOSTING_API_KEY = \"User API key\"\n  [Configuration.Additional]\n    RIMUHOSTING_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    RIMUHOSTING_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    RIMUHOSTING_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)\"\n    RIMUHOSTING_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://rimuhosting.com/dns/dyndns.jsp\"\n"
  },
  {
    "path": "providers/dns/rimuhosting/rimuhosting_test.go",
    "content": "package rimuhosting\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"rimuhosting: some credentials information are missing: RIMUHOSTING_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\texpected  string\n\t\tapiKey    string\n\t\tsecretKey string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tapiKey:    \"api_key\",\n\t\t\tsecretKey: \"api_secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing api key\",\n\t\t\tapiKey:    \"\",\n\t\t\tsecretKey: \"api_secret\",\n\t\t\texpected:  \"rimuhosting: incomplete credentials, missing API key\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/route53/fixtures/changeResourceRecordSetsResponse.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ChangeResourceRecordSetsResponse xmlns=\"https://route53.amazonaws.com/doc/2013-04-01/\">\n    <ChangeInfo>\n        <Id>/change/123456</Id>\n        <Status>PENDING</Status>\n        <SubmittedAt>2016-02-10T01:36:41.958Z</SubmittedAt>\n    </ChangeInfo>\n</ChangeResourceRecordSetsResponse>\n"
  },
  {
    "path": "providers/dns/route53/fixtures/getChangeResponse.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<GetChangeResponse xmlns=\"https://route53.amazonaws.com/doc/2013-04-01/\">\n    <ChangeInfo>\n        <Id>123456</Id>\n        <Status>INSYNC</Status>\n        <SubmittedAt>2016-02-10T01:36:41.958Z</SubmittedAt>\n    </ChangeInfo>\n</GetChangeResponse>\n"
  },
  {
    "path": "providers/dns/route53/fixtures/listHostedZonesByNameResponse.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ListHostedZonesByNameResponse xmlns=\"https://route53.amazonaws.com/doc/2013-04-01/\">\n    <HostedZones>\n        <HostedZone>\n            <Id>/hostedzone/ABCDEFG</Id>\n            <Name>example.com.</Name>\n            <CallerReference>D2224C5B-684A-DB4A-BB9A-E09E3BAFEA7A</CallerReference>\n            <Config>\n                <Comment>Test comment</Comment>\n                <PrivateZone>false</PrivateZone>\n            </Config>\n            <ResourceRecordSetCount>10</ResourceRecordSetCount>\n        </HostedZone>\n    </HostedZones>\n    <IsTruncated>true</IsTruncated>\n    <NextDNSName>example2.com</NextDNSName>\n    <NextHostedZoneId>ZLT12321321124</NextHostedZoneId>\n    <MaxItems>1</MaxItems>\n</ListHostedZonesByNameResponse>\n"
  },
  {
    "path": "providers/dns/route53/route53.go",
    "content": "// Package route53 implements a DNS provider for solving the DNS-01 challenge using AWS Route 53 DNS.\npackage route53\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/aws/retry\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials/stscreds\"\n\t\"github.com/aws/aws-sdk-go-v2/service/route53\"\n\tawstypes \"github.com/aws/aws-sdk-go-v2/service/route53/types\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sts\"\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"AWS_\"\n\n\tEnvAccessKeyID     = envNamespace + \"ACCESS_KEY_ID\"\n\tEnvSecretAccessKey = envNamespace + \"SECRET_ACCESS_KEY\"\n\tEnvRegion          = envNamespace + \"REGION\"\n\tEnvHostedZoneID    = envNamespace + \"HOSTED_ZONE_ID\"\n\tEnvMaxRetries      = envNamespace + \"MAX_RETRIES\"\n\tEnvAssumeRoleArn   = envNamespace + \"ASSUME_ROLE_ARN\"\n\tEnvExternalID      = envNamespace + \"EXTERNAL_ID\"\n\tEnvPrivateZone     = envNamespace + \"PRIVATE_ZONE\"\n\n\tEnvWaitForRecordSetsChanged = envNamespace + \"WAIT_FOR_RECORD_SETS_CHANGED\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\t// Static credential chain.\n\t// These are not set via environment for the time being and are only used if they are explicitly provided.\n\tAccessKeyID     string\n\tSecretAccessKey string\n\tSessionToken    string\n\tRegion          string\n\n\tHostedZoneID  string\n\tMaxRetries    int\n\tAssumeRoleArn string\n\tExternalID    string\n\tPrivateZone   bool\n\n\tWaitForRecordSetsChanged bool\n\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\n\tClient *route53.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tHostedZoneID:  env.GetOrFile(EnvHostedZoneID),\n\t\tMaxRetries:    env.GetOrDefaultInt(EnvMaxRetries, 5),\n\t\tAssumeRoleArn: env.GetOrDefaultString(EnvAssumeRoleArn, \"\"),\n\t\tExternalID:    env.GetOrDefaultString(EnvExternalID, \"\"),\n\t\tPrivateZone:   env.GetOrDefaultBool(EnvPrivateZone, false),\n\n\t\tWaitForRecordSetsChanged: env.GetOrDefaultBool(EnvWaitForRecordSetsChanged, true),\n\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 10),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *route53.Client\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for the AWS Route 53 service.\n//\n// AWS Credentials are automatically detected in the following locations and prioritized in the following order:\n//  1. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,\n//     AWS_REGION, [AWS_SESSION_TOKEN]\n//  2. Shared credentials file (defaults to ~/.aws/credentials)\n//  3. Amazon EC2 IAM role\n//\n// If AWS_HOSTED_ZONE_ID is not set, Lego tries to determine the correct public hosted zone via the FQDN.\n//\n// See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk\nfunc NewDNSProvider() (*DNSProvider, error) {\n\treturn NewDNSProviderConfig(NewDefaultConfig())\n}\n\n// NewDNSProviderConfig takes a given config and returns a custom configured DNSProvider instance.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"route53: the configuration of the Route53 DNS provider is nil\")\n\t}\n\n\tif config.Client != nil {\n\t\treturn &DNSProvider{client: config.Client, config: config}, nil\n\t}\n\n\tctx := context.Background()\n\n\tcfg, err := createAWSConfig(ctx, config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &DNSProvider{\n\t\tclient: route53.NewFromConfig(cfg),\n\t\tconfig: config,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\thostedZoneID, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"route53: failed to determine hosted zone ID: %w\", err)\n\t}\n\n\trecords, err := d.getExistingRecordSets(ctx, hostedZoneID, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"route53: %w\", err)\n\t}\n\n\trealValue := `\"` + info.Value + `\"`\n\n\tvar found bool\n\n\tfor _, record := range records {\n\t\tif ptr.Deref(record.Value) == realValue {\n\t\t\tfound = true\n\t\t}\n\t}\n\n\tif !found {\n\t\trecords = append(records, awstypes.ResourceRecord{Value: aws.String(realValue)})\n\t}\n\n\trecordSet := &awstypes.ResourceRecordSet{\n\t\tName:            aws.String(info.EffectiveFQDN),\n\t\tType:            \"TXT\",\n\t\tTTL:             aws.Int64(int64(d.config.TTL)),\n\t\tResourceRecords: records,\n\t}\n\n\terr = d.changeRecord(ctx, awstypes.ChangeActionUpsert, hostedZoneID, recordSet)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"route53: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\thostedZoneID, err := d.getHostedZoneID(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to determine Route 53 hosted zone ID: %w\", err)\n\t}\n\n\texistingRecords, err := d.getExistingRecordSets(ctx, hostedZoneID, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"route53: %w\", err)\n\t}\n\n\tif len(existingRecords) == 0 {\n\t\treturn nil\n\t}\n\n\tvar nonLegoRecords []awstypes.ResourceRecord\n\n\tfor _, record := range existingRecords {\n\t\tif ptr.Deref(record.Value) != `\"`+info.Value+`\"` {\n\t\t\tnonLegoRecords = append(nonLegoRecords, record)\n\t\t}\n\t}\n\n\taction := awstypes.ChangeActionUpsert\n\n\trecordSet := &awstypes.ResourceRecordSet{\n\t\tName:            aws.String(info.EffectiveFQDN),\n\t\tType:            \"TXT\",\n\t\tTTL:             aws.Int64(int64(d.config.TTL)),\n\t\tResourceRecords: nonLegoRecords,\n\t}\n\n\t// If the records are only records created by lego.\n\tif len(nonLegoRecords) == 0 {\n\t\taction = awstypes.ChangeActionDelete\n\n\t\trecordSet.ResourceRecords = existingRecords\n\t}\n\n\terr = d.changeRecord(ctx, action, hostedZoneID, recordSet)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"route53: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) changeRecord(ctx context.Context, action awstypes.ChangeAction, hostedZoneID string, recordSet *awstypes.ResourceRecordSet) error {\n\trecordSetInput := &route53.ChangeResourceRecordSetsInput{\n\t\tHostedZoneId: aws.String(hostedZoneID),\n\t\tChangeBatch: &awstypes.ChangeBatch{\n\t\t\tComment: aws.String(\"Managed by Lego\"),\n\t\t\tChanges: []awstypes.Change{{\n\t\t\t\tAction:            action,\n\t\t\t\tResourceRecordSet: recordSet,\n\t\t\t}},\n\t\t},\n\t}\n\n\tresp, err := d.client.ChangeResourceRecordSets(ctx, recordSetInput)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to change record set: %w\", err)\n\t}\n\n\tchangeID := resp.ChangeInfo.Id\n\n\tif d.config.WaitForRecordSetsChanged {\n\t\treturn wait.Retry(ctx,\n\t\t\tfunc() error {\n\t\t\t\tresp, err := d.client.GetChange(ctx, &route53.GetChangeInput{Id: changeID})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to query change status: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif resp.ChangeInfo.Status != awstypes.ChangeStatusInsync {\n\t\t\t\t\treturn fmt.Errorf(\"unable to retrieve change: ID=%s, status=%s\", ptr.Deref(changeID), resp.ChangeInfo.Status)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tbackoff.WithBackOff(backoff.NewConstantBackOff(d.config.PollingInterval)),\n\t\t\tbackoff.WithMaxElapsedTime(d.config.PropagationTimeout),\n\t\t)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) getExistingRecordSets(ctx context.Context, hostedZoneID, fqdn string) ([]awstypes.ResourceRecord, error) {\n\tlistInput := &route53.ListResourceRecordSetsInput{\n\t\tHostedZoneId:    aws.String(hostedZoneID),\n\t\tStartRecordName: aws.String(fqdn),\n\t\tStartRecordType: \"TXT\",\n\t}\n\n\trecordSetsOutput, err := d.client.ListResourceRecordSets(ctx, listInput)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif recordSetsOutput == nil {\n\t\treturn nil, nil\n\t}\n\n\tvar records []awstypes.ResourceRecord\n\n\tfor _, recordSet := range recordSetsOutput.ResourceRecordSets {\n\t\tif ptr.Deref(recordSet.Name) == fqdn {\n\t\t\trecords = append(records, recordSet.ResourceRecords...)\n\t\t}\n\t}\n\n\treturn records, nil\n}\n\nfunc (d *DNSProvider) getHostedZoneID(ctx context.Context, fqdn string) (string, error) {\n\tif d.config.HostedZoneID != \"\" {\n\t\treturn d.config.HostedZoneID, nil\n\t}\n\n\tauthZone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not find zone for FQDN %q: %w\", fqdn, err)\n\t}\n\n\t// .DNSName should not have a trailing dot\n\treqParams := &route53.ListHostedZonesByNameInput{\n\t\tDNSName: aws.String(dns01.UnFqdn(authZone)),\n\t}\n\n\tresp, err := d.client.ListHostedZonesByName(ctx, reqParams)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar hostedZoneID string\n\n\tfor _, hostedZone := range resp.HostedZones {\n\t\t// .Name has a trailing dot\n\t\tif ptr.Deref(hostedZone.Name) == authZone && d.config.PrivateZone == hostedZone.Config.PrivateZone {\n\t\t\thostedZoneID = ptr.Deref(hostedZone.Id)\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif hostedZoneID == \"\" {\n\t\treturn \"\", fmt.Errorf(\"zone %s not found for domain %s\", authZone, fqdn)\n\t}\n\n\thostedZoneID = strings.TrimPrefix(hostedZoneID, \"/hostedzone/\")\n\n\treturn hostedZoneID, nil\n}\n\nfunc createAWSConfig(ctx context.Context, config *Config) (aws.Config, error) {\n\tif err := createAWSConfigCheckParams(config); err != nil {\n\t\treturn aws.Config{}, err\n\t}\n\n\toptFns := []func(options *awsconfig.LoadOptions) error{\n\t\tawsconfig.WithRetryer(func() aws.Retryer {\n\t\t\treturn retry.NewStandard(func(options *retry.StandardOptions) {\n\t\t\t\toptions.MaxAttempts = config.MaxRetries\n\n\t\t\t\t// It uses a basic exponential backoff algorithm that returns an initial\n\t\t\t\t// delay of ~400ms with an upper limit of ~30 seconds which should prevent\n\t\t\t\t// causing a high number of consecutive throttling errors.\n\t\t\t\t// For reference: Route 53 enforces an account-wide(!) 5req/s query limit.\n\t\t\t\toptions.Backoff = retry.BackoffDelayerFunc(func(attempt int, err error) (time.Duration, error) {\n\t\t\t\t\tretryCount := min(attempt, 7)\n\n\t\t\t\t\tdelay := (1 << uint(retryCount)) * (rand.Intn(50) + 200)\n\n\t\t\t\t\treturn time.Duration(delay) * time.Millisecond, nil\n\t\t\t\t})\n\t\t\t})\n\t\t}),\n\t}\n\n\tif config.AccessKeyID != \"\" && config.SecretAccessKey != \"\" {\n\t\toptFns = append(optFns,\n\t\t\tawsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(config.AccessKeyID, config.SecretAccessKey, config.SessionToken)),\n\t\t)\n\t}\n\n\tif config.Region != \"\" {\n\t\toptFns = append(optFns, awsconfig.WithRegion(config.Region))\n\t}\n\n\tcfg, err := awsconfig.LoadDefaultConfig(ctx, optFns...)\n\tif err != nil {\n\t\treturn aws.Config{}, err\n\t}\n\n\tif config.AssumeRoleArn != \"\" {\n\t\tcfg.Credentials = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg), config.AssumeRoleArn, func(options *stscreds.AssumeRoleOptions) {\n\t\t\tif config.ExternalID != \"\" {\n\t\t\t\toptions.ExternalID = &config.ExternalID\n\t\t\t}\n\t\t})\n\t}\n\n\treturn cfg, nil\n}\n\nfunc createAWSConfigCheckParams(config *Config) error {\n\tif config == nil {\n\t\treturn errors.New(\"config is nil\")\n\t}\n\n\tswitch {\n\tcase config.SessionToken != \"\" && config.AccessKeyID == \"\" && config.SecretAccessKey == \"\":\n\t\treturn errors.New(\"SessionToken must be supplied with AccessKeyID and SecretAccessKey\")\n\n\tcase config.AccessKeyID == \"\" && config.SecretAccessKey != \"\" || config.AccessKeyID != \"\" && config.SecretAccessKey == \"\":\n\t\treturn errors.New(\"AccessKeyID and SecretAccessKey must be supplied together\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/route53/route53.toml",
    "content": "Name = \"Amazon Route 53\"\nDescription = ''''''\nURL = \"https://aws.amazon.com/route53/\"\nCode = \"route53\"\nSince = \"v0.3.0\"\n\nExample = '''\nAWS_ACCESS_KEY_ID=your_key_id \\\nAWS_SECRET_ACCESS_KEY=your_secret_access_key \\\nAWS_REGION=aws-region \\\nAWS_HOSTED_ZONE_ID=your_hosted_zone_id \\\nlego --dns route53 -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Description\n\nAWS Credentials are automatically detected in the following locations and prioritized in the following order:\n\n1. Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, [`AWS_SESSION_TOKEN`]\n2. Shared credentials file (defaults to `~/.aws/credentials`, profiles can be specified using `AWS_PROFILE`)\n3. Amazon EC2 IAM role\n\nThe AWS Region is automatically detected in the following locations and prioritized in the following order:\n\n1. Environment variables: `AWS_REGION`\n2. Shared configuration file if `AWS_SDK_LOAD_CONFIG` is set (defaults to `~/.aws/config`, profiles can be specified using `AWS_PROFILE`)\n\nIf `AWS_HOSTED_ZONE_ID` is not set, Lego tries to determine the correct public hosted zone via the FQDN.\n\nSee also:\n\n- [sessions](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/sessions.html)\n- [Setting AWS Credentials](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials)\n- [Setting AWS Region](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-the-region)\n\n## IAM Policy Examples\n\n### Broad privileges for testing purposes\n\nThe following [IAM policy](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html) document grants access to the required APIs needed by lego to complete the DNS challenge.\nA word of caution:\nThese permissions grant write access to any DNS record in any hosted zone,\nso it is recommended to narrow them down as much as possible if you are using this policy in production.\n\n```json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"route53:GetChange\",\n        \"route53:ChangeResourceRecordSets\",\n        \"route53:ListResourceRecordSets\"\n      ],\n      \"Resource\": [\n        \"arn:aws:route53:::hostedzone/*\",\n        \"arn:aws:route53:::change/*\"\n      ]\n    },\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": \"route53:ListHostedZonesByName\",\n      \"Resource\": \"*\"\n    }\n  ]\n}\n```\n\n### Least privilege policy for production purposes\n\nThe following AWS IAM policy document describes the least privilege permissions required for lego to complete the DNS challenge.\nWrite access is limited to a specified hosted zone's DNS TXT records with a key of `_acme-challenge.example.com`.\nReplace `Z11111112222222333333` with your hosted zone ID and `example.com` with your domain name to use this policy.\n\n```json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": \"route53:GetChange\",\n      \"Resource\": \"arn:aws:route53:::change/*\"\n    },\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": \"route53:ListHostedZonesByName\",\n      \"Resource\": \"*\"\n    },\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"route53:ListResourceRecordSets\"\n      ],\n      \"Resource\": [\n        \"arn:aws:route53:::hostedzone/Z11111112222222333333\"\n      ]\n    },\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"route53:ChangeResourceRecordSets\"\n      ],\n      \"Resource\": [\n        \"arn:aws:route53:::hostedzone/Z11111112222222333333\"\n      ],\n      \"Condition\": {\n        \"ForAllValues:StringEquals\": {\n          \"route53:ChangeResourceRecordSetsNormalizedRecordNames\": [\n            \"_acme-challenge.example.com\"\n          ],\n          \"route53:ChangeResourceRecordSetsRecordTypes\": [\n            \"TXT\"\n          ]\n        }\n      }\n    }\n  ]\n}\n```\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    AWS_ACCESS_KEY_ID = \"Managed by the AWS client. Access key ID (`AWS_ACCESS_KEY_ID_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead)\"\n    AWS_SECRET_ACCESS_KEY = \"Managed by the AWS client. Secret access key (`AWS_SECRET_ACCESS_KEY_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead)\"\n    AWS_REGION = \"Managed by the AWS client (`AWS_REGION_FILE` is not supported)\"\n    AWS_HOSTED_ZONE_ID = \"Override the hosted zone ID.\"\n    AWS_PROFILE = \"Managed by the AWS client (`AWS_PROFILE_FILE` is not supported)\"\n    AWS_SDK_LOAD_CONFIG = \"Managed by the AWS client. Retrieve the region from the CLI config file (`AWS_SDK_LOAD_CONFIG_FILE` is not supported)\"\n    AWS_ASSUME_ROLE_ARN = \"Managed by the AWS Role ARN (`AWS_ASSUME_ROLE_ARN_FILE` is not supported)\"\n    AWS_EXTERNAL_ID = \"Managed by STS AssumeRole API operation (`AWS_EXTERNAL_ID_FILE` is not supported)\"\n    AWS_WAIT_FOR_RECORD_SETS_CHANGED = \"Wait for changes to be INSYNC (it can be unstable)\"\n  [Configuration.Additional]\n    AWS_PRIVATE_ZONE = \"Set to true to use private zones only (default: use public zones only)\"\n    AWS_SHARED_CREDENTIALS_FILE = \"Managed by the AWS client. Shared credentials file.\"\n    AWS_MAX_RETRIES = \"The number of maximum returns the service will use to make an individual API request\"\n    AWS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 4)\"\n    AWS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    AWS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://docs.aws.amazon.com/Route53/latest/APIReference/API_Operations_Amazon_Route_53.html\"\n  GoClient = \"https://github.com/aws/aws-sdk-go-v2\"\n"
  },
  {
    "path": "providers/dns/route53/route53_integration_test.go",
    "content": "package route53\n\nimport (\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/service/route53\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLiveTTL(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\tdomain := envTest.GetDomain()\n\n\terr = provider.Present(domain, \"foo\", \"bar\")\n\trequire.NoError(t, err)\n\n\t// we need a separate R53 client here as the one in the DNS provider is unexported.\n\tfqdn := \"_acme-challenge.\" + domain + \".\"\n\n\tctx := t.Context()\n\n\tcfg, err := awsconfig.LoadDefaultConfig(ctx)\n\trequire.NoError(t, err)\n\n\tsvc := route53.NewFromConfig(cfg)\n\n\tdefer func() {\n\t\terrC := provider.CleanUp(domain, \"foo\", \"bar\")\n\t\tif errC != nil {\n\t\t\tt.Log(errC)\n\t\t}\n\t}()\n\n\tzoneID, err := provider.getHostedZoneID(t.Context(), fqdn)\n\trequire.NoError(t, err)\n\n\tparams := &route53.ListResourceRecordSetsInput{\n\t\tHostedZoneId: aws.String(zoneID),\n\t}\n\tresp, err := svc.ListResourceRecordSets(ctx, params)\n\trequire.NoError(t, err)\n\n\tfor _, v := range resp.ResourceRecordSets {\n\t\tif ptr.Deref(v.Name) == fqdn && v.Type == \"TXT\" && ptr.Deref(v.TTL) == 10 {\n\t\t\treturn\n\t\t}\n\t}\n\n\tt.Fatalf(\"Could not find a TXT record for _acme-challenge.%s with a TTL of 10\", domain)\n}\n"
  },
  {
    "path": "providers/dns/route53/route53_test.go",
    "content": "package route53\n\nimport (\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/route53\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = \"R53_DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAccessKeyID,\n\tEnvSecretAccessKey,\n\tEnvRegion,\n\tEnvHostedZoneID,\n\tEnvMaxRetries,\n\tEnvPrivateZone,\n\tEnvTTL,\n\tEnvPropagationTimeout,\n\tEnvPollingInterval,\n\tEnvWaitForRecordSetsChanged).\n\tWithDomain(envDomain).\n\tWithLiveTestRequirements(EnvAccessKeyID, EnvSecretAccessKey, EnvRegion, envDomain)\n\nfunc Test_loadCredentials_FromEnv(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\tenvTest.ClearEnv()\n\n\t_ = os.Setenv(EnvAccessKeyID, \"123\")\n\t_ = os.Setenv(EnvSecretAccessKey, \"456\")\n\t_ = os.Setenv(EnvRegion, \"us-east-1\")\n\n\tctx := t.Context()\n\n\tcfg, err := awsconfig.LoadDefaultConfig(ctx)\n\trequire.NoError(t, err)\n\n\tvalue, err := cfg.Credentials.Retrieve(ctx)\n\trequire.NoError(t, err, \"Expected credentials to be set from environment\")\n\n\texpected := aws.Credentials{\n\t\tAccessKeyID:     \"123\",\n\t\tSecretAccessKey: \"456\",\n\t\tSessionToken:    \"\",\n\t\tSource:          \"EnvConfigCredentials\",\n\t}\n\n\tassert.Equal(t, expected, value)\n}\n\nfunc Test_loadRegion_FromEnv(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\tenvTest.ClearEnv()\n\n\t_ = os.Setenv(EnvRegion, \"foo\")\n\n\tcfg, err := awsconfig.LoadDefaultConfig(t.Context())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"foo\", cfg.Region, \"Region\")\n}\n\nfunc Test_getHostedZoneID_FromEnv(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\tenvTest.ClearEnv()\n\n\texpectedZoneID := \"zoneID\"\n\n\t_ = os.Setenv(EnvHostedZoneID, expectedZoneID)\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\thostedZoneID, err := provider.getHostedZoneID(t.Context(), \"whatever\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, expectedZoneID, hostedZoneID)\n}\n\nfunc TestNewDefaultConfig(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected *Config\n\t}{\n\t\t{\n\t\t\tdesc: \"default configuration\",\n\t\t\texpected: &Config{\n\t\t\t\tMaxRetries:               5,\n\t\t\t\tTTL:                      10,\n\t\t\t\tPropagationTimeout:       2 * time.Minute,\n\t\t\t\tPollingInterval:          4 * time.Second,\n\t\t\t\tWaitForRecordSetsChanged: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"set values\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvMaxRetries:               \"10\",\n\t\t\t\tEnvTTL:                      \"99\",\n\t\t\t\tEnvPropagationTimeout:       \"60\",\n\t\t\t\tEnvPollingInterval:          \"60\",\n\t\t\t\tEnvHostedZoneID:             \"abc123\",\n\t\t\t\tEnvWaitForRecordSetsChanged: \"false\",\n\t\t\t},\n\t\t\texpected: &Config{\n\t\t\t\tMaxRetries:         10,\n\t\t\t\tTTL:                99,\n\t\t\t\tPropagationTimeout: 60 * time.Second,\n\t\t\t\tPollingInterval:    60 * time.Second,\n\t\t\t\tHostedZoneID:       \"abc123\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tfor key, value := range test.envVars {\n\t\t\t\t_ = os.Setenv(key, value)\n\t\t\t}\n\n\t\t\tconfig := NewDefaultConfig()\n\n\t\t\tassert.Equal(t, test.expected, config)\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\tenvTest.ClearEnv()\n\n\tprovider := servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tcfg := aws.Config{\n\t\t\t\tHTTPClient:       server.Client(),\n\t\t\t\tCredentials:      credentials.NewStaticCredentialsProvider(\"abc\", \"123\", \" \"),\n\t\t\t\tRegion:           \"mock-region\",\n\t\t\t\tBaseEndpoint:     aws.String(server.URL),\n\t\t\t\tRetryMaxAttempts: 1,\n\t\t\t}\n\n\t\t\treturn &DNSProvider{\n\t\t\t\tclient: route53.NewFromConfig(cfg),\n\t\t\t\tconfig: NewDefaultConfig(),\n\t\t\t}, nil\n\t\t},\n\t).\n\t\tRoute(\"GET /2013-04-01/hostedzonesbyname\",\n\t\t\tservermock.ResponseFromFixture(\"listHostedZonesByNameResponse.xml\").\n\t\t\t\tWithHeader(\"Content-Type\", \"application/xml\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"dnsname\", \"example.com\")).\n\t\tRoute(\"POST /2013-04-01/hostedzone/ABCDEFG/rrset\",\n\t\t\tservermock.ResponseFromFixture(\"changeResourceRecordSetsResponse.xml\").\n\t\t\t\tWithHeader(\"Content-Type\", \"application/xml\")).\n\t\tRoute(\"GET /2013-04-01/change/123456\",\n\t\t\tservermock.ResponseFromFixture(\"getChangeResponse.xml\").\n\t\t\t\tWithHeader(\"Content-Type\", \"application/xml\")).\n\t\tRoute(\"GET /2013-04-01/hostedzone/ABCDEFG/rrset\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithHeader(\"Content-Type\", \"application/xml\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"name\", \"_acme-challenge.example.com.\").\n\t\t\t\tWith(\"type\", \"TXT\")).\n\t\tBuild(t)\n\n\tdomain := \"example.com\"\n\tkeyAuth := \"123456d==\"\n\n\terr := provider.Present(domain, \"\", keyAuth)\n\trequire.NoError(t, err)\n}\n\nfunc Test_createAWSConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc             string\n\t\tenv              map[string]string\n\t\tconfig           *Config\n\t\twantCreds        aws.Credentials\n\t\twantDefaultChain bool\n\t\twantRegion       string\n\t\twantErr          string\n\t}{\n\t\t{\n\t\t\tdesc:    \"config is nil\",\n\t\t\twantErr: \"config is nil\",\n\t\t},\n\t\t{\n\t\t\tdesc:    \"session token without access key id or secret access key\",\n\t\t\tconfig:  &Config{SessionToken: \"foo\"},\n\t\t\twantErr: \"SessionToken must be supplied with AccessKeyID and SecretAccessKey\",\n\t\t},\n\t\t{\n\t\t\tdesc:    \"access key id without secret access key\",\n\t\t\tconfig:  &Config{AccessKeyID: \"foo\"},\n\t\t\twantErr: \"AccessKeyID and SecretAccessKey must be supplied together\",\n\t\t},\n\t\t{\n\t\t\tdesc:    \"access key id without secret access key\",\n\t\t\tconfig:  &Config{SecretAccessKey: \"foo\"},\n\t\t\twantErr: \"AccessKeyID and SecretAccessKey must be supplied together\",\n\t\t},\n\t\t{\n\t\t\tdesc:             \"credentials from default chain\",\n\t\t\tconfig:           &Config{},\n\t\t\twantDefaultChain: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"static credentials\",\n\t\t\tconfig: &Config{\n\t\t\t\tAccessKeyID:     \"one\",\n\t\t\t\tSecretAccessKey: \"two\",\n\t\t\t},\n\t\t\twantCreds: aws.Credentials{\n\t\t\t\tAccessKeyID:     \"one\",\n\t\t\t\tSecretAccessKey: \"two\",\n\t\t\t\tSessionToken:    \"\",\n\t\t\t\tSource:          credentials.StaticCredentialsName,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"static credentials with session token\",\n\t\t\tconfig: &Config{\n\t\t\t\tAccessKeyID:     \"one\",\n\t\t\t\tSecretAccessKey: \"two\",\n\t\t\t\tSessionToken:    \"three\",\n\t\t\t},\n\t\t\twantCreds: aws.Credentials{\n\t\t\t\tAccessKeyID:     \"one\",\n\t\t\t\tSecretAccessKey: \"two\",\n\t\t\t\tSessionToken:    \"three\",\n\t\t\t\tSource:          credentials.StaticCredentialsName,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:   \"region from env\",\n\t\t\tconfig: &Config{},\n\t\t\tenv: map[string]string{\n\t\t\t\t\"AWS_REGION\": \"foo\",\n\t\t\t},\n\t\t\twantDefaultChain: true,\n\t\t\twantRegion:       \"foo\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"static region\",\n\t\t\tconfig: &Config{\n\t\t\t\tRegion: \"one\",\n\t\t\t},\n\t\t\tenv: map[string]string{\n\t\t\t\t\"AWS_REGION\": \"foo\",\n\t\t\t},\n\t\t\twantDefaultChain: true,\n\t\t\twantRegion:       \"one\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.env)\n\n\t\t\tctx := t.Context()\n\n\t\t\tcfg, err := createAWSConfig(ctx, test.config)\n\t\t\trequireErr(t, err, test.wantErr)\n\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tgotCreds, err := cfg.Credentials.Retrieve(ctx)\n\n\t\t\tif test.wantDefaultChain {\n\t\t\t\tassert.NotEqual(t, credentials.StaticCredentialsName, gotCreds.Source)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.wantCreds, gotCreds)\n\t\t\t}\n\n\t\t\tif test.wantRegion != \"\" {\n\t\t\t\tassert.Equal(t, test.wantRegion, cfg.Region)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc requireErr(t *testing.T, err error, wantErr string) {\n\tt.Helper()\n\n\tswitch {\n\tcase err != nil && wantErr == \"\":\n\t\t// force the assertion error.\n\t\trequire.NoError(t, err)\n\n\tcase err == nil && wantErr != \"\":\n\t\t// force the assertion error.\n\t\trequire.EqualError(t, err, wantErr)\n\n\tcase err != nil && wantErr != \"\":\n\t\trequire.EqualError(t, err, wantErr)\n\t}\n}\n"
  },
  {
    "path": "providers/dns/safedns/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api.ukfast.io/safedns/v1\"\n\nconst authorizationHeader = \"Authorization\"\n\n// Client the ANS SafeDNS client.\ntype Client struct {\n\tauthToken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(authToken string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tauthToken:  authToken,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// AddRecord adds a DNS record.\nfunc (c *Client) AddRecord(ctx context.Context, zone string, record Record) (*AddRecordResponse, error) {\n\tendpoint := c.baseURL.JoinPath(\"zones\", dns01.UnFqdn(zone), \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trespData := &AddRecordResponse{}\n\n\terr = c.do(req, respData)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"add record: %w\", err)\n\t}\n\n\treturn respData, nil\n}\n\n// RemoveRecord removes a DNS record.\nfunc (c *Client) RemoveRecord(ctx context.Context, zone string, recordID int) error {\n\tendpoint := c.baseURL.JoinPath(\"zones\", dns01.UnFqdn(zone), \"records\", strconv.Itoa(recordID))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"remove record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(authorizationHeader, c.authToken)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code: %d] %w\", resp.StatusCode, errAPI)\n}\n"
  },
  {
    "path": "providers/dns/safedns/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"secret\")\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zones/example.com/records\",\n\t\t\tservermock.ResponseFromFixture(\"add_record.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"add_record-request.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:    \"_acme-challenge.example.com\",\n\t\tType:    \"TXT\",\n\t\tContent: `\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\"`,\n\t\tTTL:     dns01.DefaultTTL,\n\t}\n\n\tresponse, err := client.AddRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n\n\texpected := &AddRecordResponse{\n\t\tData: struct {\n\t\t\tID int `json:\"id\"`\n\t\t}{\n\t\t\tID: 1234567,\n\t\t},\n\t\tMeta: struct {\n\t\t\tLocation string `json:\"location\"`\n\t\t}{\n\t\t\tLocation: \"https://api.ukfast.io/safedns/v1/zones/example.com/records/1234567\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, response)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /zones/example.com/records\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:    \"_acme-challenge.example.com\",\n\t\tType:    \"TXT\",\n\t\tContent: `\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\"`,\n\t\tTTL:     dns01.DefaultTTL,\n\t}\n\n\t_, err := client.AddRecord(t.Context(), \"example.com\", record)\n\trequire.EqualError(t, err, \"add record: [status code: 401] Unauthenticated\")\n}\n\nfunc TestClient_RemoveRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /zones/example.com/records/1234567\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\terr := client.RemoveRecord(t.Context(), \"example.com\", 1234567)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_RemoveRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /zones/example.com/records/1234567\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\terr := client.RemoveRecord(t.Context(), \"example.com\", 1234567)\n\trequire.EqualError(t, err, \"remove record: [status code: 401] Unauthenticated\")\n}\n"
  },
  {
    "path": "providers/dns/safedns/internal/fixtures/add_record-request.json",
    "content": "{\n  \"name\": \"_acme-challenge.example.com\",\n  \"type\": \"TXT\",\n  \"content\": \"\\\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\\\"\",\n  \"ttl\": 120\n}\n"
  },
  {
    "path": "providers/dns/safedns/internal/fixtures/add_record.json",
    "content": "{\n  \"data\": {\n    \"id\": 1234567\n  },\n  \"meta\": {\n    \"location\": \"https://api.ukfast.io/safedns/v1/zones/example.com/records/1234567\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/safedns/internal/fixtures/error.json",
    "content": "{\n  \"message\": \"Unauthenticated\"\n}\n"
  },
  {
    "path": "providers/dns/safedns/internal/types.go",
    "content": "package internal\n\ntype AddRecordResponse struct {\n\tData struct {\n\t\tID int `json:\"id\"`\n\t} `json:\"data\"`\n\tMeta struct {\n\t\tLocation string `json:\"location\"`\n\t}\n}\n\ntype Record struct {\n\tName    string `json:\"name\"`\n\tType    string `json:\"type\"`\n\tContent string `json:\"content\"`\n\tTTL     int    `json:\"ttl\"`\n}\n\ntype APIError struct {\n\tMessage string `json:\"message\"`\n}\n\nfunc (a APIError) Error() string {\n\treturn a.Message\n}\n"
  },
  {
    "path": "providers/dns/safedns/safedns.go",
    "content": "// Package safedns implements a DNS provider for solving the DNS-01 challenge using ANS SafeDNS.\npackage safedns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/safedns/internal\"\n\t\"github.com/miekg/dns\"\n)\n\n// Environment variables.\nconst (\n\tenvNamespace = \"SAFEDNS_\"\n\n\tEnvAuthToken = envNamespace + \"AUTH_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAuthToken string\n\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]int\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAuthToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"safedns: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AuthToken = values[EnvAuthToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for ANS SafeDNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"safedns: supplied configuration was nil\")\n\t}\n\n\tif config.AuthToken == \"\" {\n\t\treturn nil, errors.New(\"safedns: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(config.AuthToken)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]int),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := dns01.FindZoneByFqdn(dns.Fqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"safedns: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecord := internal.Record{\n\t\tName:    dns01.UnFqdn(info.EffectiveFQDN),\n\t\tType:    \"TXT\",\n\t\tContent: fmt.Sprintf(\"%q\", info.Value),\n\t\tTTL:     d.config.TTL,\n\t}\n\n\tresp, err := d.client.AddRecord(context.Background(), zone, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"safedns: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = resp.Data.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record previously created.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"safedns: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"safedns: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\terr = d.client.RemoveRecord(context.Background(), authZone, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"safedns: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/safedns/safedns.toml",
    "content": "Name = \"ANS SafeDNS\"\nDescription = ''''''\nURL = \"https://www.ans.co.uk/\"\nCode = \"safedns\"\nSince = \"v4.6.0\"\n\nExample = '''\nSAFEDNS_AUTH_TOKEN=xxxxxx \\\nlego --dns safedns -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    SAFEDNS_AUTH_TOKEN = \"Authentication token\"\n  [Configuration.Additional]\n    SAFEDNS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    SAFEDNS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    SAFEDNS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    SAFEDNS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://developers.ukfast.io/documentation/safedns\"\n"
  },
  {
    "path": "providers/dns/safedns/safedns_test.go",
    "content": "package safedns\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAuthToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthToken: \"\",\n\t\t\t},\n\t\t\texpected: \"safedns: some credentials information are missing: SAFEDNS_AUTH_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tauthToken string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tauthToken: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"safedns: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AuthToken = test.authToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/sakuracloud/sakuracloud.go",
    "content": "// Package sakuracloud implements a DNS provider for solving the DNS-01 challenge using SakuraCloud DNS.\npackage sakuracloud\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\tclient \"github.com/sacloud/api-client-go\"\n\t\"github.com/sacloud/iaas-api-go\"\n\t\"github.com/sacloud/iaas-api-go/defaults\"\n\t\"github.com/sacloud/iaas-api-go/helper/api\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"SAKURACLOUD_\"\n\n\tEnvAccessToken       = envNamespace + \"ACCESS_TOKEN\"\n\tEnvAccessTokenSecret = envNamespace + \"ACCESS_TOKEN_SECRET\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tToken              string\n\tSecret             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient iaas.DNSAPI\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for SakuraCloud.\n// Credentials must be passed in the environment variables:\n// SAKURACLOUD_ACCESS_TOKEN & SAKURACLOUD_ACCESS_TOKEN_SECRET.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAccessToken, EnvAccessTokenSecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"sakuracloud: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvAccessToken]\n\tconfig.Secret = values[EnvAccessTokenSecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for SakuraCloud.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"sakuracloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"sakuracloud: AccessToken is missing\")\n\t}\n\n\tif config.Secret == \"\" {\n\t\treturn nil, errors.New(\"sakuracloud: AccessSecret is missing\")\n\t}\n\n\tdefaultOption, err := api.DefaultOption()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"sakuracloud: %w\", err)\n\t}\n\n\toptions := &api.CallerOptions{\n\t\tOptions: &client.Options{\n\t\t\tAccessToken:       config.Token,\n\t\t\tAccessTokenSecret: config.Secret,\n\t\t\tHttpClient:        clientdebug.Wrap(config.HTTPClient),\n\t\t\tUserAgent:         fmt.Sprintf(\"%s %s\", iaas.DefaultUserAgent, useragent.Get()),\n\t\t},\n\t}\n\n\treturn &DNSProvider{\n\t\tclient: iaas.NewDNSOp(newCallerWithOptions(api.MergeOptions(defaultOption, options))),\n\t\tconfig: config,\n\t}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.addTXTRecord(context.Background(), info.EffectiveFQDN, info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"sakuracloud: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.cleanupTXTRecord(context.Background(), info.EffectiveFQDN, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"sakuracloud: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Extracted from https://github.com/sacloud/iaas-api-go/blob/af06b3ccc2c38625d2dc684ad39590d0ae13eed3/helper/api/caller.go#L36-L81\n// Trace and fake are removed.\n// Related to https://github.com/sacloud/iaas-api-go/issues/376.\nfunc newCallerWithOptions(opts *api.CallerOptions) iaas.APICaller {\n\treturn newCaller(opts)\n}\n\nfunc newCaller(opts *api.CallerOptions) iaas.APICaller {\n\tif opts.UserAgent == \"\" {\n\t\topts.UserAgent = iaas.DefaultUserAgent\n\t}\n\n\tcaller := iaas.NewClientWithOptions(opts.Options)\n\n\tdefaults.DefaultStatePollingTimeout = 72 * time.Hour\n\n\tif opts.DefaultZone != \"\" {\n\t\tiaas.APIDefaultZone = opts.DefaultZone\n\t}\n\n\tif len(opts.Zones) > 0 {\n\t\tiaas.SakuraCloudZones = opts.Zones\n\t}\n\n\tif opts.APIRootURL != \"\" {\n\t\tif strings.HasSuffix(opts.APIRootURL, \"/\") {\n\t\t\topts.APIRootURL = strings.TrimRight(opts.APIRootURL, \"/\")\n\t\t}\n\n\t\tiaas.SakuraCloudAPIRoot = opts.APIRootURL\n\t}\n\n\treturn caller\n}\n"
  },
  {
    "path": "providers/dns/sakuracloud/sakuracloud.toml",
    "content": "Name = \"Sakura Cloud\"\nDescription = ''''''\nURL = \"https://cloud.sakura.ad.jp/\"\nCode = \"sakuracloud\"\nSince = \"v1.1.0\"\n\nExample = '''\nSAKURACLOUD_ACCESS_TOKEN=xxxxx \\\nSAKURACLOUD_ACCESS_TOKEN_SECRET=yyyyy \\\nlego --dns sakuracloud -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    SAKURACLOUD_ACCESS_TOKEN = \"Access token\"\n    SAKURACLOUD_ACCESS_TOKEN_SECRET = \"Access token secret\"\n  [Configuration.Additional]\n    SAKURACLOUD_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    SAKURACLOUD_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    SAKURACLOUD_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    SAKURACLOUD_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://developer.sakura.ad.jp/cloud/api/1.1/\"\n  GoClient = \"https://github.com/sacloud/iaas-api-go\"\n"
  },
  {
    "path": "providers/dns/sakuracloud/sakuracloud_test.go",
    "content": "package sakuracloud\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAccessToken,\n\tEnvAccessTokenSecret).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessToken:       \"123\",\n\t\t\t\tEnvAccessTokenSecret: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessToken:       \"\",\n\t\t\t\tEnvAccessTokenSecret: \"\",\n\t\t\t},\n\t\t\texpected: \"sakuracloud: some credentials information are missing: SAKURACLOUD_ACCESS_TOKEN,SAKURACLOUD_ACCESS_TOKEN_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessToken:       \"\",\n\t\t\t\tEnvAccessTokenSecret: \"456\",\n\t\t\t},\n\t\t\texpected: \"sakuracloud: some credentials information are missing: SAKURACLOUD_ACCESS_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing token secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessToken:       \"123\",\n\t\t\t\tEnvAccessTokenSecret: \"\",\n\t\t\t},\n\t\t\texpected: \"sakuracloud: some credentials information are missing: SAKURACLOUD_ACCESS_TOKEN_SECRET\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\tsecret   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\ttoken:  \"123\",\n\t\t\tsecret: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"sakuracloud: AccessToken is missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing token\",\n\t\t\tsecret:   \"456\",\n\t\t\texpected: \"sakuracloud: AccessToken is missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret\",\n\t\t\ttoken:    \"123\",\n\t\t\texpected: \"sakuracloud: AccessSecret is missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.token\n\t\t\tconfig.Secret = test.secret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/sakuracloud/wrapper.go",
    "content": "package sakuracloud\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/sacloud/iaas-api-go\"\n\t\"github.com/sacloud/iaas-api-go/search\"\n)\n\n// This mutex is required for concurrent updates.\n// see: https://github.com/go-acme/lego/pull/850\nvar mu sync.Mutex\n\nfunc (d *DNSProvider) addTXTRecord(ctx context.Context, fqdn, value string, ttl int) error {\n\tmu.Lock()\n\tdefer mu.Unlock()\n\n\tzone, err := d.getHostedZone(ctx, fqdn)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, zone.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trecords := append(zone.Records, &iaas.DNSRecord{\n\t\tName:  subDomain,\n\t\tType:  \"TXT\",\n\t\tRData: value,\n\t\tTTL:   ttl,\n\t})\n\n\t_, err = d.client.UpdateSettings(ctx, zone.ID, &iaas.DNSUpdateSettingsRequest{\n\t\tRecords:      records,\n\t\tSettingsHash: zone.SettingsHash,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"API call failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) cleanupTXTRecord(ctx context.Context, fqdn, value string) error {\n\tmu.Lock()\n\tdefer mu.Unlock()\n\n\tzone, err := d.getHostedZone(ctx, fqdn)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, zone.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar updRecords iaas.DNSRecords\n\n\tfor _, r := range zone.Records {\n\t\tif !(r.Name == subDomain && r.Type == \"TXT\" && r.RData == value) { //nolint:staticcheck // Clearer without De Morgan's law.\n\t\t\tupdRecords = append(updRecords, r)\n\t\t}\n\t}\n\n\tsettings := &iaas.DNSUpdateSettingsRequest{\n\t\tRecords:      updRecords,\n\t\tSettingsHash: zone.SettingsHash,\n\t}\n\n\t_, err = d.client.UpdateSettings(ctx, zone.ID, settings)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"API call failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (*iaas.DNS, error) {\n\tauthZone, err := dns01.FindZoneByFqdn(domain)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tzoneName := dns01.UnFqdn(authZone)\n\n\tconditions := &iaas.FindCondition{\n\t\tFilter: search.Filter{\n\t\t\tsearch.Key(\"Name\"): search.ExactMatch(zoneName),\n\t\t},\n\t}\n\n\tres, err := d.client.Find(ctx, conditions)\n\tif err != nil {\n\t\tif iaas.IsNotFoundError(err) {\n\t\t\treturn nil, fmt.Errorf(\"zone %s not found on SakuraCloud DNS: %w\", zoneName, err)\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"API call failed: %w\", err)\n\t}\n\n\tfor _, zone := range res.DNS {\n\t\tif zone.Name == zoneName {\n\t\t\treturn zone, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"zone %s not found\", zoneName)\n}\n"
  },
  {
    "path": "providers/dns/sakuracloud/wrapper_test.go",
    "content": "package sakuracloud\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"testing\"\n\n\tclient \"github.com/sacloud/api-client-go\"\n\t\"github.com/sacloud/iaas-api-go\"\n\t\"github.com/sacloud/iaas-api-go/helper/api\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupTest(t *testing.T) {\n\tt.Helper()\n\n\tt.Setenv(\"SAKURACLOUD_FAKE_MODE\", \"1\")\n\n\tcreateDummyZone(t, fakeCaller())\n}\n\nfunc fakeCaller() iaas.APICaller {\n\treturn api.NewCallerWithOptions(&api.CallerOptions{\n\t\tOptions: &client.Options{\n\t\t\tAccessToken:       \"dummy\",\n\t\t\tAccessTokenSecret: \"dummy\",\n\t\t},\n\t\tFakeMode: true,\n\t})\n}\n\nfunc createDummyZone(t *testing.T, caller iaas.APICaller) {\n\tt.Helper()\n\n\tctx := t.Context()\n\n\tdnsOp := iaas.NewDNSOp(caller)\n\n\t// cleanup\n\tzones, err := dnsOp.Find(ctx, &iaas.FindCondition{})\n\trequire.NoError(t, err)\n\n\tfor _, zone := range zones.DNS {\n\t\tif zone.Name == \"example.com\" {\n\t\t\terr = dnsOp.Delete(ctx, zone.ID)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// create dummy zone\n\t_, err = iaas.NewDNSOp(caller).Create(t.Context(), &iaas.DNSCreateRequest{Name: \"example.com\"})\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_addAndCleanupRecords(t *testing.T) {\n\tsetupTest(t)\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = \"token1\"\n\tconfig.Secret = \"secret1\"\n\n\tp, err := NewDNSProviderConfig(config)\n\trequire.NoError(t, err)\n\n\tt.Run(\"addTXTRecord\", func(t *testing.T) {\n\t\tctx := t.Context()\n\n\t\terr = p.addTXTRecord(ctx, \"test.example.com.\", \"dummyValue\", 10)\n\t\trequire.NoError(t, err)\n\n\t\tupdZone, e := p.getHostedZone(ctx, \"test.example.com.\")\n\t\trequire.NoError(t, e)\n\t\trequire.NotNil(t, updZone)\n\n\t\trequire.Len(t, updZone.Records, 1)\n\t})\n\n\tt.Run(\"cleanupTXTRecord\", func(t *testing.T) {\n\t\tctx := t.Context()\n\n\t\terr = p.cleanupTXTRecord(ctx, \"test.example.com.\", \"dummyValue\")\n\t\trequire.NoError(t, err)\n\n\t\tupdZone, e := p.getHostedZone(ctx, \"test.example.com.\")\n\t\trequire.NoError(t, e)\n\t\trequire.NotNil(t, updZone)\n\n\t\trequire.Empty(t, updZone.Records)\n\t})\n}\n\nfunc TestDNSProvider_concurrentAddAndCleanupRecords(t *testing.T) {\n\tsetupTest(t)\n\n\tdummyRecordCount := 10\n\n\tvar providers []*DNSProvider\n\n\tfor range dummyRecordCount {\n\t\tconfig := NewDefaultConfig()\n\t\tconfig.Token = \"token3\"\n\t\tconfig.Secret = \"secret3\"\n\n\t\tp, err := NewDNSProviderConfig(config)\n\t\trequire.NoError(t, err)\n\n\t\tproviders = append(providers, p)\n\t}\n\n\tvar wg sync.WaitGroup\n\n\tt.Run(\"addTXTRecord\", func(t *testing.T) {\n\t\twg.Add(len(providers))\n\n\t\tctx := t.Context()\n\n\t\tfor i, p := range providers {\n\t\t\tgo func(j int, client *DNSProvider) {\n\t\t\t\terr := client.addTXTRecord(ctx, fmt.Sprintf(\"test%d.example.com.\", j), \"dummyValue\", 10)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\twg.Done()\n\t\t\t}(i, p)\n\t\t}\n\n\t\twg.Wait()\n\n\t\tupdZone, err := providers[0].getHostedZone(ctx, \"example.com.\")\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, updZone)\n\n\t\trequire.Len(t, updZone.Records, dummyRecordCount)\n\t})\n\n\tt.Run(\"cleanupTXTRecord\", func(t *testing.T) {\n\t\twg.Add(len(providers))\n\n\t\tctx := t.Context()\n\n\t\tfor i, p := range providers {\n\t\t\tgo func(i int, client *DNSProvider) {\n\t\t\t\terr := client.cleanupTXTRecord(ctx, fmt.Sprintf(\"test%d.example.com.\", i), \"dummyValue\")\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\twg.Done()\n\t\t\t}(i, p)\n\t\t}\n\n\t\twg.Wait()\n\n\t\tupdZone, err := providers[0].getHostedZone(ctx, \"example.com.\")\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, updZone)\n\n\t\trequire.Empty(t, updZone.Records)\n\t})\n}\n"
  },
  {
    "path": "providers/dns/scaleway/scaleway.go",
    "content": "// Package scaleway implements a DNS provider for solving the DNS-01 challenge using Scaleway Domains API.\n// Token: https://www.scaleway.com/en/docs/generate-an-api-token/\npackage scaleway\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\tscwdomain \"github.com/scaleway/scaleway-sdk-go/api/domain/v2beta1\"\n\t\"github.com/scaleway/scaleway-sdk-go/scw\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"SCALEWAY_\"\n\n\tEnvAPIToken  = envNamespace + \"API_TOKEN\"\n\tEnvProjectID = envNamespace + \"PROJECT_ID\"\n\n\taltEnvNamespace = \"SCW_\"\n\n\tEnvAccessKey = altEnvNamespace + \"ACCESS_KEY\"\n\tEnvSecretKey = altEnvNamespace + \"SECRET_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst (\n\tminTTL                    = 60\n\tdefaultPollingInterval    = 10 * time.Second\n\tdefaultPropagationTimeout = 120 * time.Second\n)\n\n// The access key is not used by the Scaleway client.\nconst dumpAccessKey = \"SCWXXXXXXXXXXXXXXXXX\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tProjectID string\n\tToken     string // TODO(ldez) rename to SecretKey in the next major.\n\tAccessKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tAccessKey:          dumpAccessKey,\n\t\tTTL:                env.GetOneWithFallback(EnvTTL, minTTL, strconv.Atoi, altEnvName(EnvTTL)),\n\t\tPropagationTimeout: env.GetOneWithFallback(EnvPropagationTimeout, defaultPropagationTimeout, env.ParseSecond, altEnvName(EnvPropagationTimeout)),\n\t\tPollingInterval:    env.GetOneWithFallback(EnvPollingInterval, defaultPollingInterval, env.ParseSecond, altEnvName(EnvPollingInterval)),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *scwdomain.API\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Scaleway Domains API.\n// Credentials must be passed in the environment variables:\n// SCALEWAY_API_TOKEN, SCALEWAY_PROJECT_ID.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.GetWithFallback([]string{EnvSecretKey, EnvAPIToken})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"scaleway: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvSecretKey]\n\tconfig.AccessKey = env.GetOrDefaultString(EnvAccessKey, dumpAccessKey)\n\tconfig.ProjectID = env.GetOrFile(EnvProjectID)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for scaleway.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"scaleway: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"scaleway: credentials missing\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\tconfig.TTL = minTTL\n\t}\n\n\tconfiguration := []scw.ClientOption{\n\t\tscw.WithAuth(config.AccessKey, config.Token),\n\t\tscw.WithUserAgent(useragent.Get()),\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tconfiguration = append(configuration, scw.WithHTTPClient(clientdebug.Wrap(config.HTTPClient)))\n\t}\n\n\tif config.ProjectID != \"\" {\n\t\tconfiguration = append(configuration, scw.WithDefaultProjectID(config.ProjectID))\n\t}\n\n\t// Create a Scaleway client\n\tclientScw, err := scw.NewClient(configuration...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"scaleway: %w\", err)\n\t}\n\n\treturn &DNSProvider{config: config, client: scwdomain.NewAPI(clientScw)}, nil\n}\n\n// Timeout returns the Timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill DNS-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecords := []*scwdomain.Record{{\n\t\tData:    fmt.Sprintf(`%q`, info.Value),\n\t\tName:    info.EffectiveFQDN,\n\t\tTTL:     uint32(d.config.TTL),\n\t\tType:    scwdomain.RecordTypeTXT,\n\t\tComment: scw.StringPtr(\"used by lego\"),\n\t}}\n\n\treq := &scwdomain.UpdateDNSZoneRecordsRequest{\n\t\tDNSZone: info.EffectiveFQDN,\n\t\tChanges: []*scwdomain.RecordChange{{\n\t\t\tAdd: &scwdomain.RecordChangeAdd{Records: records},\n\t\t}},\n\t\tReturnAllRecords:        scw.BoolPtr(false),\n\t\tDisallowNewZoneCreation: true,\n\t}\n\n\t_, err := d.client.UpdateDNSZoneRecords(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"scaleway: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes a TXT record used for DNS-01 challenge.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecordIdentifier := &scwdomain.RecordIdentifier{\n\t\tName: info.EffectiveFQDN,\n\t\tType: scwdomain.RecordTypeTXT,\n\t\tData: scw.StringPtr(fmt.Sprintf(`%q`, info.Value)),\n\t}\n\n\treq := &scwdomain.UpdateDNSZoneRecordsRequest{\n\t\tDNSZone: info.EffectiveFQDN,\n\t\tChanges: []*scwdomain.RecordChange{{\n\t\t\tDelete: &scwdomain.RecordChangeDelete{IDFields: recordIdentifier},\n\t\t}},\n\t\tReturnAllRecords:        scw.BoolPtr(false),\n\t\tDisallowNewZoneCreation: true,\n\t}\n\n\t_, err := d.client.UpdateDNSZoneRecords(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"scaleway: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc altEnvName(v string) string {\n\treturn strings.ReplaceAll(v, envNamespace, altEnvNamespace)\n}\n"
  },
  {
    "path": "providers/dns/scaleway/scaleway.toml",
    "content": "Name = \"Scaleway\"\nDescription = ''''''\nURL = \"https://developers.scaleway.com/\"\nCode = \"scaleway\"\nSince = \"v3.4.0\"\n\nExample = '''\nSCW_SECRET_KEY=xxxxxxx-xxxxx-xxxx-xxx-xxxxxx \\\nlego --dns scaleway -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    SCW_SECRET_KEY = \"Secret key\"\n    SCW_PROJECT_ID = \"Project to use (optional)\"\n  [Configuration.Additional]\n    SCW_ACCESS_KEY = \"Access key\"\n    SCW_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    SCW_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    SCW_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    SCW_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://developers.scaleway.com/en/products/domain/dns/api/\"\n"
  },
  {
    "path": "providers/dns/scaleway/scaleway_test.go",
    "content": "package scaleway\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIToken, EnvSecretKey, EnvAccessKey, EnvProjectID).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken:  \"00000000-0000-0000-0000-000000000000\",\n\t\t\t\tEnvProjectID: \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken:  \"\",\n\t\t\t\tEnvProjectID: \"\",\n\t\t\t},\n\t\t\texpected: fmt.Sprintf(\"scaleway: some credentials information are missing: %s\", EnvSecretKey),\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\tttl      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"00000000-0000-0000-0000-000000000000\",\n\t\t\tttl:   minTTL,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api key\",\n\t\t\ttoken:    \"\",\n\t\t\tttl:      minTTL,\n\t\t\texpected: \"scaleway: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.TTL = test.ttl\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/selectel/selectel.go",
    "content": "// Package selectel implements a DNS provider for solving the DNS-01 challenge using Selectel Domains API.\n// Selectel Domain API reference: https://kb.selectel.com/23136054.html\n// Token: https://my.selectel.ru/profile/apikeys\npackage selectel\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/selectel\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"SELECTEL_\"\n\n\tEnvBaseURL  = envNamespace + \"BASE_URL\"\n\tEnvAPIToken = envNamespace + \"API_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config = selectel.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tBaseURL:            env.GetOrDefaultString(EnvBaseURL, \"\"),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, selectel.MinTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Selectel Domains API.\n// API token must be passed in the environment variable SELECTEL_API_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"selectel: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvAPIToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for selectel.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"selectel: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := selectel.NewDNSProviderConfig(config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"selectel: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"selectel: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"selectel: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n"
  },
  {
    "path": "providers/dns/selectel/selectel.toml",
    "content": "Name = \"Selectel\"\nDescription = ''''''\nURL = \"https://kb.selectel.com/\"\nCode = \"selectel\"\nSince = \"v1.2.0\"\n\nExample = '''\nSELECTEL_API_TOKEN=xxxxx \\\nlego --dns selectel -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    SELECTEL_API_TOKEN = \"API token\"\n  [Configuration.Additional]\n    SELECTEL_BASE_URL = \"API endpoint URL\"\n    SELECTEL_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    SELECTEL_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    SELECTEL_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    SELECTEL_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://kb.selectel.com/23136054.html\"\n"
  },
  {
    "path": "providers/dns/selectel/selectel_test.go",
    "content": "package selectel\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/selectel\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar envTest = tester.NewEnvTest(EnvAPIToken, EnvTTL)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"\",\n\t\t\t},\n\t\t\texpected: fmt.Sprintf(\"selectel: some credentials information are missing: %s\", EnvAPIToken),\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\tttl      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"123\",\n\t\t\tttl:   60,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api key\",\n\t\t\ttoken:    \"\",\n\t\t\tttl:      60,\n\t\t\texpected: \"selectel: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"bad TTL value\",\n\t\t\ttoken:    \"123\",\n\t\t\tttl:      59,\n\t\t\texpected: fmt.Sprintf(\"selectel: invalid TTL, TTL (59) must be greater than %d\", selectel.MinTTL),\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.TTL = test.ttl\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/selectelv2/selectelv2.go",
    "content": "// Package selectelv2 implements a DNS provider for solving the DNS-01 challenge using Selectel Domains APIv2.\npackage selectelv2\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\t\"github.com/miekg/dns\"\n\tselectelapi \"github.com/selectel/domains-go/pkg/v2\"\n\t\"github.com/selectel/go-selvpcclient/v4/selvpcclient\"\n\t\"golang.org/x/net/idna\"\n)\n\nconst (\n\tenvNamespace = \"SELECTELV2_\"\n\n\tEnvBaseURL        = envNamespace + \"BASE_URL\"\n\tEnvUsernameOS     = envNamespace + \"USERNAME\"\n\tEnvPasswordOS     = envNamespace + \"PASSWORD\"\n\tEnvDomainName     = envNamespace + \"ACCOUNT_ID\"\n\tEnvProjectID      = envNamespace + \"PROJECT_ID\"\n\tEnvAuthRegion     = envNamespace + \"AUTH_REGION\"\n\tEnvAuthURL        = envNamespace + \"AUTH_URL\"\n\tEnvUserDomainName = envNamespace + \"USER_DOMAIN_NAME\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst (\n\tdefaultBaseURL    = \"https://api.selectel.ru/domains/v2\"\n\tdefaultAuthRegion = \"ru-1\"\n\tdefaultAuthURL    = \"https://cloud.api.selcloud.ru/identity/v3/\"\n)\n\nconst (\n\tdefaultTTL                = 60\n\tdefaultPropagationTimeout = 120 * time.Second\n\tdefaultPollingInterval    = 5 * time.Second\n\tdefaultHTTPTimeout        = 30 * time.Second\n)\n\nconst tokenHeader = \"X-Auth-Token\"\n\nvar errNotFound = errors.New(\"rrset not found\")\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL        string\n\tUsername       string\n\tPassword       string\n\tDomainName     string\n\tProjectID      string\n\tAuthURL        string\n\tAuthRegion     string\n\tUserDomainName string\n\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tBaseURL:    env.GetOrDefaultString(EnvBaseURL, defaultBaseURL),\n\t\tAuthRegion: env.GetOrDefaultString(EnvAuthRegion, defaultAuthRegion),\n\t\tAuthURL:    env.GetOrDefaultString(EnvAuthURL, defaultAuthURL),\n\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, defaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, defaultHTTPTimeout),\n\t\t},\n\t}\n}\n\ntype DNSProvider struct {\n\tbaseClient selectelapi.DNSClient[selectelapi.Zone, selectelapi.RRSet]\n\tconfig     *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Selectel Domains APIv2.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsernameOS, EnvPasswordOS, EnvDomainName, EnvProjectID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"selectelv2: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsernameOS]\n\tconfig.Password = values[EnvPasswordOS]\n\tconfig.DomainName = values[EnvDomainName]\n\tconfig.ProjectID = values[EnvProjectID]\n\tconfig.UserDomainName = env.GetOrDefaultString(EnvUserDomainName, \"\")\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for selectel.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"selectelv2: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Username == \"\" {\n\t\treturn nil, errors.New(\"selectelv2: missing username\")\n\t}\n\n\tif config.Password == \"\" {\n\t\treturn nil, errors.New(\"selectelv2: missing password\")\n\t}\n\n\tif config.DomainName == \"\" {\n\t\treturn nil, errors.New(\"selectelv2: missing account ID\")\n\t}\n\n\tif config.ProjectID == \"\" {\n\t\treturn nil, errors.New(\"selectelv2: missing project ID\")\n\t}\n\n\theaders := http.Header{}\n\tuseragent.SetHeader(headers)\n\n\treturn &DNSProvider{\n\t\tbaseClient: selectelapi.NewClient(config.BaseURL, clientdebug.Wrap(config.HTTPClient), headers),\n\t\tconfig:     config,\n\t}, nil\n}\n\n// Timeout returns the Timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill DNS-01 challenge.\nfunc (d *DNSProvider) Present(domain, _, keyAuth string) error {\n\tctx := context.Background()\n\n\tclient, err := d.authorize(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"selectelv2: authorize: %w\", err)\n\t}\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := client.getZone(ctx, domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"selectelv2: get zone: %w\", err)\n\t}\n\n\trrset, err := client.getRRset(ctx, dns01.UnFqdn(info.EffectiveFQDN), zone.ID)\n\tif err != nil {\n\t\tif !errors.Is(err, errNotFound) {\n\t\t\treturn fmt.Errorf(\"selectelv2: get RRSet: %w\", err)\n\t\t}\n\n\t\tnewRRSet := &selectelapi.RRSet{\n\t\t\tName:    info.EffectiveFQDN,\n\t\t\tType:    selectelapi.TXT,\n\t\t\tTTL:     d.config.TTL,\n\t\t\tRecords: []selectelapi.RecordItem{{Content: fmt.Sprintf(\"%q\", info.Value)}},\n\t\t}\n\n\t\t_, err = client.CreateRRSet(ctx, zone.ID, newRRSet)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"selectelv2: create RRSet: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\trrset.Records = append(rrset.Records, selectelapi.RecordItem{Content: fmt.Sprintf(\"%q\", info.Value)})\n\n\terr = client.UpdateRRSet(ctx, zone.ID, rrset.ID, rrset)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"selectelv2: update RRSet: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes a TXT record used for DNS-01 challenge.\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\tctx := context.Background()\n\n\tclient, err := d.authorize(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"selectelv2: authorize: %w\", err)\n\t}\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := client.getZone(ctx, domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"selectelv2: get zone: %w\", err)\n\t}\n\n\trrset, err := client.getRRset(ctx, dns01.UnFqdn(info.EffectiveFQDN), zone.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"selectelv2: get RRSet: %w\", err)\n\t}\n\n\tif len(rrset.Records) <= 1 {\n\t\terr = client.DeleteRRSet(ctx, zone.ID, rrset.ID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"selectelv2: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfor i, item := range rrset.Records {\n\t\tif strings.Trim(item.Content, `\"`) == info.Value {\n\t\t\trrset.Records = append(rrset.Records[:i], rrset.Records[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\n\terr = client.UpdateRRSet(ctx, zone.ID, rrset.ID, rrset)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"selectelv2: update RRSet: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) authorize(ctx context.Context) (*clientWrapper, error) {\n\ttoken, err := obtainOpenstackToken(ctx, d.config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\textraHeaders := http.Header{}\n\textraHeaders.Set(tokenHeader, token)\n\n\treturn &clientWrapper{\n\t\tDNSClient: d.baseClient.WithHeaders(extraHeaders),\n\t}, nil\n}\n\nfunc obtainOpenstackToken(ctx context.Context, config *Config) (string, error) {\n\tvpcClient, err := selvpcclient.NewClient(&selvpcclient.ClientOptions{\n\t\tContext:        ctx,\n\t\tDomainName:     config.DomainName,\n\t\tAuthURL:        config.AuthURL,\n\t\tAuthRegion:     config.AuthRegion,\n\t\tUsername:       config.Username,\n\t\tPassword:       config.Password,\n\t\tProjectID:      config.ProjectID,\n\t\tUserDomainName: config.UserDomainName,\n\t})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"new VPC client: %w\", err)\n\t}\n\n\treturn vpcClient.GetXAuthToken(), nil\n}\n\ntype clientWrapper struct {\n\tselectelapi.DNSClient[selectelapi.Zone, selectelapi.RRSet]\n}\n\nfunc (w *clientWrapper) getZone(ctx context.Context, name string) (*selectelapi.Zone, error) {\n\tunicodeName, err := idna.ToUnicode(name)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"to unicode: %w\", err)\n\t}\n\n\tparams := &map[string]string{\"filter\": unicodeName}\n\n\tzones, err := w.ListZones(ctx, params)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"list zone: %w\", err)\n\t}\n\n\tfor _, zone := range zones.GetItems() {\n\t\tif zone.Name == dns.Fqdn(unicodeName) {\n\t\t\treturn zone, nil\n\t\t}\n\t}\n\n\tif len(strings.Split(dns01.UnFqdn(name), \".\")) == 1 {\n\t\treturn nil, fmt.Errorf(\"zone '%s' for challenge has not been found\", name)\n\t}\n\n\t// after is always defined since if no dots present we exit above.\n\t_, after, _ := strings.Cut(name, \".\")\n\n\treturn w.getZone(ctx, after)\n}\n\nfunc (w *clientWrapper) getRRset(ctx context.Context, name, zoneID string) (*selectelapi.RRSet, error) {\n\tunicodeName, err := idna.ToUnicode(name)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"to unicode: %w\", err)\n\t}\n\n\tparams := &map[string]string{\"name\": unicodeName, \"rrset_types\": string(selectelapi.TXT)}\n\n\tresp, err := w.ListRRSets(ctx, zoneID, params)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"list rrset: %w\", err)\n\t}\n\n\tfor _, rrset := range resp.GetItems() {\n\t\tif rrset.Name == dns.Fqdn(unicodeName) {\n\t\t\treturn rrset, nil\n\t\t}\n\t}\n\n\treturn nil, errNotFound\n}\n"
  },
  {
    "path": "providers/dns/selectelv2/selectelv2.toml",
    "content": "Name = \"Selectel v2\"\nDescription = ''''''\nURL = \"https://selectel.ru\"\nCode = \"selectelv2\"\nSince = \"v4.17.0\"\n\nExample = '''\nSELECTELV2_USERNAME=trex \\\nSELECTELV2_PASSWORD=xxxxx \\\nSELECTELV2_ACCOUNT_ID=1234567 \\\nSELECTELV2_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \\\nlego --dns selectelv2 -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    SELECTELV2_USERNAME = \"Openstack username\"\n    SELECTELV2_PASSWORD = \"Openstack username's password\"\n    SELECTELV2_ACCOUNT_ID = \"Selectel account ID (INT)\"\n    SELECTELV2_PROJECT_ID = \"Cloud project ID (UUID)\"\n  [Configuration.Additional]\n    SELECTELV2_BASE_URL = \"API endpoint URL\"\n    SELECTELV2_AUTH_REGION = \"Location for auth endpoint like ResellAPI or Keystone (default: 'ru-1')\"\n    SELECTELV2_AUTH_URL = \"Identity endpoint (defaul: 'https://cloud.api.selcloud.ru/identity/v3/')\"\n    SELECTELV2_USER_DOMAIN_NAME = \"To specify the domain name (account ID) where the user is located. (default: SELECTELV2_ACCOUNT_ID)\"\n    SELECTELV2_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 5)\"\n    SELECTELV2_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    SELECTELV2_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    SELECTELV2_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://developers.selectel.ru/docs/cloud-services/dns_api/dns_api_actual/\"\n  GoClient = \"https://github.com/selectel/domains-go\"\n"
  },
  {
    "path": "providers/dns/selectelv2/selectelv2_test.go",
    "content": "package selectelv2\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvUsernameOS,\n\tEnvPasswordOS,\n\tEnvDomainName,\n\tEnvUserDomainName,\n\tEnvProjectID,\n\tEnvAuthRegion,\n\tEnvAuthURL,\n).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsernameOS: \"someName\",\n\t\t\t\tEnvPasswordOS: \"qwerty\",\n\t\t\t\tEnvDomainName: \"1\",\n\t\t\t\tEnvProjectID:  \"111a11111aaa11aa1a11aaa11111aa1a\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPasswordOS: \"qwerty\",\n\t\t\t\tEnvDomainName: \"1\",\n\t\t\t\tEnvProjectID:  \"111a11111aaa11aa1a11aaa11111aa1a\",\n\t\t\t},\n\t\t\texpected: \"selectelv2: some credentials information are missing: SELECTELV2_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsernameOS: \"someName\",\n\t\t\t\tEnvDomainName: \"1\",\n\t\t\t\tEnvProjectID:  \"111a11111aaa11aa1a11aaa11111aa1a\",\n\t\t\t},\n\t\t\texpected: \"selectelv2: some credentials information are missing: SELECTELV2_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing account\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsernameOS: \"someName\",\n\t\t\t\tEnvPasswordOS: \"qwerty\",\n\t\t\t\tEnvProjectID:  \"111a11111aaa11aa1a11aaa11111aa1a\",\n\t\t\t},\n\t\t\texpected: \"selectelv2: some credentials information are missing: SELECTELV2_ACCOUNT_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing project\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsernameOS: \"someName\",\n\t\t\t\tEnvPasswordOS: \"qwerty\",\n\t\t\t\tEnvDomainName: \"1\",\n\t\t\t},\n\t\t\texpected: \"selectelv2: some credentials information are missing: SELECTELV2_PROJECT_ID\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.baseClient)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tusername  string\n\t\tpassword  string\n\t\taccount   string\n\t\tprojectID string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tusername:  \"user\",\n\t\t\tpassword:  \"secret\",\n\t\t\taccount:   \"1\",\n\t\t\tprojectID: \"111a11111aaa11aa1a11aaa11111aa1a\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing username\",\n\t\t\tpassword:  \"secret\",\n\t\t\taccount:   \"1\",\n\t\t\tprojectID: \"111a11111aaa11aa1a11aaa11111aa1a\",\n\t\t\texpected:  \"selectelv2: missing username\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing password\",\n\t\t\tusername:  \"user\",\n\t\t\taccount:   \"1\",\n\t\t\tprojectID: \"111a11111aaa11aa1a11aaa11111aa1a\",\n\t\t\texpected:  \"selectelv2: missing password\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing account\",\n\t\t\tusername:  \"user\",\n\t\t\tpassword:  \"secret\",\n\t\t\tprojectID: \"111a11111aaa11aa1a11aaa11111aa1a\",\n\t\t\texpected:  \"selectelv2: missing account ID\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing projectID\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t\taccount:  \"1\",\n\t\t\texpected: \"selectelv2: missing project ID\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\t\t\tconfig.DomainName = test.account\n\t\t\tconfig.ProjectID = test.projectID\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.baseClient)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/selfhostde/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://selfhost.de/cgi-bin/api.pl\"\n\n// Client the SelfHost client.\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tbaseURL    string\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(username, password string) *Client {\n\treturn &Client{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\tbaseURL:    defaultBaseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// UpdateTXTRecord updates content of an existing TXT record.\nfunc (c *Client) UpdateTXTRecord(ctx context.Context, recordID, content string) error {\n\tendpoint, err := url.Parse(c.baseURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parse URL:  %w\", err)\n\t}\n\n\tquery := endpoint.Query()\n\tquery.Set(\"username\", c.username)\n\tquery.Set(\"password\", c.password)\n\tquery.Set(\"rid\", recordID)\n\tquery.Set(\"content\", content)\n\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"new HTTP request: %w\", err)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/selfhostde/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient := NewClient(\"user\", \"secret\")\n\tclient.baseURL = server.URL\n\tclient.HTTPClient = server.Client()\n\n\treturn client, nil\n}\n\nfunc TestClient_UpdateTXTRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /\", nil, servermock.CheckQueryParameter().Strict().\n\t\t\tWith(\"rid\", \"123456\").\n\t\t\tWith(\"content\", \"txt\").\n\t\t\tWith(\"username\", \"user\").\n\t\t\tWith(\"password\", \"secret\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.UpdateTXTRecord(t.Context(), \"123456\", \"txt\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_UpdateTXTRecord_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /\", servermock.Noop().WithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\terr := client.UpdateTXTRecord(t.Context(), \"123456\", \"txt\")\n\trequire.EqualError(t, err, \"unexpected status code: [status code: 400] body: \")\n}\n"
  },
  {
    "path": "providers/dns/selfhostde/internal/readme.md",
    "content": "# SelfHost.(de|eu)\n\nSelfHost doesn't provide an official API documentation and there are no endpoints for create a TXT record or delete a TXT record.\n\n## More\n\nThe documentation found at https://kirk.selfhost.de/cgi-bin/selfhost?p=document&name=api (PDF) describes the DynDNS/ddns API endpoint and is not used by our client.\n"
  },
  {
    "path": "providers/dns/selfhostde/mapping.go",
    "content": "package selfhostde\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nconst (\n\tlineSep   = \",\"\n\trecordSep = \":\"\n)\n\ntype Seq struct {\n\tcursor int\n\tids    []string\n}\n\nfunc NewSeq(ids ...string) *Seq {\n\treturn &Seq{ids: ids}\n}\n\nfunc (s *Seq) Next() string {\n\tif len(s.ids) == 1 {\n\t\treturn s.ids[0]\n\t}\n\n\tv := s.ids[s.cursor]\n\n\tif s.cursor < len(s.ids)-1 {\n\t\ts.cursor++\n\t} else {\n\t\ts.cursor = 0\n\t}\n\n\treturn v\n}\n\nfunc parseRecordsMapping(raw string) (map[string]*Seq, error) {\n\traw = strings.ReplaceAll(raw, \" \", \"\")\n\n\tif raw == \"\" {\n\t\treturn nil, errors.New(\"empty mapping\")\n\t}\n\n\tacc := map[string]*Seq{}\n\n\tfor {\n\t\tindex, err := safeIndex(raw, lineSep)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif index != -1 {\n\t\t\tname, seq, err := parseLine(raw[:index])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tacc[name] = seq\n\n\t\t\t// Data for the next iteration.\n\t\t\traw = raw[index+1:]\n\n\t\t\tcontinue\n\t\t}\n\n\t\tname, seq, errP := parseLine(raw)\n\t\tif errP != nil {\n\t\t\treturn nil, errP\n\t\t}\n\n\t\tacc[name] = seq\n\n\t\treturn acc, nil\n\t}\n}\n\nfunc parseLine(line string) (string, *Seq, error) {\n\tidx, err := safeIndex(line, recordSep)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tif idx == -1 {\n\t\treturn \"\", nil, fmt.Errorf(\"missing %q: %s\", recordSep, line)\n\t}\n\n\tname, rawIDs := line[:idx], line[idx+1:]\n\n\tvar (\n\t\tids   []string\n\t\tcount int\n\t)\n\n\tfor {\n\t\tidx, err = safeIndex(rawIDs, recordSep)\n\t\tif err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\n\t\tif count == 2 {\n\t\t\treturn \"\", nil, fmt.Errorf(\"too many record IDs for one domain: %s\", line)\n\t\t}\n\n\t\tif idx != -1 {\n\t\t\tids = append(ids, rawIDs[:idx])\n\t\t\tcount++\n\n\t\t\t// Data for the next iteration.\n\t\t\trawIDs = rawIDs[idx+1:]\n\n\t\t\tcontinue\n\t\t}\n\n\t\tids = append(ids, rawIDs)\n\n\t\treturn name, NewSeq(ids...), nil\n\t}\n}\n\nfunc safeIndex(v, sep string) (int, error) {\n\tindex := strings.Index(v, sep)\n\tif index == 0 {\n\t\treturn 0, fmt.Errorf(\"first char is %q: %s\", sep, v)\n\t}\n\n\tif index == len(v)-1 {\n\t\treturn 0, fmt.Errorf(\"last char is %q: %s\", sep, v)\n\t}\n\n\treturn index, nil\n}\n"
  },
  {
    "path": "providers/dns/selfhostde/mapping_test.go",
    "content": "package selfhostde\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_parseRecordsMapping(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\trawData  string\n\t\texpected map[string]*Seq\n\t}{\n\t\t{\n\t\t\tdesc:    \"one domain, one record id\",\n\t\t\trawData: \"example.com:123\",\n\t\t\texpected: map[string]*Seq{\n\t\t\t\t\"example.com\": NewSeq(\"123\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:    \"several domain, one record id\",\n\t\t\trawData: \"example.com:123, example.org:456,foo.example.com:789\",\n\t\t\texpected: map[string]*Seq{\n\t\t\t\t\"example.com\":     NewSeq(\"123\"),\n\t\t\t\t\"example.org\":     NewSeq(\"456\"),\n\t\t\t\t\"foo.example.com\": NewSeq(\"789\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:    \"one domain, 2 record ids\",\n\t\t\trawData: \"example.com:123:456\",\n\t\t\texpected: map[string]*Seq{\n\t\t\t\t\"example.com\": NewSeq(\"123\", \"456\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:    \"several domain, 2 record ids\",\n\t\t\trawData: \"example.com:123:321, example.org:456:654,foo.example.com:789:987\",\n\t\t\texpected: map[string]*Seq{\n\t\t\t\t\"example.com\":     NewSeq(\"123\", \"321\"),\n\t\t\t\t\"example.org\":     NewSeq(\"456\", \"654\"),\n\t\t\t\t\"foo.example.com\": NewSeq(\"789\", \"987\"),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tmapping, err := parseRecordsMapping(test.rawData)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.expected, mapping)\n\t\t})\n\t}\n}\n\nfunc Test_parseRecordsMapping_error(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\trawData  string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"empty\",\n\t\t\trawData:  \"\",\n\t\t\texpected: \"empty mapping\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"only spaces\",\n\t\t\trawData:  \"    \",\n\t\t\texpected: \"empty mapping\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"one domain, no record id\",\n\t\t\trawData:  \"example.com\",\n\t\t\texpected: `missing \":\": example.com`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"one domain, more than 2 record ids\",\n\t\t\trawData:  \"example.com:123:456:789\",\n\t\t\texpected: \"too many record IDs for one domain: example.com:123:456:789\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"several domain, more than 2 record ids\",\n\t\t\trawData:  \"example.com:123, example.org:456:789:147\",\n\t\t\texpected: \"too many record IDs for one domain: example.org:456:789:147\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"no ids, ends with 2 dots\",\n\t\t\trawData:  \"example.com:\",\n\t\t\texpected: `last char is \":\": example.com:`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"no ids,starts with 2 dots\",\n\t\t\trawData:  \":example.com\",\n\t\t\texpected: `first char is \":\": :example.com`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"with ids but ends with 2 dots\",\n\t\t\trawData:  \"example.com:123:\",\n\t\t\texpected: `last char is \":\": 123:`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"only 2 dots\",\n\t\t\trawData:  \":\",\n\t\t\texpected: `first char is \":\": :`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"only comma\",\n\t\t\trawData:  \",\",\n\t\t\texpected: `first char is \",\": ,`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"ends with comma\",\n\t\t\trawData:  \"example.com,\",\n\t\t\texpected: `last char is \",\": example.com,`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"combo\",\n\t\t\trawData:  \"::::,::\",\n\t\t\texpected: `first char is \":\": ::::`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t_, err := parseRecordsMapping(test.rawData)\n\t\t\trequire.EqualError(t, err, test.expected)\n\t\t})\n\t}\n}\n\nfunc TestSeq_Next(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tids      []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tdesc:     \"one value\",\n\t\t\tids:      []string{\"a\"},\n\t\t\texpected: []string{\"a\", \"a\", \"a\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"two values\",\n\t\t\tids:      []string{\"a\", \"b\"},\n\t\t\texpected: []string{\"a\", \"b\", \"a\", \"b\"},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"three values\",\n\t\t\tids:      []string{\"a\", \"b\", \"c\"},\n\t\t\texpected: []string{\"a\", \"b\", \"c\", \"a\", \"b\", \"c\", \"a\"},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tseq := NewSeq(test.ids...)\n\t\t\tfor _, s := range test.expected {\n\t\t\t\tassert.Equal(t, s, seq.Next())\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/selfhostde/selfhostde.go",
    "content": "// Package selfhostde implements a DNS provider for solving the DNS-01 challenge using SelfHost.(de|eu).\npackage selfhostde\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/selfhostde/internal\"\n)\n\n// Environment variables.\nconst (\n\tenvNamespace = \"SELFHOSTDE_\"\n\n\tEnvUsername       = envNamespace + \"USERNAME\"\n\tEnvPassword       = envNamespace + \"PASSWORD\"\n\tEnvRecordsMapping = envNamespace + \"RECORDS_MAPPING\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername string\n\tPassword string\n\n\tRecordsMapping   map[string]*Seq\n\trecordsMappingMu sync.Mutex\n\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 4*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 30*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\nfunc (c *Config) getSeqNext(domain string) (string, error) {\n\teffectiveDomain := strings.TrimPrefix(domain, \"_acme-challenge.\")\n\n\tc.recordsMappingMu.Lock()\n\tdefer c.recordsMappingMu.Unlock()\n\n\tseq, ok := c.RecordsMapping[effectiveDomain]\n\tif !ok {\n\t\t// fallback\n\t\tseq, ok = c.RecordsMapping[domain]\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"record mapping not found for %q\", effectiveDomain)\n\t\t}\n\t}\n\n\treturn seq.Next(), nil\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for SelfHost.(de|eu).\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword, EnvRecordsMapping)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"selfhostde: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\tmapping, err := parseRecordsMapping(values[EnvRecordsMapping])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"selfhostde: malformed records mapping: %w\", err)\n\t}\n\n\tconfig.RecordsMapping = mapping\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for SelfHost.(de|eu).\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"selfhostde: supplied configuration is nil\")\n\t}\n\n\tif config.Username == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"selfhostde: credentials missing\")\n\t}\n\n\tif len(config.RecordsMapping) == 0 {\n\t\treturn nil, errors.New(\"selfhostde: missing record mapping\")\n\t}\n\n\tfor domain, seq := range config.RecordsMapping {\n\t\tif seq == nil || len(seq.ids) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"selfhostde: missing record ID for %q\", domain)\n\t\t}\n\t}\n\n\tclient := internal.NewClient(config.Username, config.Password)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecordID, err := d.config.getSeqNext(dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"selfhostde: %w\", err)\n\t}\n\n\terr = d.client.UpdateTXTRecord(context.Background(), recordID, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"selfhostde: update DNS TXT record (id=%s): %w\", recordID, err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = recordID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record previously created.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"selfhostde: unknown record ID for %q\", dns01.UnFqdn(info.EffectiveFQDN))\n\t}\n\n\terr := d.client.UpdateTXTRecord(context.Background(), recordID, \"empty\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"selfhostde: emptied DNS TXT record (id=%s): %w\", recordID, err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/selfhostde/selfhostde.toml",
    "content": "Name = \"SelfHost.(de|eu)\"\nDescription = ''''''\nURL = \"https://www.selfhost.de\"\nCode = \"selfhostde\"\nSince = \"v4.19.0\"\n\nExample = '''\nSELFHOSTDE_USERNAME=xxx \\\nSELFHOSTDE_PASSWORD=yyy \\\nSELFHOSTDE_RECORDS_MAPPING=my.example.com:123 \\\nlego --dns selfhostde -d '*.example.com' -d example.com run\n'''\n\nAdditional = \"\"\"\nSelfHost.de doesn't have an API to create or delete TXT records,\nthere is only an \"unofficial\" and undocumented endpoint to update an existing TXT record.\n\nSo, before using lego to request a certificate for a given domain or wildcard (such as `my.example.org` or `*.my.example.org`),\nyou must create:\n\n- one TXT record named `_acme-challenge.my.example.org` if you are **not** using wildcard for this domain.\n- two TXT records named `_acme-challenge.my.example.org` if you are using wildcard for this domain.\n\nAfter that you must edit the TXT record(s) to get the ID(s).\n\nYou then must prepare the `SELFHOSTDE_RECORDS_MAPPING` environment variable with the following format:\n\n```\n<domain_A>:<record_id_A1>:<record_id_A2>,<domain_B>:<record_id_B1>:<record_id_B2>,<domain_C>:<record_id_C1>:<record_id_C2>\n```\n\nwhere each group of domain + record ID(s) is separated with a comma (`,`),\nand the domain and record ID(s) are separated with a colon (`:`).\n\nFor example, if you want to create or renew a certificate for `my.example.org`, `*.my.example.org`, and `other.example.org`,\nyou would need:\n\n- two separate records for `_acme-challenge.my.example.org`\n- and another separate record for `_acme-challenge.other.example.org`\n\nThe resulting environment variable would then be: `SELFHOSTDE_RECORDS_MAPPING=my.example.com:123:456,other.example.com:789`\n\n\"\"\"\n\n[Configuration]\n  [Configuration.Credentials]\n    SELFHOSTDE_USERNAME = \"Username\"\n    SELFHOSTDE_PASSWORD = \"Password\"\n    SELFHOSTDE_RECORDS_MAPPING = \"Record IDs mapping with domains (ex: example.com:123:456,example.org:789,foo.example.com:147)\"\n  [Configuration.Additional]\n    SELFHOSTDE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 30)\"\n    SELFHOSTDE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 240)\"\n    SELFHOSTDE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    SELFHOSTDE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n"
  },
  {
    "path": "providers/dns/selfhostde/selfhostde_test.go",
    "content": "package selfhostde\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvUsername, EnvPassword, EnvRecordsMapping).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername:       \"user\",\n\t\t\t\tEnvPassword:       \"secret\",\n\t\t\t\tEnvRecordsMapping: \"example.com:123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPassword:       \"secret\",\n\t\t\t\tEnvRecordsMapping: \"example.com:123\",\n\t\t\t},\n\t\t\texpected: \"selfhostde: some credentials information are missing: SELFHOSTDE_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername:       \"user\",\n\t\t\t\tEnvRecordsMapping: \"example.com:123\",\n\t\t\t},\n\t\t\texpected: \"selfhostde: some credentials information are missing: SELFHOSTDE_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing records mapping\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"selfhostde: some credentials information are missing: SELFHOSTDE_RECORDS_MAPPING\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid records mapping\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername:       \"user\",\n\t\t\t\tEnvPassword:       \"secret\",\n\t\t\t\tEnvRecordsMapping: \"example.com\",\n\t\t\t},\n\t\t\texpected: `selfhostde: malformed records mapping: missing \":\": example.com`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing information\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"selfhostde: some credentials information are missing: SELFHOSTDE_USERNAME,SELFHOSTDE_PASSWORD,SELFHOSTDE_RECORDS_MAPPING\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tusername      string\n\t\tpassword      string\n\t\trecordMapping map[string]*Seq\n\t\texpected      string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t\trecordMapping: map[string]*Seq{\n\t\t\t\t\"example.com\": NewSeq(\"123\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tpassword: \"secret\",\n\t\t\trecordMapping: map[string]*Seq{\n\t\t\t\t\"example.com\": NewSeq(\"123\"),\n\t\t\t},\n\t\t\texpected: \"selfhostde: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tusername: \"user\",\n\t\t\trecordMapping: map[string]*Seq{\n\t\t\t\t\"example.com\": NewSeq(\"123\"),\n\t\t\t},\n\t\t\texpected: \"selfhostde: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing sequence\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t\trecordMapping: map[string]*Seq{\n\t\t\t\t\"example.com\": nil,\n\t\t\t},\n\t\t\texpected: `selfhostde: missing record ID for \"example.com\"`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"empty sequence\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t\trecordMapping: map[string]*Seq{\n\t\t\t\t\"example.com\": NewSeq(),\n\t\t\t},\n\t\t\texpected: `selfhostde: missing record ID for \"example.com\"`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing records mapping\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"selfhostde: missing record mapping\",\n\t\t},\n\t\t{\n\t\t\tdesc:          \"empty records mapping\",\n\t\t\tusername:      \"user\",\n\t\t\tpassword:      \"secret\",\n\t\t\trecordMapping: map[string]*Seq{},\n\t\t\texpected:      \"selfhostde: missing record mapping\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing information\",\n\t\t\texpected: \"selfhostde: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\t\t\tconfig.RecordsMapping = test.recordMapping\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/servercow/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst baseAPIURL = \"https://api.servercow.de/dns/v1/domains\"\n\n// Client the Servercow client.\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a Servercow client.\nfunc NewClient(username, password string) *Client {\n\tbaseURL, _ := url.Parse(baseAPIURL)\n\n\treturn &Client{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// GetRecords from API.\nfunc (c *Client) GetRecords(ctx context.Context, domain string) ([]Record, error) {\n\tendpoint := c.baseURL.JoinPath(domain)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar records []Record\n\n\terr = c.do(req, &records)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn records, nil\n}\n\n// CreateUpdateRecord creates or updates a record.\nfunc (c *Client) CreateUpdateRecord(ctx context.Context, domain string, data Record) (*Message, error) {\n\tendpoint := c.baseURL.JoinPath(domain)\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar msg Message\n\n\terr = c.do(req, &msg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif msg.ErrorMsg != \"\" {\n\t\treturn nil, msg\n\t}\n\n\treturn &msg, nil\n}\n\n// DeleteRecord deletes a record.\nfunc (c *Client) DeleteRecord(ctx context.Context, domain string, data Record) (*Message, error) {\n\tendpoint := c.baseURL.JoinPath(domain)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar msg Message\n\n\terr = c.do(req, &msg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif msg.ErrorMsg != \"\" {\n\t\treturn nil, msg\n\t}\n\n\treturn &msg, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(\"X-Auth-Username\", c.username)\n\treq.Header.Set(\"X-Auth-Password\", c.password)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\t// Note the API always return 200 even if the authentication failed.\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = unmarshal(raw, result)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\t// Content-Type should be added even if there is no request body.\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\treturn req, nil\n}\n\nfunc unmarshal(raw []byte, v any) error {\n\terr := json.Unmarshal(raw, v)\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tvar utErr *json.UnmarshalTypeError\n\n\tif !errors.As(err, &utErr) {\n\t\treturn fmt.Errorf(\"unmarshaling %T error: %w: %s\", v, err, string(raw))\n\t}\n\n\tvar apiErr Message\n\n\terrU := json.Unmarshal(raw, &apiErr)\n\tif errU != nil {\n\t\treturn fmt.Errorf(\"unmarshaling %T error: %w: %s\", v, err, string(raw))\n\t}\n\n\treturn apiErr\n}\n"
  },
  {
    "path": "providers/dns/servercow/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(\"X-Auth-Username\", \"user\").\n\t\t\tWith(\"X-Auth-Password\", \"secret\"),\n\t)\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /example.com\", servermock.ResponseFromFixture(\"records-01.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\trecordsJSON, err := json.Marshal(records)\n\trequire.NoError(t, err)\n\n\texpectedContent, err := os.ReadFile(\"./fixtures/records-01.json\")\n\trequire.NoError(t, err)\n\n\tassert.JSONEq(t, string(expectedContent), string(recordsJSON))\n}\n\nfunc TestClient_GetRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /example.com\", servermock.JSONEncode(Message{ErrorMsg: \"authentication failed\"})).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"example.com\")\n\trequire.Error(t, err)\n\n\tassert.Nil(t, records)\n}\n\nfunc TestClient_CreateUpdateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /example.com\",\n\t\t\tservermock.JSONEncode(Message{Message: \"ok\"}),\n\t\t\tservermock.CheckRequestJSONBody(`{\"name\":\"_acme-challenge.www\",\"type\":\"TXT\",\"ttl\":30,\"content\":[\"aaa\",\"bbb\"]}`)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:    \"_acme-challenge.www\",\n\t\tType:    \"TXT\",\n\t\tTTL:     30,\n\t\tContent: Value{\"aaa\", \"bbb\"},\n\t}\n\n\tmsg, err := client.CreateUpdateRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n\n\texpected := &Message{Message: \"ok\"}\n\tassert.Equal(t, expected, msg)\n}\n\nfunc TestClient_CreateUpdateRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /example.com\",\n\t\t\tservermock.JSONEncode(Message{ErrorMsg: \"parameter type must be cname, txt, tlsa, caa, a or aaaa\"})).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName: \"_acme-challenge.www\",\n\t}\n\n\tmsg, err := client.CreateUpdateRecord(t.Context(), \"example.com\", record)\n\trequire.Error(t, err)\n\n\tassert.Nil(t, msg)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /example.com\",\n\t\t\tservermock.JSONEncode(Message{Message: \"ok\"}),\n\t\t\tservermock.CheckRequestJSONBody(`{\"name\":\"_acme-challenge.www\",\"type\":\"TXT\"}`)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName: \"_acme-challenge.www\",\n\t\tType: \"TXT\",\n\t}\n\n\tmsg, err := client.DeleteRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n\n\texpected := &Message{Message: \"ok\"}\n\tassert.Equal(t, expected, msg)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /example.com\",\n\t\t\tservermock.JSONEncode(Message{ErrorMsg: \"parameter type must be cname, txt, tlsa, caa, a or aaaa\"})).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName: \"_acme-challenge.www\",\n\t}\n\n\tmsg, err := client.DeleteRecord(t.Context(), \"example.com\", record)\n\trequire.Error(t, err)\n\n\tassert.Nil(t, msg)\n}\n"
  },
  {
    "path": "providers/dns/servercow/internal/fixtures/records-01.json",
    "content": "[\n  {\n    \"name\": \"letsencrypt\",\n    \"ttl\": 120,\n    \"type\": \"A\",\n    \"content\": \"1.1.1.1\"\n  },\n  {\n    \"name\": \"diskover\",\n    \"ttl\": 120,\n    \"type\": \"CAA\",\n    \"content\": \"0 issue \\\"letsencrypt.org\\\"\"\n  },\n  {\n    \"name\": \"diskover\",\n    \"ttl\": 120,\n    \"type\": \"AAAA\",\n    \"content\": \":::::\"\n  },\n  {\n    \"name\": \"diskover\",\n    \"ttl\": 120,\n    \"type\": \"A\",\n    \"content\": \"1.1.1.1\"\n  },\n  {\n    \"name\": \"portainer\",\n    \"ttl\": 120,\n    \"type\": \"CAA\",\n    \"content\": \"0 issue \\\"letsencrypt.org\\\"\"\n  },\n  {\n    \"name\": \"portainer\",\n    \"ttl\": 120,\n    \"type\": \"AAAA\",\n    \"content\": \":::::\"\n  },\n  {\n    \"name\": \"portainer\",\n    \"ttl\": 120,\n    \"type\": \"A\",\n    \"content\": \"1.1.1.1\"\n  },\n  {\n    \"name\": \"lego\",\n    \"ttl\": 120,\n    \"type\": \"A\",\n    \"content\": \"1.1.1.1\"\n  },\n  {\n    \"name\": \"traefik\",\n    \"ttl\": 120,\n    \"type\": \"CAA\",\n    \"content\": \"0 issue \\\"letsencrypt.org\\\"\"\n  },\n  {\n    \"name\": \"traefik\",\n    \"ttl\": 120,\n    \"type\": \"AAAA\",\n    \"content\": \":::::\"\n  },\n  {\n    \"name\": \"traefik\",\n    \"ttl\": 120,\n    \"type\": \"A\",\n    \"content\": \"1.1.1.1\"\n  },\n  {\n    \"name\": \"spaghetti\",\n    \"ttl\": 120,\n    \"type\": \"CAA\",\n    \"content\": \"0 issue \\\"letsencrypt.org\\\"\"\n  },\n  {\n    \"name\": \"spaghetti\",\n    \"ttl\": 120,\n    \"type\": \"AAAA\",\n    \"content\": \":::::\"\n  },\n  {\n    \"name\": \"spaghetti\",\n    \"ttl\": 120,\n    \"type\": \"A\",\n    \"content\": \"1.1.1.1\"\n  },\n  {\n    \"name\": \"dragonstone\",\n    \"ttl\": 120,\n    \"type\": \"CAA\",\n    \"content\": \"0 issue \\\"letsencrypt.org\\\"\"\n  },\n  {\n    \"name\": \"dragonstone\",\n    \"ttl\": 120,\n    \"type\": \"A\",\n    \"content\": \"1.1.1.1\"\n  },\n  {\n    \"name\": \"_acme-challenge.sample\",\n    \"ttl\": 20,\n    \"type\": \"TXT\",\n    \"content\": [\n      \"txtxtxtxtxtxtxt\",\n      \"acbdefghijklmnopqrstuvwxyz\"\n    ]\n  },\n  {\n    \"name\": \"\",\n    \"ttl\": 120,\n    \"type\": \"CAA\",\n    \"content\": \"0 issue \\\"letsencrypt.org\\\"\"\n  },\n  {\n    \"name\": \"\",\n    \"ttl\": 120,\n    \"type\": \"AAAA\",\n    \"content\": \":::::\"\n  },\n  {\n    \"name\": \"\",\n    \"ttl\": 120,\n    \"type\": \"A\",\n    \"content\": \"1.1.1.1\"\n  }\n]\n"
  },
  {
    "path": "providers/dns/servercow/internal/types.go",
    "content": "package internal\n\nimport \"encoding/json\"\n\n// Record is the record representation.\ntype Record struct {\n\tName string `json:\"name\"`\n\tType string `json:\"type\"`\n\tTTL  int    `json:\"ttl,omitempty\"`\n\n\tContent Value `json:\"content,omitempty\"`\n}\n\n// Value is the value of a record.\n// Allows to handle dynamic type (string and string array).\ntype Value []string\n\nfunc (v Value) MarshalJSON() ([]byte, error) {\n\tif len(v) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tif len(v) == 1 {\n\t\treturn json.Marshal(v[0])\n\t}\n\n\tcontent, err := json.Marshal([]string(v))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn content, nil\n}\n\nfunc (v *Value) UnmarshalJSON(b []byte) error {\n\tif b[0] == '[' {\n\t\treturn json.Unmarshal(b, (*[]string)(v))\n\t}\n\n\tvar s string\n\tif err := json.Unmarshal(b, &s); err != nil {\n\t\treturn err\n\t}\n\n\t*v = append(*v, s)\n\n\treturn nil\n}\n\n// Message is the basic response representation.\n// Can be an error.\ntype Message struct {\n\tMessage  string `json:\"message,omitempty\"`\n\tErrorMsg string `json:\"error,omitempty\"`\n}\n\nfunc (a Message) Error() string {\n\treturn a.ErrorMsg\n}\n"
  },
  {
    "path": "providers/dns/servercow/internal/types_test.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestValue_MarshalJSON(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\trecord   Record\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"empty content\",\n\t\t\trecord: Record{\n\t\t\t\tName:    \"_acme-challenge.www\",\n\t\t\t\tType:    \"TXT\",\n\t\t\t\tTTL:     30,\n\t\t\t\tContent: Value{},\n\t\t\t},\n\t\t\texpected: `{\"name\":\"_acme-challenge.www\",\"type\":\"TXT\",\"ttl\":30}`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"content with a single value\",\n\t\t\trecord: Record{\n\t\t\t\tName:    \"_acme-challenge.www\",\n\t\t\t\tType:    \"TXT\",\n\t\t\t\tTTL:     30,\n\t\t\t\tContent: Value{\"aaa\"},\n\t\t\t},\n\t\t\texpected: `{\"name\":\"_acme-challenge.www\",\"type\":\"TXT\",\"ttl\":30,\"content\":\"aaa\"}`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"content with multiple values\",\n\t\t\trecord: Record{\n\t\t\t\tName:    \"_acme-challenge.www\",\n\t\t\t\tType:    \"TXT\",\n\t\t\t\tTTL:     30,\n\t\t\t\tContent: Value{\"aaa\", \"bbb\", \"ccc\"},\n\t\t\t},\n\t\t\texpected: `{\"name\":\"_acme-challenge.www\",\"type\":\"TXT\",\"ttl\":30,\"content\":[\"aaa\",\"bbb\",\"ccc\"]}`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tcontent, err := json.Marshal(test.record)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.JSONEq(t, test.expected, string(content))\n\t\t})\n\t}\n}\n\nfunc TestValue_UnmarshalJSON(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tdata     string\n\t\texpected Record\n\t}{\n\t\t{\n\t\t\tdesc: \"empty content\",\n\t\t\tdata: `{\"name\":\"_acme-challenge.www\",\"type\":\"TXT\",\"ttl\":30}`,\n\t\t\texpected: Record{\n\t\t\t\tName:    \"_acme-challenge.www\",\n\t\t\t\tType:    \"TXT\",\n\t\t\t\tTTL:     30,\n\t\t\t\tContent: Value(nil),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"content with a single value\",\n\t\t\tdata: `{\"name\":\"_acme-challenge.www\",\"type\":\"TXT\",\"ttl\":30,\"content\":\"aaa\"}`,\n\t\t\texpected: Record{\n\t\t\t\tName:    \"_acme-challenge.www\",\n\t\t\t\tType:    \"TXT\",\n\t\t\t\tTTL:     30,\n\t\t\t\tContent: Value{\"aaa\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"content with multiple values\",\n\t\t\tdata: `{\"name\":\"_acme-challenge.www\",\"type\":\"TXT\",\"ttl\":30,\"content\":[\"aaa\",\"bbb\",\"ccc\"]}`,\n\t\t\texpected: Record{\n\t\t\t\tName:    \"_acme-challenge.www\",\n\t\t\t\tType:    \"TXT\",\n\t\t\t\tTTL:     30,\n\t\t\t\tContent: Value{\"aaa\", \"bbb\", \"ccc\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\trecord := Record{}\n\t\t\terr := json.Unmarshal([]byte(test.data), &record)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.expected, record)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/servercow/servercow.go",
    "content": "// Package servercow implements a DNS provider for solving the DNS-01 challenge using Servercow DNS.\npackage servercow\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/servercow/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"SERVERCOW_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername string\n\tPassword string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"servercow: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Servercow.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config.Username == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"servercow: incomplete credentials, missing username and/or password\")\n\t}\n\n\tclient := internal.NewClient(config.Username, config.Password)\n\n\tif config.HTTPClient == nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := getAuthZone(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"servercow: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\trecords, err := d.client.GetRecords(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"servercow: %w\", err)\n\t}\n\n\trecordName, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"servercow: %w\", err)\n\t}\n\n\trecord := findRecords(records, recordName)\n\n\t// TXT record entry already existing\n\tif record != nil {\n\t\tif slices.Contains(record.Content, info.Value) {\n\t\t\treturn nil\n\t\t}\n\n\t\trequest := internal.Record{\n\t\t\tName:    record.Name,\n\t\t\tTTL:     record.TTL,\n\t\t\tType:    record.Type,\n\t\t\tContent: append(record.Content, info.Value),\n\t\t}\n\n\t\t_, err = d.client.CreateUpdateRecord(ctx, authZone, request)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"servercow: failed to update TXT records: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\trequest := internal.Record{\n\t\tType:    \"TXT\",\n\t\tName:    recordName,\n\t\tTTL:     d.config.TTL,\n\t\tContent: internal.Value{info.Value},\n\t}\n\n\t_, err = d.client.CreateUpdateRecord(ctx, authZone, request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"servercow: failed to create TXT record %s: %w\", info.EffectiveFQDN, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record previously created.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := getAuthZone(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"servercow: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\trecords, err := d.client.GetRecords(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"servercow: failed to get TXT records: %w\", err)\n\t}\n\n\trecordName, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"servercow: %w\", err)\n\t}\n\n\trecord := findRecords(records, recordName)\n\tif record == nil {\n\t\treturn nil\n\t}\n\n\tif !slices.Contains(record.Content, info.Value) {\n\t\treturn nil\n\t}\n\n\t// only 1 record value, the whole record must be deleted.\n\tif len(record.Content) == 1 {\n\t\t_, err = d.client.DeleteRecord(ctx, authZone, *record)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"servercow: failed to delete TXT records: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\trequest := internal.Record{\n\t\tName: record.Name,\n\t\tType: record.Type,\n\t\tTTL:  record.TTL,\n\t}\n\n\tfor _, val := range record.Content {\n\t\tif val != info.Value {\n\t\t\trequest.Content = append(request.Content, val)\n\t\t}\n\t}\n\n\t_, err = d.client.CreateUpdateRecord(ctx, authZone, request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"servercow: failed to update TXT records: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc getAuthZone(domain string) (string, error) {\n\tauthZone, err := dns01.FindZoneByFqdn(domain)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\treturn dns01.UnFqdn(authZone), nil\n}\n\nfunc findRecords(records []internal.Record, name string) *internal.Record {\n\tfor _, r := range records {\n\t\tif r.Type == \"TXT\" && r.Name == name {\n\t\t\treturn &r\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/servercow/servercow.toml",
    "content": "Name = \"Servercow\"\nDescription = ''''''\nURL = \"https://servercow.de/\"\nCode = \"servercow\"\nSince = \"v3.4.0\"\n\nExample = '''\nSERVERCOW_USERNAME=xxxxxxxx \\\nSERVERCOW_PASSWORD=xxxxxxxx \\\nlego --dns servercow -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    SERVERCOW_USERNAME = \"API username\"\n    SERVERCOW_PASSWORD = \"API password\"\n  [Configuration.Additional]\n    SERVERCOW_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    SERVERCOW_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    SERVERCOW_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    SERVERCOW_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://wiki.servercow.de/en/domains/dns_api/api-syntax/\"\n"
  },
  {
    "path": "providers/dns/servercow/servercow_test.go",
    "content": "package servercow\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvUsername,\n\tEnvPassword).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"123\",\n\t\t\t\tEnvPassword: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"servercow: some credentials information are missing: SERVERCOW_USERNAME,SERVERCOW_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"api_password\",\n\t\t\t},\n\t\t\texpected: \"servercow: some credentials information are missing: SERVERCOW_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"api_username\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"servercow: some credentials information are missing: SERVERCOW_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\texpected string\n\t\tusername string\n\t\tpassword string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"api_username\",\n\t\t\tpassword: \"api_password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"servercow: incomplete credentials, missing username and/or password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api key\",\n\t\t\tusername: \"\",\n\t\t\tpassword: \"api_password\",\n\t\t\texpected: \"servercow: incomplete credentials, missing username and/or password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret key\",\n\t\t\tusername: \"api_username\",\n\t\t\tpassword: \"\",\n\t\t\texpected: \"servercow: incomplete credentials, missing username and/or password\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/shellrent/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// DefaultBaseURL the default API endpoint.\nconst defaultBaseURL = \"https://manager.shellrent.com/api2\"\n\nconst authorizationHeader = \"Authorization\"\n\n// Client the Shellrent API client.\ntype Client struct {\n\tusername string\n\ttoken    string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(username, token string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\ttoken:      token,\n\t\tusername:   username,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}\n}\n\n// ListServices lists service IDs.\n// https://api.shellrent.com/elenco-dei-servizi-acquistati\nfunc (c *Client) ListServices(ctx context.Context) ([]int, error) {\n\tendpoint := c.baseURL.JoinPath(\"purchase\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := Response[[]IntOrString]{}\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif result.Code != 0 {\n\t\treturn nil, result.Base\n\t}\n\n\tvar ids []int\n\n\tfor _, datum := range result.Data {\n\t\tids = append(ids, datum.Value())\n\t}\n\n\treturn ids, nil\n}\n\n// GetServiceDetails gets service details.\n// https://api.shellrent.com/dettagli-servizio-acquistato\nfunc (c *Client) GetServiceDetails(ctx context.Context, serviceID int) (*ServiceDetails, error) {\n\tendpoint := c.baseURL.JoinPath(\"purchase\", \"details\", strconv.Itoa(serviceID))\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := Response[*ServiceDetails]{}\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif result.Code != 0 {\n\t\treturn nil, result.Base\n\t}\n\n\treturn result.Data, nil\n}\n\n// GetDomainDetails gets domain details.\n// https://api.shellrent.com/dettagli-dominio\nfunc (c *Client) GetDomainDetails(ctx context.Context, domainID int) (*DomainDetails, error) {\n\tendpoint := c.baseURL.JoinPath(\"domain\", \"details\", strconv.Itoa(domainID))\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := Response[*DomainDetails]{}\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif result.Code != 0 {\n\t\treturn nil, result.Base\n\t}\n\n\treturn result.Data, nil\n}\n\n// CreateRecord created a record.\n// https://api.shellrent.com/creazione-record-dns-di-un-dominio\nfunc (c *Client) CreateRecord(ctx context.Context, domainID int, record Record) (int, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns_record\", \"store\", strconv.Itoa(domainID))\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tresult := Response[*Record]{}\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif result.Code != 0 {\n\t\treturn 0, result.Base\n\t}\n\n\treturn result.Data.ID.Value(), nil\n}\n\n// DeleteRecord deletes a record.\n// https://api.shellrent.com/eliminazione-record-dns-di-un-dominio\nfunc (c *Client) DeleteRecord(ctx context.Context, domainID, recordID int) error {\n\tendpoint := c.baseURL.JoinPath(\"dns_record\", \"remove\", strconv.Itoa(domainID), strconv.Itoa(recordID))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresult := Response[any]{}\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif result.Code != 0 {\n\t\treturn result.Base\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(authorizationHeader, c.username+\".\"+c.token)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar response Base\n\n\terr := json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn response\n}\n\n// TTLRounder rounds the given TTL in seconds to the next accepted value.\n// Accepted TTL values are:\n//   - 3600\n//   - 14400\n//   - 28800\n//   - 57600\n//   - 86400\nfunc TTLRounder(ttl int) int {\n\tfor _, validTTL := range []int{3600, 14400, 28800, 57600, 86400} {\n\t\tif ttl <= validTTL {\n\t\t\treturn validTTL\n\t\t}\n\t}\n\n\treturn 3600\n}\n"
  },
  {
    "path": "providers/dns/shellrent/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"user.secret\"))\n}\n\nfunc TestClient_ListServices(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /purchase\", servermock.ResponseFromFixture(\"purchase.json\")).\n\t\tBuild(t)\n\n\tservices, err := client.ListServices(t.Context())\n\trequire.NoError(t, err)\n\n\texpected := []int{2018, 10039, 10128}\n\n\tassert.Equal(t, expected, services)\n}\n\nfunc TestClient_ListServices_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /purchase\", servermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\t_, err := client.ListServices(t.Context())\n\trequire.EqualError(t, err, \"code 2: Token di autorizzazione non valido\")\n}\n\nfunc TestClient_ListServices_error_status(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /purchase\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.ListServices(t.Context())\n\trequire.EqualError(t, err, \"code 2: Token di autorizzazione non valido\")\n}\n\nfunc TestClient_GetServiceDetails(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /purchase/details/123\", servermock.ResponseFromFixture(\"purchase-details.json\")).\n\t\tBuild(t)\n\n\tservices, err := client.GetServiceDetails(t.Context(), 123)\n\trequire.NoError(t, err)\n\n\texpected := &ServiceDetails{ID: 123, Name: \"example\", DomainID: 456}\n\n\tassert.Equal(t, expected, services)\n}\n\nfunc TestClient_GetServiceDetails_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /purchase/details/123\", servermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\t_, err := client.GetServiceDetails(t.Context(), 123)\n\trequire.EqualError(t, err, \"code 2: Token di autorizzazione non valido\")\n}\n\nfunc TestClient_GetServiceDetails_error_status(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /purchase/details/123\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetServiceDetails(t.Context(), 123)\n\trequire.EqualError(t, err, \"code 2: Token di autorizzazione non valido\")\n}\n\nfunc TestClient_GetDomainDetails(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domain/details/123\", servermock.ResponseFromFixture(\"domain-details.json\")).\n\t\tBuild(t)\n\n\tservices, err := client.GetDomainDetails(t.Context(), 123)\n\trequire.NoError(t, err)\n\n\texpected := &DomainDetails{ID: 123, DomainName: \"example.com\", DomainNameASCII: \"example.com\"}\n\n\tassert.Equal(t, expected, services)\n}\n\nfunc TestClient_GetDomainDetails_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domain/details/123\", servermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\t_, err := client.GetDomainDetails(t.Context(), 123)\n\trequire.EqualError(t, err, \"code 2: Token di autorizzazione non valido\")\n}\n\nfunc TestClient_GetDomainDetails_error_status(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domain/details/123\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetDomainDetails(t.Context(), 123)\n\trequire.EqualError(t, err, \"code 2: Token di autorizzazione non valido\")\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns_record/store/123\", servermock.ResponseFromFixture(\"dns_record-store.json\")).\n\t\tBuild(t)\n\n\tservices, err := client.CreateRecord(t.Context(), 123, Record{})\n\trequire.NoError(t, err)\n\n\texpected := 2255674\n\n\tassert.Equal(t, expected, services)\n}\n\nfunc TestClient_CreateRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns_record/store/123\", servermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\t_, err := client.CreateRecord(t.Context(), 123, Record{})\n\trequire.EqualError(t, err, \"code 2: Token di autorizzazione non valido\")\n}\n\nfunc TestClient_CreateRecord_error_status(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns_record/store/123\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.CreateRecord(t.Context(), 123, Record{})\n\trequire.EqualError(t, err, \"code 2: Token di autorizzazione non valido\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns_record/remove/123/456\", servermock.ResponseFromFixture(\"dns_record-remove.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), 123, 456)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns_record/remove/123/456\", servermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), 123, 456)\n\trequire.EqualError(t, err, \"code 2: Token di autorizzazione non valido\")\n}\n\nfunc TestClient_DeleteRecord_error_status(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns_record/remove/123/456\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), 123, 456)\n\trequire.EqualError(t, err, \"code 2: Token di autorizzazione non valido\")\n}\n\nfunc TestTTLRounder(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tvalue    int\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tdesc:     \"lower than 3600\",\n\t\t\tvalue:    123,\n\t\t\texpected: 3600,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"lower than 14400\",\n\t\t\tvalue:    12341,\n\t\t\texpected: 14400,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"lower than 28800\",\n\t\t\tvalue:    28341,\n\t\t\texpected: 28800,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"lower than 57600\",\n\t\t\tvalue:    56600,\n\t\t\texpected: 57600,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"rounded to 86400\",\n\t\t\tvalue:    86000,\n\t\t\texpected: 86400,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"default\",\n\t\t\tvalue:    100000,\n\t\t\texpected: 3600,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tttl := TTLRounder(test.value)\n\n\t\t\tassert.Equal(t, test.expected, ttl)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/shellrent/internal/fixtures/dns_record-remove.json",
    "content": "{\n  \"error\": 0,\n  \"message\": \"\"\n}\n"
  },
  {
    "path": "providers/dns/shellrent/internal/fixtures/dns_record-store.json",
    "content": "{\n  \"error\": 0,\n  \"title\": \"\",\n  \"message\": \"Record DNS aggiunto con successo\",\n  \"data\": {\n    \"id\": \"2255674\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/shellrent/internal/fixtures/domain-details.json",
    "content": "{\n  \"error\": 0,\n  \"message\": \"\",\n  \"data\": {\n    \"id\": 123,\n    \"domain_name\": \"example.com\",\n    \"domain_name_ascii\": \"example.com\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/shellrent/internal/fixtures/error.json",
    "content": "{\n  \"error\": 2,\n  \"title\": \"\",\n  \"message\": \"Token di autorizzazione non valido\",\n  \"data\": null\n}\n"
  },
  {
    "path": "providers/dns/shellrent/internal/fixtures/purchase-details.json",
    "content": "{\n  \"error\": 0,\n  \"message\": \"\",\n  \"data\": {\n    \"ID\": 123,\n    \"name\": \"example\",\n    \"domain_id\": 456\n  }\n}\n"
  },
  {
    "path": "providers/dns/shellrent/internal/fixtures/purchase.json",
    "content": "{\n  \"error\": 0,\n  \"message\": \"\",\n  \"data\": [\n    2018,\n    10039,\n    10128\n  ]\n}\n"
  },
  {
    "path": "providers/dns/shellrent/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n)\n\ntype Response[T any] struct {\n\tBase\n\n\tData T `json:\"data\"`\n}\n\ntype Base struct {\n\tCode    int    `json:\"error\"`\n\tMessage string `json:\"message\"`\n}\n\nfunc (b Base) Error() string {\n\treturn fmt.Sprintf(\"code %d: %s\", b.Code, b.Message)\n}\n\ntype ServiceDetails struct {\n\tID       int    `json:\"id,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tDomainID int    `json:\"domain_id,omitempty\"`\n}\n\ntype DomainDetails struct {\n\tID              int    `json:\"id\"`\n\tDomainName      string `json:\"domain_name\"`\n\tDomainNameASCII string `json:\"domain_name_ascii\"`\n}\n\ntype Record struct {\n\tID          IntOrString `json:\"id,omitempty\"`\n\tType        string      `json:\"type,omitempty\"`\n\tHost        string      `json:\"host,omitempty\"`\n\tTTL         int         `json:\"ttl,omitempty\"` // It can be set to the following values (number of seconds): 3600, 14400, 28800, 57600, 86400\n\tDestination string      `json:\"destination,omitempty\"`\n}\n\ntype IntOrString int\n\nfunc (m *IntOrString) Value() int {\n\tif m == nil {\n\t\treturn 0\n\t}\n\n\treturn int(*m)\n}\n\nfunc (m *IntOrString) UnmarshalJSON(data []byte) error {\n\tif len(data) == 0 {\n\t\treturn nil\n\t}\n\n\traw := string(data)\n\tif data[0] == '\"' {\n\t\tvar err error\n\n\t\traw, err = strconv.Unquote(string(data))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tv, err := strconv.Atoi(raw)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t*m = IntOrString(v)\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/shellrent/shellrent.go",
    "content": "package shellrent\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/shellrent/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"SHELLRENT_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvToken    = envNamespace + \"TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultTTL = 3600\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\ntype reqKey struct {\n\tdomainID int\n\trecordID int\n}\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername           string\n\tToken              string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, defaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]reqKey\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Shellrent.\n// Credentials must be passed in the environment variable: SHELLRENT_USERNAME, SHELLRENT_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"shellrent: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Token = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Shellrent.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"shellrent: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Username == \"\" {\n\t\treturn nil, errors.New(\"shellrent: missing credentials: username\")\n\t}\n\n\tif config.Token == \"\" {\n\t\treturn nil, errors.New(\"shellrent: missing credentials: token\")\n\t}\n\n\tclient := internal.NewClient(config.Username, config.Token)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]reqKey),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.findZone(ctx, dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"shellrent: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.DomainName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"shellrent: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tType:        \"TXT\",\n\t\tHost:        subDomain,\n\t\tTTL:         internal.TTLRounder(d.config.TTL),\n\t\tDestination: info.Value,\n\t}\n\n\trecordID, err := d.client.CreateRecord(ctx, zone.ID, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"shellrent: create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = reqKey{domainID: zone.ID, recordID: recordID}\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// gets the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\tkey, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"shellrent: unknown request key for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\terr := d.client.DeleteRecord(ctx, key.domainID, key.recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"shellrent: delete record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) findZone(ctx context.Context, domain string) (*internal.DomainDetails, error) {\n\tservices, err := d.client.ListServices(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"list services: %w\", err)\n\t}\n\n\tfor _, service := range services {\n\t\tdetails, err := d.client.GetServiceDetails(ctx, service)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"get service details: %w\", err)\n\t\t}\n\n\t\tdomainDetails, err := d.client.GetDomainDetails(ctx, details.DomainID)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"get domain details: %w\", err)\n\t\t}\n\n\t\tdomain := domain\n\n\t\tfor {\n\t\t\ti := strings.Index(domain, \".\")\n\t\t\tif i == -1 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif strings.EqualFold(domainDetails.DomainName, domain) {\n\t\t\t\treturn domainDetails, nil\n\t\t\t}\n\n\t\t\tdomain = domain[i+1:]\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"zone not found\")\n}\n"
  },
  {
    "path": "providers/dns/shellrent/shellrent.toml",
    "content": "Name = \"Shellrent\"\nDescription = ''''''\nURL = \"https://www.shellrent.com/\"\nCode = \"shellrent\"\nSince = \"v4.16.0\"\n\nExample = '''\nSHELLRENT_USERNAME=xxxx \\\nSHELLRENT_TOKEN=yyyy \\\nlego --dns shellrent -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    SHELLRENT_USERNAME = \"Username\"\n    SHELLRENT_TOKEN = \"Token\"\n  [Configuration.Additional]\n    SHELLRENT_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    SHELLRENT_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 300)\"\n    SHELLRENT_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)\"\n    SHELLRENT_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api.shellrent.com/section/api2\"\n"
  },
  {
    "path": "providers/dns/shellrent/shellrent_test.go",
    "content": "package shellrent\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvUsername,\n\tEnvToken).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvToken:    \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"secret\",\n\t\t\t},\n\t\t\texpected: \"shellrent: some credentials information are missing: SHELLRENT_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t},\n\t\t\texpected: \"shellrent: some credentials information are missing: SHELLRENT_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\ttoken    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"user\",\n\t\t\ttoken:    \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tusername: \"\",\n\t\t\ttoken:    \"secret\",\n\t\t\texpected: \"shellrent: missing credentials: username\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing token\",\n\t\t\tusername: \"user\",\n\t\t\ttoken:    \"\",\n\t\t\texpected: \"shellrent: missing credentials: token\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/simply/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api.simply.com/2/\"\n\n// Client is a Simply.com API client.\ntype Client struct {\n\taccountName string\n\tapiKey      string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(accountName, apiKey string) (*Client, error) {\n\tif accountName == \"\" {\n\t\treturn nil, errors.New(\"credentials missing: accountName\")\n\t}\n\n\tif apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing: apiKey\")\n\t}\n\n\tbaseURL, err := url.Parse(defaultBaseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\taccountName: accountName,\n\t\tapiKey:      apiKey,\n\t\tbaseURL:     baseURL,\n\t\tHTTPClient:  &http.Client{Timeout: 5 * time.Second},\n\t}, nil\n}\n\n// GetRecords lists all the records in the zone.\nfunc (c *Client) GetRecords(ctx context.Context, zoneName string) ([]Record, error) {\n\tendpoint := c.createEndpoint(zoneName, \"/\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\tresult := &apiResponse[[]Record, json.RawMessage]{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Records, nil\n}\n\n// AddRecord adds a record.\nfunc (c *Client) AddRecord(ctx context.Context, zoneName string, record Record) (int64, error) {\n\tendpoint := c.createEndpoint(zoneName, \"/\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\tresult := &apiResponse[json.RawMessage, recordHeader]{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn result.Record.ID, nil\n}\n\n// EditRecord updates a record.\nfunc (c *Client) EditRecord(ctx context.Context, zoneName string, id int64, record Record) error {\n\tendpoint := c.createEndpoint(zoneName, strconv.FormatInt(id, 10))\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treturn c.do(req, &apiResponse[json.RawMessage, json.RawMessage]{})\n}\n\n// DeleteRecord deletes a record.\nfunc (c *Client) DeleteRecord(ctx context.Context, zoneName string, id int64) error {\n\tendpoint := c.createEndpoint(zoneName, strconv.FormatInt(id, 10))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treturn c.do(req, &apiResponse[json.RawMessage, json.RawMessage]{})\n}\n\nfunc (c *Client) createEndpoint(zoneName, uri string) *url.URL {\n\treturn c.baseURL.JoinPath(\"my\", \"products\", zoneName, \"dns\", \"records\", strings.TrimSuffix(uri, \"/\"))\n}\n\nfunc (c *Client) do(req *http.Request, result Response) error {\n\treq.SetBasicAuth(c.accountName, c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode >= http.StatusInternalServerError {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tif result.GetStatus() != http.StatusOK {\n\t\treturn fmt.Errorf(\"unexpected error: %s\", result.GetMessage())\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/simply/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"accountname\", \"apikey\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithBasicAuth(\"accountname\", \"apikey\"))\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /my/products/azone01/dns/records\",\n\t\t\tservermock.ResponseFromFixture(\"get_records.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"azone01\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{\n\t\t\tID:       1,\n\t\t\tName:     \"@\",\n\t\t\tTTL:      3600,\n\t\t\tData:     \"ns1.simply.com\",\n\t\t\tType:     \"NS\",\n\t\t\tPriority: 0,\n\t\t},\n\t\t{\n\t\t\tID:       2,\n\t\t\tName:     \"@\",\n\t\t\tTTL:      3600,\n\t\t\tData:     \"ns2.simply.com\",\n\t\t\tType:     \"NS\",\n\t\t\tPriority: 0,\n\t\t},\n\t\t{\n\t\t\tID:       3,\n\t\t\tName:     \"@\",\n\t\t\tTTL:      3600,\n\t\t\tData:     \"ns3.simply.com\",\n\t\t\tType:     \"NS\",\n\t\t\tPriority: 0,\n\t\t},\n\t\t{\n\t\t\tID:       4,\n\t\t\tName:     \"@\",\n\t\t\tTTL:      3600,\n\t\t\tData:     \"ns4.simply.com\",\n\t\t\tType:     \"NS\",\n\t\t\tPriority: 0,\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /my/products/azone01/dns/records\",\n\t\t\tservermock.ResponseFromFixture(\"bad_auth_error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"azone01\")\n\trequire.Error(t, err)\n\n\tassert.Nil(t, records)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /my/products/azone01/dns/records\",\n\t\t\tservermock.ResponseFromFixture(\"add_record.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:     \"arecord01\",\n\t\tData:     \"content\",\n\t\tType:     \"TXT\",\n\t\tTTL:      120,\n\t\tPriority: 0,\n\t}\n\n\trecordID, err := client.AddRecord(t.Context(), \"azone01\", record)\n\trequire.NoError(t, err)\n\n\tassert.EqualValues(t, 123456789, recordID)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /my/products/azone01/dns/records\",\n\t\t\tservermock.ResponseFromFixture(\"bad_zone_error.json\").\n\t\t\t\tWithStatusCode(http.StatusNotFound)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:     \"arecord01\",\n\t\tData:     \"content\",\n\t\tType:     \"TXT\",\n\t\tTTL:      120,\n\t\tPriority: 0,\n\t}\n\n\trecordID, err := client.AddRecord(t.Context(), \"azone01\", record)\n\trequire.Error(t, err)\n\n\tassert.Zero(t, recordID)\n}\n\nfunc TestClient_EditRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /my/products/azone01/dns/records/123456789\",\n\t\t\tservermock.ResponseFromFixture(\"success.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:     \"arecord01\",\n\t\tData:     \"content\",\n\t\tType:     \"TXT\",\n\t\tTTL:      120,\n\t\tPriority: 0,\n\t}\n\n\terr := client.EditRecord(t.Context(), \"azone01\", 123456789, record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_EditRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /my/products/azone01/dns/records/123456789\",\n\t\t\tservermock.ResponseFromFixture(\"invalid_record_id.json\").\n\t\t\t\tWithStatusCode(http.StatusNotFound)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:     \"arecord01\",\n\t\tData:     \"content\",\n\t\tType:     \"TXT\",\n\t\tTTL:      120,\n\t\tPriority: 0,\n\t}\n\n\terr := client.EditRecord(t.Context(), \"azone01\", 123456789, record)\n\trequire.Error(t, err)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /my/products/azone01/dns/records/123456789\",\n\t\t\tservermock.ResponseFromFixture(\"success.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"azone01\", 123456789)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /my/products/azone01/dns/records/123456789\",\n\t\t\tservermock.ResponseFromFixture(\"invalid_record_id.json\").\n\t\t\t\tWithStatusCode(http.StatusNotFound)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"azone01\", 123456789)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "providers/dns/simply/internal/fixtures/add_record.json",
    "content": "{\n  \"status\": 200,\n  \"message\": \"success\",\n  \"record\": {\n    \"id\": 123456789\n  }\n}\n"
  },
  {
    "path": "providers/dns/simply/internal/fixtures/bad_auth_error.json",
    "content": "{\n  \"status\": 400,\n  \"message\": \"Invalid account authorization\"\n}\n"
  },
  {
    "path": "providers/dns/simply/internal/fixtures/bad_zone_error.json",
    "content": "{\n  \"status\": 404,\n  \"message\": \"Unknown or invalid product reference\"\n}\n"
  },
  {
    "path": "providers/dns/simply/internal/fixtures/get_records.json",
    "content": "{\n  \"status\": 200,\n  \"message\": \"success\",\n  \"records\": [\n    {\n      \"record_id\": 1,\n      \"name\": \"@\",\n      \"ttl\": 3600,\n      \"data\": \"ns1.simply.com\",\n      \"type\": \"NS\",\n      \"priority\": 0\n    },\n    {\n      \"record_id\": 2,\n      \"name\": \"@\",\n      \"ttl\": 3600,\n      \"data\": \"ns2.simply.com\",\n      \"type\": \"NS\",\n      \"priority\": 0\n    },\n    {\n      \"record_id\": 3,\n      \"name\": \"@\",\n      \"ttl\": 3600,\n      \"data\": \"ns3.simply.com\",\n      \"type\": \"NS\",\n      \"priority\": 0\n    },\n    {\n      \"record_id\": 4,\n      \"name\": \"@\",\n      \"ttl\": 3600,\n      \"data\": \"ns4.simply.com\",\n      \"type\": \"NS\",\n      \"priority\": 0\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/simply/internal/fixtures/invalid_record_id_error.json",
    "content": "{\n  \"status\": 404,\n  \"message\": \"Unknown DNS record\"\n}\n"
  },
  {
    "path": "providers/dns/simply/internal/fixtures/success.json",
    "content": "{\n  \"status\": 200,\n  \"message\": \"success\"\n}\n"
  },
  {
    "path": "providers/dns/simply/internal/types.go",
    "content": "package internal\n\n// Record represents the content of a DNS record.\ntype Record struct {\n\tID       int64  `json:\"record_id,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tData     string `json:\"data,omitempty\"`\n\tType     string `json:\"type,omitempty\"`\n\tTTL      int    `json:\"ttl,omitempty\"`\n\tPriority int    `json:\"priority,omitempty\"`\n}\n\ntype Response interface {\n\tGetStatus() int\n\tGetMessage() string\n}\n\n// apiResponse represents an API response.\ntype apiResponse[S any, R any] struct {\n\tStatus  int    `json:\"status\"`\n\tMessage string `json:\"message\"`\n\tRecords S      `json:\"records,omitempty\"`\n\tRecord  R      `json:\"record,omitempty\"`\n}\n\nfunc (a apiResponse[S, R]) GetStatus() int {\n\treturn a.Status\n}\n\nfunc (a apiResponse[S, R]) GetMessage() string {\n\treturn a.Message\n}\n\ntype recordHeader struct {\n\tID int64 `json:\"id\"`\n}\n"
  },
  {
    "path": "providers/dns/simply/simply.go",
    "content": "// Package simply implements a DNS provider for solving the DNS-01 challenge using Simply.com.\npackage simply\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/simply/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"SIMPLY_\"\n\n\tEnvAccountName = envNamespace + \"ACCOUNT_NAME\"\n\tEnvAPIKey      = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAccountName        string\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]int64\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Simply.com.\n// Credentials must be passed in the environment variable: SIMPLY_ACCOUNT_NAME, SIMPLY_API_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAccountName, EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"simply: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AccountName = values[EnvAccountName]\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Simply.com.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"simply: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.AccountName == \"\" {\n\t\treturn nil, errors.New(\"simply: missing credentials: account name\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"simply: missing credentials: api key\")\n\t}\n\n\tclient, err := internal.NewClient(config.AccountName, config.APIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"simply: failed to create client: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]int64),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"simply: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"regru: %w\", err)\n\t}\n\n\trecordBody := internal.Record{\n\t\tName: subDomain,\n\t\tData: info.Value,\n\t\tType: \"TXT\",\n\t\tTTL:  d.config.TTL,\n\t}\n\n\trecordID, err := d.client.AddRecord(context.Background(), authZone, recordBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"simply: failed to add record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = recordID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"simply: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\t// gets the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"simply: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\terr = d.client.DeleteRecord(context.Background(), authZone, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"simply: failed to delete TXT records: fqdn=%s, recordID=%d: %w\", info.EffectiveFQDN, recordID, err)\n\t}\n\n\t// deletes record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/simply/simply.toml",
    "content": "Name = \"Simply.com\"\nDescription = ''''''\nURL = \"https://www.simply.com/en/domains/\"\nCode = \"simply\"\nSince = \"v4.4.0\"\n\nExample = '''\nSIMPLY_ACCOUNT_NAME=xxxxxx \\\nSIMPLY_API_KEY=yyyyyy \\\nlego --dns simply -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    SIMPLY_ACCOUNT_NAME = \"Account name\"\n    SIMPLY_API_KEY = \"API key\"\n  [Configuration.Additional]\n    SIMPLY_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    SIMPLY_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 300)\"\n    SIMPLY_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    SIMPLY_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.simply.com/en/docs/api/\"\n  Spec = \"https://generator.swagger.io/?url=https://api.simply.com/2/openapi.json#/\"\n"
  },
  {
    "path": "providers/dns/simply/simply_test.go",
    "content": "package simply\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAccountName, EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccountName: \"S000000\",\n\t\t\t\tEnvAPIKey:      \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials: account name\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccountName: \"\",\n\t\t\t\tEnvAPIKey:      \"secret\",\n\t\t\t},\n\t\t\texpected: \"simply: some credentials information are missing: SIMPLY_ACCOUNT_NAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials: api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccountName: \"S000000\",\n\t\t\t\tEnvAPIKey:      \"\",\n\t\t\t},\n\t\t\texpected: \"simply: some credentials information are missing: SIMPLY_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials: all\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccountName: \"\",\n\t\t\t\tEnvAPIKey:      \"\",\n\t\t\t},\n\t\t\texpected: \"simply: some credentials information are missing: SIMPLY_ACCOUNT_NAME,SIMPLY_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc        string\n\t\taccountName string\n\t\tapiKey      string\n\t\texpected    string\n\t}{\n\t\t{\n\t\t\tdesc:        \"success\",\n\t\t\taccountName: \"S000000\",\n\t\t\tapiKey:      \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing account name\",\n\t\t\tapiKey:   \"secret\",\n\t\t\texpected: \"simply: missing credentials: account name\",\n\t\t},\n\t\t{\n\t\t\tdesc:        \"missing api key\",\n\t\t\taccountName: \"S000000\",\n\t\t\texpected:    \"simply: missing credentials: api key\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AccountName = test.accountName\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/sonic/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst baseURL = \"https://public-api.sonic.net/dyndns\"\n\n// Client Sonic client.\ntype Client struct {\n\tuserID string\n\tapiKey string\n\n\tbaseURL    string\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a Client.\nfunc NewClient(userID, apiKey string) (*Client, error) {\n\tif userID == \"\" || apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials are missing\")\n\t}\n\n\treturn &Client{\n\t\tuserID:     userID,\n\t\tapiKey:     apiKey,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// SetRecord creates or updates a TXT records.\n// Sonic does not provide a delete record API endpoint.\n// https://public-api.sonic.net/dyndns#updating_or_adding_host_records\nfunc (c *Client) SetRecord(ctx context.Context, hostname, value string, ttl int) error {\n\tpayload := &Record{\n\t\tUserID:   c.userID,\n\t\tAPIKey:   c.apiKey,\n\t\tHostname: hostname,\n\t\tValue:    value,\n\t\tTTL:      ttl,\n\t\tType:     \"TXT\",\n\t}\n\n\tbody, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t}\n\n\tendpoint, err := url.JoinPath(c.baseURL, \"host\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewReader(body))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treq.Header.Set(\"content-type\", \"application/json\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tr := APIResponse{}\n\n\terr = json.Unmarshal(raw, &r)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tif r.Result != 200 {\n\t\treturn fmt.Errorf(\"API response code: %d, %s\", r.Result, r.Message)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/sonic/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient, err := NewClient(\"foo\", \"secret\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.baseURL = server.URL\n\tclient.HTTPClient = server.Client()\n\n\treturn client, nil\n}\n\nfunc TestClient_SetRecord(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tresponse string\n\t\tassert   require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tresponse: `{\"message\":\"OK\",\"result\":200}`,\n\t\t\tassert:   require.NoError,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"failure\",\n\t\t\tresponse: `{\"message\":\"Not Found :  the information you requested was not found.\",\"result\":404}`,\n\t\t\tassert:   require.Error,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithJSONHeaders()).\n\t\t\t\tRoute(\"PUT /host\",\n\t\t\t\t\tservermock.RawStringResponse(test.response),\n\t\t\t\t\tservermock.CheckRequestJSONBody(`{\"userid\":\"foo\",\"apikey\":\"secret\",\"hostname\":\"example.com\",\"value\":\"txttxttxt\",\"ttl\":10,\"type\":\"TXT\"}`)).\n\t\t\t\tBuild(t)\n\n\t\t\terr := client.SetRecord(t.Context(), \"example.com\", \"txttxttxt\", 10)\n\t\t\ttest.assert(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/sonic/internal/types.go",
    "content": "package internal\n\ntype APIResponse struct {\n\tMessage string `json:\"message\"`\n\tResult  int    `json:\"result\"`\n}\n\n// Record holds the Sonic API representation of a Domain Record.\ntype Record struct {\n\tUserID   string `json:\"userid\"`\n\tAPIKey   string `json:\"apikey\"`\n\tHostname string `json:\"hostname\"`\n\tValue    string `json:\"value\"`\n\tTTL      int    `json:\"ttl\"`\n\tType     string `json:\"type\"`\n}\n"
  },
  {
    "path": "providers/dns/sonic/sonic.go",
    "content": "// Package sonic implements a DNS provider for solving the DNS-01 challenge using  Sonic.\npackage sonic\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/sonic/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"SONIC_\"\n\n\tEnvUserID = envNamespace + \"USER_ID\"\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUserID             string\n\tAPIKey             string\n\tHTTPClient         *http.Client\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Sonic.\n// Credentials must be passed in the environment variables:\n// SONIC_USERID and SONIC_APIKEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUserID, EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"sonic: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.UserID = values[EnvUserID]\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Sonic.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"sonic: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.UserID, config.APIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"sonic: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.client.SetRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"sonic: unable to create record for %s: %w\", info.EffectiveFQDN, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT records matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.client.SetRecord(context.Background(), dns01.UnFqdn(info.EffectiveFQDN), \"_\", d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"sonic: unable to clean record for %s: %w\", info.EffectiveFQDN, err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n"
  },
  {
    "path": "providers/dns/sonic/sonic.toml",
    "content": "Name = \"Sonic\"\nDescription = ''''''\nURL = \"https://www.sonic.com/\"\nCode = \"sonic\"\nSince = \"v4.4.0\"\n\nExample = '''\nSONIC_USER_ID=12345 \\\nSONIC_API_KEY=4d6fbf2f9ab0fa11697470918d37625851fc0c51 \\\nlego --dns sonic -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## API keys\n\nThe API keys must be generated by calling the `dyndns/api_key` endpoint.\n\nExample:\n\n```bash\n$ curl -X POST -H \"Content-Type: application/json\" --data '{\"username\":\"notarealuser\",\"password\":\"notarealpassword\",\"hostname\":\"example.com\"}' https://public-api.sonic.net/dyndns/api_key\n{\"userid\":\"12345\",\"apikey\":\"4d6fbf2f9ab0fa11697470918d37625851fc0c51\",\"result\":200,\"message\":\"OK\"}\n```\n\nSee https://public-api.sonic.net/dyndns/#requesting_an_api_key for additional details.\n\nThis `userid` and `apikey` combo allow modifications to any DNS entries connected to the managed domain (hostname).\n\nHostname should be the toplevel domain managed e.g. `example.com` not `www.example.com`.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    SONIC_USER_ID = \"User ID\"\n    SONIC_API_KEY = \"API Key\"\n  [Configuration.Additional]\n    SONIC_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    SONIC_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    SONIC_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    SONIC_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n    SONIC_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://public-api.sonic.net/dyndns/\"\n\n"
  },
  {
    "path": "providers/dns/sonic/sonic_test.go",
    "content": "package sonic\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey, EnvUserID).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUserID: \"dummy\",\n\t\t\t\tEnvAPIKey: \"dummy\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing all credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"sonic: some credentials information are missing: SONIC_USER_ID,SONIC_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"no userid\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"dummy\",\n\t\t\t},\n\t\t\texpected: \"sonic: some credentials information are missing: SONIC_USER_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"no apikey\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUserID: \"dummy\",\n\t\t\t},\n\t\t\texpected: `sonic: some credentials information are missing: SONIC_API_KEY`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tuserID   string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tuserID: \"dummy\",\n\t\t\tapiKey: \"dummy\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing all credentials\",\n\t\t\texpected: \"sonic: credentials are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing userid\",\n\t\t\tapiKey:   \"dummy\",\n\t\t\texpected: \"sonic: credentials are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing apikey\",\n\t\t\tuserID:   \"dummy\",\n\t\t\texpected: \"sonic: credentials are missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.UserID = test.userID\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/spaceship/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://spaceship.dev/api/v1/\"\n\n// Client the Spaceship API client.\ntype Client struct {\n\tapiKey    string\n\tapiSecret string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiKey, apiSecret string) (*Client, error) {\n\tif apiKey == \"\" || apiSecret == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tapiSecret:  apiSecret,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Add(\"X-Api-Secret\", c.apiSecret)\n\treq.Header.Add(\"X-Api-Key\", c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) AddRecord(ctx context.Context, domain string, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"dns\", \"records\", domain)\n\n\treq, err := newJSONRequest(ctx, http.MethodPut, endpoint, Foo{Items: []Record{record}})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, domain string, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"dns\", \"records\", domain)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, []Record{record})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.do(req, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) GetRecords(ctx context.Context, domain string) ([]Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns\", \"records\", domain)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result GetRecordsResponse\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Items, nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/spaceship/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"key\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWith(\"X-Api-Key\", \"key\").\n\t\t\tWith(\"X-Api-Secret\", \"secret\"),\n\t)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /dns/records/example.com\", nil,\n\t\t\tservermock.CheckRequestJSONBody(`{\"items\":[{\"type\":\"TXT\",\"name\":\"@\",\"ttl\":60}]}`)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType: \"TXT\",\n\t\tName: \"@\",\n\t\tTTL:  60,\n\t}\n\n\terr := client.AddRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"PUT /dns/records/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnprocessableEntity)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType: \"TXT\",\n\t\tName: \"@\",\n\t\tTTL:  60,\n\t}\n\n\terr := client.AddRecord(t.Context(), \"example.com\", record)\n\trequire.EqualError(t, err, \"^$, name: The domain name contains invalid characters\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns/records/example.com\", nil,\n\t\t\tservermock.CheckRequestJSONBody(`[{\"type\":\"TXT\",\"name\":\"@\",\"ttl\":60}]`)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType: \"TXT\",\n\t\tName: \"@\",\n\t\tTTL:  60,\n\t}\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns/records/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnprocessableEntity)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType: \"TXT\",\n\t\tName: \"@\",\n\t\tTTL:  60,\n\t}\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", record)\n\trequire.EqualError(t, err, \"^$, name: The domain name contains invalid characters\")\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/records/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"get-records.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{Type: \"A\", Name: \"@\", TTL: 3600},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetRecords_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/records/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnprocessableEntity)).\n\t\tBuild(t)\n\n\t_, err := client.GetRecords(t.Context(), \"example.com\")\n\trequire.EqualError(t, err, \"^$, name: The domain name contains invalid characters\")\n}\n"
  },
  {
    "path": "providers/dns/spaceship/internal/fixtures/error.json",
    "content": "{\n  \"detail\": \"^$\",\n  \"data\": [\n    {\n      \"field\": \"name\",\n      \"details\": \"The domain name contains invalid characters\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/spaceship/internal/fixtures/get-records.json",
    "content": "{\n  \"items\": [\n    {\n      \"type\": \"A\",\n      \"name\": \"@\",\n      \"ttl\": 3600,\n      \"group\": {\n        \"type\": \"custom\"\n      }\n    }\n  ],\n  \"total\": 100\n}\n"
  },
  {
    "path": "providers/dns/spaceship/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype APIError struct {\n\tDetail string `json:\"detail\"`\n\tData   []struct {\n\t\tField   string `json:\"field\"`\n\t\tDetails string `json:\"details\"`\n\t} `json:\"data\"`\n}\n\nfunc (a *APIError) Error() string {\n\tmsg := []string{a.Detail}\n\n\tfor _, datum := range a.Data {\n\t\tmsg = append(msg, fmt.Sprintf(\"%s: %s\", datum.Field, datum.Details))\n\t}\n\n\treturn strings.Join(msg, \", \")\n}\n\ntype Foo struct {\n\tForce bool     `json:\"force,omitempty\"`\n\tItems []Record `json:\"items,omitempty\"`\n}\n\ntype Record struct {\n\tType       string `json:\"type,omitempty\"`\n\tName       string `json:\"name,omitempty\"`\n\tValue      string `json:\"value,omitempty\"`\n\tAddress    string `json:\"address,omitempty\"`\n\tNameserver string `json:\"nameserver,omitempty\"`\n\tAliasName  string `json:\"aliasName,omitempty\"`\n\tPointer    string `json:\"pointer,omitempty\"`\n\tCName      string `json:\"cname,omitempty\"`\n\tExchange   string `json:\"exchange,omitempty\"`\n\tTTL        int    `json:\"ttl,omitempty\"`\n}\n\ntype GetRecordsResponse struct {\n\tItems []Record `json:\"items\"`\n\tTotal int      `json:\"total\"`\n}\n"
  },
  {
    "path": "providers/dns/spaceship/spaceship.go",
    "content": "// Package spaceship implements a DNS provider for solving the DNS-01 challenge using Spaceship.\npackage spaceship\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/spaceship/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"SPACESHIP_\"\n\n\tEnvAPIKey    = envNamespace + \"API_KEY\"\n\tEnvAPISecret = envNamespace + \"API_SECRET\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey    string\n\tAPISecret string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Spaceship.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey, EnvAPISecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"spaceship: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.APISecret = values[EnvAPISecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Spaceship.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"spaceship: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.APIKey, config.APISecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"spaceship: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"spaceship: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"spaceship: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tType:  \"TXT\",\n\t\tName:  subDomain,\n\t\tValue: info.Value,\n\t\tTTL:   d.config.TTL,\n\t}\n\n\terr = d.client.AddRecord(context.Background(), dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"spaceship: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"spaceship: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"spaceship: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tType:  \"TXT\",\n\t\tName:  subDomain,\n\t\tValue: info.Value,\n\t}\n\n\terr = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"spaceship: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/spaceship/spaceship.toml",
    "content": "Name = \"Spaceship\"\nDescription = ''''''\nURL = \"https://www.spaceship.com/\"\nCode = \"spaceship\"\nSince = \"v4.22.0\"\n\nExample = '''\nSPACESHIP_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nSPACESHIP_API_SECRET=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns spaceship -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    SPACESHIP_API_KEY = \"API key\"\n    SPACESHIP_API_SECRET = \"API secret\"\n  [Configuration.Additional]\n    SPACESHIP_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    SPACESHIP_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    SPACESHIP_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    SPACESHIP_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://docs.spaceship.dev/#tag/DNS-records\"\n"
  },
  {
    "path": "providers/dns/spaceship/spaceship_test.go",
    "content": "package spaceship\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey, EnvAPISecret).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"key\",\n\t\t\t\tEnvAPISecret: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"\",\n\t\t\t\tEnvAPISecret: \"secret\",\n\t\t\t},\n\t\t\texpected: \"spaceship: some credentials information are missing: SPACESHIP_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey:    \"key\",\n\t\t\t\tEnvAPISecret: \"\",\n\t\t\t},\n\t\t\texpected: \"spaceship: some credentials information are missing: SPACESHIP_API_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"spaceship: some credentials information are missing: SPACESHIP_API_KEY,SPACESHIP_API_SECRET\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tapiKey    string\n\t\tapiSecret string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tapiKey:    \"key\",\n\t\t\tapiSecret: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing API key\",\n\t\t\tapiSecret: \"secret\",\n\t\t\texpected:  \"spaceship: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API secret\",\n\t\t\tapiKey:   \"key\",\n\t\t\texpected: \"spaceship: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"spaceship: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.APISecret = test.apiSecret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/stackpath/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"golang.org/x/net/publicsuffix\"\n)\n\nconst defaultBaseURL = \"https://gateway.stackpath.com/dns/v1/stacks/\"\n\n// Client the API client for Stackpath.\ntype Client struct {\n\tstackID string\n\n\tbaseURL    *url.URL\n\thttpClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(stackID string, hc *http.Client) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tbaseURL:    baseURL,\n\t\tstackID:    stackID,\n\t\thttpClient: hc,\n\t}\n}\n\n// GetZones gets all zones.\n// https://stackpath.dev/reference/getzones\nfunc (c *Client) GetZones(ctx context.Context, domain string) (*Zone, error) {\n\tendpoint := c.baseURL.JoinPath(c.stackID, \"zones\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttld, err := publicsuffix.EffectiveTLDPlusOne(dns01.UnFqdn(domain))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := req.URL.Query()\n\tquery.Add(\"page_request.filter\", fmt.Sprintf(\"domain='%s'\", tld))\n\treq.URL.RawQuery = query.Encode()\n\n\tvar zones Zones\n\n\terr = c.do(req, &zones)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(zones.Zones) == 0 {\n\t\treturn nil, fmt.Errorf(\"did not find zone with domain %s\", domain)\n\t}\n\n\treturn &zones.Zones[0], nil\n}\n\n// GetZoneRecords gets all records.\n// https://stackpath.dev/reference/getzonerecords\nfunc (c *Client) GetZoneRecords(ctx context.Context, name string, zone *Zone) ([]Record, error) {\n\tendpoint := c.baseURL.JoinPath(c.stackID, \"zones\", zone.ID, \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := req.URL.Query()\n\tquery.Add(\"page_request.filter\", fmt.Sprintf(\"name='%s' and type='TXT'\", name))\n\treq.URL.RawQuery = query.Encode()\n\n\tvar records Records\n\n\terr = c.do(req, &records)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(records.Records) == 0 {\n\t\treturn nil, fmt.Errorf(\"did not find record with name %s\", name)\n\t}\n\n\treturn records.Records, nil\n}\n\n// CreateZoneRecord creates a record.\n// https://stackpath.dev/reference/createzonerecord\nfunc (c *Client) CreateZoneRecord(ctx context.Context, zone *Zone, record Record) error {\n\tendpoint := c.baseURL.JoinPath(c.stackID, \"zones\", zone.ID, \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\n// DeleteZoneRecord deletes a record.\n// https://stackpath.dev/reference/deletezonerecord\nfunc (c *Client) DeleteZoneRecord(ctx context.Context, zone *Zone, record Record) error {\n\tendpoint := c.baseURL.JoinPath(c.stackID, \"zones\", zone.ID, \"records\", record.ID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\terrResp := &ErrorResponse{}\n\n\terr := json.Unmarshal(raw, errResp)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn errResp\n}\n"
  },
  {
    "path": "providers/dns/stackpath/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"STACK_ID\", server.Client())\n\n\t\t\tclient.baseURL, _ = url.Parse(server.URL + \"/\")\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders(),\n\t)\n}\n\nfunc TestClient_GetZoneRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /STACK_ID/zones/A/records\",\n\t\t\tservermock.ResponseFromFixture(\"get_zone_records.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"page_request.filter\", \"name='foo1' and type='TXT'\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetZoneRecords(t.Context(), \"foo1\", &Zone{ID: \"A\", Domain: \"test\"})\n\trequire.NoError(t, err)\n\n\texpected := []Record{\n\t\t{ID: \"1\", Name: \"foo1\", Type: \"TXT\", TTL: 120, Data: \"txtTXTtxt\"},\n\t\t{ID: \"2\", Name: \"foo2\", Type: \"TXT\", TTL: 121, Data: \"TXTtxtTXT\"},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetZoneRecords_apiError(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /STACK_ID/zones/A/records\",\n\t\t\tservermock.RawStringResponse(`\n{\n\t\"code\": 401,\n\t\"error\": \"an unauthorized request is attempted.\"\n}`).WithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetZoneRecords(t.Context(), \"foo1\", &Zone{ID: \"A\", Domain: \"test\"})\n\n\texpected := &ErrorResponse{Code: 401, Message: \"an unauthorized request is attempted.\"}\n\tassert.Equal(t, expected, err)\n}\n\nfunc TestClient_GetZones(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /STACK_ID/zones\",\n\t\t\tservermock.ResponseFromFixture(\"get_zones.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"page_request.filter\", \"domain='foo.com'\")).\n\t\tBuild(t)\n\n\tzone, err := client.GetZones(t.Context(), \"sub.foo.com\")\n\trequire.NoError(t, err)\n\n\texpected := &Zone{ID: \"A\", Domain: \"foo.com\"}\n\n\tassert.Equal(t, expected, zone)\n}\n"
  },
  {
    "path": "providers/dns/stackpath/internal/fixtures/get_zone_records.json",
    "content": "{\n  \"records\": [\n    {\"id\":\"1\",\"name\":\"foo1\",\"type\":\"TXT\",\"ttl\":120,\"data\":\"txtTXTtxt\"},\n    {\"id\":\"2\",\"name\":\"foo2\",\"type\":\"TXT\",\"ttl\":121,\"data\":\"TXTtxtTXT\"}\n  ]\n}\n"
  },
  {
    "path": "providers/dns/stackpath/internal/fixtures/get_zones.json",
    "content": "{\n  \"pageInfo\": {\n    \"totalCount\": \"5\",\n    \"hasPreviousPage\": false,\n    \"hasNextPage\": false,\n    \"startCursor\": \"1\",\n    \"endCursor\": \"1\"\n  },\n  \"zones\": [\n    {\n      \"stackId\": \"my_stack\",\n      \"accountId\": \"my_account\",\n      \"id\": \"A\",\n      \"domain\": \"foo.com\",\n      \"version\": \"1\",\n      \"labels\": {\n        \"property1\": \"val1\",\n        \"property2\": \"val2\"\n      },\n      \"created\": \"2018-10-07T02:31:49Z\",\n      \"updated\": \"2018-10-07T02:31:49Z\",\n      \"nameservers\": [\n        \"1.1.1.1\"\n      ],\n      \"verified\": \"2018-10-07T02:31:49Z\",\n      \"status\": \"ACTIVE\",\n      \"disabled\": false\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/stackpath/internal/identity.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"golang.org/x/oauth2/clientcredentials\"\n)\n\nconst defaultAuthURL = \"https://gateway.stackpath.com/identity/v1/oauth2/token\"\n\nfunc CreateOAuthClient(ctx context.Context, clientID, clientSecret string) *http.Client {\n\tconfig := &clientcredentials.Config{\n\t\tTokenURL:     defaultAuthURL,\n\t\tClientID:     clientID,\n\t\tClientSecret: clientSecret,\n\t}\n\n\treturn config.Client(ctx)\n}\n"
  },
  {
    "path": "providers/dns/stackpath/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\n// Zones is the response struct from the Stackpath api GetZones.\ntype Zones struct {\n\tZones []Zone `json:\"zones\"`\n}\n\n// Zone a DNS zone representation.\ntype Zone struct {\n\tID     string\n\tDomain string\n}\n\n// Records is the response struct from the Stackpath api GetZoneRecords.\ntype Records struct {\n\tRecords []Record `json:\"records\"`\n}\n\n// Record a DNS record representation.\ntype Record struct {\n\tID   string `json:\"id,omitempty\"`\n\tName string `json:\"name\"`\n\tType string `json:\"type\"`\n\tTTL  int    `json:\"ttl\"`\n\tData string `json:\"data\"`\n}\n\n// ErrorResponse the API error response representation.\ntype ErrorResponse struct {\n\tCode    int    `json:\"code\"`\n\tMessage string `json:\"error\"`\n}\n\nfunc (e *ErrorResponse) Error() string {\n\treturn fmt.Sprintf(\"%d %s\", e.Code, e.Message)\n}\n"
  },
  {
    "path": "providers/dns/stackpath/stackpath.go",
    "content": "// Package stackpath implements a DNS provider for solving the DNS-01 challenge using Stackpath DNS.\n// https://developer.stackpath.com/en/api/dns/\npackage stackpath\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/stackpath/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"STACKPATH_\"\n\n\tEnvClientID     = envNamespace + \"CLIENT_ID\"\n\tEnvClientSecret = envNamespace + \"CLIENT_SECRET\"\n\tEnvStackID      = envNamespace + \"STACK_ID\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tClientID           string\n\tClientSecret       string\n\tStackID            string\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Stackpath.\n// Credentials must be passed in the environment variables:\n// STACKPATH_CLIENT_ID, STACKPATH_CLIENT_SECRET, and STACKPATH_STACK_ID.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvClientID, EnvClientSecret, EnvStackID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"stackpath: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.ClientID = values[EnvClientID]\n\tconfig.ClientSecret = values[EnvClientSecret]\n\tconfig.StackID = values[EnvStackID]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Stackpath.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"stackpath: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.ClientID == \"\" || config.ClientSecret == \"\" {\n\t\treturn nil, errors.New(\"stackpath: credentials missing\")\n\t}\n\n\tif config.StackID == \"\" {\n\t\treturn nil, errors.New(\"stackpath: stack id missing\")\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: internal.NewClient(config.StackID,\n\t\t\tclientdebug.Wrap(\n\t\t\t\tinternal.CreateOAuthClient(context.Background(), config.ClientID, config.ClientSecret),\n\t\t\t),\n\t\t),\n\t}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzone, err := d.client.GetZones(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"stackpath: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"stackpath: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tName: subDomain,\n\t\tType: \"TXT\",\n\t\tTTL:  d.config.TTL,\n\t\tData: info.Value,\n\t}\n\n\treturn d.client.CreateZoneRecord(ctx, zone, record)\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzone, err := d.client.GetZones(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"stackpath: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"stackpath: %w\", err)\n\t}\n\n\trecords, err := d.client.GetZoneRecords(ctx, subDomain, zone)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, record := range records {\n\t\terr = d.client.DeleteZoneRecord(ctx, zone, record)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"stackpath: failed to delete TXT record: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/stackpath/stackpath.toml",
    "content": "Name = \"Stackpath\"\nDescription = ''''''\nURL = \"https://www.stackpath.com/\"\nCode = \"stackpath\"\nSince = \"v1.1.0\"\n\nExample = '''\nSTACKPATH_CLIENT_ID=xxxxx \\\nSTACKPATH_CLIENT_SECRET=yyyyy \\\nSTACKPATH_STACK_ID=zzzzz \\\nlego --dns stackpath -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    STACKPATH_CLIENT_ID = \"Client ID\"\n    STACKPATH_CLIENT_SECRET = \"Client secret\"\n    STACKPATH_STACK_ID = \"Stack ID\"\n  [Configuration.Additional]\n    STACKPATH_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    STACKPATH_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    STACKPATH_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n\n[Links]\n  API = \"https://developer.stackpath.com/en/api/dns/#tag/Zone\"\n"
  },
  {
    "path": "providers/dns/stackpath/stackpath_test.go",
    "content": "package stackpath\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvClientID,\n\tEnvClientSecret,\n\tEnvStackID).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvClientID:     \"test@example.com\",\n\t\t\t\tEnvClientSecret: \"123\",\n\t\t\t\tEnvStackID:      \"ID\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvClientID:     \"\",\n\t\t\t\tEnvClientSecret: \"\",\n\t\t\t\tEnvStackID:      \"\",\n\t\t\t},\n\t\t\texpected: \"stackpath: some credentials information are missing: STACKPATH_CLIENT_ID,STACKPATH_CLIENT_SECRET,STACKPATH_STACK_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing client id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvClientID:     \"\",\n\t\t\t\tEnvClientSecret: \"123\",\n\t\t\t\tEnvStackID:      \"ID\",\n\t\t\t},\n\t\t\texpected: \"stackpath: some credentials information are missing: STACKPATH_CLIENT_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing client secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvClientID:     \"test@example.com\",\n\t\t\t\tEnvClientSecret: \"\",\n\t\t\t\tEnvStackID:      \"ID\",\n\t\t\t},\n\t\t\texpected: \"stackpath: some credentials information are missing: STACKPATH_CLIENT_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing stack id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvClientID:     \"test@example.com\",\n\t\t\t\tEnvClientSecret: \"123\",\n\t\t\t\tEnvStackID:      \"\",\n\t\t\t},\n\t\t\texpected: \"stackpath: some credentials information are missing: STACKPATH_STACK_ID\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.NotNil(t, p)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := map[string]struct {\n\t\tconfig      *Config\n\t\texpectedErr string\n\t}{\n\t\t\"no_config\": {\n\t\t\tconfig:      nil,\n\t\t\texpectedErr: \"stackpath: the configuration of the DNS provider is nil\",\n\t\t},\n\t\t\"no_client_id\": {\n\t\t\tconfig: &Config{\n\t\t\t\tClientSecret: \"secret\",\n\t\t\t\tStackID:      \"stackID\",\n\t\t\t},\n\t\t\texpectedErr: \"stackpath: credentials missing\",\n\t\t},\n\t\t\"no_client_secret\": {\n\t\t\tconfig: &Config{\n\t\t\t\tClientID: \"clientID\",\n\t\t\t\tStackID:  \"stackID\",\n\t\t\t},\n\t\t\texpectedErr: \"stackpath: credentials missing\",\n\t\t},\n\t\t\"no_stack_id\": {\n\t\t\tconfig: &Config{\n\t\t\t\tClientID:     \"clientID\",\n\t\t\t\tClientSecret: \"secret\",\n\t\t\t},\n\t\t\texpectedErr: \"stackpath: stack id missing\",\n\t\t},\n\t}\n\n\tfor desc, test := range testCases {\n\t\tt.Run(desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tp, err := NewDNSProviderConfig(test.config)\n\t\t\trequire.EqualError(t, err, test.expectedErr)\n\t\t\tassert.Nil(t, p)\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/syse/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\nconst defaultBaseURL = \"https://www.syse.no/api\"\n\n// Client the Syse API client.\ntype Client struct {\n\tcredentials map[string]string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(credentials map[string]string) (*Client, error) {\n\tif len(credentials) == 0 {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tcredentials: credentials,\n\t\tBaseURL:     baseURL,\n\t\tHTTPClient:  &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) CreateRecord(ctx context.Context, zone string, record Record) (*Record, error) {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", zone)\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.SetBasicAuth(zone, c.credentials[zone])\n\n\tresult := new(Record)\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", zone, recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.SetBasicAuth(zone, c.credentials[zone])\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\traw, _ := io.ReadAll(resp.Body)\n\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/syse/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(map[string]string{\n\t\t\t\t\"example.com\": \"secret\",\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders(),\n\t)\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"create_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:    \"TXT\",\n\t\tPrefix:  \"_acme-challenge\",\n\t\tContent: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tActive:  true,\n\t\tTTL:     120,\n\t}\n\n\tresult, err := client.CreateRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tID:      \"1234\",\n\t\tType:    \"TXT\",\n\t\tPrefix:  \"_acme-challenge\",\n\t\tContent: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tActive:  true,\n\t\tTTL:     120,\n\t}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_CreateRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/example.com\",\n\t\t\tservermock.RawStringResponse(http.StatusText(http.StatusUnauthorized)).\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tType:    \"TXT\",\n\t\tPrefix:  \"_acme-challenge\",\n\t\tContent: \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tActive:  true,\n\t\tTTL:     120,\n\t}\n\n\t_, err := client.CreateRecord(t.Context(), \"example.com\", record)\n\trequire.EqualError(t, err, \"unexpected status code: [status code: 401] body: Unauthorized\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns/example.com/1234\",\n\t\t\tservermock.Noop()).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", \"1234\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns/example.com/1234\",\n\t\t\tservermock.RawStringResponse(http.StatusText(http.StatusUnauthorized)).\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", \"1234\")\n\trequire.EqualError(t, err, \"unexpected status code: [status code: 401] body: Unauthorized\")\n}\n"
  },
  {
    "path": "providers/dns/syse/internal/fixtures/create_record-request.json",
    "content": "{\n  \"content\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n  \"active\": true,\n  \"ttl\": 120,\n  \"prefix\": \"_acme-challenge\",\n  \"type\": \"TXT\"\n}\n"
  },
  {
    "path": "providers/dns/syse/internal/fixtures/create_record.json",
    "content": "{\n  \"id\": \"1234\",\n  \"content\": \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n  \"active\": true,\n  \"ttl\": 120,\n  \"prefix\": \"_acme-challenge\",\n  \"type\": \"TXT\"\n}\n"
  },
  {
    "path": "providers/dns/syse/internal/types.go",
    "content": "package internal\n\ntype Record struct {\n\tID       string `json:\"id,omitempty\"`\n\tType     string `json:\"type,omitempty\"`\n\tPrefix   string `json:\"prefix,omitempty\"`\n\tContent  string `json:\"content,omitempty\"`\n\tPriority int    `json:\"prio,omitempty\"`\n\tTTL      int    `json:\"ttl,omitempty\"`\n\tActive   bool   `json:\"active,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/syse/syse.go",
    "content": "// Package syse implements a DNS provider for solving the DNS-01 challenge using Syse.\npackage syse\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/syse/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"SYSE_\"\n\n\tEnvCredentials = envNamespace + \"CREDENTIALS\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tCredentials map[string]string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 1200*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Syse.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvCredentials)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"syse: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\n\tcredentials, err := env.ParsePairs(values[EnvCredentials])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"syse: credentials: %w\", err)\n\t}\n\n\tconfig.Credentials = credentials\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Syse.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"syse: the configuration of the DNS provider is nil\")\n\t}\n\n\tif len(config.Credentials) == 0 {\n\t\treturn nil, errors.New(\"syse: missing credentials\")\n\t}\n\n\tfor domain, password := range config.Credentials {\n\t\tif domain == \"\" {\n\t\t\treturn nil, fmt.Errorf(`syse: missing domain: \"%s:%s\"`, domain, password)\n\t\t}\n\n\t\tif password == \"\" {\n\t\t\treturn nil, fmt.Errorf(`syse: missing password: \"%s:%s\"`, domain, password)\n\t\t}\n\t}\n\n\tclient, err := internal.NewClient(config.Credentials)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"syse: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"syse: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"syse: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tType:    \"TXT\",\n\t\tPrefix:  subDomain,\n\t\tContent: info.Value,\n\t\tTTL:     d.config.TTL,\n\t\tActive:  true,\n\t}\n\n\tnewRecord, err := d.client.CreateRecord(context.Background(), dns01.UnFqdn(authZone), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"syse: create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = newRecord.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"syse: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// gets the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"syse: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\terr = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"syse: delete record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/syse/syse.toml",
    "content": "Name = \"Syse\"\nDescription = ''''''\nURL = \"https://www.syse.no/\"\nCode = \"syse\"\nSince = \"v4.30.0\"\n\nExample = '''\nSYSE_CREDENTIALS=example.com:password \\\nlego --dns syse -d '*.example.com' -d example.com run\n\nSYSE_CREDENTIALS=example.org:password1,example.com:password2 \\\nlego --dns syse -d '*.example.org' -d example.org -d '*.example.com' -d example.com\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    SYSE_CREDENTIALS = \"Comma-separated list of `zone:password` credential pairs\"\n  [Configuration.Additional]\n    SYSE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    SYSE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 1200)\"\n    SYSE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    SYSE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.syse.no/api/dns\"\n"
  },
  {
    "path": "providers/dns/syse/syse_test.go",
    "content": "package syse\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvCredentials).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCredentials: \"example.org:123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success multiple domains\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCredentials: \"example.org:123,example.com:456,example.net:789\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCredentials: \",\",\n\t\t\t},\n\t\t\texpected: `syse: credentials: incorrect pair: `,\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCredentials: \"example.org:\",\n\t\t\t},\n\t\t\texpected: `syse: missing password: \"example.org:\"`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing domain\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCredentials: \":123\",\n\t\t\t},\n\t\t\texpected: `syse: missing domain: \":123\"`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid credentials, partial\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCredentials: \"example.org:123,example.net\",\n\t\t\t},\n\t\t\texpected: \"syse: credentials: incorrect pair: example.net\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvCredentials: \"\",\n\t\t\t},\n\t\t\texpected: \"syse: some credentials information are missing: SYSE_CREDENTIALS\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tcreds    map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\tcreds: map[string]string{\"example.org\": \"123\"},\n\t\t},\n\t\t{\n\t\t\tdesc: \"success multiple domains\",\n\t\t\tcreds: map[string]string{\n\t\t\t\t\"example.org\": \"123\",\n\t\t\t\t\"example.com\": \"456\",\n\t\t\t\t\"example.net\": \"789\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"syse: missing credentials\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing domain\",\n\t\t\tcreds:    map[string]string{\"\": \"123\"},\n\t\t\texpected: `syse: missing domain: \":123\"`,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tcreds:    map[string]string{\"example.org\": \"\"},\n\t\t\texpected: `syse: missing password: \"example.org:\"`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Credentials = test.creds\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Credentials = map[string]string{\n\t\t\t\t\"example.org\": \"secret\",\n\t\t\t}\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders(),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"/\", servermock.DumpRequest()).\n\t\tRoute(\"POST /dns/example.com\",\n\t\t\tservermock.ResponseFromInternal(\"create_record.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromInternal(\"create_record-request.json\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"DELETE /dns/example.com/1234\",\n\t\t\tservermock.Noop()).\n\t\tBuild(t)\n\n\tprovider.recordIDs[\"abc\"] = \"1234\"\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/technitium/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\nconst statusSuccess = \"ok\"\n\n// Client the Technitium API client.\ntype Client struct {\n\tapiToken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(baseURL, apiToken string) (*Client, error) {\n\tif apiToken == \"\" {\n\t\treturn nil, errors.New(\"missing credentials\")\n\t}\n\n\tif baseURL == \"\" {\n\t\treturn nil, errors.New(\"missing server URL\")\n\t}\n\n\tapiEndpoint, err := url.Parse(baseURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\tapiToken:   apiToken,\n\t\tbaseURL:    apiEndpoint,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// AddRecord adds a resource record for an authoritative zone.\n// https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md#add-record\nfunc (c *Client) AddRecord(ctx context.Context, record Record) (*Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"zones\", \"records\", \"add\")\n\n\treq, err := c.newFormRequest(ctx, endpoint, record)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tresult := &APIResponse[AddRecordResponse]{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif result.Status != statusSuccess {\n\t\treturn nil, result\n\t}\n\n\treturn result.Response.AddedRecord, nil\n}\n\n// DeleteRecord deletes a record from an authoritative zone.\n// https://github.com/TechnitiumSoftware/DnsServer/blob/master/APIDOCS.md#delete-record\nfunc (c *Client) DeleteRecord(ctx context.Context, record Record) error {\n\tendpoint := c.baseURL.JoinPath(\"api\", \"zones\", \"records\", \"delete\")\n\n\treq, err := c.newFormRequest(ctx, endpoint, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\tresult := &APIResponse[any]{}\n\n\terr = c.do(req, result)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif result.Status != statusSuccess {\n\t\treturn result\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode >= http.StatusBadRequest {\n\t\treturn parseError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) newFormRequest(ctx context.Context, endpoint *url.URL, payload any) (*http.Request, error) {\n\tvalues := url.Values{}\n\n\tif payload != nil {\n\t\tvar err error\n\n\t\tvalues, err = querystring.Values(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request body: %w\", err)\n\t\t}\n\t}\n\n\tvalues.Set(\"token\", c.apiToken)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(values.Encode()))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIResponse[any]\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/technitium/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(server.URL, \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithContentTypeFromURLEncoded())\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /api/zones/records/add\",\n\t\t\tservermock.ResponseFromFixture(\"add-record.json\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"domain\", \"_acme-challenge.example.com\").\n\t\t\t\tWith(\"text\", \"txtTXTtxt\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"token\", \"secret\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tDomain: \"_acme-challenge.example.com\",\n\t\tType:   \"TXT\",\n\t\tText:   \"txtTXTtxt\",\n\t}\n\n\tnewRecord, err := client.AddRecord(t.Context(), record)\n\trequire.NoError(t, err)\n\n\texpected := &Record{Name: \"example.com\", Type: \"A\"}\n\n\tassert.Equal(t, expected, newRecord)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /api/zones/records/add\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tDomain: \"_acme-challenge.example.com\",\n\t\tType:   \"TXT\",\n\t\tText:   \"txtTXTtxt\",\n\t}\n\n\t_, err := client.AddRecord(t.Context(), record)\n\trequire.Error(t, err)\n\n\tassert.EqualError(t, err, \"Status: error, ErrorMessage: error message, StackTrace: application stack trace, InnerErrorMessage: inner exception message\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /api/zones/records/delete\",\n\t\t\tservermock.ResponseFromFixture(\"delete-record.json\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"domain\", \"_acme-challenge.example.com\").\n\t\t\t\tWith(\"text\", \"txtTXTtxt\").\n\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\tWith(\"token\", \"secret\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tDomain: \"_acme-challenge.example.com\",\n\t\tType:   \"TXT\",\n\t\tText:   \"txtTXTtxt\",\n\t}\n\n\terr := client.DeleteRecord(t.Context(), record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /api/zones/records/delete\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tDomain: \"_acme-challenge.example.com\",\n\t\tType:   \"TXT\",\n\t\tText:   \"txtTXTtxt\",\n\t}\n\n\terr := client.DeleteRecord(t.Context(), record)\n\trequire.Error(t, err)\n\n\tassert.EqualError(t, err, \"Status: error, ErrorMessage: error message, StackTrace: application stack trace, InnerErrorMessage: inner exception message\")\n}\n"
  },
  {
    "path": "providers/dns/technitium/internal/fixtures/add-record.json",
    "content": "{\n  \"response\": {\n    \"zone\": {\n      \"name\": \"example.com\",\n      \"type\": \"Primary\",\n      \"internal\": false,\n      \"dnssecStatus\": \"SignedWithNSEC\",\n      \"disabled\": false\n    },\n    \"addedRecord\": {\n      \"disabled\": false,\n      \"name\": \"example.com\",\n      \"type\": \"A\",\n      \"ttl\": 3600,\n      \"rData\": {\n        \"ipAddress\": \"3.3.3.3\"\n      },\n      \"dnssecStatus\": \"Unknown\",\n      \"lastUsedOn\": \"0001-01-01T00:00:00\"\n    }\n  },\n  \"status\": \"ok\"\n}\n"
  },
  {
    "path": "providers/dns/technitium/internal/fixtures/delete-record.json",
    "content": "{\n  \"response\": {},\n  \"status\": \"ok\"\n}\n"
  },
  {
    "path": "providers/dns/technitium/internal/fixtures/error.json",
    "content": "{\n  \"status\": \"error\",\n  \"errorMessage\": \"error message\",\n  \"stackTrace\": \"application stack trace\",\n  \"innerErrorMessage\": \"inner exception message\"\n}\n"
  },
  {
    "path": "providers/dns/technitium/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype APIResponse[T any] struct {\n\tStatus string `json:\"status\"` // ok/error/invalid-token\n\n\tResponse T `json:\"response\"`\n\n\tErrorMessage      string `json:\"errorMessage\"`\n\tStackTrace        string `json:\"stackTrace\"`\n\tInnerErrorMessage string `json:\"innerErrorMessage\"`\n}\n\nfunc (a *APIResponse[T]) Error() string {\n\tmsg := fmt.Sprintf(\"Status: %s\", a.Status)\n\n\tif a.ErrorMessage != \"\" {\n\t\tmsg += fmt.Sprintf(\", ErrorMessage: %s\", a.ErrorMessage)\n\t}\n\n\tif a.StackTrace != \"\" {\n\t\tmsg += fmt.Sprintf(\", StackTrace: %s\", a.StackTrace)\n\t}\n\n\tif a.InnerErrorMessage != \"\" {\n\t\tmsg += fmt.Sprintf(\", InnerErrorMessage: %s\", a.InnerErrorMessage)\n\t}\n\n\treturn msg\n}\n\ntype AddRecordResponse struct {\n\tZone        *Zone   `json:\"zone\"`\n\tAddedRecord *Record `json:\"addedRecord\"`\n}\n\ntype Record struct {\n\tName   string `json:\"name,omitempty\" url:\"-\"`\n\tDomain string `json:\"domain,omitempty\" url:\"domain\"`\n\tType   string `json:\"type,omitempty\" url:\"type\"`\n\tText   string `json:\"text,omitempty\" url:\"text\"`\n}\n\ntype Zone struct {\n\tName string `json:\"name\"`\n\tType string `json:\"type\"`\n}\n"
  },
  {
    "path": "providers/dns/technitium/technitium.go",
    "content": "// Package technitium implements a DNS provider for solving the DNS-01 challenge using Technitium.\npackage technitium\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/technitium/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"TECHNITIUM_\"\n\n\tEnvServerBaseURL = envNamespace + \"SERVER_BASE_URL\"\n\tEnvAPIToken      = envNamespace + \"API_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL  string\n\tAPIToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Technitium.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvServerBaseURL, EnvAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"technitium: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.BaseURL = values[EnvServerBaseURL]\n\tconfig.APIToken = values[EnvAPIToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Technitium.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"technitium: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.BaseURL, config.APIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"technitium: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecord := internal.Record{\n\t\tDomain: info.EffectiveFQDN,\n\t\tType:   \"TXT\",\n\t\tText:   info.Value,\n\t}\n\n\t_, err := d.client.AddRecord(context.Background(), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"technitium: add record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trecord := internal.Record{\n\t\tDomain: info.EffectiveFQDN,\n\t\tType:   \"TXT\",\n\t\tText:   info.Value,\n\t}\n\n\terr := d.client.DeleteRecord(context.Background(), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"technitium: delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/technitium/technitium.toml",
    "content": "Name = \"Technitium\"\nDescription = ''''''\nURL = \"https://technitium.com/\"\nCode = \"technitium\"\nSince = \"v4.20.0\"\n\nExample = '''\nTECHNITIUM_SERVER_BASE_URL=\"https://localhost:5380\" \\\nTECHNITIUM_API_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns technitium -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\nTechnitium DNS Server supports Dynamic Updates (RFC2136) for primary zones,\nso you can also use the [RFC2136 provider](https://go-acme.github.io/lego/dns/rfc2136/index.html).\n\n[RFC2136 provider](https://go-acme.github.io/lego/dns/rfc2136/index.html) is much better compared to the HTTP API option from security perspective.\nTechnitium recommends to use it in production over the HTTP API.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    TECHNITIUM_SERVER_BASE_URL = \"Server base URL\"\n    TECHNITIUM_API_TOKEN = \"API token\"\n  [Configuration.Additional]\n    TECHNITIUM_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    TECHNITIUM_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    TECHNITIUM_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    TECHNITIUM_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://github.com/TechnitiumSoftware/DnsServer/blob/0f83d23e605956b66ac76921199e241d9cc061bd/APIDOCS.md\"\n  Article = \"https://blog.technitium.com/2023/03/\"\n"
  },
  {
    "path": "providers/dns/technitium/technitium_test.go",
    "content": "package technitium\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvServerBaseURL, EnvAPIToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerBaseURL: \"https://localhost:5380\",\n\t\t\t\tEnvAPIToken:      \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing server base URL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerBaseURL: \"\",\n\t\t\t\tEnvAPIToken:      \"secret\",\n\t\t\t},\n\t\t\texpected: \"technitium: some credentials information are missing: TECHNITIUM_SERVER_BASE_URL\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvServerBaseURL: \"https://localhost:5380\",\n\t\t\t\tEnvAPIToken:      \"\",\n\t\t\t},\n\t\t\texpected: \"technitium: some credentials information are missing: TECHNITIUM_API_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"technitium: some credentials information are missing: TECHNITIUM_SERVER_BASE_URL,TECHNITIUM_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tbaseURL  string\n\t\ttoken    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:    \"success\",\n\t\t\tbaseURL: \"https://localhost:5380\",\n\t\t\ttoken:   \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing server base URL\",\n\t\t\ttoken:    \"secret\",\n\t\t\texpected: \"technitium: missing server URL\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing token\",\n\t\t\tbaseURL:  \"https://localhost:5380\",\n\t\t\texpected: \"technitium: missing credentials\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"technitium: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.BaseURL = test.baseURL\n\t\t\tconfig.APIToken = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/tencentcloud/tencentcloud.go",
    "content": "// Package tencentcloud implements a DNS provider for solving the DNS-01 challenge using Tencent Cloud DNS.\npackage tencentcloud\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\tdnspod \"github.com/go-acme/tencentclouddnspod/v20210323\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"TENCENTCLOUD_\"\n\n\tEnvSecretID     = envNamespace + \"SECRET_ID\"\n\tEnvSecretKey    = envNamespace + \"SECRET_KEY\"\n\tEnvRegion       = envNamespace + \"REGION\"\n\tEnvSessionToken = envNamespace + \"SESSION_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tSecretID     string\n\tSecretKey    string\n\tRegion       string\n\tSessionToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *dnspod.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Tencent Cloud DNS.\n// Credentials must be passed in the environment variable: TENCENTCLOUD_SECRET_ID, TENCENTCLOUD_SECRET_KEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvSecretID, EnvSecretKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"tencentcloud: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.SecretID = values[EnvSecretID]\n\tconfig.SecretKey = values[EnvSecretKey]\n\tconfig.Region = env.GetOrDefaultString(EnvRegion, \"\")\n\tconfig.SessionToken = env.GetOrDefaultString(EnvSessionToken, \"\")\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Tencent Cloud DNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"tencentcloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tvar credential *common.Credential\n\n\tswitch {\n\tcase config.SecretID != \"\" && config.SecretKey != \"\" && config.SessionToken != \"\":\n\t\tcredential = common.NewTokenCredential(config.SecretID, config.SecretKey, config.SessionToken)\n\tcase config.SecretID != \"\" && config.SecretKey != \"\":\n\t\tcredential = common.NewCredential(config.SecretID, config.SecretKey)\n\tdefault:\n\t\treturn nil, errors.New(\"tencentcloud: credentials missing\")\n\t}\n\n\tcpf := profile.NewClientProfile()\n\tcpf.HttpProfile.Endpoint = \"dnspod.tencentcloudapi.com\"\n\tcpf.HttpProfile.ReqTimeout = int(math.Round(config.HTTPTimeout.Seconds()))\n\n\tclient, err := dnspod.NewClient(credential, config.Region, cpf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"tencentcloud: %w\", err)\n\t}\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzone, err := d.getHostedZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"tencentcloud: failed to get hosted zone: %w\", err)\n\t}\n\n\trecordName, err := extractRecordName(info.EffectiveFQDN, *zone.Name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"tencentcloud: failed to extract record name: %w\", err)\n\t}\n\n\trequest := dnspod.NewCreateRecordRequest()\n\trequest.Domain = zone.Name\n\trequest.DomainId = zone.DomainId\n\trequest.SubDomain = common.StringPtr(recordName)\n\trequest.RecordType = common.StringPtr(\"TXT\")\n\trequest.RecordLine = common.StringPtr(\"默认\")\n\trequest.Value = common.StringPtr(info.Value)\n\trequest.TTL = common.Uint64Ptr(uint64(d.config.TTL))\n\n\t_, err = dnspod.CreateRecordWithContext(ctx, d.client, request)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dnspod: API call failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\tzone, err := d.getHostedZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"tencentcloud: failed to get hosted zone: %w\", err)\n\t}\n\n\trecords, err := d.findTxtRecords(ctx, zone, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"tencentcloud: failed to find TXT records: %w\", err)\n\t}\n\n\tfor _, record := range records {\n\t\trequest := dnspod.NewDeleteRecordRequest()\n\t\trequest.Domain = zone.Name\n\t\trequest.DomainId = zone.DomainId\n\t\trequest.RecordId = record.RecordId\n\n\t\t_, err := dnspod.DeleteRecordWithContext(ctx, d.client, request)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"tencentcloud: delete record failed: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/tencentcloud/tencentcloud.toml",
    "content": "Name = \"Tencent Cloud DNS\"\nDescription = ''''''\nURL = \"https://cloud.tencent.com/product/dns\"\nCode = \"tencentcloud\"\nSince = \"v4.6.0\"\n\nExample = '''\nTENCENTCLOUD_SECRET_ID=abcdefghijklmnopqrstuvwx \\\nTENCENTCLOUD_SECRET_KEY=your-secret-key \\\nlego --dns tencentcloud -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    TENCENTCLOUD_SECRET_ID = \"Access key ID\"\n    TENCENTCLOUD_SECRET_KEY = \"Access Key secret\"\n  [Configuration.Additional]\n    TENCENTCLOUD_SESSION_TOKEN = \"Access Key token\"\n    TENCENTCLOUD_REGION = \"Region\"\n    TENCENTCLOUD_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    TENCENTCLOUD_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    TENCENTCLOUD_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)\"\n    TENCENTCLOUD_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://cloud.tencent.com/document/product/1427/56153\"\n  GoClient = \"https://github.com/tencentcloud/tencentcloud-sdk-go\"\n"
  },
  {
    "path": "providers/dns/tencentcloud/tencentcloud_test.go",
    "content": "package tencentcloud\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvSecretID, EnvSecretKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretID:  \"123\",\n\t\t\t\tEnvSecretKey: \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretID:  \"\",\n\t\t\t\tEnvSecretKey: \"\",\n\t\t\t},\n\t\t\texpected: \"tencentcloud: some credentials information are missing: TENCENTCLOUD_SECRET_ID,TENCENTCLOUD_SECRET_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretID:  \"\",\n\t\t\t\tEnvSecretKey: \"456\",\n\t\t\t},\n\t\t\texpected: \"tencentcloud: some credentials information are missing: TENCENTCLOUD_SECRET_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretID:  \"123\",\n\t\t\t\tEnvSecretKey: \"\",\n\t\t\t},\n\t\t\texpected: \"tencentcloud: some credentials information are missing: TENCENTCLOUD_SECRET_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tsecretID  string\n\t\tsecretKey string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tsecretID:  \"123\",\n\t\t\tsecretKey: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"tencentcloud: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing secret id\",\n\t\t\tsecretKey: \"456\",\n\t\t\texpected:  \"tencentcloud: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret key\",\n\t\t\tsecretID: \"123\",\n\t\t\texpected: \"tencentcloud: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.SecretID = test.secretID\n\t\t\tconfig.SecretKey = test.secretKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/tencentcloud/wrapper.go",
    "content": "package tencentcloud\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\tdnspod \"github.com/go-acme/tencentclouddnspod/v20210323\"\n\t\"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common\"\n\terrorsdk \"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors\"\n\t\"golang.org/x/net/idna\"\n)\n\nfunc (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (*dnspod.DomainListItem, error) {\n\trequest := dnspod.NewDescribeDomainListRequest()\n\n\tvar domains []*dnspod.DomainListItem\n\n\tfor {\n\t\tresponse, err := dnspod.DescribeDomainListWithContext(ctx, d.client, request)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"API call failed: %w\", err)\n\t\t}\n\n\t\tdomains = append(domains, response.Response.DomainList...)\n\n\t\tif uint64(len(domains)) >= *response.Response.DomainCountInfo.AllTotal {\n\t\t\tbreak\n\t\t}\n\n\t\trequest.Offset = common.Int64Ptr(int64(len(domains)))\n\t}\n\n\tauthZone, err := dns01.FindZoneByFqdn(domain)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tvar hostedZone *dnspod.DomainListItem\n\n\tfor _, zone := range domains {\n\t\tunfqdn := dns01.UnFqdn(authZone)\n\t\tif *zone.Name == unfqdn || *zone.Punycode == unfqdn {\n\t\t\thostedZone = zone\n\t\t}\n\t}\n\n\tif hostedZone == nil {\n\t\treturn nil, fmt.Errorf(\"zone %s not found in dnspod for domain %s\", authZone, domain)\n\t}\n\n\treturn hostedZone, nil\n}\n\nfunc (d *DNSProvider) findTxtRecords(ctx context.Context, zone *dnspod.DomainListItem, fqdn string) ([]*dnspod.RecordListItem, error) {\n\trecordName, err := extractRecordName(fqdn, *zone.Name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trequest := dnspod.NewDescribeRecordListRequest()\n\trequest.Domain = zone.Name\n\trequest.DomainId = zone.DomainId\n\trequest.Subdomain = common.StringPtr(recordName)\n\trequest.RecordType = common.StringPtr(\"TXT\")\n\trequest.RecordLine = common.StringPtr(\"默认\")\n\n\tresponse, err := dnspod.DescribeRecordListWithContext(ctx, d.client, request)\n\tif err != nil {\n\t\tvar sdkError *errorsdk.TencentCloudSDKError\n\t\tif errors.As(err, &sdkError) {\n\t\t\tif sdkError.Code == dnspod.RESOURCENOTFOUND_NODATAOFRECORD {\n\t\t\t\treturn nil, nil\n\t\t\t}\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\treturn response.Response.RecordList, nil\n}\n\nfunc extractRecordName(fqdn, zone string) (string, error) {\n\tasciiDomain, err := idna.ToASCII(zone)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"fail to convert punycode: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, asciiDomain)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn subDomain, nil\n}\n"
  },
  {
    "path": "providers/dns/timewebcloud/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"golang.org/x/oauth2\"\n)\n\nconst defaultBaseURL = \"https://api.timeweb.cloud/api\"\n\n// Client Timeweb Cloud client.\ntype Client struct {\n\tbaseURL    *url.URL\n\thttpClient *http.Client\n}\n\n// NewClient creates a Client.\nfunc NewClient(hc *http.Client) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\tif hc == nil {\n\t\thc = &http.Client{Timeout: 10 * time.Second}\n\t}\n\n\treturn &Client{\n\t\tbaseURL:    baseURL,\n\t\thttpClient: hc,\n\t}\n}\n\n// CreateRecord creates a DNS record.\n// https://timeweb.cloud/api-docs#tag/Domeny/operation/createDomainDNSRecord\nfunc (c *Client) CreateRecord(ctx context.Context, zone string, record DNSRecord) (*DNSRecord, error) {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"domains\", dns01.UnFqdn(zone), \"dns-records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trespData := &CreateRecordResponse{}\n\n\terr = c.do(req, respData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn respData.DNSRecord, nil\n}\n\n// DeleteRecord deletes a DNS record.\n// https://timeweb.cloud/api-docs#tag/Domeny/operation/deleteDomainDNSRecord\nfunc (c *Client) DeleteRecord(ctx context.Context, zone string, recordID int) error {\n\tendpoint := c.baseURL.JoinPath(\"v1\", \"domains\", dns01.UnFqdn(zone), \"dns-records\", strconv.Itoa(recordID))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar response ErrorResponse\n\n\terr := json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn response\n}\n\nfunc OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {\n\tif client == nil {\n\t\tclient = &http.Client{Timeout: 10 * time.Second}\n\t}\n\n\tclient.Transport = &oauth2.Transport{\n\t\tSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),\n\t\tBase:   client.Transport,\n\t}\n\n\treturn client\n}\n"
  },
  {
    "path": "providers/dns/timewebcloud/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(OAuthStaticAccessToken(server.Client(), \"secret\"))\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer secret\"),\n\t)\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /v1/domains/example.com/dns-records\",\n\t\t\tservermock.ResponseFromFixture(\"createDomainDNSRecord.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"type\":\"TXT\",\"value\":\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\",\"subdomain\":\"_acme-challenge\"}`)).\n\t\tBuild(t)\n\n\tpayload := DNSRecord{\n\t\tType:      \"TXT\",\n\t\tValue:     \"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\",\n\t\tSubDomain: \"_acme-challenge\",\n\t}\n\n\tresponse, err := client.CreateRecord(t.Context(), \"example.com.\", payload)\n\trequire.NoError(t, err)\n\n\texpected := &DNSRecord{\n\t\tType: \"TXT\",\n\t\tID:   123,\n\t}\n\n\tassert.Equal(t, expected, response)\n}\n\nfunc TestClient_CreateRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /v1/domains/example.com/dns-records\",\n\t\t\tservermock.ResponseFromFixture(\"error_bad_request.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\t_, err := client.CreateRecord(t.Context(), \"example.com.\", DNSRecord{})\n\trequire.Error(t, err)\n\n\tassert.EqualError(t, err, \"400: Value must be a number conforming to the specified constraints (bad_request) [15095f25-aac3-4d60-a788-96cb5136f186]\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v1/domains/example.com/dns-records/123\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com.\", 123)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v1/domains/example.com/dns-records/123\",\n\t\t\tservermock.ResponseFromFixture(\"error_unauthorized.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com.\", 123)\n\trequire.Error(t, err)\n\n\tassert.EqualError(t, err, \"401: Unauthorized (unauthorized) [15095f25-aac3-4d60-a788-96cb5136f186]\")\n}\n"
  },
  {
    "path": "providers/dns/timewebcloud/internal/fixtures/createDomainDNSRecord.json",
    "content": "{\n  \"dns_record\": {\n    \"type\": \"TXT\",\n    \"id\": 123,\n    \"data\": {\n      \"priority\": 0,\n      \"subdomain\": \"example.com\",\n      \"value\": \"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\"\n    }\n  },\n  \"response_id\": \"15095f25-aac3-4d60-a788-96cb5136f186\"\n}\n"
  },
  {
    "path": "providers/dns/timewebcloud/internal/fixtures/error_bad_request.json",
    "content": "{\n\n  \"status_code\": 400,\n  \"message\": \"Value must be a number conforming to the specified constraints\",\n  \"error_code\": \"bad_request\",\n  \"response_id\": \"15095f25-aac3-4d60-a788-96cb5136f186\"\n\n}\n"
  },
  {
    "path": "providers/dns/timewebcloud/internal/fixtures/error_unauthorized.json",
    "content": "{\n  \"status_code\": 401,\n  \"message\": \"Unauthorized\",\n  \"error_code\": \"unauthorized\",\n  \"response_id\": \"15095f25-aac3-4d60-a788-96cb5136f186\"\n}\n"
  },
  {
    "path": "providers/dns/timewebcloud/internal/readme.md",
    "content": "There is an [official API client](https://github.com/timeweb-cloud/sdk-go) but this client is completely broken:\n- the code is generated and the module name is `github.com/GIT_USER_ID/GIT_REPO_ID`\n- the code contains redeclared constants\n- Even with fixes to the module name and the redeclared constants, the module doesn't compile.\n\nhttps://github.com/timeweb-cloud/sdk-go/pull/1\n\nSo, for now, this API client is unusable.\n"
  },
  {
    "path": "providers/dns/timewebcloud/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype DNSRecord struct {\n\tID    int    `json:\"id,omitempty\"`\n\tType  string `json:\"type,omitempty\"`\n\tValue string `json:\"value,omitempty\"`\n\n\t// SubDomain is the full name of a subdomain (not only the subdomain label).\n\tSubDomain string `json:\"subdomain,omitempty\"`\n}\n\ntype CreateRecordResponse struct {\n\tDNSRecord *DNSRecord `json:\"dns_record,omitempty\"`\n}\n\ntype ErrorResponse struct {\n\tStatusCode int    `json:\"status_code,omitempty\"`\n\tErrorCode  string `json:\"error_code,omitempty\"`\n\tMessage    string `json:\"message,omitempty\"`\n\tResponseID string `json:\"response_id,omitempty\"`\n}\n\nfunc (a ErrorResponse) Error() string {\n\treturn fmt.Sprintf(\"%d: %s (%s) [%s]\", a.StatusCode, a.Message, a.ErrorCode, a.ResponseID)\n}\n"
  },
  {
    "path": "providers/dns/timewebcloud/timewebcloud.go",
    "content": "// Package timewebcloud implements a DNS provider for solving the DNS-01 challenge using Timeweb Cloud.\npackage timewebcloud\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/timewebcloud/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"TIMEWEBCLOUD_\"\n\n\tEnvAuthToken = envNamespace + \"AUTH_TOKEN\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAuthToken string\n\n\tHTTPClient         *http.Client\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]int\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Timeweb Cloud.\n// API token must be passed in the environment variable TIMEWEBCLOUD_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAuthToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"timewebcloud: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AuthToken = values[EnvAuthToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig returns a DNSProvider instance configured for Timeweb Cloud.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"timewebcloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.AuthToken == \"\" {\n\t\treturn nil, errors.New(\"timewebcloud: authentication token is missing\")\n\t}\n\n\tclient := internal.NewClient(\n\t\tclientdebug.Wrap(\n\t\t\tinternal.OAuthStaticAccessToken(config.HTTPClient, config.AuthToken),\n\t\t),\n\t)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]int),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"timewebcloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecord := internal.DNSRecord{\n\t\tType:      \"TXT\",\n\t\tValue:     info.Value,\n\t\tSubDomain: dns01.UnFqdn(info.EffectiveFQDN),\n\t}\n\n\tresponse, err := d.client.CreateRecord(context.Background(), authZone, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"timewebcloud: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = response.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"timewebcloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"timewebcloud: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\terr = d.client.DeleteRecord(context.Background(), authZone, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"timewebcloud: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/timewebcloud/timewebcloud.toml",
    "content": "Name = \"Timeweb Cloud\"\nDescription = ''''''\nURL = \"https://timeweb.cloud/\"\nCode = \"timewebcloud\"\nSince = \"v4.20.0\"\n\nExample = '''\nTIMEWEBCLOUD_AUTH_TOKEN=xxxxxx \\\nlego --dns timewebcloud -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    TIMEWEBCLOUD_AUTH_TOKEN = \"Authentication token\"\n  [Configuration.Additional]\n    TIMEWEBCLOUD_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    TIMEWEBCLOUD_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    TIMEWEBCLOUD_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://timeweb.cloud/api-docs\"\n"
  },
  {
    "path": "providers/dns/timewebcloud/timewebcloud_test.go",
    "content": "package timewebcloud\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAuthToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthToken: \"johndoe\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthToken: \"\",\n\t\t\t},\n\t\t\texpected: \"timewebcloud: some credentials information are missing: TIMEWEBCLOUD_AUTH_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tauthToken string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tauthToken: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"timewebcloud: authentication token is missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AuthToken = test.authToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/todaynic/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\nconst defaultBaseURL = \"https://todapi.now.cn:2443\"\n\n// Client the TodayNIC API client.\ntype Client struct {\n\tauthUserID string\n\tapiKey     string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(authUserID, apiKey string) (*Client, error) {\n\tif authUserID == \"\" || apiKey == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tauthUserID: authUserID,\n\t\tapiKey:     apiKey,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) AddRecord(ctx context.Context, record Record) (int, error) {\n\tendpoint := c.BaseURL.JoinPath(\"api\", \"dns\", \"add-domain-record.json\")\n\n\tquery, err := querystring.Values(record)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treq, err := c.newRequest(ctx, endpoint, query)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvar result APIResponse\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn result.ID, nil\n}\n\nfunc (c *Client) DeleteRecord(ctx context.Context, recordID int) error {\n\tendpoint := c.BaseURL.JoinPath(\"api\", \"dns\", \"delete-domain-record.json\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"Id\", strconv.Itoa(recordID))\n\n\treq, err := c.newRequest(ctx, endpoint, query)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) newRequest(ctx context.Context, endpoint *url.URL, query url.Values) (*http.Request, error) {\n\tquery.Set(\"auth-userid\", c.authUserID)\n\tquery.Set(\"api-key\", c.apiKey)\n\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/todaynic/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"user123\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders(),\n\t)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/dns/add-domain-record.json\",\n\t\t\tservermock.ResponseFromFixture(\"add_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"Domain\", \"example.com\").\n\t\t\t\tWith(\"Host\", \"_acme-challenge\").\n\t\t\t\tWith(\"Type\", \"TXT\").\n\t\t\t\tWith(\"Value\", \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\").\n\t\t\t\tWith(\"Ttl\", \"600\").\n\t\t\t\tWith(\"auth-userid\", \"user123\").\n\t\t\t\tWith(\"api-key\", \"secret\"),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tDomain: \"example.com\",\n\t\tHost:   \"_acme-challenge\",\n\t\tType:   \"TXT\",\n\t\tValue:  \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:    \"600\",\n\t}\n\n\trecordID, err := client.AddRecord(t.Context(), record)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 11554102, recordID)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/dns/add-domain-record.json\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusNotFound),\n\t\t).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tDomain: \"example.com\",\n\t\tHost:   \"_acme-challenge\",\n\t\tType:   \"TXT\",\n\t\tValue:  \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\",\n\t\tTTL:    \"600\",\n\t}\n\n\t_, err := client.AddRecord(t.Context(), record)\n\trequire.EqualError(t, err, \"host.repeat (2d5876b2-f272-43e9-acc1-4c6a3d3683b1)\")\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /api/dns/delete-domain-record.json\",\n\t\t\tservermock.ResponseFromFixture(\"add_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"Id\", \"123\").\n\t\t\t\tWith(\"auth-userid\", \"user123\").\n\t\t\t\tWith(\"api-key\", \"secret\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), 123)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/todaynic/internal/fixtures/add_record.json",
    "content": "{\n  \"RequestId\": \"f60ea4d9-67ef-49fa-bbae-06178a6e7293\",\n  \"Id\": 11554102\n}\n"
  },
  {
    "path": "providers/dns/todaynic/internal/fixtures/error.json",
    "content": "{\n  \"RequestId\": \"2d5876b2-f272-43e9-acc1-4c6a3d3683b1\",\n  \"error\": \"host.repeat\"\n}\n"
  },
  {
    "path": "providers/dns/todaynic/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype APIError struct {\n\tRequestID string `json:\"RequestId\"`\n\tMessage   string `json:\"error\"`\n}\n\nfunc (a *APIError) Error() string {\n\treturn fmt.Sprintf(\"%s (%s)\", a.Message, a.RequestID)\n}\n\ntype Record struct {\n\tDomain string `url:\"Domain,omitempty\"`\n\tHost   string `url:\"Host,omitempty\"`\n\tType   string `url:\"Type,omitempty\"`\n\tValue  string `url:\"Value,omitempty\"`\n\tMx     string `url:\"Mx,omitempty\"`\n\tTTL    string `url:\"Ttl,omitempty\"`\n}\n\ntype APIResponse struct {\n\tRequestID string `json:\"RequestId\"`\n\tID        int    `json:\"Id\"`\n}\n"
  },
  {
    "path": "providers/dns/todaynic/todaynic.go",
    "content": "// Package todaynic implements a DNS provider for solving the DNS-01 challenge using TodayNIC.\npackage todaynic\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/todaynic/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"TODAYNIC_\"\n\n\tEnvAuthUserID = envNamespace + \"AUTH_USER_ID\"\n\tEnvAPIKey     = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAuthUserID string\n\tAPIKey     string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]int\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for TodayNIC.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAuthUserID, EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"todaynic: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AuthUserID = values[EnvAuthUserID]\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for TodayNIC.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"todaynic: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.AuthUserID, config.APIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"todaynic: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]int),\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"todaynic: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"todaynic: %w\", err)\n\t}\n\n\trecord := internal.Record{\n\t\tDomain: dns01.UnFqdn(authZone),\n\t\tHost:   subDomain,\n\t\tType:   \"TXT\",\n\t\tValue:  info.Value,\n\t\tTTL:    strconv.Itoa(d.config.TTL),\n\t}\n\n\trecordID, err := d.client.AddRecord(context.Background(), record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"todaynic: add record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = recordID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"todaynic: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\terr := d.client.DeleteRecord(context.Background(), recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"todaynic: delete record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/todaynic/todaynic.toml",
    "content": "Name = \"TodayNIC/时代互联\"\nDescription = ''''''\nURL = \"https://www.todaynic.com/\"\nCode = \"todaynic\"\nSince = \"v4.32.0\"\n\nExample = '''\nTODAYNIC_AUTH_USER_ID=\"xxx\" \\\nTODAYNIC_API_KEY=\"yyy\" \\\nlego --dns todaynic -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    TODAYNIC_AUTH_USER_ID = \"account ID\"\n    TODAYNIC_API_KEY = \"API key\"\n  [Configuration.Additional]\n    TODAYNIC_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    TODAYNIC_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    TODAYNIC_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)\"\n    TODAYNIC_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.todaynic.com/partner/mode_Http_Api_detail.php\"\n  apipost =  \"https://docs.apipost.net/docs/detail/49dcef10a876000?target_id=0\"\n"
  },
  {
    "path": "providers/dns/todaynic/todaynic_test.go",
    "content": "package todaynic\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAuthUserID, EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthUserID: \"user123\",\n\t\t\t\tEnvAPIKey:     \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing user ID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthUserID: \"\",\n\t\t\t\tEnvAPIKey:     \"secret\",\n\t\t\t},\n\t\t\texpected: \"todaynic: some credentials information are missing: TODAYNIC_AUTH_USER_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthUserID: \"user123\",\n\t\t\t\tEnvAPIKey:     \"\",\n\t\t\t},\n\t\t\texpected: \"todaynic: some credentials information are missing: TODAYNIC_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"todaynic: some credentials information are missing: TODAYNIC_AUTH_USER_ID,TODAYNIC_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc       string\n\t\tauthUserID string\n\t\tapiKey     string\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tdesc:       \"success\",\n\t\t\tauthUserID: \"user123\",\n\t\t\tapiKey:     \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing user ID\",\n\t\t\tapiKey:   \"secret\",\n\t\t\texpected: \"todaynic: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:       \"missing API key\",\n\t\t\tauthUserID: \"user123\",\n\t\t\texpected:   \"todaynic: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"todaynic: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AuthUserID = test.authUserID\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AuthUserID = \"user123\"\n\t\t\tconfig.APIKey = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders(),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /api/dns/add-domain-record.json\",\n\t\t\tservermock.ResponseFromInternal(\"add_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"Domain\", \"example.com\").\n\t\t\t\tWith(\"Host\", \"_acme-challenge\").\n\t\t\t\tWith(\"Type\", \"TXT\").\n\t\t\t\tWith(\"Value\", \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\").\n\t\t\t\tWith(\"Ttl\", \"600\").\n\t\t\t\tWith(\"auth-userid\", \"user123\").\n\t\t\t\tWith(\"api-key\", \"secret\"),\n\t\t).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /api/dns/delete-domain-record.json\",\n\t\t\tservermock.ResponseFromInternal(\"add_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"Id\", \"123\").\n\t\t\t\tWith(\"auth-userid\", \"user123\").\n\t\t\t\tWith(\"api-key\", \"secret\"),\n\t\t).\n\t\tBuild(t)\n\n\tprovider.recordIDs[\"abc\"] = 123\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/transip/fixtures/private.key",
    "content": ""
  },
  {
    "path": "providers/dns/transip/transip.go",
    "content": "// Package transip implements a DNS provider for solving the DNS-01 challenge using TransIP.\npackage transip\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/transip/gotransip/v6\"\n\ttransipdomain \"github.com/transip/gotransip/v6/domain\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"TRANSIP_\"\n\n\tEnvAccountName    = envNamespace + \"ACCOUNT_NAME\"\n\tEnvPrivateKeyPath = envNamespace + \"PRIVATE_KEY_PATH\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAccountName        string\n\tPrivateKeyPath     string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int64\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                int64(env.GetOrDefaultInt(EnvTTL, 10)),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig     *Config\n\trepository transipdomain.Repository\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for TransIP.\n// Credentials must be passed in the environment variables:\n// TRANSIP_ACCOUNTNAME, TRANSIP_PRIVATEKEYPATH.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAccountName, EnvPrivateKeyPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"transip: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AccountName = values[EnvAccountName]\n\tconfig.PrivateKeyPath = values[EnvPrivateKeyPath]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for TransIP.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"transip: the configuration of the DNS provider is nil\")\n\t}\n\n\tcfg := gotransip.ClientConfiguration{\n\t\tAccountName:    config.AccountName,\n\t\tPrivateKeyPath: config.PrivateKeyPath,\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tcfg.HTTPClient = config.HTTPClient\n\t} else {\n\t\t// Uses an explicit default HTTP client because the desec.NewDefaultClientOptions uses the http.DefaultClient.\n\t\tcfg.HTTPClient = &http.Client{Timeout: 30 * time.Second}\n\t}\n\n\tclient, err := gotransip.NewClient(cfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"transip: %w\", err)\n\t}\n\n\trepo := transipdomain.Repository{Client: client}\n\n\treturn &DNSProvider{repository: repo, config: config}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"transip: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// get the subDomain\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"transip: %w\", err)\n\t}\n\n\tdomainName := dns01.UnFqdn(authZone)\n\n\tentry := transipdomain.DNSEntry{\n\t\tName:    subDomain,\n\t\tExpire:  int(d.config.TTL),\n\t\tType:    \"TXT\",\n\t\tContent: info.Value,\n\t}\n\n\terr = d.repository.AddDNSEntry(domainName, entry)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"transip: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"transip: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// get the subDomain\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"transip: %w\", err)\n\t}\n\n\tdomainName := dns01.UnFqdn(authZone)\n\n\t// get all DNS entries\n\tdnsEntries, err := d.repository.GetDNSEntries(domainName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"transip: error for %s in CleanUp: %w\", info.EffectiveFQDN, err)\n\t}\n\n\t// loop through the existing entries and remove the specific record\n\tfor _, entry := range dnsEntries {\n\t\tif entry.Name == subDomain && entry.Content == info.Value {\n\t\t\tif err = d.repository.RemoveDNSEntry(domainName, entry); err != nil {\n\t\t\t\treturn fmt.Errorf(\"transip: couldn't get Record ID in CleanUp: %w\", err)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/transip/transip.toml",
    "content": "Name = \"TransIP\"\nDescription = ''''''\nURL = \"https://www.transip.nl/\"\nCode = \"transip\"\nSince = \"v2.0.0\"\n\nExample = '''\nTRANSIP_ACCOUNT_NAME = \"Account name\" \\\nTRANSIP_PRIVATE_KEY_PATH = \"transip.key\" \\\nlego --dns transip -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    TRANSIP_ACCOUNT_NAME = \"Account name\"\n    TRANSIP_PRIVATE_KEY_PATH = \"Private key path\"\n  [Configuration.Additional]\n    TRANSIP_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    TRANSIP_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 600)\"\n    TRANSIP_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 10)\"\n    TRANSIP_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api.transip.eu/rest/docs.html\"\n  GoClient = \"https://github.com/transip/gotransip\"\n\n"
  },
  {
    "path": "providers/dns/transip/transip_test.go",
    "content": "package transip\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAccountName,\n\tEnvPrivateKeyPath).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccountName:    \"johndoe\",\n\t\t\t\tEnvPrivateKeyPath: \"./fixtures/private.key\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing all credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccountName:    \"\",\n\t\t\t\tEnvPrivateKeyPath: \"\",\n\t\t\t},\n\t\t\texpected: \"transip: some credentials information are missing: TRANSIP_ACCOUNT_NAME,TRANSIP_PRIVATE_KEY_PATH\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing account name\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccountName:    \"\",\n\t\t\t\tEnvPrivateKeyPath: \"./fixtures/private.key\",\n\t\t\t},\n\t\t\texpected: \"transip: some credentials information are missing: TRANSIP_ACCOUNT_NAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing private key path\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccountName:    \"johndoe\",\n\t\t\t\tEnvPrivateKeyPath: \"\",\n\t\t\t},\n\t\t\texpected: \"transip: some credentials information are missing: TRANSIP_PRIVATE_KEY_PATH\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.repository)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n\n\t// The error message for a file not existing is different on Windows and Linux.\n\t// Therefore, we test if the error type is the same.\n\tt.Run(\"could not open private key path\", func(t *testing.T) {\n\t\tdefer envTest.RestoreEnv()\n\n\t\tenvTest.ClearEnv()\n\n\t\tenvTest.Apply(map[string]string{\n\t\t\tEnvAccountName:    \"johndoe\",\n\t\t\tEnvPrivateKeyPath: \"./fixtures/non/existent/private.key\",\n\t\t})\n\n\t\t_, err := NewDNSProvider()\n\t\trequire.ErrorIs(t, err, os.ErrNotExist)\n\t})\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc           string\n\t\taccountName    string\n\t\tprivateKeyPath string\n\t\texpected       string\n\t}{\n\t\t{\n\t\t\tdesc:           \"success\",\n\t\t\taccountName:    \"johndoe\",\n\t\t\tprivateKeyPath: \"./fixtures/private.key\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing all credentials\",\n\t\t\texpected: \"transip: AccountName is required\",\n\t\t},\n\t\t{\n\t\t\tdesc:           \"missing account name\",\n\t\t\tprivateKeyPath: \"./fixtures/private.key\",\n\t\t\texpected:       \"transip: AccountName is required\",\n\t\t},\n\t\t{\n\t\t\tdesc:        \"missing private key path\",\n\t\t\taccountName: \"johndoe\",\n\t\t\texpected:    \"transip: PrivateKeyReader, token or PrivateKeyReader is required\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AccountName = test.accountName\n\t\t\tconfig.PrivateKeyPath = test.privateKeyPath\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.repository)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n\n\t// The error message for a file not existing is different on Windows and Linux.\n\t// Therefore, we test if the error type is the same.\n\tt.Run(\"could not open private key path\", func(t *testing.T) {\n\t\tconfig := NewDefaultConfig()\n\t\tconfig.AccountName = \"johndoe\"\n\t\tconfig.PrivateKeyPath = \"./fixtures/non/existent/private.key\"\n\n\t\t_, err := NewDNSProviderConfig(config)\n\t\trequire.ErrorIs(t, err, os.ErrNotExist)\n\t})\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/ultradns/ultradns.go",
    "content": "// Package ultradns implements a DNS provider for solving the DNS-01 challenge using ultradns.\npackage ultradns\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\t\"github.com/ultradns/ultradns-go-sdk/pkg/client\"\n\t\"github.com/ultradns/ultradns-go-sdk/pkg/record\"\n\t\"github.com/ultradns/ultradns-go-sdk/pkg/rrset\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"ULTRADNS_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\tEnvEndpoint = envNamespace + \"ENDPOINT\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nconst defaultEndpoint = \"https://api.ultradns.com/\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *client.Client\n}\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername string\n\tPassword string\n\tEndpoint string\n\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tEndpoint:           env.GetOrDefaultString(EnvEndpoint, defaultEndpoint),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second),\n\t}\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for ultradns.\n// Credentials must be passed in the environment variables:\n// ULTRADNS_USERNAME and ULTRADNS_PASSWORD.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ultradns: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for ultradns.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"ultradns: the configuration of the DNS provider is nil\")\n\t}\n\n\tultraConfig := client.Config{\n\t\tUsername:  config.Username,\n\t\tPassword:  config.Password,\n\t\tHostURL:   config.Endpoint,\n\t\tUserAgent: useragent.Get(),\n\t}\n\n\tuClient, err := client.NewClient(ultraConfig)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ultradns: %w\", err)\n\t}\n\n\treturn &DNSProvider{config: config, client: uClient}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ultradns: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecordService, err := record.Get(d.client)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ultradns: %w\", err)\n\t}\n\n\trrSetKeyData := &rrset.RRSetKey{\n\t\tOwner:      info.EffectiveFQDN,\n\t\tZone:       authZone,\n\t\tRecordType: \"TXT\",\n\t}\n\n\tresp, _, _ := recordService.Read(rrSetKeyData)\n\n\trrSetData := &rrset.RRSet{\n\t\tOwnerName: info.EffectiveFQDN,\n\t\tTTL:       d.config.TTL,\n\t\tRRType:    \"TXT\",\n\t\tRData:     []string{info.Value},\n\t}\n\n\tif resp != nil && resp.StatusCode == http.StatusOK {\n\t\t_, err = recordService.Update(rrSetKeyData, rrSetData)\n\t} else {\n\t\t_, err = recordService.Create(rrSetKeyData, rrSetData)\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ultradns: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ultradns: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecordService, err := record.Get(d.client)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ultradns: %w\", err)\n\t}\n\n\trrSetKeyData := &rrset.RRSetKey{\n\t\tOwner:      info.EffectiveFQDN,\n\t\tZone:       authZone,\n\t\tRecordType: \"TXT\",\n\t}\n\n\t_, err = recordService.Delete(rrSetKeyData)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ultradns: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/ultradns/ultradns.toml",
    "content": "Name = \"Ultradns\"\nDescription = ''''''\nURL = \"https://vercara.com/authoritative-dns\"\nCode = \"ultradns\"\nSince = \"v4.10.0\"\n\nExample = '''\nULTRADNS_USERNAME=username \\\nULTRADNS_PASSWORD=password \\\nlego --dns ultradns -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    ULTRADNS_USERNAME = \"API Username\"\n    ULTRADNS_PASSWORD = \"API Password\"\n  [Configuration.Additional]\n    ULTRADNS_ENDPOINT = \"API endpoint URL, defaults to https://api.ultradns.com/\"\n    ULTRADNS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    ULTRADNS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 4)\"\n    ULTRADNS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n\n[Links]\n  API = \"https://ultra-portalstatic.ultradns.com/static/docs/REST-API_User_Guide.pdf\"\n  GoClient = \"https://github.com/ultradns/ultradns-go-sdk\"\n"
  },
  {
    "path": "providers/dns/ultradns/ultradns_test.go",
    "content": "package ultradns\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvUsername,\n\tEnvPassword,\n\tEnvEndpoint,\n\tEnvTTL,\n\tEnvPropagationTimeout,\n\tEnvPollingInterval).\n\tWithDomain(envDomain)\n\nfunc TestNewDefaultConfig(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected *Config\n\t}{\n\t\t{\n\t\t\tdesc: \"default configuration\",\n\t\t\texpected: &Config{\n\t\t\t\tEndpoint:           \"https://api.ultradns.com/\",\n\t\t\t\tTTL:                120,\n\t\t\t\tPropagationTimeout: 2 * time.Minute,\n\t\t\t\tPollingInterval:    4 * time.Second,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"input configuration\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvEndpoint:           \"https://example.com/\",\n\t\t\t\tEnvTTL:                \"99\",\n\t\t\t\tEnvPropagationTimeout: \"60\",\n\t\t\t\tEnvPollingInterval:    \"60\",\n\t\t\t},\n\t\t\texpected: &Config{\n\t\t\t\tEndpoint:           \"https://example.com/\",\n\t\t\t\tTTL:                99,\n\t\t\t\tPropagationTimeout: 60 * time.Second,\n\t\t\t\tPollingInterval:    60 * time.Second,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tenvTest.ClearEnv()\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tconfig := NewDefaultConfig()\n\n\t\t\tassert.Equal(t, test.expected, config)\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProvider(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"missing username and password\",\n\t\t\texpected: \"ultradns: some credentials information are missing: ULTRADNS_USERNAME,ULTRADNS_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPassword: \"password\",\n\t\t\t},\n\t\t\texpected: \"ultradns: some credentials information are missing: ULTRADNS_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"username\",\n\t\t\t},\n\t\t\texpected: \"ultradns: some credentials information are missing: ULTRADNS_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"username\",\n\t\t\t\tEnvPassword: \"password\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tenvTest.ClearEnv()\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.config)\n\t\t\t\tassert.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"api_username\",\n\t\t\tpassword: \"api_password\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"ultradns: Missing required parameters: [ username, password ]\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tusername: \"\",\n\t\t\tpassword: \"api_password\",\n\t\t\texpected: \"ultradns: Missing required parameters: [ username ]\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tusername: \"api_username\",\n\t\t\tpassword: \"\",\n\t\t\texpected: \"ultradns: Missing required parameters: [ password ]\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/uniteddomains/uniteddomains.go",
    "content": "// Package uniteddomains implements a DNS provider for solving the DNS-01 challenge using United-Domains.\npackage uniteddomains\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ionos\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"UNITEDDOMAINS_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultBaseURL = \"https://dnsapi.united-domains.de/dns\"\n\nconst minTTL = 300\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config = ionos.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 15*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for United-Domains.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"uniteddomains: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for United-Domains.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"uniteddomains: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := ionos.NewDNSProviderConfig(config, defaultBaseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"uniteddomains: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"uniteddomains: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"uniteddomains: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/uniteddomains/uniteddomains.toml",
    "content": "Name = \"United-Domains\"\nDescription = ''''''\nURL = \"https://www.united-domains.de/\"\nCode = \"uniteddomains\"\nSince = \"v4.29.0\"\n\nExample = '''\nUNITEDDOMAINS_API_KEY=xxxxxxxx \\\nlego --dns uniteddomains -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    UNITEDDOMAINS_API_KEY = \"API key `<prefix>.<secret>` https://www.united-domains.de/help/faq-article/getting-started-with-the-united-domains-dns-api/\"\n  [Configuration.Additional]\n    UNITEDDOMAINS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    UNITEDDOMAINS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 900)\"\n    UNITEDDOMAINS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    UNITEDDOMAINS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.united-domains.de/dns-apidoc/\"\n"
  },
  {
    "path": "providers/dns/uniteddomains/uniteddomains_test.go",
    "content": "package uniteddomains\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"uniteddomains: some credentials information are missing: UNITEDDOMAINS_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\ttll      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"123\",\n\t\t\ttll:    minTTL,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\ttll:      minTTL,\n\t\t\texpected: \"uniteddomains: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"invalid TTL\",\n\t\t\tapiKey:   \"123\",\n\t\t\ttll:      30,\n\t\t\texpected: \"uniteddomains: invalid TTL, TTL (30) must be greater than 300\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.TTL = test.tll\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/variomedia/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api.variomedia.de\"\n\nconst authorizationHeader = \"Authorization\"\n\n// Client the API client for Variomedia.\ntype Client struct {\n\tapiToken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(apiToken string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tapiToken:   apiToken,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}\n}\n\n// CreateDNSRecord creates a new DNS entry.\n// https://api.variomedia.de/docs/dns-records.html#erstellen\nfunc (c *Client) CreateDNSRecord(ctx context.Context, record DNSRecord) (*CreateDNSRecordResponse, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns-records\")\n\n\tdata := CreateDNSRecordRequest{Data: Data{\n\t\tType:       \"dns-record\",\n\t\tAttributes: record,\n\t}}\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result CreateDNSRecordResponse\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &result, nil\n}\n\n// DeleteDNSRecord deletes a DNS record.\n// https://api.variomedia.de/docs/dns-records.html#l%C3%B6schen\nfunc (c *Client) DeleteDNSRecord(ctx context.Context, id string) (*DeleteRecordResponse, error) {\n\tendpoint := c.baseURL.JoinPath(\"dns-records\", id)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result DeleteRecordResponse\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &result, nil\n}\n\n// GetJob returns a single job based on its ID.\n// https://api.variomedia.de/docs/job-queue.html\nfunc (c *Client) GetJob(ctx context.Context, id string) (*GetJobResponse, error) {\n\tendpoint := c.baseURL.JoinPath(\"queue-jobs\", id)\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result GetJobResponse\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &result, nil\n}\n\nfunc (c *Client) do(req *http.Request, data any) error {\n\treq.Header.Set(authorizationHeader, \"token \"+c.apiToken)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, data)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/vnd.variomedia.v1+json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/vnd.api+json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn errAPI\n}\n"
  },
  {
    "path": "providers/dns/variomedia/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"secret\")\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithAccept(\"application/vnd.variomedia.v1+json\").\n\t\t\tWithAuthorization(\"token secret\"))\n}\n\nfunc TestClient_CreateDNSRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns-records\",\n\t\t\tservermock.ResponseFromFixture(\"POST_dns-records.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentType(\"application/vnd.api+json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"data\":{\"type\":\"dns-record\",\"attributes\":{\"record_type\":\"TXT\",\"name\":\"_acme-challenge\",\"domain\":\"example.com\",\"data\":\"test\",\"ttl\":300}}}`)).\n\t\tBuild(t)\n\n\trecord := DNSRecord{\n\t\tRecordType: \"TXT\",\n\t\tName:       \"_acme-challenge\",\n\t\tDomain:     \"example.com\",\n\t\tData:       \"test\",\n\t\tTTL:        300,\n\t}\n\n\tresp, err := client.CreateDNSRecord(t.Context(), record)\n\trequire.NoError(t, err)\n\n\texpected := &CreateDNSRecordResponse{\n\t\tData: struct {\n\t\t\tType       string `json:\"type\"`\n\t\t\tID         string `json:\"id\"`\n\t\t\tAttributes struct {\n\t\t\t\tStatus string `json:\"status\"`\n\t\t\t} `json:\"attributes\"`\n\t\t\tLinks struct {\n\t\t\t\tQueueJob  string `json:\"queue-job\"`\n\t\t\t\tDNSRecord string `json:\"dns-record\"`\n\t\t\t} `json:\"links\"`\n\t\t}{\n\t\t\tType: \"queue-job\",\n\t\t\tID:   \"18181818\",\n\t\t\tAttributes: struct {\n\t\t\t\tStatus string `json:\"status\"`\n\t\t\t}{\n\t\t\t\tStatus: \"pending\",\n\t\t\t},\n\t\t\tLinks: struct {\n\t\t\t\tQueueJob  string `json:\"queue-job\"`\n\t\t\t\tDNSRecord string `json:\"dns-record\"`\n\t\t\t}{\n\t\t\t\tQueueJob:  \"https://api.variomedia.de/queue-jobs/18181818\",\n\t\t\t\tDNSRecord: \"https://api.variomedia.de/dns-records/19191919\",\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, resp)\n}\n\nfunc TestClient_DeleteDNSRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns-records/test\",\n\t\t\tservermock.ResponseFromFixture(\"DELETE_dns-records_pending.json\")).\n\t\tBuild(t)\n\n\tresp, err := client.DeleteDNSRecord(t.Context(), \"test\")\n\trequire.NoError(t, err)\n\n\texpected := &DeleteRecordResponse{\n\t\tData: struct {\n\t\t\tID         string `json:\"id\"`\n\t\t\tType       string `json:\"type\"`\n\t\t\tAttributes struct {\n\t\t\t\tJobType string `json:\"job_type\"`\n\t\t\t\tStatus  string `json:\"status\"`\n\t\t\t} `json:\"attributes\"`\n\t\t\tLinks struct {\n\t\t\t\tSelf   string `json:\"self\"`\n\t\t\t\tObject string `json:\"object\"`\n\t\t\t} `json:\"links\"`\n\t\t}{\n\t\t\tID:   \"303030\",\n\t\t\tType: \"queue-job\",\n\t\t\tAttributes: struct {\n\t\t\t\tJobType string `json:\"job_type\"`\n\t\t\t\tStatus  string `json:\"status\"`\n\t\t\t}{\n\t\t\t\tStatus: \"pending\",\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, resp)\n}\n\nfunc TestClient_GetJob(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /queue-jobs/test\",\n\t\t\tservermock.ResponseFromFixture(\"GET_queue-jobs.json\")).\n\t\tBuild(t)\n\n\tresp, err := client.GetJob(t.Context(), \"test\")\n\trequire.NoError(t, err)\n\n\texpected := &GetJobResponse{\n\t\tData: struct {\n\t\t\tID         string `json:\"id\"`\n\t\t\tType       string `json:\"type\"`\n\t\t\tAttributes struct {\n\t\t\t\tJobType string `json:\"job_type\"`\n\t\t\t\tStatus  string `json:\"status\"`\n\t\t\t} `json:\"attributes\"`\n\t\t\tLinks struct {\n\t\t\t\tSelf   string `json:\"self\"`\n\t\t\t\tObject string `json:\"object\"`\n\t\t\t} `json:\"links\"`\n\t\t}{\n\t\t\tID:   \"171717\",\n\t\t\tType: \"queue-job\",\n\t\t\tAttributes: struct {\n\t\t\t\tJobType string `json:\"job_type\"`\n\t\t\t\tStatus  string `json:\"status\"`\n\t\t\t}{\n\t\t\t\tJobType: \"dns-record\",\n\t\t\t\tStatus:  \"done\",\n\t\t\t},\n\t\t\tLinks: struct {\n\t\t\t\tSelf   string `json:\"self\"`\n\t\t\t\tObject string `json:\"object\"`\n\t\t\t}{\n\t\t\t\tSelf:   \"https://api.variomedia.de/queue-jobs/171717\",\n\t\t\t\tObject: \"https://api.variomedia.de/dns-records/212121\",\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, resp)\n}\n"
  },
  {
    "path": "providers/dns/variomedia/internal/fixtures/DELETE_dns-records_done.json",
    "content": "{\n  \"data\": {\n    \"id\": \"303030\",\n    \"type\": \"queue-job\",\n    \"attributes\": {\n      \"job_type\": \"dns-record\",\n      \"status\": \"done\"\n    },\n    \"relationships\": {\n      \"owner\": {\n        \"data\": {\n          \"id\": \"505050\",\n          \"type\": \"customer\"\n        }\n      }\n    },\n    \"links\": {\n      \"self\": \"https://api.variomedia.de/queue-jobs/303030\",\n      \"object\": \"https://api.variomedia.de/dns-records/212121\"\n    }\n  },\n  \"links\": {\n    \"self\": \"https://api.variomedia.de/queue-jobs/303030\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/variomedia/internal/fixtures/DELETE_dns-records_pending.json",
    "content": "{\n  \"data\": {\n    \"id\": \"303030\",\n    \"type\": \"queue-job\",\n    \"attributes\": {\n      \"status\": \"pending\"\n    },\n    \"links\": {\n      \"queue-job\": \"https://api.variomedia.de/queue-jobs/303030\"\n    }\n  },\n  \"links\": {\n    \"self\": \"https://api.variomedia.de/dns-records/212121\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/variomedia/internal/fixtures/GET_dns-records.json",
    "content": "{\n  \"data\": {\n    \"id\": \"20202020\",\n    \"type\": \"dns-record\",\n    \"links\": {\n      \"self\": \"https://api.variomedia.de/dns-records/20202020\"\n    },\n    \"attributes\": {\n      \"record_type\": \"TXT\",\n      \"fqdn\": \"my-test-record.example.com\",\n      \"fqdn_ace\": \"my-test-record.example.com\",\n      \"name\": \"my-test-record\",\n      \"name_ace\": \"my-test-record\",\n      \"domain\": \"example.com\",\n      \"data\": \"test\",\n      \"ttl\": 300\n    }\n  },\n  \"links\": {\n    \"self\": \"https://api.variomedia.de/dns-records\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/variomedia/internal/fixtures/GET_queue-jobs.json",
    "content": "{\n  \"data\": {\n    \"id\": \"171717\",\n    \"type\": \"queue-job\",\n    \"links\": {\n      \"self\": \"https://api.variomedia.de/queue-jobs/171717\",\n      \"object\": \"https://api.variomedia.de/dns-records/212121\"\n    },\n    \"attributes\": {\n      \"job_type\": \"dns-record\",\n      \"status\": \"done\"\n    },\n    \"relationships\": {\n      \"owner\": {\n        \"data\": {\n          \"id\": \"505050\",\n          \"type\": \"customer\"\n        }\n      }\n    }\n  },\n  \"links\": {\n    \"self\": \"https://api.variomedia.de/queue-jobs/171717\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/variomedia/internal/fixtures/POST_dns-records.json",
    "content": "{\n  \"data\": {\n    \"type\": \"queue-job\",\n    \"id\": \"18181818\",\n    \"attributes\": {\n      \"status\": \"pending\"\n    },\n    \"links\": {\n      \"queue-job\": \"https://api.variomedia.de/queue-jobs/18181818\",\n      \"dns-record\": \"https://api.variomedia.de/dns-records/19191919\"\n    }\n  },\n  \"links\": {\n    \"self\": \"https://api.variomedia.de/dns-records\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/variomedia/internal/fixtures/error.json",
    "content": "{\n  \"errors\": [\n    {\n      \"status\": \"401\",\n      \"title\": \"The server could not verify that you are authorized to access the URL requested. You either supplied the wrong credentials (e.g. a bad password), or your browser doesn't understand how to supply the credentials required.\",\n      \"id\": \"unauthorized\"\n    }\n  ],\n  \"links\": {\n    \"self\": \"https://api.variomedia.de/dns-records\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/variomedia/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype CreateDNSRecordRequest struct {\n\tData Data `json:\"data\"`\n}\n\ntype Data struct {\n\tType       string    `json:\"type\"`\n\tAttributes DNSRecord `json:\"attributes\"`\n}\n\ntype DNSRecord struct {\n\tRecordType string `json:\"record_type,omitempty\"`\n\tName       string `json:\"name,omitempty\"`\n\tDomain     string `json:\"domain,omitempty\"`\n\tData       string `json:\"data,omitempty\"`\n\tTTL        int    `json:\"ttl,omitempty\"`\n}\n\ntype APIError struct {\n\tErrors []ErrorItem `json:\"errors\"`\n}\n\nfunc (a APIError) Error() string {\n\tvar parts []string\n\tfor _, data := range a.Errors {\n\t\tparts = append(parts, fmt.Sprintf(\"status: %s, title: %s, id: %s\", data.Status, data.Title, data.ID))\n\t}\n\n\treturn strings.Join(parts, \", \")\n}\n\ntype ErrorItem struct {\n\tStatus string `json:\"status,omitempty\"`\n\tTitle  string `json:\"title,omitempty\"`\n\tID     string `json:\"id,omitempty\"`\n}\n\ntype CreateDNSRecordResponse struct {\n\tData struct {\n\t\tType       string `json:\"type\"`\n\t\tID         string `json:\"id\"`\n\t\tAttributes struct {\n\t\t\tStatus string `json:\"status\"`\n\t\t} `json:\"attributes\"`\n\t\tLinks struct {\n\t\t\tQueueJob  string `json:\"queue-job\"`\n\t\t\tDNSRecord string `json:\"dns-record\"`\n\t\t} `json:\"links\"`\n\t} `json:\"data\"`\n}\n\ntype GetJobResponse struct {\n\tData struct {\n\t\tID         string `json:\"id\"`\n\t\tType       string `json:\"type\"`\n\t\tAttributes struct {\n\t\t\tJobType string `json:\"job_type\"`\n\t\t\tStatus  string `json:\"status\"`\n\t\t} `json:\"attributes\"`\n\t\tLinks struct {\n\t\t\tSelf   string `json:\"self\"`\n\t\t\tObject string `json:\"object\"`\n\t\t} `json:\"links\"`\n\t} `json:\"data\"`\n}\n\ntype DeleteRecordResponse struct {\n\tData struct {\n\t\tID         string `json:\"id\"`\n\t\tType       string `json:\"type\"`\n\t\tAttributes struct {\n\t\t\tJobType string `json:\"job_type\"`\n\t\t\tStatus  string `json:\"status\"`\n\t\t} `json:\"attributes\"`\n\t\tLinks struct {\n\t\t\tSelf   string `json:\"self\"`\n\t\t\tObject string `json:\"object\"`\n\t\t} `json:\"links\"`\n\t} `json:\"data\"`\n}\n"
  },
  {
    "path": "providers/dns/variomedia/variomedia.go",
    "content": "// Package variomedia implements a DNS provider for solving the DNS-01 challenge using Variomedia DNS.\npackage variomedia\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/log\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/variomedia/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"VARIOMEDIA_\"\n\n\tEnvAPIToken = envNamespace + \"API_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"variomedia: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIToken = values[EnvAPIToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Variomedia.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config.APIToken == \"\" {\n\t\treturn nil, errors.New(\"variomedia: missing credentials\")\n\t}\n\n\tclient := internal.NewClient(config.APIToken)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Sequential All DNS challenges for this provider will be resolved sequentially.\n// Returns the interval between each iteration.\nfunc (d *DNSProvider) Sequential() time.Duration {\n\treturn d.config.SequenceInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"variomedia: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"variomedia: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\trecord := internal.DNSRecord{\n\t\tRecordType: \"TXT\",\n\t\tName:       subDomain,\n\t\tDomain:     dns01.UnFqdn(authZone),\n\t\tData:       info.Value,\n\t\tTTL:        d.config.TTL,\n\t}\n\n\tcdrr, err := d.client.CreateDNSRecord(ctx, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"variomedia: %w\", err)\n\t}\n\n\terr = d.waitJob(ctx, domain, cdrr.Data.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"variomedia: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = strings.TrimPrefix(cdrr.Data.Links.DNSRecord, \"https://api.variomedia.de/dns-records/\")\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record previously created.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tctx := context.Background()\n\n\t// get the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"variomedia: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\tddrr, err := d.client.DeleteDNSRecord(ctx, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"variomedia: %w\", err)\n\t}\n\n\terr = d.waitJob(ctx, domain, ddrr.Data.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"variomedia: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) waitJob(ctx context.Context, domain, id string) error {\n\treturn wait.Retry(ctx,\n\t\tfunc() error {\n\t\t\tresult, err := d.client.GetJob(ctx, id)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"apply change on %s: %w\", domain, err)\n\t\t\t}\n\n\t\t\tlog.Infof(\"variomedia: [%s] %s: %s %s\", domain, result.Data.ID, result.Data.Attributes.JobType, result.Data.Attributes.Status)\n\n\t\t\tif result.Data.Attributes.Status != \"done\" {\n\t\t\t\treturn fmt.Errorf(\"apply change on %s: status: %s\", domain, result.Data.Attributes.Status)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t\tbackoff.WithBackOff(backoff.NewConstantBackOff(d.config.PollingInterval)),\n\t\tbackoff.WithMaxElapsedTime(d.config.PropagationTimeout),\n\t)\n}\n"
  },
  {
    "path": "providers/dns/variomedia/variomedia.toml",
    "content": "Name = \"Variomedia\"\nDescription = ''''''\nURL = \"https://www.variomedia.de/\"\nCode = \"variomedia\"\nSince = \"v4.8.0\"\n\nExample = '''\nVARIOMEDIA_API_TOKEN=xxxx \\\nlego --dns variomedia -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    VARIOMEDIA_API_TOKEN = \"API token\"\n  [Configuration.Additional]\n    VARIOMEDIA_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    VARIOMEDIA_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    VARIOMEDIA_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    VARIOMEDIA_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n    VARIOMEDIA_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api.variomedia.de/docs/dns-records.html\"\n"
  },
  {
    "path": "providers/dns/variomedia/variomedia_test.go",
    "content": "package variomedia\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API token\",\n\t\t\texpected: \"variomedia: some credentials information are missing: VARIOMEDIA_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\texpected string\n\t\tapiToken string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tapiToken: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api token\",\n\t\t\tapiToken: \"\",\n\t\t\texpected: \"variomedia: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIToken = test.apiToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/vegadns/fixtures/create_record.json",
    "content": "{\n  \"status\": \"ok\",\n  \"record\": {\n    \"name\": \"_acme-challenge.example.com\",\n    \"value\": \"my_challenge\",\n    \"record_type\": \"TXT\",\n    \"ttl\": 3600,\n    \"record_id\": 3,\n    \"location_id\": null,\n    \"domain_id\": 1\n  }\n}\n"
  },
  {
    "path": "providers/dns/vegadns/fixtures/record_delete.json",
    "content": "{\n  \"status\": \"ok\"\n}\n"
  },
  {
    "path": "providers/dns/vegadns/fixtures/records.json",
    "content": "{\n  \"status\": \"ok\",\n  \"total_records\": 2,\n  \"domain\": {\n    \"status\": \"active\",\n    \"domain\": \"example.com\",\n    \"owner_id\": 0,\n    \"domain_id\": 1\n  },\n  \"records\": [\n    {\n      \"retry\": \"2048\",\n      \"minimum\": \"2560\",\n      \"refresh\": \"16384\",\n      \"email\": \"hostmaster.example.com\",\n      \"record_type\": \"SOA\",\n      \"expire\": \"1048576\",\n      \"ttl\": 86400,\n      \"record_id\": 1,\n      \"nameserver\": \"ns1.example.com\",\n      \"domain_id\": 1,\n      \"serial\": \"\"\n    },\n    {\n      \"name\": \"example.com\",\n      \"value\": \"ns1.example.com\",\n      \"record_type\": \"NS\",\n      \"ttl\": 3600,\n      \"record_id\": 2,\n      \"location_id\": null,\n      \"domain_id\": 1\n    },\n    {\n      \"name\": \"_acme-challenge.example.com\",\n      \"value\": \"my_challenge\",\n      \"record_type\": \"TXT\",\n      \"ttl\": 3600,\n      \"record_id\": 3,\n      \"location_id\": null,\n      \"domain_id\": 1\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/vegadns/fixtures/token.json",
    "content": "{\n  \"access_token\": \"699dd4ff-e381-46b8-8bf8-5de49dd56c1f\",\n  \"token_type\": \"bearer\",\n  \"expires_in\": 3600\n}\n"
  },
  {
    "path": "providers/dns/vegadns/vegadns.go",
    "content": "// Package vegadns implements a DNS provider for solving the DNS-01 challenge using VegaDNS.\npackage vegadns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/nrdcg/vegadns\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"VEGADNS_\"\n\n\tEnvKey    = \"SECRET_VEGADNS_KEY\"\n\tEnvSecret = \"SECRET_VEGADNS_SECRET\"\n\tEnvURL    = envNamespace + \"URL\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL   string\n\tAPIKey    string\n\tAPISecret string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 10),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 12*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, time.Minute),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *vegadns.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for VegaDNS.\n// Credentials must be passed in the environment variables:\n// VEGADNS_URL, SECRET_VEGADNS_KEY, SECRET_VEGADNS_SECRET.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vegadns: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.BaseURL = values[EnvURL]\n\tconfig.APIKey = env.GetOrFile(EnvKey)\n\tconfig.APISecret = env.GetOrFile(EnvSecret)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for VegaDNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"vegadns: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.HTTPClient == nil {\n\t\tconfig.HTTPClient = &http.Client{Timeout: 30 * time.Second}\n\t}\n\n\tconfig.HTTPClient = clientdebug.Wrap(config.HTTPClient)\n\n\tclient, err := vegadns.NewClient(config.BaseURL,\n\t\tvegadns.WithOAuth(config.APIKey, config.APISecret),\n\t\tvegadns.WithHTTPClient(config.HTTPClient),\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vegadns: %w\", err)\n\t}\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tdomainID, err := d.findDomainID(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vegadns: find domain ID for %s: %w\", info.EffectiveFQDN, err)\n\t}\n\n\terr = d.client.CreateTXTRecord(ctx, domainID, dns01.UnFqdn(info.EffectiveFQDN), info.Value, d.config.TTL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vegadns: create TXT record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tdomainID, err := d.findDomainID(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vegadns: find domain ID for %s: %w\", info.EffectiveFQDN, err)\n\t}\n\n\trecordID, err := d.findRecordID(ctx, domainID, dns01.UnFqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vegadns: find record ID for %d: %w\", domainID, err)\n\t}\n\n\terr = d.client.DeleteRecord(ctx, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vegadns: delete record: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) findDomainID(ctx context.Context, fqdn string) (int, error) {\n\tfor host := range dns01.UnFqdnDomainsSeq(fqdn) {\n\t\tid, err := d.client.GetDomainID(ctx, host)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn id, nil\n\t}\n\n\treturn 0, errors.New(\"domain not found\")\n}\n\nfunc (d *DNSProvider) findRecordID(ctx context.Context, domainID int, name string) (int, error) {\n\trecords, err := d.client.GetRecords(ctx, domainID)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"get records: %w\", err)\n\t}\n\n\tfor _, r := range records {\n\t\tif r.Name == name && r.RecordType == \"TXT\" {\n\t\t\treturn r.RecordID, nil\n\t\t}\n\t}\n\n\treturn 0, errors.New(\"record not found\")\n}\n"
  },
  {
    "path": "providers/dns/vegadns/vegadns.toml",
    "content": "Name = \"VegaDNS\"\nDescription = ''''''\nURL = \"https://github.com/shupp/VegaDNS-API\"\nCode = \"vegadns\"\nSince = \"v1.1.0\"\n\nExample = ''''''\n\n[Configuration]\n  [Configuration.Credentials]\n    SECRET_VEGADNS_KEY = \"API key\"\n    SECRET_VEGADNS_SECRET = \"API secret\"\n    VEGADNS_URL = \"API endpoint URL\"\n  [Configuration.Additional]\n    VEGADNS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 60)\"\n    VEGADNS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 720)\"\n    VEGADNS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 10)\"\n\n[Links]\n  API = \"https://github.com/shupp/VegaDNS-API\"\n  GoClient = \"https://github.com/OpenDNS/vegadns2client\"\n\n"
  },
  {
    "path": "providers/dns/vegadns/vegadns_test.go",
    "content": "package vegadns\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst testDomain = \"example.com\"\n\nvar envTest = tester.NewEnvTest(EnvKey, EnvSecret, EnvURL)\n\nfunc TestNewDNSProvider_Fail(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\tenvTest.ClearEnv()\n\n\t_, err := NewDNSProvider()\n\trequire.Error(t, err, \"VEGADNS_URL env missing\")\n}\n\nfunc TestDNSProvider_TimeoutSuccess(t *testing.T) {\n\tdefer envTest.RestoreEnv()\n\n\tenvTest.ClearEnv()\n\n\tprovider := mockBuilder().Build(t)\n\n\ttimeout, interval := provider.Timeout()\n\tassert.Equal(t, 12*time.Minute, timeout)\n\tassert.Equal(t, 1*time.Minute, interval)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\thandler       http.Handler\n\t\tbuilder       *servermock.Builder[*DNSProvider]\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"POST /1.0/token\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"token.json\")).\n\t\t\t\tRoute(\"GET /1.0/domains\", getDomainHandler()).\n\t\t\t\tRoute(\"POST /1.0/records\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"create_record.json\").\n\t\t\t\t\t\tWithStatusCode(http.StatusCreated)),\n\t\t},\n\t\t{\n\t\t\tdesc: \"fail to find the zone\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"POST /1.0/token\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"token.json\")).\n\t\t\t\tRoute(\"GET /1.0/domains\",\n\t\t\t\t\tservermock.Noop().\n\t\t\t\t\t\tWithStatusCode(http.StatusNotFound)),\n\t\t\texpectedError: \"vegadns: find domain ID for _acme-challenge.example.com.: domain not found\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"fail to create TXT record\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"POST /1.0/token\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"token.json\")).\n\t\t\t\tRoute(\"GET /1.0/domains\", getDomainHandler()).\n\t\t\t\tRoute(\"POST /1.0/records\",\n\t\t\t\t\tservermock.Noop().\n\t\t\t\t\t\tWithStatusCode(http.StatusBadRequest)),\n\t\t\texpectedError: \"vegadns: create TXT record: bad answer from VegaDNS (code: 400, message: )\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tprovider := test.builder.Build(t)\n\n\t\t\terr := provider.Present(testDomain, \"token\", \"keyAuth\")\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tbuilder       *servermock.Builder[*DNSProvider]\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"POST /1.0/token\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"token.json\")).\n\t\t\t\tRoute(\"GET /1.0/domains\", getDomainHandler()).\n\t\t\t\tRoute(\"GET /1.0/records\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"records.json\"),\n\t\t\t\t\tservermock.CheckQueryParameter().With(\"domain_id\", \"1\")).\n\t\t\t\tRoute(\"DELETE /1.0/records/3\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"record_delete.json\")),\n\t\t},\n\t\t{\n\t\t\tdesc: \"fail to find the zone\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"POST /1.0/token\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"token.json\")).\n\t\t\t\tRoute(\"GET /1.0/domains\",\n\t\t\t\t\tservermock.Noop().\n\t\t\t\t\t\tWithStatusCode(http.StatusNotFound)),\n\t\t\texpectedError: \"vegadns: find domain ID for _acme-challenge.example.com.: domain not found\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"fail to get record ID\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"POST /1.0/token\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"token.json\")).\n\t\t\t\tRoute(\"GET /1.0/domains\", getDomainHandler()).\n\t\t\t\tRoute(\"GET /1.0/records\",\n\t\t\t\t\tservermock.Noop().\n\t\t\t\t\t\tWithStatusCode(http.StatusNotFound),\n\t\t\t\t\tservermock.CheckQueryParameter().With(\"domain_id\", \"1\")),\n\t\t\texpectedError: \"vegadns: find record ID for 1: get records: bad answer from VegaDNS (code: 404, message: )\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tprovider := test.builder.Build(t)\n\n\t\t\terr := provider.CleanUp(testDomain, \"token\", \"keyAuth\")\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc getDomainHandler() http.HandlerFunc {\n\treturn func(rw http.ResponseWriter, req *http.Request) {\n\t\tif req.URL.Query().Get(\"search\") == testDomain {\n\t\t\tfmt.Fprint(rw, `\n{\n  \"domains\":[\n    {\n      \"domain_id\":1,\n      \"domain\":\"example.com\",\n      \"status\":\"active\",\n      \"owner_id\":0\n    }\n  ]\n}\n`)\n\n\t\t\treturn\n\t\t}\n\n\t\trw.WriteHeader(http.StatusNotFound)\n\t}\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {\n\t\tenvTest.Apply(map[string]string{\n\t\t\tEnvKey:    \"key\",\n\t\t\tEnvSecret: \"secret\",\n\t\t\tEnvURL:    server.URL,\n\t\t})\n\n\t\treturn NewDNSProvider()\n\t})\n}\n"
  },
  {
    "path": "providers/dns/vercel/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"golang.org/x/oauth2\"\n)\n\nconst defaultBaseURL = \"https://api.vercel.com\"\n\n// Client Vercel client.\ntype Client struct {\n\tteamID string\n\n\tbaseURL    *url.URL\n\thttpClient *http.Client\n}\n\n// NewClient creates a Client.\nfunc NewClient(hc *http.Client, teamID string) *Client {\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\tif hc == nil {\n\t\thc = &http.Client{Timeout: 10 * time.Second}\n\t}\n\n\treturn &Client{\n\t\tteamID:     teamID,\n\t\tbaseURL:    baseURL,\n\t\thttpClient: hc,\n\t}\n}\n\n// CreateRecord creates a DNS record.\n// https://vercel.com/docs/rest-api#endpoints/dns/create-a-dns-record\nfunc (c *Client) CreateRecord(ctx context.Context, zone string, record Record) (*CreateRecordResponse, error) {\n\tendpoint := c.baseURL.JoinPath(\"v2\", \"domains\", dns01.UnFqdn(zone), \"records\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trespData := &CreateRecordResponse{}\n\n\terr = c.do(req, respData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn respData, nil\n}\n\n// DeleteRecord deletes a DNS record.\n// https://vercel.com/docs/rest-api#endpoints/dns/delete-a-dns-record\nfunc (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error {\n\tendpoint := c.baseURL.JoinPath(\"v2\", \"domains\", dns01.UnFqdn(zone), \"records\", recordID)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tif c.teamID != \"\" {\n\t\tquery := req.URL.Query()\n\t\tquery.Add(\"teamId\", c.teamID)\n\t\treq.URL.RawQuery = query.Encode()\n\t}\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar response APIErrorResponse\n\n\terr := json.Unmarshal(raw, &response)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code: %d] %w\", resp.StatusCode, response.Error)\n}\n\nfunc OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {\n\tif client == nil {\n\t\tclient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tclient.Transport = &oauth2.Transport{\n\t\tSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),\n\t\tBase:   client.Transport,\n\t}\n\n\treturn client\n}\n"
  },
  {
    "path": "providers/dns/vercel/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(OAuthStaticAccessToken(server.Client(), \"secret\"), \"123\")\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"Bearer secret\"))\n}\n\nfunc TestClient_CreateRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /v2/domains/example.com/records\",\n\t\t\tservermock.RawStringResponse(`{\n\t\t\t\"uid\": \"9e2eab60-0ba5-4dff-b481-2999c9764b84\",\n\t\t\t\"updated\": 1\n\t\t}`),\n\t\t\tservermock.CheckRequestJSONBody(`{\"name\":\"_acme-challenge.example.com.\",\"type\":\"TXT\",\"value\":\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\",\"ttl\":60}`),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"teamId\", \"123\")).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName:  \"_acme-challenge.example.com.\",\n\t\tType:  \"TXT\",\n\t\tValue: \"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\",\n\t\tTTL:   60,\n\t}\n\n\tresp, err := client.CreateRecord(t.Context(), \"example.com.\", record)\n\trequire.NoError(t, err)\n\n\texpected := &CreateRecordResponse{\n\t\tUID:     \"9e2eab60-0ba5-4dff-b481-2999c9764b84\",\n\t\tUpdated: 1,\n\t}\n\n\tassert.Equal(t, expected, resp)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /v2/domains/example.com/records/1234567\", nil,\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"teamId\", \"123\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com.\", \"1234567\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/vercel/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype Record struct {\n\tID    string `json:\"id,omitempty\"`\n\tSlug  string `json:\"slug,omitempty\"`\n\tName  string `json:\"name,omitempty\"`\n\tType  string `json:\"type,omitempty\"`\n\tValue string `json:\"value,omitempty\"`\n\tTTL   int    `json:\"ttl,omitempty\"`\n}\n\n// CreateRecordResponse represents a response from Vercel's API after making a DNS record.\ntype CreateRecordResponse struct {\n\tUID     string `json:\"uid\"`\n\tUpdated int    `json:\"updated,omitempty\"`\n}\n\ntype APIErrorResponse struct {\n\tError *APIError `json:\"error\"`\n}\n\ntype APIError struct {\n\tCode    string `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\nfunc (a APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", a.Code, a.Message)\n}\n"
  },
  {
    "path": "providers/dns/vercel/vercel.go",
    "content": "// Package vercel implements a DNS provider for solving the DNS-01 challenge using Vercel DNS.\npackage vercel\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/vercel/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"VERCEL_\"\n\n\tEnvAuthToken = envNamespace + \"API_TOKEN\"\n\tEnvTeamID    = envNamespace + \"TEAM_ID\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAuthToken          string\n\tTeamID             string\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 60),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\trecordIDs   map[string]string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Vercel.\n// Credentials must be passed in the environment variables: VERCEL_API_TOKEN, VERCEL_TEAM_ID.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAuthToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vercel: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AuthToken = values[EnvAuthToken]\n\tconfig.TeamID = env.GetOrDefaultString(EnvTeamID, \"\")\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Digital Ocean.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"vercel: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.AuthToken == \"\" {\n\t\treturn nil, errors.New(\"vercel: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(\n\t\tclientdebug.Wrap(\n\t\t\tinternal.OAuthStaticAccessToken(config.HTTPClient, config.AuthToken),\n\t\t),\n\t\tconfig.TeamID,\n\t)\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    client,\n\t\trecordIDs: make(map[string]string),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vercel: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\trecord := internal.Record{\n\t\tName:  info.EffectiveFQDN,\n\t\tType:  \"TXT\",\n\t\tValue: info.Value,\n\t\tTTL:   d.config.TTL,\n\t}\n\n\trespData, err := d.client.CreateRecord(context.Background(), authZone, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vercel: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = respData.UID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vercel: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// get the record's unique ID from when we created it\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"vercel: unknown record ID for '%s'\", info.EffectiveFQDN)\n\t}\n\n\terr = d.client.DeleteRecord(context.Background(), authZone, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vercel: %w\", err)\n\t}\n\n\t// Delete record ID from map\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/vercel/vercel.toml",
    "content": "Name = \"Vercel\"\nDescription = ''''''\nURL = \"https://vercel.com\"\nCode = \"vercel\"\nSince = \"v4.7.0\"\n\nExample = '''\nVERCEL_API_TOKEN=xxxxxx \\\nlego --dns vercel -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    VERCEL_API_TOKEN = \"Authentication token\"\n  [Configuration.Additional]\n    VERCEL_TEAM_ID = \"Team ID (ex: team_xxxxxxxxxxxxxxxxxxxxxxxx)\"\n    VERCEL_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 5)\"\n    VERCEL_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    VERCEL_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    VERCEL_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://vercel.com/docs/rest-api#endpoints/dns\"\n"
  },
  {
    "path": "providers/dns/vercel/vercel_test.go",
    "content": "package vercel\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAuthToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAuthToken: \"\",\n\t\t\t},\n\t\t\texpected: \"vercel: some credentials information are missing: VERCEL_API_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tauthToken string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tauthToken: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"vercel: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AuthToken = test.authToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.recordIDs)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/versio/fixtures/error_failToCreateTXT.json",
    "content": "{\n  \"error\": {\n    \"code\": 400,\n    \"message\": \"ProcessError|DNS record invalid type _acme-challenge.example.eu. TST\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/versio/fixtures/error_failToFindZone.json",
    "content": "{\n  \"error\": {\n    \"code\": 401,\n    \"message\": \"ObjectDoesNotExist|Domain not found\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/versio/fixtures/token.json",
    "content": "{\n  \"access_token\":\"699dd4ff-e381-46b8-8bf8-5de49dd56c1f\",\n  \"token_type\":\"bearer\",\n  \"expires_in\":3600\n}\n"
  },
  {
    "path": "providers/dns/versio/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// DefaultBaseURL default API endpoint.\nconst DefaultBaseURL = \"https://www.versio.nl/api/v1/\"\n\n// Client the API client for Versio DNS.\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(username, password string) *Client {\n\tbaseURL, _ := url.Parse(DefaultBaseURL)\n\n\treturn &Client{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// UpdateDomain updates domain information.\n// https://www.versio.nl/RESTapidoc/#api-Domains-Update\nfunc (c *Client) UpdateDomain(ctx context.Context, domain string, msg *DomainInfo) (*DomainInfoResponse, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", domain, \"update\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, msg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trespData := &DomainInfoResponse{}\n\n\terr = c.do(req, respData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn respData, nil\n}\n\n// GetDomain gets domain information.\n// https://www.versio.nl/RESTapidoc/#api-Domains-Domain\nfunc (c *Client) GetDomain(ctx context.Context, domain string) (*DomainInfoResponse, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", domain)\n\n\tquery := endpoint.Query()\n\tquery.Set(\"show_dns_records\", \"true\")\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trespData := &DomainInfoResponse{}\n\n\terr = c.do(req, respData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn respData, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tif c.username != \"\" && c.password != \"\" {\n\t\treq.SetBasicAuth(c.username, c.password)\n\t}\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif resp != nil {\n\t\tdefer func() { _ = resp.Body.Close() }()\n\t}\n\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif err = json.Unmarshal(raw, result); err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tresponse := &ErrorResponse{}\n\n\terr := json.Unmarshal(raw, response)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code: %d] %w\", resp.StatusCode, response.Message)\n}\n"
  },
  {
    "path": "providers/dns/versio/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithBasicAuth(\"user\", \"secret\"))\n}\n\nfunc TestClient_GetDomain(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"get-domain.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"show_dns_records\", \"true\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetDomain(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := &DomainInfoResponse{DomainInfo: DomainInfo{DNSRecords: []Record{\n\t\t{Type: \"MX\", Name: \"example.com\", Value: \"fallback.axc.eu\", Priority: 20, TTL: 3600},\n\t\t{Type: \"TXT\", Name: \"example.com\", Value: \"\\\"v=spf1 a mx ip4:127.0.0.1 a:spf.spamexperts.axc.nl ~all\\\"\", Priority: 0, TTL: 3600},\n\t\t{Type: \"A\", Name: \"example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"ftp.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"localhost.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"pop.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"smtp.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"www.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"dev.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"_domainkey.domain.com.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"MX\", Name: \"example.com\", Value: \"spamfilter2.axc.eu\", Priority: 0, TTL: 3600},\n\t\t{Type: \"A\", Name: \"redirect.example.com\", Value: \"localhost\", Priority: 10, TTL: 14400},\n\t}}}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_GetDomain_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /domains/example.com\",\n\t\t\tservermock.ResponseFromFixture(\"get-domain-error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\t_, err := client.GetDomain(t.Context(), \"example.com\")\n\trequire.ErrorAs(t, err, &ErrorMessage{})\n}\n\nfunc TestClient_UpdateDomain(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/example.com/update\",\n\t\t\tservermock.ResponseFromFixture(\"update-domain.json\"),\n\t\t\tservermock.CheckRequestJSONBodyFromFixture(\"update-domain-request.json\")).\n\t\tBuild(t)\n\n\tmsg := &DomainInfo{DNSRecords: []Record{\n\t\t{Type: \"MX\", Name: \"example.com\", Value: \"fallback.axc.eu\", Priority: 20, TTL: 3600},\n\t\t{Type: \"TXT\", Name: \"example.com\", Value: \"\\\"v=spf1 a mx ip4:127.0.0.1 a:spf.spamexperts.axc.nl ~all\\\"\", Priority: 0, TTL: 3600},\n\t\t{Type: \"A\", Name: \"example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"ftp.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"localhost.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"pop.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"smtp.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"www.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"dev.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"_domainkey.domain.com.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"MX\", Name: \"example.com\", Value: \"spamfilter2.axc.eu\", Priority: 0, TTL: 3600},\n\t\t{Type: \"A\", Name: \"redirect.example.com\", Value: \"localhost\", Priority: 10, TTL: 14400},\n\t}}\n\n\trecords, err := client.UpdateDomain(t.Context(), \"example.com\", msg)\n\trequire.NoError(t, err)\n\n\texpected := &DomainInfoResponse{DomainInfo: DomainInfo{DNSRecords: []Record{\n\t\t{Type: \"MX\", Name: \"example.com\", Value: \"fallback.axc.eu\", Priority: 20, TTL: 3600},\n\t\t{Type: \"TXT\", Name: \"example.com\", Value: \"\\\"v=spf1 a mx ip4:127.0.0.1 a:spf.spamexperts.axc.nl ~all\\\"\", Priority: 0, TTL: 3600},\n\t\t{Type: \"A\", Name: \"example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"ftp.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"localhost.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"pop.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"smtp.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"www.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"dev.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"_domainkey.domain.com.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"MX\", Name: \"example.com\", Value: \"spamfilter2.axc.eu\", Priority: 0, TTL: 3600},\n\t\t{Type: \"A\", Name: \"redirect.example.com\", Value: \"localhost\", Priority: 10, TTL: 14400},\n\t}}}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_UpdateDomain_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/example.com/update\",\n\t\t\tservermock.ResponseFromFixture(\"update-domain-error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\tmsg := &DomainInfo{DNSRecords: []Record{\n\t\t{Type: \"MX\", Name: \"example.com\", Value: \"fallback.axc.eu\", Priority: 20, TTL: 3600},\n\t\t{Type: \"TXT\", Name: \"example.com\", Value: \"\\\"v=spf1 a mx ip4:127.0.0.1 a:spf.spamexperts.axc.nl ~all\\\"\", Priority: 0, TTL: 3600},\n\t\t{Type: \"A\", Name: \"example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"ftp.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"localhost.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"pop.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"smtp.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"www.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"dev.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"A\", Name: \"_domainkey.domain.com.example.com\", Value: \"185.13.227.159\", Priority: 0, TTL: 14400},\n\t\t{Type: \"MX\", Name: \"example.com\", Value: \"spamfilter2.axc.eu\", Priority: 0, TTL: 3600},\n\t\t{Type: \"A\", Name: \"redirect.example.com\", Value: \"localhost\", Priority: 10, TTL: 14400},\n\t}}\n\n\t_, err := client.UpdateDomain(t.Context(), \"example.com\", msg)\n\trequire.ErrorAs(t, err, &ErrorMessage{})\n}\n"
  },
  {
    "path": "providers/dns/versio/internal/fixtures/README.md",
    "content": "\nNote: the snippets from the API documentation are wrong:\ninvalid field type (ex: prio, TTL), and JSON format contains errors.\n\nSo the files inside the fixtures have been partially adapted to fit the reality.\n"
  },
  {
    "path": "providers/dns/versio/internal/fixtures/get-domain-error.json",
    "content": "{\n  \"error\": {\n    \"code\": 401,\n    \"message\": \"You are not authorized to access this resource. Did you supply the correct credentials and have you IP (xxxx:xxxx:xxx:xxxx:xxxx:xxxx:xxxx:xxxx) whitelisted for API use?\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/versio/internal/fixtures/get-domain.json",
    "content": "{\n  \"domainInfo\": {\n    \"domain\": \"example.com\",\n    \"status\": \"OK\",\n    \"expire-date\": \"2020-10-01\",\n    \"registrant_id\": \"4334\",\n    \"reseller_id\": \"3253\",\n    \"category_id\": \"674\",\n    \"dnstemplate_id\": \"674\",\n    \"lock\": false,\n    \"auto_renew\": false,\n    \"epp_code\": \"3fFerggEg\",\n    \"ns\": [],\n    \"dns_management\": true,\n    \"dns_records\": [\n      {\n        \"type\": \"MX\",\n        \"name\": \"example.com\",\n        \"value\": \"fallback.axc.eu\",\n        \"prio\": 20,\n        \"ttl\": 3600\n      },\n      {\n        \"type\": \"TXT\",\n        \"name\": \"example.com\",\n        \"value\": \"\\\"v=spf1 a mx ip4:127.0.0.1 a:spf.spamexperts.axc.nl ~all\\\"\",\n        \"prio\": 0,\n        \"ttl\": 3600\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"ftp.example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"localhost.example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"pop.example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"smtp.example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"www.example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"dev.example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"_domainkey.domain.com.example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"MX\",\n        \"name\": \"example.com\",\n        \"value\": \"spamfilter2.axc.eu\",\n        \"prio\": 0,\n        \"ttl\": 3600\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"redirect.example.com\",\n        \"value\": \"localhost\",\n        \"prio\": 10,\n        \"ttl\": 14400\n      }\n    ],\n    \"dns_redirections\": [\n      {\n        \"from\": \"redirect.example.com\",\n        \"destination\": \"http:\\/\\/www.google.nl\"\n      }\n    ],\n    \"dnssec_keys\": [\n      {\n        \"flags\": 256,\n        \"algorithm\": 3,\n        \"public_key\": \"AwEAAZKsuPDwO1+Usao2X1rgdFhdT3LAxy5cbRNFNEy1qsauwSIYov5SU4GlG6ylXIVQwHF5AWfbD7lcZzw1IlNegvaLnoirJjcYZhz4ppQU5+M/1hfH7aNZIsyz7AhHwX7gpOeUdGBXTiXQ3m7ksGccVQ79h7yl2fiBDCryBSf49vOTqo3dI7KZM48vmeqOxPth3ANMXzt6osHENGIchdGgIOVy5Y7AsVecL4V+lbn2t47fFfJ2O9PwuuDBzO0HCCT/mmYVsvZ33kgc7QPFKB3LojoXdHFHl1jCsC98phIVGzJR54H2xRohQvfC2WAXFEx+YNDW1yv7zQFrUVVMFwCe/E8=\"\n      },\n      {\n        \"flags\": 257,\n        \"algorithm\": 8,\n        \"public_key\": \"AwEAAZKsuPDwO1+Usao2X1rgdFhdT3LAxy5cbRNFNEy1qsauwSIYov5SU4GlG6ylXIVQwHF5AWfbD7lcZzw1IlNegvaLnoirJjcYZhz4ppQU5+M/1hfH7aNZIsyz7AhHwX7gpOeUdGBXTiXQ3m7ksGccVQ79h7yl2fiBDCryBSf49vOTqo3dI7KZM48vmeqOxPth3ANMXzt6osHENGIchdGgIOVy5Y7AsVecL4V+lbn2t47fFfJ2O9PwuuDBzO0HCCT/mmYVsvZ33kgc7QPFKB3LojoXdHFHl1jCsC98phIVGzJR54H2xRohQvfC2WAXFEx+YNDW1yv7zQFrUVVMFwCe/E8=\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/versio/internal/fixtures/update-domain-error.json",
    "content": "{\n  \"error\": {\n    \"code\": 401,\n    \"message\": \"You are not authorized to access this resource. Did you supply the correct credentials and have you IP (xxxx:xxxx:xxx:xxxx:xxxx:xxxx:xxxx:xxxx) whitelisted for API use?\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/versio/internal/fixtures/update-domain-request.json",
    "content": "{\n  \"dns_records\": [\n    {\n      \"type\": \"MX\",\n      \"name\": \"example.com\",\n      \"value\": \"fallback.axc.eu\",\n      \"prio\": 20,\n      \"ttl\": 3600\n    },\n    {\n      \"type\": \"TXT\",\n      \"name\": \"example.com\",\n      \"value\": \"\\\"v=spf1 a mx ip4:127.0.0.1 a:spf.spamexperts.axc.nl ~all\\\"\",\n      \"ttl\": 3600\n    },\n    {\n      \"type\": \"A\",\n      \"name\": \"example.com\",\n      \"value\": \"185.13.227.159\",\n      \"ttl\": 14400\n    },\n    {\n      \"type\": \"A\",\n      \"name\": \"ftp.example.com\",\n      \"value\": \"185.13.227.159\",\n      \"ttl\": 14400\n    },\n    {\n      \"type\": \"A\",\n      \"name\": \"localhost.example.com\",\n      \"value\": \"185.13.227.159\",\n      \"ttl\": 14400\n    },\n    {\n      \"type\": \"A\",\n      \"name\": \"pop.example.com\",\n      \"value\": \"185.13.227.159\",\n      \"ttl\": 14400\n    },\n    {\n      \"type\": \"A\",\n      \"name\": \"smtp.example.com\",\n      \"value\": \"185.13.227.159\",\n      \"ttl\": 14400\n    },\n    {\n      \"type\": \"A\",\n      \"name\": \"www.example.com\",\n      \"value\": \"185.13.227.159\",\n      \"ttl\": 14400\n    },\n    {\n      \"type\": \"A\",\n      \"name\": \"dev.example.com\",\n      \"value\": \"185.13.227.159\",\n      \"ttl\": 14400\n    },\n    {\n      \"type\": \"A\",\n      \"name\": \"_domainkey.domain.com.example.com\",\n      \"value\": \"185.13.227.159\",\n      \"ttl\": 14400\n    },\n    {\n      \"type\": \"MX\",\n      \"name\": \"example.com\",\n      \"value\": \"spamfilter2.axc.eu\",\n      \"ttl\": 3600\n    },\n    {\n      \"type\": \"A\",\n      \"name\": \"redirect.example.com\",\n      \"value\": \"localhost\",\n      \"prio\": 10,\n      \"ttl\": 14400\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/versio/internal/fixtures/update-domain.json",
    "content": "{\n  \"domainInfo\": {\n    \"domain\": \"example.com\",\n    \"status\": \"OK\",\n    \"expire-date\": \"2020-10-01\",\n    \"registrant_id\": \"4334\",\n    \"reseller_id\": \"3253\",\n    \"category_id\": \"674\",\n    \"dnstemplate_id\": \"674\",\n    \"lock\": false,\n    \"auto_renew\": false,\n    \"epp_code\": \"3fFerggEg\",\n    \"ns\": [],\n    \"dns_management\": true,\n    \"dns_records\": [\n      {\n        \"type\": \"MX\",\n        \"name\": \"example.com\",\n        \"value\": \"fallback.axc.eu\",\n        \"prio\": 20,\n        \"ttl\": 3600\n      },\n      {\n        \"type\": \"TXT\",\n        \"name\": \"example.com\",\n        \"value\": \"\\\"v=spf1 a mx ip4:127.0.0.1 a:spf.spamexperts.axc.nl ~all\\\"\",\n        \"prio\": 0,\n        \"ttl\": 3600\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"ftp.example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"localhost.example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"pop.example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"smtp.example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"www.example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"dev.example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"_domainkey.domain.com.example.com\",\n        \"value\": \"185.13.227.159\",\n        \"prio\": 0,\n        \"ttl\": 14400\n      },\n      {\n        \"type\": \"MX\",\n        \"name\": \"example.com\",\n        \"value\": \"spamfilter2.axc.eu\",\n        \"prio\": 0,\n        \"ttl\": 3600\n      },\n      {\n        \"type\": \"A\",\n        \"name\": \"redirect.example.com\",\n        \"value\": \"localhost\",\n        \"prio\": 10,\n        \"ttl\": 14400\n      }\n    ],\n    \"dns_redirections\": [\n      {\n        \"from\": \"redirect.example.com\",\n        \"destination\": \"http:\\/\\/www.google.nl\"\n      }\n    ],\n    \"dnssec_keys\": [\n      {\n        \"flags\": 256,\n        \"algorithm\": 3,\n        \"public_key\": \"AwEAAZKsuPDwO1+Usao2X1rgdFhdT3LAxy5cbRNFNEy1qsauwSIYov5SU4GlG6ylXIVQwHF5AWfbD7lcZzw1IlNegvaLnoirJjcYZhz4ppQU5+M/1hfH7aNZIsyz7AhHwX7gpOeUdGBXTiXQ3m7ksGccVQ79h7yl2fiBDCryBSf49vOTqo3dI7KZM48vmeqOxPth3ANMXzt6osHENGIchdGgIOVy5Y7AsVecL4V+lbn2t47fFfJ2O9PwuuDBzO0HCCT/mmYVsvZ33kgc7QPFKB3LojoXdHFHl1jCsC98phIVGzJR54H2xRohQvfC2WAXFEx+YNDW1yv7zQFrUVVMFwCe/E8=\"\n      },\n      {\n        \"flags\": 257,\n        \"algorithm\": 8,\n        \"public_key\": \"AwEAAZKsuPDwO1+Usao2X1rgdFhdT3LAxy5cbRNFNEy1qsauwSIYov5SU4GlG6ylXIVQwHF5AWfbD7lcZzw1IlNegvaLnoirJjcYZhz4ppQU5+M/1hfH7aNZIsyz7AhHwX7gpOeUdGBXTiXQ3m7ksGccVQ79h7yl2fiBDCryBSf49vOTqo3dI7KZM48vmeqOxPth3ANMXzt6osHENGIchdGgIOVy5Y7AsVecL4V+lbn2t47fFfJ2O9PwuuDBzO0HCCT/mmYVsvZ33kgc7QPFKB3LojoXdHFHl1jCsC98phIVGzJR54H2xRohQvfC2WAXFEx+YNDW1yv7zQFrUVVMFwCe/E8=\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/versio/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype DomainInfoResponse struct {\n\tDomainInfo DomainInfo `json:\"domainInfo\"`\n}\n\ntype DomainInfo struct {\n\tDNSRecords []Record `json:\"dns_records\"`\n}\n\ntype Record struct {\n\tType     string `json:\"type,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tValue    string `json:\"value,omitempty\"`\n\tPriority int    `json:\"prio,omitempty\"`\n\tTTL      int    `json:\"ttl,omitempty\"`\n}\n\ntype ErrorResponse struct {\n\tMessage ErrorMessage `json:\"error\"`\n}\n\ntype ErrorMessage struct {\n\tCode    int    `json:\"code,omitempty\"`\n\tMessage string `json:\"message,omitempty\"`\n}\n\nfunc (e ErrorMessage) Error() string {\n\treturn fmt.Sprintf(\"%d: %s\", e.Code, e.Message)\n}\n"
  },
  {
    "path": "providers/dns/versio/versio.go",
    "content": "// Package versio implements a DNS provider for solving the DNS-01 challenge using versio DNS.\npackage versio\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/versio/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"VERSIO_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\tEnvEndpoint = envNamespace + \"ENDPOINT\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvSequenceInterval   = envNamespace + \"SEQUENCE_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tBaseURL            *url.URL\n\tTTL                int\n\tUsername           string\n\tPassword           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tSequenceInterval   time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\tbaseURL, err := url.Parse(env.GetOrDefaultString(EnvEndpoint, internal.DefaultBaseURL))\n\tif err != nil {\n\t\tbaseURL, _ = url.Parse(internal.DefaultBaseURL)\n\t}\n\n\treturn &Config{\n\t\tBaseURL:            baseURL,\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 300),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),\n\t\tSequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n\n\tdnsEntriesMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"versio: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Versio.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"versio: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Username == \"\" {\n\t\treturn nil, errors.New(\"versio: the versio username is missing\")\n\t}\n\n\tif config.Password == \"\" {\n\t\treturn nil, errors.New(\"versio: the versio password is missing\")\n\t}\n\n\tclient := internal.NewClient(config.Username, config.Password)\n\n\tif config.BaseURL != nil {\n\t\tclient.BaseURL = config.BaseURL\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"versio: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// use mutex to prevent race condition from getDNSRecords until postDNSRecords\n\td.dnsEntriesMu.Lock()\n\tdefer d.dnsEntriesMu.Unlock()\n\n\tctx := context.Background()\n\n\tzoneName := dns01.UnFqdn(authZone)\n\n\tdomains, err := d.client.GetDomain(ctx, zoneName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"versio: %w\", err)\n\t}\n\n\ttxtRecord := internal.Record{\n\t\tType:  \"TXT\",\n\t\tName:  info.EffectiveFQDN,\n\t\tValue: `\"` + info.Value + `\"`,\n\t\tTTL:   d.config.TTL,\n\t}\n\n\t// Add new txtRecord to existing array of DNSRecords.\n\t// We'll need all the dns_records to add a new TXT record.\n\tmsg := &domains.DomainInfo\n\tmsg.DNSRecords = append(msg.DNSRecords, txtRecord)\n\n\t_, err = d.client.UpdateDomain(ctx, zoneName, msg)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"versio: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"versio: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t// use mutex to prevent race condition from getDNSRecords until postDNSRecords\n\td.dnsEntriesMu.Lock()\n\tdefer d.dnsEntriesMu.Unlock()\n\n\tctx := context.Background()\n\n\tzoneName := dns01.UnFqdn(authZone)\n\n\tdomains, err := d.client.GetDomain(ctx, zoneName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"versio: %w\", err)\n\t}\n\n\t// loop through the existing entries and remove the specific record\n\tmsg := &internal.DomainInfo{}\n\n\tfor _, e := range domains.DomainInfo.DNSRecords {\n\t\tif e.Name != info.EffectiveFQDN {\n\t\t\tmsg.DNSRecords = append(msg.DNSRecords, e)\n\t\t}\n\t}\n\n\t_, err = d.client.UpdateDomain(ctx, zoneName, msg)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"versio: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/versio/versio.toml",
    "content": "Name = \"Versio.[nl|eu|uk]\"\nDescription = ''''''\nURL = \"https://www.versio.nl/domeinnamen\"\nCode = \"versio\"\nSince = \"v2.7.0\"\n\nExample = '''\nVERSIO_USERNAME=<your login> \\\nVERSIO_PASSWORD=<your password> \\\nlego --dns versio -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\nTo test with the sandbox environment set ```VERSIO_ENDPOINT=https://www.versio.nl/testapi/v1/```\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    VERSIO_USERNAME = \"Basic authentication username\"\n    VERSIO_PASSWORD = \"Basic authentication password\"\n  [Configuration.Additional]\n    VERSIO_ENDPOINT = \"The endpoint URL of the API Server\"\n    VERSIO_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 5)\"\n    VERSIO_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    VERSIO_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n    VERSIO_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    VERSIO_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.versio.nl/RESTapidoc/\"\n"
  },
  {
    "path": "providers/dns/versio/versio_test.go",
    "content": "package versio\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst testDomain = \"example.com\"\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvUsername, EnvPassword, EnvEndpoint).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"me@example.com\",\n\t\t\t\tEnvPassword: \"SECRET\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPassword: \"me@example.com\",\n\t\t\t},\n\t\t\texpected: \"versio: some credentials information are missing: VERSIO_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"TOKEN\",\n\t\t\t},\n\t\t\texpected: \"versio: some credentials information are missing: VERSIO_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"versio: some credentials information are missing: VERSIO_USERNAME,VERSIO_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tconfig   *Config\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tconfig: &Config{\n\t\t\t\tUsername: \"me@example.com\",\n\t\t\t\tPassword: \"PW\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"nil config\",\n\t\t\tconfig:   nil,\n\t\t\texpected: \"versio: the configuration of the DNS provider is nil\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tconfig: &Config{\n\t\t\t\tPassword: \"PW\",\n\t\t\t},\n\t\t\texpected: \"versio: the versio username is missing\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tconfig: &Config{\n\t\t\t\tUsername: \"UN\",\n\t\t\t},\n\t\t\texpected: \"versio: the versio password is missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tp, err := NewDNSProviderConfig(test.config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tbuilder       *servermock.Builder[*DNSProvider]\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc: \"Success\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"GET /domains/example.com\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"token.json\"),\n\t\t\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\t\t\tWith(\"show_dns_records\", \"true\")).\n\t\t\t\tRoute(\"POST /domains/example.com/update\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"token.json\")),\n\t\t},\n\t\t{\n\t\t\tdesc: \"FailToFindZone\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"GET /domains/example.com\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"error_failToFindZone.json\").\n\t\t\t\t\t\tWithStatusCode(http.StatusUnauthorized)),\n\t\t\texpectedError: `versio: [status code: 401] 401: ObjectDoesNotExist|Domain not found`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"FailToCreateTXT\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"GET /domains/example.com\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"token.json\"),\n\t\t\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\t\t\tWith(\"show_dns_records\", \"true\")).\n\t\t\t\tRoute(\"POST /domains/example.com/update\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"error_failToCreateTXT.json\").\n\t\t\t\t\t\tWithStatusCode(http.StatusBadRequest)),\n\t\t\texpectedError: `versio: [status code: 400] 400: ProcessError|DNS record invalid type _acme-challenge.example.eu. TST`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tprovider := test.builder.Build(t)\n\n\t\t\terr := provider.Present(testDomain, \"token\", \"keyAuth\")\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tbuilder       *servermock.Builder[*DNSProvider]\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc: \"Success\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"GET /domains/example.com\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"token.json\"),\n\t\t\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\t\t\tWith(\"show_dns_records\", \"true\")).\n\t\t\t\tRoute(\"POST /domains/example.com/update\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"token.json\")),\n\t\t},\n\t\t{\n\t\t\tdesc: \"FailToFindZone\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"GET /domains/example.com\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"error_failToFindZone.json\").\n\t\t\t\t\t\tWithStatusCode(http.StatusUnauthorized)),\n\t\t\texpectedError: `versio: [status code: 401] 401: ObjectDoesNotExist|Domain not found`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tprovider := test.builder.Build(t)\n\n\t\t\terr := provider.CleanUp(testDomain, \"token\", \"keyAuth\")\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {\n\t\tenvTest.Apply(map[string]string{\n\t\t\tEnvUsername: \"me@example.com\",\n\t\t\tEnvPassword: \"secret\",\n\t\t\tEnvEndpoint: server.URL,\n\t\t})\n\n\t\tprovider, err := NewDNSProvider()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tprovider.client.HTTPClient = server.Client()\n\n\t\treturn provider, nil\n\t})\n}\n"
  },
  {
    "path": "providers/dns/vinyldns/fixtures/recordSetChange-create.json",
    "content": "{\n  \"changeType\": \"Create\",\n  \"created\": \"2021-03-04T00:49:00Z\",\n  \"id\": \"27ba5c17-a217-4e8d-b662-b1dc8bee588f\",\n  \"recordSet\": {\n    \"account\": \"\",\n    \"created\": \"2021-03-04T00:49:00Z\",\n    \"id\": \"10000000-0000-0000-0000-000000000000\",\n    \"name\": \"_acme-challenge.host\",\n    \"records\": [\n      {\n        \"text\": \"O2UTPYgIzRNt5N27EVcNKDxv6goSF7ru3zi3chZXKUw\"\n      }\n    ],\n    \"status\": \"Active\",\n    \"ttl\": 30,\n    \"type\": \"TXT\",\n    \"updated\": \"2021-03-04T00:49:00Z\",\n    \"zoneId\": \"00000000-0000-0000-0000-000000000000\"\n  },\n  \"singleBatchChangeIds\": [],\n  \"status\": \"Complete\",\n  \"userId\": \"50000000-0000-0000-0000-000000000000\",\n  \"zone\": {\n    \"account\": \"system\",\n    \"acl\": {\n      \"rules\": []\n    },\n    \"adminGroupId\": \"40000000-0000-0000-0000-000000000000\",\n    \"created\": \"2020-07-15T21:15:36Z\",\n    \"email\": \"Ops@company.invalid\",\n    \"id\": \"00000000-0000-0000-0000-000000000000\",\n    \"isTest\": false,\n    \"latestSync\": \"2020-07-15T21:15:36Z\",\n    \"name\": \"example.com.\",\n    \"shared\": false,\n    \"status\": \"Active\",\n    \"updated\": \"2021-03-03T18:02:47Z\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/vinyldns/fixtures/recordSetChange-delete.json",
    "content": "{\n  \"changeType\": \"Delete\",\n  \"created\": \"2021-03-04T00:49:00Z\",\n  \"id\": \"27ba5c17-a217-4e8d-b662-b1dc8bee588f\",\n  \"recordSet\": {\n    \"account\": \"\",\n    \"created\": \"2021-03-04T00:49:00Z\",\n    \"id\": \"10000000-0000-0000-0000-000000000000\",\n    \"name\": \"_acme-challenge.host\",\n    \"records\": [\n      {\n        \"text\": \"O2UTPYgIzRNt5N27EVcNKDxv6goSF7ru3zi3chZXKUw\"\n      }\n    ],\n    \"status\": \"Active\",\n    \"ttl\": 30,\n    \"type\": \"TXT\",\n    \"updated\": \"2021-03-04T00:49:00Z\",\n    \"zoneId\": \"00000000-0000-0000-0000-000000000000\"\n  },\n  \"singleBatchChangeIds\": [],\n  \"status\": \"Complete\",\n  \"userId\": \"50000000-0000-0000-0000-000000000000\",\n  \"zone\": {\n    \"account\": \"system\",\n    \"acl\": {\n      \"rules\": []\n    },\n    \"adminGroupId\": \"40000000-0000-0000-0000-000000000000\",\n    \"created\": \"2020-07-15T21:15:36Z\",\n    \"email\": \"Ops@company.invalid\",\n    \"id\": \"00000000-0000-0000-0000-000000000000\",\n    \"isTest\": false,\n    \"latestSync\": \"2020-07-15T21:15:36Z\",\n    \"name\": \"example.com.\",\n    \"shared\": false,\n    \"status\": \"Active\",\n    \"updated\": \"2021-03-03T18:02:47Z\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/vinyldns/fixtures/recordSetDelete.json",
    "content": "{\n  \"changeType\": \"Delete\",\n  \"created\": \"2021-03-04T16:21:54Z\",\n  \"id\": \"20000000-0000-0000-0000-000000000000\",\n  \"recordSet\": {\n    \"account\": \"\",\n    \"created\": \"2021-03-04T16:21:54Z\",\n    \"id\": \"11000000-0000-0000-0000-000000000000\",\n    \"name\": \"_acme-challenge.host\",\n    \"records\": [\n      {\n        \"text\": \"O2UTPYgIzRNt5N27EVcNKDxv6goSF7ru3zi3chZXKUw\"\n      }\n    ],\n    \"status\": \"Pending\",\n    \"ttl\": 30,\n    \"type\": \"TXT\",\n    \"zoneId\": \"00000000-0000-0000-0000-000000000000\"\n  },\n  \"singleBatchChangeIds\": [],\n  \"status\": \"Pending\",\n  \"userId\": \"50000000-0000-0000-0000-000000000000\",\n  \"zone\": {\n    \"account\": \"system\",\n    \"acl\": {\n      \"rules\": []\n    },\n    \"adminGroupId\": \"40000000-0000-0000-0000-000000000000\",\n    \"created\": \"2020-07-15T21:15:36Z\",\n    \"email\": \"Ops@company.invalid\",\n    \"id\": \"00000000-0000-0000-0000-000000000000\",\n    \"isTest\": false,\n    \"latestSync\": \"2020-07-15T21:15:36Z\",\n    \"name\": \"example.com.\",\n    \"shared\": false,\n    \"status\": \"Active\",\n    \"updated\": \"2021-03-03T18:02:47Z\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/vinyldns/fixtures/recordSetUpdate-create.json",
    "content": "{\n  \"changeType\": \"Create\",\n  \"created\": \"2021-03-04T16:21:54Z\",\n  \"id\": \"20000000-0000-0000-0000-000000000000\",\n  \"recordSet\": {\n    \"account\": \"\",\n    \"created\": \"2021-03-04T16:21:54Z\",\n    \"id\": \"11000000-0000-0000-0000-000000000000\",\n    \"name\": \"_acme-challenge.host\",\n    \"records\": [\n      {\n        \"text\": \"O2UTPYgIzRNt5N27EVcNKDxv6goSF7ru3zi3chZXKUw\"\n      }\n    ],\n    \"status\": \"Pending\",\n    \"ttl\": 30,\n    \"type\": \"TXT\",\n    \"zoneId\": \"00000000-0000-0000-0000-000000000000\"\n  },\n  \"singleBatchChangeIds\": [],\n  \"status\": \"Pending\",\n  \"userId\": \"50000000-0000-0000-0000-000000000000\",\n  \"zone\": {\n    \"account\": \"system\",\n    \"acl\": {\n      \"rules\": []\n    },\n    \"adminGroupId\": \"40000000-0000-0000-0000-000000000000\",\n    \"created\": \"2020-07-15T21:15:36Z\",\n    \"email\": \"Ops@company.invalid\",\n    \"id\": \"00000000-0000-0000-0000-000000000000\",\n    \"isTest\": false,\n    \"latestSync\": \"2020-07-15T21:15:36Z\",\n    \"name\": \"example.com.\",\n    \"shared\": false,\n    \"status\": \"Active\",\n    \"updated\": \"2021-03-03T18:02:47Z\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/vinyldns/fixtures/recordSetsListAll-empty.json",
    "content": "{\n  \"maxItems\": 100,\n  \"nameSort\": \"ASC\",\n  \"recordNameFilter\": \"_acme-challenge.host\",\n  \"recordSets\": []\n}\n"
  },
  {
    "path": "providers/dns/vinyldns/fixtures/recordSetsListAll.json",
    "content": "{\n  \"maxItems\": 100,\n  \"nameSort\": \"ASC\",\n  \"recordNameFilter\": \"_acme-challenge.host\",\n  \"recordSets\": [\n    {\n      \"accessLevel\": \"Delete\",\n      \"account\": \"\",\n      \"created\": \"2021-03-04T00:51:43Z\",\n      \"fqdn\": \"_acme-challenge.host.example.com.\",\n      \"id\": \"30000000-0000-0000-0000-000000000000\",\n      \"name\": \"_acme-challenge.host\",\n      \"records\": [\n        {\n          \"text\": \"O2UTPYgIzRNt5N27EVcNKDxv6goSF7ru3zi3chZXKUw\"\n        }\n      ],\n      \"status\": \"Active\",\n      \"ttl\": 30,\n      \"type\": \"TXT\",\n      \"updated\": \"2021-03-04T00:51:43Z\",\n      \"zoneId\": \"00000000-0000-0000-0000-000000000000\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/vinyldns/fixtures/zoneByName.json",
    "content": "{\n  \"zone\": {\n    \"accessLevel\": \"Delete\",\n    \"account\": \"system\",\n    \"acl\": {\n      \"rules\": []\n    },\n    \"adminGroupId\": \"40000000-0000-0000-0000-000000000000\",\n    \"adminGroupName\": \"OpsTeam\",\n    \"created\": \"2020-07-15T21:15:36Z\",\n    \"email\": \"Ops@company.invalid\",\n    \"id\": \"00000000-0000-0000-0000-000000000000\",\n    \"latestSync\": \"2020-07-15T21:15:36Z\",\n    \"name\": \"example.com.\",\n    \"shared\": false,\n    \"status\": \"Active\",\n    \"updated\": \"2021-03-03T18:02:47Z\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/vinyldns/vinyldns.go",
    "content": "// Package vinyldns implements a DNS provider for solving the DNS-01 challenge using VinylDNS.\npackage vinyldns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n\t\"github.com/vinyldns/go-vinyldns/vinyldns\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"VINYLDNS_\"\n\n\tEnvAccessKey  = envNamespace + \"ACCESS_KEY\"\n\tEnvSecretKey  = envNamespace + \"SECRET_KEY\"\n\tEnvHost       = envNamespace + \"HOST\"\n\tEnvQuoteValue = envNamespace + \"QUOTE_VALUE\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAccessKey  string\n\tSecretKey  string\n\tHost       string\n\tQuoteValue bool\n\n\tTTL                int\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 30),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *vinyldns.Client\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for VinylDNS.\n// Credentials must be passed in the environment variables:\n// VINYLDNS_ACCESS_KEY, VINYLDNS_SECRET_KEY, VINYLDNS_HOST.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAccessKey, EnvSecretKey, EnvHost)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vinyldns: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AccessKey = values[EnvAccessKey]\n\tconfig.SecretKey = values[EnvSecretKey]\n\tconfig.Host = values[EnvHost]\n\tconfig.QuoteValue = env.GetOrDefaultBool(EnvQuoteValue, false)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for VinylDNS.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"vinyldns: the configuration of the VinylDNS DNS provider is nil\")\n\t}\n\n\tif config.AccessKey == \"\" || config.SecretKey == \"\" {\n\t\treturn nil, errors.New(\"vinyldns: credentials are missing\")\n\t}\n\n\tif config.Host == \"\" {\n\t\treturn nil, errors.New(\"vinyldns: host is missing\")\n\t}\n\n\tclient := vinyldns.NewClient(vinyldns.ClientConfiguration{\n\t\tAccessKey: config.AccessKey,\n\t\tSecretKey: config.SecretKey,\n\t\tHost:      config.Host,\n\t\tUserAgent: useragent.Get(),\n\t})\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t} else {\n\t\t// For compatibility, it should be removed in v5.\n\t\tclient.HTTPClient.Timeout = 30 * time.Second\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\texistingRecord, err := d.getRecordSet(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vinyldns: %w\", err)\n\t}\n\n\tvalue := d.formatValue(info.Value)\n\n\trecord := vinyldns.Record{Text: value}\n\n\tif existingRecord == nil || existingRecord.ID == \"\" {\n\t\terr = d.createRecordSet(ctx, info.EffectiveFQDN, []vinyldns.Record{record})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"vinyldns: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfor _, i := range existingRecord.Records {\n\t\tif i.Text == value {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\trecords := existingRecord.Records\n\trecords = append(records, record)\n\n\terr = d.updateRecordSet(ctx, existingRecord, records)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vinyldns: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\texistingRecord, err := d.getRecordSet(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vinyldns: %w\", err)\n\t}\n\n\tif existingRecord == nil || existingRecord.ID == \"\" || len(existingRecord.Records) == 0 {\n\t\treturn nil\n\t}\n\n\tvalue := d.formatValue(info.Value)\n\n\tvar records []vinyldns.Record\n\n\tfor _, i := range existingRecord.Records {\n\t\tif i.Text != value {\n\t\t\trecords = append(records, i)\n\t\t}\n\t}\n\n\tif len(records) == 0 {\n\t\terr = d.deleteRecordSet(ctx, existingRecord)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"vinyldns: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\terr = d.updateRecordSet(ctx, existingRecord, records)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vinyldns: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) formatValue(v string) string {\n\tif d.config.QuoteValue {\n\t\treturn strconv.Quote(v)\n\t}\n\n\treturn v\n}\n"
  },
  {
    "path": "providers/dns/vinyldns/vinyldns.toml",
    "content": "Name = \"VinylDNS\"\nDescription = ''''''\nURL = \"https://www.vinyldns.io\"\nCode = \"vinyldns\"\nSince = \"v4.4.0\"\n\nExample = '''\nVINYLDNS_ACCESS_KEY=xxxxxx \\\nVINYLDNS_SECRET_KEY=yyyyy \\\nVINYLDNS_HOST=https://api.vinyldns.example.org:9443 \\\nlego --dns vinyldns -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\nThe vinyldns integration makes use of dotted hostnames to ease permission management.\nUsers are required to have DELETE ACL level or zone admin permissions on the VinylDNS zone containing the target host.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    VINYLDNS_ACCESS_KEY = \"The VinylDNS API key\"\n    VINYLDNS_SECRET_KEY = \"The VinylDNS API Secret key\"\n    VINYLDNS_HOST = \"The VinylDNS API URL\"\n  [Configuration.Additional]\n    VINYLDNS_QUOTE_VALUE = \"Adds quotes around the TXT record value (Default: false)\"\n    VINYLDNS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 4)\"\n    VINYLDNS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    VINYLDNS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 30)\"\n    VINYLDNS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.vinyldns.io/api/\"\n  GoClient = \"https://github.com/vinyldns/go-vinyldns\"\n"
  },
  {
    "path": "providers/dns/vinyldns/vinyldns_test.go",
    "content": "package vinyldns\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nconst (\n\ttargetRootDomain  = \"example.com\"\n\ttargetDomain      = \"host.\" + targetRootDomain\n\tzoneID            = \"00000000-0000-0000-0000-000000000000\"\n\tnewRecordSetID    = \"11000000-0000-0000-0000-000000000000\"\n\tnewCreateChangeID = \"20000000-0000-0000-0000-000000000000\"\n\trecordID          = \"30000000-0000-0000-0000-000000000000\"\n)\n\nvar envTest = tester.NewEnvTest(\n\tEnvAccessKey,\n\tEnvSecretKey,\n\tEnvHost).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKey: \"123\",\n\t\t\t\tEnvSecretKey: \"456\",\n\t\t\t\tEnvHost:      \"https://example.org\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing all credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvHost: \"https://example.org\",\n\t\t\t},\n\t\t\texpected: \"vinyldns: some credentials information are missing: VINYLDNS_ACCESS_KEY,VINYLDNS_SECRET_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretKey: \"456\",\n\t\t\t\tEnvHost:      \"https://example.org\",\n\t\t\t},\n\t\t\texpected: \"vinyldns: some credentials information are missing: VINYLDNS_ACCESS_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKey: \"123\",\n\t\t\t\tEnvHost:      \"https://example.org\",\n\t\t\t},\n\t\t\texpected: \"vinyldns: some credentials information are missing: VINYLDNS_SECRET_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing host\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKey: \"123\",\n\t\t\t\tEnvSecretKey: \"456\",\n\t\t\t},\n\t\t\texpected: \"vinyldns: some credentials information are missing: VINYLDNS_HOST\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\taccessKey string\n\t\tsecretKey string\n\t\thost      string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\taccessKey: \"123\",\n\t\t\tsecretKey: \"456\",\n\t\t\thost:      \"https://example.org\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing all credentials\",\n\t\t\thost:     \"https://example.org\",\n\t\t\texpected: \"vinyldns: credentials are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing access key\",\n\t\t\tsecretKey: \"456\",\n\t\t\thost:      \"https://example.org\",\n\t\t\texpected:  \"vinyldns: credentials are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing secret key\",\n\t\t\taccessKey: \"123\",\n\t\t\thost:      \"https://example.org\",\n\t\t\texpected:  \"vinyldns: credentials are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing host\",\n\t\t\taccessKey: \"123\",\n\t\t\tsecretKey: \"456\",\n\t\t\texpected:  \"vinyldns: host is missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AccessKey = test.accessKey\n\t\t\tconfig.SecretKey = test.secretKey\n\t\t\tconfig.Host = test.host\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {\n\t\tconfig := NewDefaultConfig()\n\t\tconfig.AccessKey = \"foo\"\n\t\tconfig.SecretKey = \"bar\"\n\t\tconfig.Host = server.URL\n\t\tconfig.HTTPClient = server.Client()\n\n\t\treturn NewDNSProviderConfig(config)\n\t})\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc    string\n\t\tkeyAuth string\n\t\tbuilder *servermock.Builder[*DNSProvider]\n\t}{\n\t\t{\n\t\t\tdesc:    \"new record\",\n\t\t\tkeyAuth: \"123456d==\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"GET /zones/name/\"+targetRootDomain+\".\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"zoneByName.json\")).\n\t\t\t\tRoute(\"GET /zones/\"+zoneID+\"/recordsets\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"recordSetsListAll-empty.json\")).\n\t\t\t\tRoute(\"POST /zones/\"+zoneID+\"/recordsets\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"recordSetUpdate-create.json\").\n\t\t\t\t\t\tWithStatusCode(http.StatusAccepted)).\n\t\t\t\tRoute(\"GET /zones/\"+zoneID+\"/recordsets/\"+newRecordSetID+\"/changes/\"+newCreateChangeID,\n\t\t\t\t\tservermock.ResponseFromFixture(\"recordSetChange-create.json\")),\n\t\t},\n\t\t{\n\t\t\tdesc:    \"existing record\",\n\t\t\tkeyAuth: \"123456d==\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"GET /zones/name/\"+targetRootDomain+\".\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"zoneByName.json\")).\n\t\t\t\tRoute(\"GET /zones/\"+zoneID+\"/recordsets\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"recordSetsListAll.json\")),\n\t\t},\n\t\t{\n\t\t\tdesc:    \"duplicate key\",\n\t\t\tkeyAuth: \"abc123!!\",\n\t\t\tbuilder: mockBuilder().\n\t\t\t\tRoute(\"GET /zones/name/\"+targetRootDomain+\".\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"zoneByName.json\")).\n\t\t\t\tRoute(\"GET /zones/\"+zoneID+\"/recordsets\",\n\t\t\t\t\tservermock.ResponseFromFixture(\"recordSetsListAll.json\")).\n\t\t\t\tRoute(\"PUT /zones/\"+zoneID+\"/recordsets/\"+recordID,\n\t\t\t\t\tservermock.ResponseFromFixture(\"recordSetUpdate-create.json\").\n\t\t\t\t\t\tWithStatusCode(http.StatusAccepted)).\n\t\t\t\tRoute(\"GET /zones/\"+zoneID+\"/recordsets/\"+newRecordSetID+\"/changes/\"+newCreateChangeID,\n\t\t\t\t\tservermock.ResponseFromFixture(\"recordSetChange-create.json\")),\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tprovider := test.builder.Build(t)\n\n\t\t\terr := provider.Present(targetDomain, \"token\"+test.keyAuth, test.keyAuth)\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"GET /zones/name/\"+targetRootDomain+\".\",\n\t\t\tservermock.ResponseFromFixture(\"zoneByName.json\")).\n\t\tRoute(\"GET /zones/\"+zoneID+\"/recordsets\",\n\t\t\tservermock.ResponseFromFixture(\"recordSetsListAll.json\")).\n\t\tRoute(\"DELETE /zones/\"+zoneID+\"/recordsets/\"+recordID,\n\t\t\tservermock.ResponseFromFixture(\"recordSetDelete.json\").\n\t\t\t\tWithStatusCode(http.StatusAccepted)).\n\t\tRoute(\"GET /zones/\"+zoneID+\"/recordsets/\"+newRecordSetID+\"/changes/\"+newCreateChangeID,\n\t\t\tservermock.ResponseFromFixture(\"recordSetChange-delete.json\")).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(targetDomain, \"123456d==\", \"123456d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/vinyldns/wrapper.go",
    "content": "package vinyldns\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/cenkalti/backoff/v5\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/wait\"\n\t\"github.com/vinyldns/go-vinyldns/vinyldns\"\n)\n\nfunc (d *DNSProvider) getRecordSet(fqdn string) (*vinyldns.RecordSet, error) {\n\tzoneName, hostName, err := splitDomain(fqdn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tzone, err := d.client.ZoneByName(zoneName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tallRecordSets, err := d.client.RecordSetsListAll(zone.ID, vinyldns.ListFilter{NameFilter: hostName})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar recordSets []vinyldns.RecordSet\n\n\tfor _, i := range allRecordSets {\n\t\tif i.Type == \"TXT\" {\n\t\t\trecordSets = append(recordSets, i)\n\t\t}\n\t}\n\n\tswitch {\n\tcase len(recordSets) > 1:\n\t\treturn nil, fmt.Errorf(\"ambiguous recordset definition of %s\", fqdn)\n\tcase len(recordSets) == 1:\n\t\treturn &recordSets[0], nil\n\tdefault:\n\t\treturn nil, nil\n\t}\n}\n\nfunc (d *DNSProvider) createRecordSet(ctx context.Context, fqdn string, records []vinyldns.Record) error {\n\tzoneName, hostName, err := splitDomain(fqdn)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tzone, err := d.client.ZoneByName(zoneName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trecordSet := vinyldns.RecordSet{\n\t\tName:    hostName,\n\t\tZoneID:  zone.ID,\n\t\tType:    \"TXT\",\n\t\tTTL:     d.config.TTL,\n\t\tRecords: records,\n\t}\n\n\tresp, err := d.client.RecordSetCreate(&recordSet)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn d.waitForChanges(ctx, \"CreateRS\", resp)\n}\n\nfunc (d *DNSProvider) updateRecordSet(ctx context.Context, recordSet *vinyldns.RecordSet, newRecords []vinyldns.Record) error {\n\toperation := \"delete\"\n\tif len(recordSet.Records) < len(newRecords) {\n\t\toperation = \"add\"\n\t}\n\n\trecordSet.Records = newRecords\n\trecordSet.TTL = d.config.TTL\n\n\tresp, err := d.client.RecordSetUpdate(recordSet)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn d.waitForChanges(ctx, \"UpdateRS - \"+operation, resp)\n}\n\nfunc (d *DNSProvider) deleteRecordSet(ctx context.Context, existingRecord *vinyldns.RecordSet) error {\n\tresp, err := d.client.RecordSetDelete(existingRecord.ZoneID, existingRecord.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn d.waitForChanges(ctx, \"DeleteRS\", resp)\n}\n\nfunc (d *DNSProvider) waitForChanges(ctx context.Context, operation string, resp *vinyldns.RecordSetUpdateResponse) error {\n\treturn wait.Retry(ctx,\n\t\tfunc() error {\n\t\t\tchange, err := d.client.RecordSetChange(resp.Zone.ID, resp.RecordSet.ID, resp.ChangeID)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to query change status: %w\", err)\n\t\t\t}\n\n\t\t\tif change.Status != \"Complete\" {\n\t\t\t\treturn fmt.Errorf(\"waiting operation: %s, zoneID: %s, recordsetID: %s, changeID: %s\",\n\t\t\t\t\toperation, resp.Zone.ID, resp.RecordSet.ID, resp.ChangeID)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t\tbackoff.WithBackOff(backoff.NewConstantBackOff(d.config.PollingInterval)),\n\t\tbackoff.WithMaxElapsedTime(d.config.PropagationTimeout),\n\t)\n}\n\n// splitDomain splits the hostname from the authoritative zone, and returns both parts.\nfunc splitDomain(fqdn string) (string, string, error) {\n\tzone, err := dns01.FindZoneByFqdn(fqdn)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"could not find zone: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, zone)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\treturn zone, subDomain, nil\n}\n"
  },
  {
    "path": "providers/dns/virtualname/virtualname.go",
    "content": "// Package virtualname implements a DNS provider for solving the DNS-01 challenge using Virtualname DNS.\npackage virtualname\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/tecnocratica\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"VIRTUALNAME_\"\n\n\tEnvToken = envNamespace + \"TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultBaseURL = \"https://api.virtualname.net/v1\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config = tecnocratica.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Virtualname.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"virtualname: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Virtualname.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"virtualname: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := tecnocratica.NewDNSProviderConfig(config, defaultBaseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"virtualname: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"virtualname: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"virtualname: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n"
  },
  {
    "path": "providers/dns/virtualname/virtualname.toml",
    "content": "Name = \"Virtualname\"\nDescription = ''''''\nURL = \"https://www.virtualname.es/\"\nCode = \"virtualname\"\nSince = \"v4.30.0\"\n\nExample = '''\nVIRTUALNAME_TOKEN=xxxxxx \\\nlego --dns virtualname -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    VIRTUALNAME_TOKEN = \"API token\"\n  [Configuration.Additional]\n    VIRTUALNAME_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    VIRTUALNAME_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 300)\"\n    VIRTUALNAME_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    VIRTUALNAME_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://developers.virtualname.net/#dns\"\n"
  },
  {
    "path": "providers/dns/virtualname/virtualname_test.go",
    "content": "package virtualname\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials: token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvToken: \"\",\n\t\t\t},\n\t\t\texpected: \"virtualname: some credentials information are missing: VIRTUALNAME_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing token\",\n\t\t\texpected: \"virtualname: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/vkcloud/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/gophercloud/gophercloud\"\n\t\"github.com/gophercloud/gophercloud/openstack\"\n)\n\n// Client VK client.\ntype Client struct {\n\topenstack     *gophercloud.ProviderClient\n\tauthOpts      gophercloud.AuthOptions\n\tauthenticated bool\n\tbaseURL       *url.URL\n}\n\n// NewClient creates a Client.\nfunc NewClient(endpoint string, authOpts gophercloud.AuthOptions) (*Client, error) {\n\terr := validateAuthOptions(authOpts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\topenstackClient, err := openstack.NewClient(authOpts.IdentityEndpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"new client: %w\", err)\n\t}\n\n\tbaseURL, err := url.Parse(endpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parse URL: %w\", err)\n\t}\n\n\treturn &Client{\n\t\topenstack: openstackClient,\n\t\tauthOpts:  authOpts,\n\t\tbaseURL:   baseURL,\n\t}, nil\n}\n\nfunc (c *Client) ListZones() ([]DNSZone, error) {\n\tendpoint := c.baseURL.JoinPath(\"/\")\n\n\tvar zones []DNSZone\n\n\topts := &gophercloud.RequestOpts{JSONResponse: &zones}\n\n\terr := c.request(http.MethodGet, endpoint, opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn zones, nil\n}\n\nfunc (c *Client) ListTXTRecords(zoneUUID string) ([]DNSTXTRecord, error) {\n\tendpoint := c.baseURL.JoinPath(zoneUUID, \"txt\", \"/\")\n\n\tvar records []DNSTXTRecord\n\n\topts := &gophercloud.RequestOpts{JSONResponse: &records}\n\n\terr := c.request(http.MethodGet, endpoint, opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn records, nil\n}\n\nfunc (c *Client) CreateTXTRecord(zoneUUID string, record *DNSTXTRecord) error {\n\tendpoint := c.baseURL.JoinPath(zoneUUID, \"txt\", \"/\")\n\n\topts := &gophercloud.RequestOpts{\n\t\tJSONBody:     record,\n\t\tJSONResponse: record,\n\t}\n\n\treturn c.request(http.MethodPost, endpoint, opts)\n}\n\nfunc (c *Client) DeleteTXTRecord(zoneUUID, recordUUID string) error {\n\tendpoint := c.baseURL.JoinPath(zoneUUID, \"txt\", recordUUID)\n\n\treturn c.request(http.MethodDelete, endpoint, &gophercloud.RequestOpts{})\n}\n\nfunc (c *Client) request(method string, endpoint *url.URL, options *gophercloud.RequestOpts) error {\n\tif err := c.lazyAuth(); err != nil {\n\t\treturn fmt.Errorf(\"auth: %w\", err)\n\t}\n\n\t_, err := c.openstack.Request(method, endpoint.String(), options)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"request: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) lazyAuth() error {\n\tif c.authenticated {\n\t\treturn nil\n\t}\n\n\terr := openstack.Authenticate(c.openstack, c.authOpts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.authenticated = true\n\n\treturn nil\n}\n\nfunc validateAuthOptions(opts gophercloud.AuthOptions) error {\n\tif opts.TenantID == \"\" {\n\t\treturn errors.New(\"project id is missing in credentials information\")\n\t}\n\n\tif opts.Username == \"\" {\n\t\treturn errors.New(\"username is missing in credentials information\")\n\t}\n\n\tif opts.Password == \"\" {\n\t\treturn errors.New(\"password is missing in credentials information\")\n\t}\n\n\tif opts.IdentityEndpoint == \"\" {\n\t\treturn errors.New(\"identity endpoint is missing in config\")\n\t}\n\n\tif opts.DomainName == \"\" {\n\t\treturn errors.New(\"domain name is missing in config\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/vkcloud/internal/types.go",
    "content": "package internal\n\ntype DNSZone struct {\n\tUUID          string `json:\"uuid,omitempty\"`\n\tTenant        string `json:\"tenant,omitempty\"`\n\tSoaPrimaryDNS string `json:\"soa_primary_dns,omitempty\"`\n\tSoaAdminEmail string `json:\"soa_admin_email,omitempty\"`\n\tSoaSerial     int    `json:\"soa_serial,omitempty\"`\n\tSoaRefresh    int    `json:\"soa_refresh,omitempty\"`\n\tSoaRetry      int    `json:\"soa_retry,omitempty\"`\n\tSoaExpire     int    `json:\"soa_expire,omitempty\"`\n\tSoaTTL        int    `json:\"soa_ttl,omitempty\"`\n\tZone          string `json:\"zone,omitempty\"`\n\tStatus        string `json:\"status,omitempty\"`\n}\n\ntype DNSTXTRecord struct {\n\tUUID    string `json:\"uuid,omitempty\"`\n\tName    string `json:\"name,omitempty\"`\n\tDNS     string `json:\"dns,omitempty\"`\n\tContent string `json:\"content,omitempty\"`\n\tTTL     int    `json:\"ttl,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/vkcloud/vkcloud.go",
    "content": "// Package vkcloud implements a DNS provider for solving the DNS-01 challenge using VK Cloud.\npackage vkcloud\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/vkcloud/internal\"\n\t\"github.com/gophercloud/gophercloud\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"VK_CLOUD_\"\n\n\tEnvDNSEndpoint = envNamespace + \"DNS_ENDPOINT\"\n\n\tEnvIdentityEndpoint = envNamespace + \"IDENTITY_ENDPOINT\"\n\tEnvDomainName       = envNamespace + \"DOMAIN_NAME\"\n\n\tEnvProjectID = envNamespace + \"PROJECT_ID\"\n\tEnvUsername  = envNamespace + \"USERNAME\"\n\tEnvPassword  = envNamespace + \"PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nconst (\n\tdefaultIdentityEndpoint = \"https://infra.mail.ru/identity/v3/\"\n\tdefaultDNSEndpoint      = \"https://mcs.mail.ru/public-dns/v2/dns\"\n)\n\nconst defaultDomainName = \"users\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tProjectID string\n\tUsername  string\n\tPassword  string\n\n\tDNSEndpoint string\n\n\tIdentityEndpoint string\n\tDomainName       string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 60),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *internal.Client\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for VK Cloud.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvProjectID, EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vkcloud: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.ProjectID = values[EnvProjectID]\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\tconfig.IdentityEndpoint = env.GetOrDefaultString(EnvIdentityEndpoint, defaultIdentityEndpoint)\n\tconfig.DomainName = env.GetOrDefaultString(EnvDomainName, defaultDomainName)\n\tconfig.DNSEndpoint = env.GetOrDefaultString(EnvDNSEndpoint, defaultDNSEndpoint)\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for VK Cloud.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"vkcloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.DNSEndpoint == \"\" {\n\t\treturn nil, errors.New(\"vkcloud: DNS endpoint is missing in config\")\n\t}\n\n\tauthOpts := gophercloud.AuthOptions{\n\t\tIdentityEndpoint: config.IdentityEndpoint,\n\t\tUsername:         config.Username,\n\t\tPassword:         config.Password,\n\t\tDomainName:       config.DomainName,\n\t\tTenantID:         config.ProjectID,\n\t}\n\n\tclient, err := internal.NewClient(config.DNSEndpoint, authOpts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vkcloud: unable to build VK Cloud client: %w\", err)\n\t}\n\n\treturn &DNSProvider{\n\t\tclient: client,\n\t\tconfig: config,\n\t}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vkcloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tzones, err := d.client.ListZones()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vkcloud: unable to fetch dns zones: %w\", err)\n\t}\n\n\tvar zoneUUID string\n\n\tfor _, zone := range zones {\n\t\tif zone.Zone == authZone {\n\t\t\tzoneUUID = zone.UUID\n\t\t}\n\t}\n\n\tif zoneUUID == \"\" {\n\t\treturn fmt.Errorf(\"vkcloud: cant find dns zone %s in VK Cloud\", authZone)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vkcloud: %w\", err)\n\t}\n\n\terr = d.upsertTXTRecord(zoneUUID, subDomain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vkcloud: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vkcloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tzones, err := d.client.ListZones()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vkcloud: unable to fetch dns zones: %w\", err)\n\t}\n\n\tvar zoneUUID string\n\n\tfor _, zone := range zones {\n\t\tif zone.Zone == authZone {\n\t\t\tzoneUUID = zone.UUID\n\t\t}\n\t}\n\n\tif zoneUUID == \"\" {\n\t\treturn nil\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vkcloud: %w\", err)\n\t}\n\n\terr = d.removeTXTRecord(zoneUUID, subDomain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vkcloud: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) upsertTXTRecord(zoneUUID, name, value string) error {\n\trecords, err := d.client.ListTXTRecords(zoneUUID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, record := range records {\n\t\tif record.Name == name && record.Content == value {\n\t\t\t// The DNSRecord is already present, nothing to do\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn d.client.CreateTXTRecord(zoneUUID, &internal.DNSTXTRecord{\n\t\tName:    name,\n\t\tContent: value,\n\t\tTTL:     d.config.TTL,\n\t})\n}\n\nfunc (d *DNSProvider) removeTXTRecord(zoneUUID, name, value string) error {\n\trecords, err := d.client.ListTXTRecords(zoneUUID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tname = dns01.UnFqdn(name)\n\tfor _, record := range records {\n\t\tif record.Name == name && record.Content == value {\n\t\t\treturn d.client.DeleteTXTRecord(zoneUUID, record.UUID)\n\t\t}\n\t}\n\n\t// The DNSRecord is not present, nothing to do\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/vkcloud/vkcloud.toml",
    "content": "Name = \"VK Cloud\"\nDescription = ''''''\nURL = \"https://mcs.mail.ru/\"\nCode = \"vkcloud\"\nSince = \"v4.9.0\"\n\nExample = '''\nVK_CLOUD_PROJECT_ID=\"<your_project_id>\" \\\nVK_CLOUD_USERNAME=\"<your_email>\" \\\nVK_CLOUD_PASSWORD=\"<your_password>\" \\\nlego --dns vkcloud -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## Credential information\n\nYou can find all required and additional information on [\"Project/Keys\" page](https://mcs.mail.ru/app/en/project/keys) of your cloud.\n\n| ENV Variable               | Parameter from page |\n|----------------------------|---------------------|\n| VK_CLOUD_PROJECT_ID        | Project ID          |\n| VK_CLOUD_USERNAME          | Username            |\n| VK_CLOUD_DOMAIN_NAME       | User Domain Name    |\n| VK_CLOUD_IDENTITY_ENDPOINT | Identity endpoint   |\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    VK_CLOUD_PROJECT_ID = \"String ID of project in VK Cloud\"\n    VK_CLOUD_USERNAME = \"Email of VK Cloud account\"\n    VK_CLOUD_PASSWORD = \"Password for VK Cloud account\"\n  [Configuration.Additional]\n    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\"\n    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\"\n    VK_CLOUD_DOMAIN_NAME=\"Openstack users domain name. Defaults to `users` but can be changed for usage with private clouds\"\n    VK_CLOUD_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    VK_CLOUD_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    VK_CLOUD_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n\n[Links]\n  API = \"https://mcs.mail.ru/docs/networks/vnet/networks/publicdns/api\"\n"
  },
  {
    "path": "providers/dns/vkcloud/vkcloud_test.go",
    "content": "package vkcloud\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nconst (\n\tfakeProjectID = \"an_project_id_from_vk_cloud_ui\"\n\tfakeUsername  = \"vkclouduser@email.address\"\n\tfakePasswd    = \"vkcloudpasswd\"\n)\n\nvar envTest = tester.NewEnvTest(EnvProjectID, EnvUsername, EnvPassword).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvProjectID: fakeProjectID,\n\t\t\t\tEnvUsername:  fakeUsername,\n\t\t\t\tEnvPassword:  fakePasswd,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing project id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: fakeUsername,\n\t\t\t\tEnvPassword: fakePasswd,\n\t\t\t},\n\t\t\texpected: \"vkcloud: some credentials information are missing: VK_CLOUD_PROJECT_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvProjectID: fakeProjectID,\n\t\t\t\tEnvPassword:  fakePasswd,\n\t\t\t},\n\t\t\texpected: \"vkcloud: some credentials information are missing: VK_CLOUD_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvProjectID: fakeProjectID,\n\t\t\t\tEnvUsername:  fakeUsername,\n\t\t\t},\n\t\t\texpected: \"vkcloud: some credentials information are missing: VK_CLOUD_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tconfig   *Config\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tconfig: &Config{\n\t\t\t\tProjectID:        fakeProjectID,\n\t\t\t\tUsername:         fakeUsername,\n\t\t\t\tPassword:         fakePasswd,\n\t\t\t\tDNSEndpoint:      defaultDNSEndpoint,\n\t\t\t\tIdentityEndpoint: defaultIdentityEndpoint,\n\t\t\t\tDomainName:       defaultDomainName,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"nil config\",\n\t\t\tconfig:   nil,\n\t\t\texpected: \"vkcloud: the configuration of the DNS provider is nil\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing project id\",\n\t\t\tconfig: &Config{\n\t\t\t\tUsername:         fakeUsername,\n\t\t\t\tPassword:         fakePasswd,\n\t\t\t\tDNSEndpoint:      defaultDNSEndpoint,\n\t\t\t\tIdentityEndpoint: defaultIdentityEndpoint,\n\t\t\t\tDomainName:       defaultDomainName,\n\t\t\t},\n\t\t\texpected: \"vkcloud: unable to build VK Cloud client: project id is missing in credentials information\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tconfig: &Config{\n\t\t\t\tProjectID:        fakeProjectID,\n\t\t\t\tPassword:         fakePasswd,\n\t\t\t\tDNSEndpoint:      defaultDNSEndpoint,\n\t\t\t\tIdentityEndpoint: defaultIdentityEndpoint,\n\t\t\t\tDomainName:       defaultDomainName,\n\t\t\t},\n\t\t\texpected: \"vkcloud: unable to build VK Cloud client: username is missing in credentials information\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tconfig: &Config{\n\t\t\t\tProjectID:        fakeProjectID,\n\t\t\t\tUsername:         fakeUsername,\n\t\t\t\tDNSEndpoint:      defaultDNSEndpoint,\n\t\t\t\tIdentityEndpoint: defaultIdentityEndpoint,\n\t\t\t\tDomainName:       defaultDomainName,\n\t\t\t},\n\t\t\texpected: \"vkcloud: unable to build VK Cloud client: password is missing in credentials information\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing dns endpoint\",\n\t\t\tconfig: &Config{\n\t\t\t\tProjectID:        fakeProjectID,\n\t\t\t\tUsername:         fakeUsername,\n\t\t\t\tPassword:         fakePasswd,\n\t\t\t\tIdentityEndpoint: defaultIdentityEndpoint,\n\t\t\t\tDomainName:       defaultDomainName,\n\t\t\t},\n\t\t\texpected: \"vkcloud: DNS endpoint is missing in config\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing identity endpoint\",\n\t\t\tconfig: &Config{\n\t\t\t\tProjectID:   fakeProjectID,\n\t\t\t\tUsername:    fakeUsername,\n\t\t\t\tPassword:    fakePasswd,\n\t\t\t\tDNSEndpoint: defaultDNSEndpoint,\n\t\t\t\tDomainName:  defaultDomainName,\n\t\t\t},\n\t\t\texpected: \"vkcloud: unable to build VK Cloud client: identity endpoint is missing in config\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing domain name\",\n\t\t\tconfig: &Config{\n\t\t\t\tProjectID:        fakeProjectID,\n\t\t\t\tUsername:         fakeUsername,\n\t\t\t\tPassword:         fakePasswd,\n\t\t\t\tDNSEndpoint:      defaultDNSEndpoint,\n\t\t\t\tIdentityEndpoint: defaultIdentityEndpoint,\n\t\t\t},\n\t\t\texpected: \"vkcloud: unable to build VK Cloud client: domain name is missing in config\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tp, err := NewDNSProviderConfig(test.config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/volcengine/volcengine.go",
    "content": "// Package volcengine implements a DNS provider for solving the DNS-01 challenge using Volcano Engine.\npackage volcengine\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/ptr\"\n\t\"github.com/volcengine/volc-sdk-golang/base\"\n\tvolc \"github.com/volcengine/volc-sdk-golang/service/dns\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"VOLC_\"\n\n\tEnvAccessKey = envNamespace + \"ACCESSKEY\"\n\tEnvSecretKey = envNamespace + \"SECRETKEY\"\n\n\tEnvRegion = envNamespace + \"REGION\"\n\tEnvHost   = envNamespace + \"HOST\"\n\tEnvScheme = envNamespace + \"SCHEME\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// https://www.volcengine.com/docs/6758/170354\nconst defaultTTL = 600\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAccessKey string\n\tSecretKey string\n\n\tRegion string\n\tHost   string\n\tScheme string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPTimeout        time.Duration\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tScheme: env.GetOrDefaultString(EnvScheme, \"https\"),\n\t\tHost:   env.GetOrDefaultString(EnvHost, \"open.volcengineapi.com\"),\n\t\tRegion: env.GetOrDefaultString(EnvRegion, volc.DefaultRegion),\n\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, defaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 4*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, volc.Timeout*time.Second),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *volc.Client\n\tconfig *Config\n\n\trecordIDs   map[string]*string\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Volcano Engine.\n// Credentials must be passed in the environment variable: VOLC_ACCESSKEY, VOLC_SECRETKEY.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAccessKey, EnvSecretKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"volcengine: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.AccessKey = values[EnvAccessKey]\n\tconfig.SecretKey = values[EnvSecretKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Volcano Engine.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"volcengine: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.AccessKey == \"\" || config.SecretKey == \"\" {\n\t\treturn nil, errors.New(\"volcengine: missing credentials\")\n\t}\n\n\treturn &DNSProvider{\n\t\tconfig:    config,\n\t\tclient:    newClient(config),\n\t\trecordIDs: make(map[string]*string),\n\t}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tzone, err := d.getZone(ctx, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"volcengine: get zone ID: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, ptr.Deref(zone.ZoneName))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"volcengine: %w\", err)\n\t}\n\n\tcrr := &volc.CreateRecordRequest{\n\t\tHost:  ptr.Pointer(subDomain),\n\t\tTTL:   ptr.Pointer(int64(d.config.TTL)),\n\t\tType:  ptr.Pointer(\"TXT\"),\n\t\tValue: ptr.Pointer(info.Value),\n\t\tZID:   zone.ZID,\n\t}\n\n\trecord, err := d.client.CreateRecord(ctx, crr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"volcengine: create record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = record.RecordID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// gets the record's unique ID\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"volcengine: unknown record ID for '%s' '%s'\", info.EffectiveFQDN, token)\n\t}\n\n\tdrr := &volc.DeleteRecordRequest{RecordID: recordID}\n\n\terr := d.client.DeleteRecord(context.Background(), drr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"volcengine: delete record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\nfunc (d *DNSProvider) getZone(ctx context.Context, fqdn string) (volc.TopZoneResponse, error) {\n\tfor domain := range dns01.UnFqdnDomainsSeq(fqdn) {\n\t\tlzr := &volc.ListZonesRequest{\n\t\t\tKey:        ptr.Pointer(dns01.UnFqdn(domain)),\n\t\t\tSearchMode: ptr.Pointer(\"exact\"),\n\t\t}\n\n\t\tzones, err := d.client.ListZones(ctx, lzr)\n\t\tif err != nil {\n\t\t\treturn volc.TopZoneResponse{}, fmt.Errorf(\"list zones: %w\", err)\n\t\t}\n\n\t\ttotal := ptr.Deref(zones.Total)\n\n\t\tif total == 0 || len(zones.Zones) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif total > 1 {\n\t\t\treturn volc.TopZoneResponse{}, fmt.Errorf(\"too many zone for %s\", domain)\n\t\t}\n\n\t\treturn zones.Zones[0], nil\n\t}\n\n\treturn volc.TopZoneResponse{}, fmt.Errorf(\"zone no found for fqdn: %s\", fqdn)\n}\n\n// https://github.com/volcengine/volc-sdk-golang/tree/main/service/dns\n// https://github.com/volcengine/volc-sdk-golang/blob/main/example/dns/demo_dns_test.go\nfunc newClient(config *Config) *volc.Client {\n\t// https://github.com/volcengine/volc-sdk-golang/blob/fae992a31d02754e271c322095413d374ea4ea1b/service/dns/config.go#L20-L35\n\tserviceInfo := &base.ServiceInfo{\n\t\tTimeout: config.HTTPTimeout,\n\t\tHost:    config.Host,\n\t\tHeader:  http.Header{\"Accept\": []string{\"application/json\"}},\n\t\tScheme:  config.Scheme,\n\t\tCredentials: base.Credentials{\n\t\t\tService:         volc.ServiceName,\n\t\t\tRegion:          config.Region,\n\t\t\tAccessKeyID:     config.AccessKey,\n\t\t\tSecretAccessKey: config.SecretKey,\n\t\t},\n\t}\n\n\t// https://github.com/volcengine/volc-sdk-golang/blob/fae992a31d02754e271c322095413d374ea4ea1b/service/dns/caller.go#L17-L19\n\tclient := base.NewClient(serviceInfo, nil)\n\n\t// https://github.com/volcengine/volc-sdk-golang/blob/fae992a31d02754e271c322095413d374ea4ea1b/service/dns/caller.go#L25-L34\n\tcaller := &volc.VolcCaller{Volc: client}\n\tcaller.Volc.SetAccessKey(serviceInfo.Credentials.AccessKeyID)\n\tcaller.Volc.SetSecretKey(serviceInfo.Credentials.SecretAccessKey)\n\tcaller.Volc.SetHost(serviceInfo.Host)\n\tcaller.Volc.SetScheme(serviceInfo.Scheme)\n\tcaller.Volc.SetTimeout(serviceInfo.Timeout)\n\n\treturn volc.NewClient(caller)\n}\n"
  },
  {
    "path": "providers/dns/volcengine/volcengine.toml",
    "content": "Name = \"Volcano Engine/火山引擎\"\nDescription = ''''''\nURL = \"https://www.volcengine.com/\"\nCode = \"volcengine\"\nSince = \"v4.19.0\"\n\nExample = '''\nVOLC_ACCESSKEY=xxx \\\nVOLC_SECRETKEY=yyy \\\nlego --dns volcengine -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    VOLC_ACCESSKEY = \"Access Key ID (AK)\"\n    VOLC_SECRETKEY = \"Secret Access Key (SK)\"\n  [Configuration.Additional]\n    VOLC_REGION = \"Region\"\n    VOLC_HOST = \"API host\"\n    VOLC_SCHEME = \"API scheme\"\n    VOLC_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    VOLC_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 240)\"\n    VOLC_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)\"\n    VOLC_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 15)\"\n\n[Links]\n  API = \"https://www.volcengine.com/docs/6758/155086\"\n  GoClient = \"https://github.com/volcengine/volc-sdk-golang\"\n"
  },
  {
    "path": "providers/dns/volcengine/volcengine_test.go",
    "content": "package volcengine\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(\n\tEnvAccessKey,\n\tEnvSecretKey,\n\tEnvRegion,\n\tEnvHost,\n\tEnvScheme).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKey: \"access\",\n\t\t\t\tEnvSecretKey: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing access key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvSecretKey: \"secret\",\n\t\t\t},\n\t\t\texpected: \"volcengine: some credentials information are missing: VOLC_ACCESSKEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAccessKey: \"access\",\n\t\t\t},\n\t\t\texpected: \"volcengine: some credentials information are missing: VOLC_SECRETKEY\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"volcengine: some credentials information are missing: VOLC_ACCESSKEY,VOLC_SECRETKEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\texpected  string\n\t\taccessKey string\n\t\tsecretKey string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\taccessKey: \"access\",\n\t\t\tsecretKey: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing access key\",\n\t\t\tsecretKey: \"secret\",\n\t\t\texpected:  \"volcengine: missing credentials\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing secret key\",\n\t\t\taccessKey: \"access\",\n\t\t\texpected:  \"volcengine: missing credentials\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"volcengine: missing credentials\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.AccessKey = test.accessKey\n\t\t\tconfig.SecretKey = test.secretKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/vscale/vscale.go",
    "content": "// Package vscale implements a DNS provider for solving the DNS-01 challenge using Vscale Domains API.\n// Vscale Domain API reference: https://developers.vscale.io/documentation/api/v1/#api-Domains\n// Token: https://vscale.io/panel/settings/tokens/\npackage vscale\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/selectel\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"VSCALE_\"\n\n\tEnvBaseURL  = envNamespace + \"BASE_URL\"\n\tEnvAPIToken = envNamespace + \"API_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultBaseURL = \"https://api.vscale.io/v1/domains\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config = selectel.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tBaseURL:            env.GetOrDefaultString(EnvBaseURL, defaultBaseURL),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, selectel.MinTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Vscale Domains API.\n// API token must be passed in the environment variable VSCALE_API_TOKEN.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vscale: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Token = values[EnvAPIToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Vscale.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"vscale: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.BaseURL == \"\" {\n\t\tconfig.BaseURL = defaultBaseURL\n\t}\n\n\tprovider, err := selectel.NewDNSProviderConfig(config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vscale: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vscale: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vscale: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n"
  },
  {
    "path": "providers/dns/vscale/vscale.toml",
    "content": "Name = \"Vscale\"\nDescription = ''''''\nURL = \"https://vscale.io/\"\nCode = \"vscale\"\nSince = \"v2.0.0\"\n\nExample = '''\nVSCALE_API_TOKEN=xxxxx \\\nlego --dns vscale -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    VSCALE_API_TOKEN = \"API token\"\n  [Configuration.Additional]\n    VSCALE_BASE_URL = \"API endpoint URL\"\n    VSCALE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    VSCALE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    VSCALE_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    VSCALE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://developers.vscale.io/documentation/api/v1/#api-Domains_Records\"\n"
  },
  {
    "path": "providers/dns/vscale/vscale_test.go",
    "content": "package vscale\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/selectel\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar envTest = tester.NewEnvTest(EnvAPIToken, EnvTTL)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIToken: \"\",\n\t\t\t},\n\t\t\texpected: fmt.Sprintf(\"vscale: some credentials information are missing: %s\", EnvAPIToken),\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\ttoken    string\n\t\tttl      int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:  \"success\",\n\t\t\ttoken: \"123\",\n\t\t\tttl:   60,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api key\",\n\t\t\ttoken:    \"\",\n\t\t\tttl:      60,\n\t\t\texpected: \"vscale: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"bad TTL value\",\n\t\t\ttoken:    \"123\",\n\t\t\tttl:      59,\n\t\t\texpected: fmt.Sprintf(\"vscale: invalid TTL, TTL (59) must be greater than %d\", selectel.MinTTL),\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.TTL = test.ttl\n\t\t\tconfig.Token = test.token\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\tassert.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/vultr/vultr.go",
    "content": "// Package vultr implements a DNS provider for solving the DNS-01 challenge using the Vultr DNS.\n// See https://www.vultr.com/api/#dns\npackage vultr\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/vultr/govultr/v3\"\n\t\"golang.org/x/oauth2\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"VULTR_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n\tHTTPTimeout        time.Duration // TODO(ldez): remove in v5\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPTimeout:        env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *govultr.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance with a configured Vultr client.\n// Authentication uses the VULTR_API_KEY environment variable.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vultr: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Vultr.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"vultr: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"vultr: credentials missing\")\n\t}\n\n\tauthClient := OAuthStaticAccessToken(config.HTTPClient, config.APIKey)\n\tauthClient.Timeout = config.HTTPTimeout\n\n\tclient := govultr.NewClient(clientdebug.Wrap(authClient))\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Present creates a TXT record to fulfill the DNS-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\tzoneDomain, err := d.getHostedZone(ctx, domain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vultr: %w\", err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zoneDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vultr: %w\", err)\n\t}\n\n\treq := govultr.DomainRecordCreateReq{\n\t\tName:     subDomain,\n\t\tType:     \"TXT\",\n\t\tData:     `\"` + info.Value + `\"`,\n\t\tTTL:      d.config.TTL,\n\t\tPriority: func(v int) *int { return &v }(0),\n\t}\n\n\t_, resp, err := d.client.DomainRecord.Create(ctx, zoneDomain, &req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vultr: %w\", extendError(resp, err))\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\t// TODO(ldez) replace domain by FQDN to follow CNAME.\n\tzoneDomain, records, err := d.findTxtRecords(ctx, domain, info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"vultr: %w\", err)\n\t}\n\n\tvar allErr []string\n\n\tfor _, rec := range records {\n\t\terr := d.client.DomainRecord.Delete(ctx, zoneDomain, rec.ID)\n\t\tif err != nil {\n\t\t\tallErr = append(allErr, err.Error())\n\t\t}\n\t}\n\n\tif len(allErr) > 0 {\n\t\treturn errors.New(strings.Join(allErr, \": \"))\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (string, error) {\n\tlistOptions := &govultr.ListOptions{PerPage: 25}\n\n\tvar hostedDomain govultr.Domain\n\n\tfor {\n\t\tdomains, meta, resp, err := d.client.Domain.List(ctx, listOptions)\n\t\tif err != nil {\n\t\t\treturn \"\", extendError(resp, err)\n\t\t}\n\n\t\tfor _, dom := range domains {\n\t\t\tif strings.HasSuffix(domain, dom.Domain) && len(dom.Domain) > len(hostedDomain.Domain) {\n\t\t\t\thostedDomain = dom\n\t\t\t}\n\t\t}\n\n\t\tif domain == hostedDomain.Domain {\n\t\t\tbreak\n\t\t}\n\n\t\tif meta.Links.Next == \"\" {\n\t\t\tbreak\n\t\t}\n\n\t\tlistOptions.Cursor = meta.Links.Next\n\t}\n\n\tif hostedDomain.Domain == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no matching domain found for domain %s\", domain)\n\t}\n\n\treturn hostedDomain.Domain, nil\n}\n\nfunc (d *DNSProvider) findTxtRecords(ctx context.Context, domain, fqdn string) (string, []govultr.DomainRecord, error) {\n\tzoneDomain, err := d.getHostedZone(ctx, domain)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(fqdn, zoneDomain)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tlistOptions := &govultr.ListOptions{PerPage: 25}\n\n\tvar records []govultr.DomainRecord\n\n\tfor {\n\t\tresult, meta, resp, err := d.client.DomainRecord.List(ctx, zoneDomain, listOptions)\n\t\tif err != nil {\n\t\t\treturn \"\", records, extendError(resp, err)\n\t\t}\n\n\t\tfor _, record := range result {\n\t\t\tif record.Type == \"TXT\" && record.Name == subDomain {\n\t\t\t\trecords = append(records, record)\n\t\t\t}\n\t\t}\n\n\t\tif meta.Links.Next == \"\" {\n\t\t\tbreak\n\t\t}\n\n\t\tlistOptions.Cursor = meta.Links.Next\n\t}\n\n\treturn zoneDomain, records, nil\n}\n\nfunc OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {\n\tif client == nil {\n\t\tclient = &http.Client{Timeout: 5 * time.Second}\n\t}\n\n\tclient.Transport = &oauth2.Transport{\n\t\tSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),\n\t\tBase:   client.Transport,\n\t}\n\n\treturn client\n}\n\nfunc extendError(resp *http.Response, err error) error {\n\tmsg := \"API call failed\"\n\tif resp != nil {\n\t\tmsg += fmt.Sprintf(\" (%d)\", resp.StatusCode)\n\t}\n\n\treturn fmt.Errorf(\"%s: %w\", msg, err)\n}\n"
  },
  {
    "path": "providers/dns/vultr/vultr.toml",
    "content": "Name = \"Vultr\"\nDescription = ''''''\nURL = \"https://www.vultr.com/\"\nCode = \"vultr\"\nSince = \"v0.3.1\"\n\nExample = '''\nVULTR_API_KEY=xxxxx \\\nlego --dns vultr -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    VULTR_API_KEY = \"API key\"\n  [Configuration.Additional]\n    VULTR_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    VULTR_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    VULTR_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    VULTR_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.vultr.com/api/#dns\"\n  GoClient = \"https://github.com/vultr/govultr\"\n"
  },
  {
    "path": "providers/dns/vultr/vultr_test.go",
    "content": "package vultr\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/vultr/govultr/v3\"\n)\n\nconst envDomain = envNamespace + \"TEST_DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"vultr: some credentials information are missing: VULTR_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"vultr: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_getHostedZone(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc              string\n\t\tdomain            string\n\t\texpected          string\n\t\texpectedPageCount int\n\t}{\n\t\t{\n\t\t\tdesc:              \"exact match, in latest page\",\n\t\t\tdomain:            \"test.my.example.com\",\n\t\t\texpected:          \"test.my.example.com\",\n\t\t\texpectedPageCount: 5,\n\t\t},\n\t\t{\n\t\t\tdesc:              \"exact match, in the middle\",\n\t\t\tdomain:            \"my.example.com\",\n\t\t\texpected:          \"my.example.com\",\n\t\t\texpectedPageCount: 3,\n\t\t},\n\t\t{\n\t\t\tdesc:              \"exact match, first page\",\n\t\t\tdomain:            \"example.com\",\n\t\t\texpected:          \"example.com\",\n\t\t\texpectedPageCount: 1,\n\t\t},\n\t\t{\n\t\t\tdesc:              \"match on apex\",\n\t\t\tdomain:            \"test.example.org\",\n\t\t\texpected:          \"example.org\",\n\t\t\texpectedPageCount: 5,\n\t\t},\n\t\t{\n\t\t\tdesc:              \"match on parent\",\n\t\t\tdomain:            \"test.my.example.net\",\n\t\t\texpected:          \"my.example.net\",\n\t\t\texpectedPageCount: 5,\n\t\t},\n\t}\n\n\tdomains := []govultr.Domain{{Domain: \"example.com\"}, {Domain: \"example.org\"}, {Domain: \"example.net\"}}\n\n\tfor i := range 50 {\n\t\tdomains = append(domains, govultr.Domain{Domain: fmt.Sprintf(\"my%02d.example.com\", i)})\n\t}\n\n\tdomains = append(domains, govultr.Domain{Domain: \"my.example.com\"}, govultr.Domain{Domain: \"my.example.net\"})\n\n\tfor i := 50; i < 100; i++ {\n\t\tdomains = append(domains, govultr.Domain{Domain: fmt.Sprintf(\"my%02d.example.com\", i)})\n\t}\n\n\tdomains = append(domains, govultr.Domain{Domain: \"test.my.example.com\"})\n\n\ttype domainsBase struct {\n\t\tDomains []govultr.Domain `json:\"domains\"`\n\t\tMeta    *govultr.Meta    `json:\"meta\"`\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar pageCount int\n\n\t\t\tprovider := servermock.NewBuilder(\n\t\t\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\t\t\tclient := govultr.NewClient(server.Client())\n\t\t\t\t\terr := client.SetBaseURL(server.URL)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\treturn &DNSProvider{client: client}, nil\n\t\t\t\t},\n\t\t\t).\n\t\t\t\tRoute(\"GET /v2/domains\", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\t\tpageCount++\n\n\t\t\t\t\tquery := req.URL.Query()\n\t\t\t\t\tcursor, _ := strconv.Atoi(query.Get(\"cursor\"))\n\t\t\t\t\tperPage, _ := strconv.Atoi(query.Get(\"per_page\"))\n\n\t\t\t\t\tvar next string\n\t\t\t\t\tif len(domains)/perPage > cursor {\n\t\t\t\t\t\tnext = strconv.Itoa(cursor + 1)\n\t\t\t\t\t}\n\n\t\t\t\t\tstart := cursor * perPage\n\t\t\t\t\tif len(domains) < start {\n\t\t\t\t\t\tstart = cursor * len(domains)\n\t\t\t\t\t}\n\n\t\t\t\t\tend := min(len(domains), (cursor+1)*perPage)\n\n\t\t\t\t\tdb := domainsBase{\n\t\t\t\t\t\tDomains: domains[start:end],\n\t\t\t\t\t\tMeta: &govultr.Meta{\n\t\t\t\t\t\t\tTotal: len(domains),\n\t\t\t\t\t\t\tLinks: &govultr.Links{Next: next},\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\n\t\t\t\t\terr := json.NewEncoder(rw).Encode(db)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t})).\n\t\t\t\tBuild(t)\n\n\t\t\tzone, err := provider.getHostedZone(t.Context(), test.domain)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, test.expected, zone)\n\t\t\tassert.Equal(t, test.expectedPageCount, pageCount)\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/webnames/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://www.webnames.ru/scripts/json_domain_zone_manager.pl\"\n\n// Client the Webnames API client.\ntype Client struct {\n\tapiKey string\n\n\tbaseURL    string\n\tHTTPClient *http.Client\n}\n\n// NewClient Creates a new Client.\nfunc NewClient(apiKey string) *Client {\n\treturn &Client{\n\t\tapiKey:     apiKey,\n\t\tbaseURL:    defaultBaseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}\n}\n\n// AddTXTRecord adds a TXT record.\n// Inspired by https://github.com/regtime-ltd/certbot-dns-webnames/blob/master/authenticator.sh\nfunc (c *Client) AddTXTRecord(ctx context.Context, domain, subDomain, value string) error {\n\tdata := url.Values{}\n\tdata.Set(\"domain\", domain)\n\tdata.Set(\"type\", \"TXT\")\n\tdata.Set(\"record\", subDomain+\":\"+value)\n\tdata.Set(\"action\", \"add\")\n\n\treturn c.doRequest(ctx, data)\n}\n\n// RemoveTXTRecord removes a TXT record.\n// Inspired by https://github.com/regtime-ltd/certbot-dns-webnames/blob/master/cleanup.sh\nfunc (c *Client) RemoveTXTRecord(ctx context.Context, domain, subDomain, value string) error {\n\tdata := url.Values{}\n\tdata.Set(\"domain\", domain)\n\tdata.Set(\"type\", \"TXT\")\n\tdata.Set(\"record\", subDomain+\":\"+value)\n\tdata.Set(\"action\", \"delete\")\n\n\treturn c.doRequest(ctx, data)\n}\n\nfunc (c *Client) doRequest(ctx context.Context, data url.Values) error {\n\tdata.Set(\"apikey\", c.apiKey)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, strings.NewReader(data.Encode()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tvar r APIResponse\n\n\terr = json.Unmarshal(raw, &r)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tif r.Result == \"OK\" {\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"%s: %s\", r.Result, r.Details)\n}\n"
  },
  {
    "path": "providers/dns/webnames/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"secret\")\n\t\t\tclient.baseURL = server.URL\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded(),\n\t)\n}\n\nfunc TestClient_AddTXTRecord(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tfilename string\n\t\trequire  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tdesc:     \"ok\",\n\t\t\tfilename: \"ok.json\",\n\t\t\trequire:  require.NoError,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"error\",\n\t\t\tfilename: \"error.json\",\n\t\t\trequire:  require.Error,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(\"POST /\",\n\t\t\t\t\tservermock.ResponseFromFixture(test.filename),\n\t\t\t\t\tservermock.CheckForm().Strict().\n\t\t\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\t\t\tWith(\"record\", \"foo:txtTXTtxt\").\n\t\t\t\t\t\tWith(\"action\", \"add\").\n\t\t\t\t\t\tWith(\"apikey\", \"secret\"),\n\t\t\t\t).\n\t\t\t\tBuild(t)\n\n\t\t\tdomain := \"example.com\"\n\t\t\tsubDomain := \"foo\"\n\t\t\tcontent := \"txtTXTtxt\"\n\n\t\t\terr := client.AddTXTRecord(t.Context(), domain, subDomain, content)\n\t\t\ttest.require(t, err)\n\t\t})\n\t}\n}\n\nfunc TestClient_RemoveTxtRecord(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tfilename string\n\t\trequire  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tdesc:     \"ok\",\n\t\t\tfilename: \"ok.json\",\n\t\t\trequire:  require.NoError,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"error\",\n\t\t\tfilename: \"error.json\",\n\t\t\trequire:  require.Error,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := mockBuilder().\n\t\t\t\tRoute(\"POST /\",\n\t\t\t\t\tservermock.ResponseFromFixture(test.filename),\n\t\t\t\t\tservermock.CheckForm().Strict().\n\t\t\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\t\t\tWith(\"type\", \"TXT\").\n\t\t\t\t\t\tWith(\"record\", \"foo:txtTXTtxt\").\n\t\t\t\t\t\tWith(\"action\", \"delete\").\n\t\t\t\t\t\tWith(\"apikey\", \"secret\"),\n\t\t\t\t).\n\t\t\t\tBuild(t)\n\n\t\t\tdomain := \"example.com\"\n\t\t\tsubDomain := \"foo\"\n\t\t\tcontent := \"txtTXTtxt\"\n\n\t\t\terr := client.RemoveTXTRecord(t.Context(), domain, subDomain, content)\n\t\t\ttest.require(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/webnames/internal/fixtures/error.json",
    "content": "{\n  \"result\": \"ERROR\",\n  \"details\": \"zone_manager_unavailable\"\n}\n"
  },
  {
    "path": "providers/dns/webnames/internal/fixtures/ok.json",
    "content": "{\n  \"result\": \"OK\",\n  \"details\": 1\n}\n"
  },
  {
    "path": "providers/dns/webnames/internal/types.go",
    "content": "package internal\n\nimport \"encoding/json\"\n\ntype APIResponse struct {\n\tResult  string          `json:\"result\"`\n\tDetails json.RawMessage `json:\"details\"`\n}\n"
  },
  {
    "path": "providers/dns/webnames/webnames.go",
    "content": "// Package webnames implements a DNS provider for solving the DNS-01 challenge using webnames.ru DNS.\npackage webnames\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/webnames/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace    = \"WEBNAMESRU_\"\n\taltEnvNamespace = \"WEBNAMES_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIKey string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOneWithFallback(EnvPropagationTimeout, dns01.DefaultPropagationTimeout, env.ParseSecond, altEnvName(EnvPropagationTimeout)),\n\t\tPollingInterval:    env.GetOneWithFallback(EnvPollingInterval, dns01.DefaultPollingInterval, env.ParseSecond, altEnvName(EnvPollingInterval)),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOneWithFallback(EnvHTTPTimeout, 20*time.Second, env.ParseSecond, altEnvName(EnvHTTPTimeout)),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a new DNS provider using\n// environment variable WEBNAMESRU_API_KEY for adding and removing the DNS record.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.GetWithFallback([]string{EnvAPIKey, altEnvName(EnvAPIKey)})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"webnamesru: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Webnames.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"webnamesru: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"webnamesru: credentials missing\")\n\t}\n\n\tclient := internal.NewClient(config.APIKey)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"webnamesru: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"webnamesru: %w\", err)\n\t}\n\n\terr = d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(authZone), subDomain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"webnamesru: failed to create TXT records [domain: %s, sub domain: %s]: %w\",\n\t\t\tdns01.UnFqdn(authZone), subDomain, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp clears Webnames TXT record.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"webnamesru: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"webnamesru: %w\", err)\n\t}\n\n\terr = d.client.RemoveTXTRecord(context.Background(), dns01.UnFqdn(authZone), subDomain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"webnamesru: failed to remove TXT records [domain: %s, sub domain: %s]: %w\",\n\t\t\tdns01.UnFqdn(authZone), subDomain, err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc altEnvName(v string) string {\n\treturn strings.ReplaceAll(v, envNamespace, altEnvNamespace)\n}\n"
  },
  {
    "path": "providers/dns/webnames/webnames.toml",
    "content": "Name = \"webnames.ru\"\nDescription = ''''''\nURL = \"https://www.webnames.ru/\"\nCode = \"webnames\"\nAliases = [\"webnamesru\"]\nSince = \"v4.15.0\"\n\nExample = '''\nWEBNAMESRU_API_KEY=xxxxxx \\\nlego --dns webnamesru -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## API Key\n\nTo obtain the key, you need to change the DNS server to `*.nameself.com`: Personal account / My domains and services / Select the required domain / DNS servers\n\nThe API key can be found: Personal account / My domains and services / Select the required domain / Zone management / acme.sh or certbot settings\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    WEBNAMESRU_API_KEY = \"Domain API key\"\n  [Configuration.Additional]\n    WEBNAMESRU_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    WEBNAMESRU_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    WEBNAMESRU_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://github.com/regtime-ltd/certbot-dns-webnames\"\n"
  },
  {
    "path": "providers/dns/webnames/webnames_test.go",
    "content": "package webnames\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"webnamesru: some credentials information are missing: WEBNAMESRU_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"123\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"webnamesru: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/webnamesca/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/useragent\"\n)\n\nconst defaultBaseURL = \"https://www.webnames.ca/_/APICore\"\n\n// Client the webnames.ca API client.\ntype Client struct {\n\tuser string\n\tkey  string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(user, key string) (*Client, error) {\n\tif user == \"\" || key == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tuser:       user,\n\t\tkey:        key,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) AddTXTRecord(ctx context.Context, domainName, hostName, value string) ([]DNSRecordSet, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", domainName, \"add-txt-record\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"hostName\", hostName)\n\tquery.Set(\"txt\", value)\n\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result APIResponse[*DNSInfo]\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Result.DNSRecordSets, nil\n}\n\nfunc (c *Client) DeleteTXTRecord(ctx context.Context, domainName, hostName, value string) ([]DNSRecordSet, error) {\n\tendpoint := c.BaseURL.JoinPath(\"domains\", domainName, \"delete-txt-record\")\n\n\tquery := endpoint.Query()\n\tquery.Set(\"hostName\", hostName)\n\tquery.Set(\"txt\", value)\n\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result APIResponse[*DNSInfo]\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Result.DNSRecordSets, nil\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\tuseragent.SetHeader(req.Header)\n\n\treq.Header.Set(\"API-User\", c.user)\n\treq.Header.Set(\"API-Key\", c.key)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar errAPI APIError\n\n\terr := json.Unmarshal(raw, &errAPI)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn &errAPI\n}\n"
  },
  {
    "path": "providers/dns/webnamesca/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"user\", \"secret\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWith(\"API-User\", \"user\").\n\t\t\tWith(\"API-Key\", \"secret\").\n\t\t\tWithJSONHeaders(),\n\t)\n}\n\nfunc TestClient_AddTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/example.com/add-txt-record\",\n\t\t\tservermock.ResponseFromFixture(\"add_txt_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"hostName\", \"foo.example.com\").\n\t\t\t\tWith(\"txt\", \"value\")).\n\t\tBuild(t)\n\n\tresult, err := client.AddTXTRecord(t.Context(), \"example.com\", \"foo.example.com\", \"value\")\n\trequire.NoError(t, err)\n\n\texpected := []DNSRecordSet{{\n\t\tHostname: \"_acme-challenge.example.com\",\n\t\tType:     \"TXT\",\n\t\tRecords:  []string{\"value\"},\n\t}}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_AddTXTRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /domains/example.com/add-txt-record\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\t_, err := client.AddTXTRecord(t.Context(), \"example.com\", \"foo.example.com\", \"value\")\n\trequire.EqualError(t, err, \"message: User does not exist., details: string, logiD: 35579, result: {}\")\n}\n\nfunc TestClient_DeleteTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domains/example.com/delete-txt-record\",\n\t\t\tservermock.ResponseFromFixture(\"delete_txt_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"hostName\", \"foo.example.com\").\n\t\t\t\tWith(\"txt\", \"value\")).\n\t\tBuild(t)\n\n\tresult, err := client.DeleteTXTRecord(t.Context(), \"example.com\", \"foo.example.com\", \"value\")\n\trequire.NoError(t, err)\n\n\texpected := []DNSRecordSet{{\n\t\tHostname: \"_acme-challenge.example.com\",\n\t\tType:     \"TXT\",\n\t\tRecords:  []string{\"value\"},\n\t}}\n\n\tassert.Equal(t, expected, result)\n}\n\nfunc TestClient_DeleteTXTRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /domains/example.com/delete-txt-record\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusBadRequest)).\n\t\tBuild(t)\n\n\t_, err := client.DeleteTXTRecord(t.Context(), \"example.com\", \"foo.example.com\", \"value\")\n\trequire.EqualError(t, err, \"message: User does not exist., details: string, logiD: 35579, result: {}\")\n}\n"
  },
  {
    "path": "providers/dns/webnamesca/internal/fixtures/add_txt_record.json",
    "content": "{\n  \"result\": {\n    \"domainAdvancedDNSConfigID\": 3258480,\n    \"domainID\": 1333334,\n    \"dtCreated\": \"2025-10-30T11:55:23.243\",\n    \"dtModified\": \"2025-10-30T11:55:23.177\",\n    \"timeToLive\": 21600,\n    \"soAorigin\": \"hosting.webnames.ca\",\n    \"soArefresh\": 21600,\n    \"soAretry\": 180,\n    \"soAexpire\": 1209600,\n    \"soAnegcache\": 3600,\n    \"forwardingURL\": null,\n    \"gripping\": false,\n    \"name\": null,\n    \"dtSubmitted\": \"2025-10-30T11:55:24.927\",\n    \"dtRequestedDNSChange\": null,\n    \"type\": \"REAL_DOMAIN\",\n    \"userManaged\": false,\n    \"effectiveMgmtOption\": \"AD\",\n    \"urlForwardRootOnly\": false,\n    \"enableDNSSEC\": false,\n    \"dnsRecordSets\": [\n      {\n        \"hostname\": \"_acme-challenge.example.com\",\n        \"type\": \"TXT\",\n        \"records\": [\n          \"value\"\n        ]\n      }\n    ]\n  },\n  \"logID\": 36014\n}\n"
  },
  {
    "path": "providers/dns/webnamesca/internal/fixtures/delete_txt_record.json",
    "content": "{\n  \"errorMessage\": \"string\",\n  \"errorDetails\": \"string\",\n  \"logID\": 0,\n  \"result\": {\n    \"domainAdvancedDNSConfigID\": 0,\n    \"domainID\": 0,\n    \"dtCreated\": \"2025-10-29T21:22:31.478\",\n    \"dtModified\": \"2025-10-29T21:22:31.478\",\n    \"timeToLive\": 0,\n    \"soAorigin\": \"string\",\n    \"soArefresh\": 0,\n    \"soAretry\": 0,\n    \"soAexpire\": 0,\n    \"soAnegcache\": 0,\n    \"forwardingURL\": \"string\",\n    \"gripping\": true,\n    \"name\": \"string\",\n    \"dtSubmitted\": \"2025-10-29T21:22:31.478\",\n    \"dtRequestedDNSChange\": \"2025-10-29T21:22:31.478\",\n    \"type\": \"string\",\n    \"userManaged\": true,\n    \"effectiveMgmtOption\": \"string\",\n    \"urlForwardRootOnly\": true,\n    \"enableDNSSEC\": true,\n    \"dnsRecordSets\": [\n      {\n        \"hostname\": \"_acme-challenge.example.com\",\n        \"type\": \"TXT\",\n        \"records\": [\n          \"value\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "providers/dns/webnamesca/internal/fixtures/error.json",
    "content": "{\n  \"errorMessage\": \"User does not exist.\",\n  \"errorDetails\": \"string\",\n  \"logID\": 35579,\n  \"result\": {}\n}\n"
  },
  {
    "path": "providers/dns/webnamesca/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\ntype APIError struct {\n\tErrorMessage string          `json:\"errorMessage,omitempty\"`\n\tErrorDetails string          `json:\"errorDetails,omitempty\"`\n\tLogID        int             `json:\"logID,omitempty\"`\n\tResult       json.RawMessage `json:\"result,omitempty\"`\n}\n\nfunc (a *APIError) Error() string {\n\treturn fmt.Sprintf(\"message: %s, details: %s, logiD: %d, result: %s\", a.ErrorMessage, a.ErrorDetails, a.LogID, a.Result)\n}\n\ntype APIResponse[T any] struct {\n\tResult T   `json:\"result,omitempty\"`\n\tLogID  int `json:\"logID,omitempty\"`\n}\n\ntype DNSInfo struct {\n\tDomainID      int            `json:\"domainID,omitempty\"`\n\tDNSRecordSets []DNSRecordSet `json:\"dnsRecordSets,omitempty\"`\n}\n\ntype DNSRecordSet struct {\n\tHostname string   `json:\"hostname\"`\n\tType     string   `json:\"type\"`\n\tRecords  []string `json:\"records\"`\n}\n"
  },
  {
    "path": "providers/dns/webnamesca/webnamesca.go",
    "content": "// Package webnamesca implements a DNS provider for solving the DNS-01 challenge using webnames.ca.\npackage webnamesca\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/webnamesca/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"WEBNAMESCA_\"\n\n\tEnvAPIUser = envNamespace + \"API_USER\"\n\tEnvAPIKey  = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tAPIUser string\n\tAPIKey  string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for webnames.ca.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIUser, EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"webnamesca: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIUser = values[EnvAPIUser]\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for webnames.ca.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"webnamesca: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.APIUser, config.APIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"webnamesca: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"webnamesca: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t_, err = d.client.AddTXTRecord(context.Background(), dns01.UnFqdn(authZone), dns01.UnFqdn(info.EffectiveFQDN), info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"webnamesca: add TXT record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"webnamesca: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\t_, err = d.client.DeleteTXTRecord(context.Background(), dns01.UnFqdn(authZone), dns01.UnFqdn(info.EffectiveFQDN), info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"webnamesca: delete TXT record: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/webnamesca/webnamesca.toml",
    "content": "Name = \"webnames.ca\"\nDescription = ''''''\nURL = \"https://www.webnames.ca/\"\nCode = \"webnamesca\"\nSince = \"v4.28.0\"\n\nExample = '''\nWEBNAMESCA_API_USER=\"xxx\" \\\nWEBNAMESCA_API_KEY=\"yyy\" \\\nlego --dns webnamesca -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    WEBNAMESCA_API_USER = \"API username\"\n    WEBNAMESCA_API_KEY = \"API key\"\n  [Configuration.Additional]\n    WEBNAMESCA_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    WEBNAMESCA_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    WEBNAMESCA_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 120)\"\n    WEBNAMESCA_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.webnames.ca/_/swagger/index.html\"\n"
  },
  {
    "path": "providers/dns/webnamesca/webnamesca_test.go",
    "content": "package webnamesca\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIUser, EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser: \"user\",\n\t\t\t\tEnvAPIKey:  \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing EnvAPIUser\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser: \"\",\n\t\t\t\tEnvAPIKey:  \"secret\",\n\t\t\t},\n\t\t\texpected: \"webnamesca: some credentials information are missing: WEBNAMESCA_API_USER\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing EnvAPIKey\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser: \"user\",\n\t\t\t\tEnvAPIKey:  \"\",\n\t\t\t},\n\t\t\texpected: \"webnamesca: some credentials information are missing: WEBNAMESCA_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"webnamesca: some credentials information are missing: WEBNAMESCA_API_USER,WEBNAMESCA_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiUser  string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:    \"success\",\n\t\t\tapiUser: \"user\",\n\t\t\tapiKey:  \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing apiUser\",\n\t\t\tapiKey:   \"secret\",\n\t\t\texpected: \"webnamesca: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing apiKey\",\n\t\t\tapiUser:  \"user\",\n\t\t\texpected: \"webnamesca: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"webnamesca: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIUser = test.apiUser\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder() *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIUser = \"user\"\n\t\t\tconfig.APIKey = \"secret\"\n\t\t\tconfig.HTTPClient = server.Client()\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tp.client.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn p, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithJSONHeaders().\n\t\t\tWith(\"API-User\", \"user\").\n\t\t\tWith(\"API-Key\", \"secret\"),\n\t)\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"POST /domains/example.com/add-txt-record\",\n\t\t\tservermock.ResponseFromInternal(\"add_txt_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"hostName\", \"_acme-challenge.example.com\").\n\t\t\t\tWith(\"txt\", \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\")).\n\t\tBuild(t)\n\n\terr := provider.Present(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDNSProvider_CleanUp(t *testing.T) {\n\tprovider := mockBuilder().\n\t\tRoute(\"DELETE /domains/example.com/delete-txt-record\",\n\t\t\tservermock.ResponseFromInternal(\"delete_txt_record.json\"),\n\t\t\tservermock.CheckQueryParameter().Strict().\n\t\t\t\tWith(\"hostName\", \"_acme-challenge.example.com\").\n\t\t\t\tWith(\"txt\", \"ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY\")).\n\t\tBuild(t)\n\n\terr := provider.CleanUp(\"example.com\", \"abc\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/websupport/websupport.go",
    "content": "// Package websupport implements a DNS provider for solving the DNS-01 challenge using Websupport.\npackage websupport\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/active24\"\n)\n\nconst baseAPIDomain = \"websupport.sk\"\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"WEBSUPPORT_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\tEnvSecret = envNamespace + \"SECRET\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config = active24.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Websupport.\n// Credentials must be passed in the environment variables: WEBSUPPORT_API_KEY, WEBSUPPORT_SECRET.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey, EnvSecret)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"websupport: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.Secret = values[EnvSecret]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Websupport.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"websupport: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := active24.NewDNSProviderConfig(config, baseAPIDomain)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"websupport: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"websupport: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"websupport: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n"
  },
  {
    "path": "providers/dns/websupport/websupport.toml",
    "content": "Name = \"Websupport\"\nDescription = ''''''\nURL = \"https://websupport.sk\"\nCode = \"websupport\"\nSince = \"v4.10.0\"\n\nExample = '''\nWEBSUPPORT_API_KEY=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nWEBSUPPORT_SECRET=\"yyyyyyyyyyyyyyyyyyyyy\" \\\nlego --dns websupport -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    WEBSUPPORT_API_KEY = \"API key\"\n    WEBSUPPORT_SECRET = \"API secret\"\n  [Configuration.Additional]\n    WEBSUPPORT_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    WEBSUPPORT_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    WEBSUPPORT_SEQUENCE_INTERVAL = \"Time between sequential requests in seconds (Default: 60)\"\n    WEBSUPPORT_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 600)\"\n    WEBSUPPORT_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://rest.websupport.sk/v2/docs\"\n  APIv1 = \"https://rest.websupport.sk/docs/v1.service#services\"\n"
  },
  {
    "path": "providers/dns/websupport/websupport_test.go",
    "content": "package websupport\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey, EnvSecret).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"user\",\n\t\t\t\tEnvSecret: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t\tEnvSecret: \"secret\",\n\t\t\t},\n\t\t\texpected: \"websupport: some credentials information are missing: WEBSUPPORT_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing secret\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"user\",\n\t\t\t\tEnvSecret: \"\",\n\t\t\t},\n\t\t\texpected: \"websupport: some credentials information are missing: WEBSUPPORT_SECRET\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"websupport: some credentials information are missing: WEBSUPPORT_API_KEY,WEBSUPPORT_SECRET\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiKey   string\n\t\tsecret   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:   \"success\",\n\t\t\tapiKey: \"user\",\n\t\t\tsecret: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing API key\",\n\t\t\tapiKey:   \"\",\n\t\t\tsecret:   \"secret\",\n\t\t\texpected: \"websupport: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing secret\",\n\t\t\tapiKey:   \"user\",\n\t\t\tsecret:   \"\",\n\t\t\texpected: \"websupport: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"websupport: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.Secret = test.secret\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/wedos/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst baseURL = \"https://api.wedos.com/wapi/json\"\n\n// Client the API client for Webos.\ntype Client struct {\n\tusername string\n\tpassword string\n\n\tbaseURL    string\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(username, password string) *Client {\n\treturn &Client{\n\t\tusername:   username,\n\t\tpassword:   password,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}\n}\n\n// GetRecords lists all the records in the zone.\n// https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-rows-list/\nfunc (c *Client) GetRecords(ctx context.Context, zone string) ([]DNSRow, error) {\n\tpayload := map[string]any{\n\t\t\"domain\": dns01.UnFqdn(zone),\n\t}\n\n\treq, err := c.newRequest(ctx, commandDNSRowsList, payload)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := APIResponse[Rows]{}\n\n\terr = c.do(req, &result)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result.Response.Data.Rows, err\n}\n\n// AddRecord adds a record in the zone, either by updating existing records or creating new ones.\n// https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-add-row/\n// https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-row-update/\nfunc (c *Client) AddRecord(ctx context.Context, zone string, record DNSRow) error {\n\tpayload := DNSRowRequest{\n\t\tDomain: dns01.UnFqdn(zone),\n\t\tTTL:    record.TTL,\n\t\tType:   record.Type,\n\t\tData:   record.Data,\n\t}\n\n\tcmd := commandDNSRowAdd\n\n\tif record.ID == \"\" {\n\t\tpayload.Name = record.Name\n\t} else {\n\t\tcmd = commandDNSRowUpdate\n\t\tpayload.ID = record.ID\n\t}\n\n\treq, err := c.newRequest(ctx, cmd, payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, &APIResponse[json.RawMessage]{})\n}\n\n// DeleteRecord deletes a record from the zone.\n// If a record does not have an ID, it will be looked up.\n// https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-row-delete/\nfunc (c *Client) DeleteRecord(ctx context.Context, zone, recordID string) error {\n\tpayload := DNSRowRequest{\n\t\tDomain: dns01.UnFqdn(zone),\n\t\tID:     recordID,\n\t}\n\n\treq, err := c.newRequest(ctx, commandDNSRowDelete, payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, &APIResponse[json.RawMessage]{})\n}\n\n// Commit not really required, all changes will be auto-committed after 5 minutes.\n// https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-domain-commit/\nfunc (c *Client) Commit(ctx context.Context, zone string) error {\n\tpayload := map[string]any{\n\t\t\"name\": dns01.UnFqdn(zone),\n\t}\n\n\treq, err := c.newRequest(ctx, commandDNSDomainCommit, payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, &APIResponse[json.RawMessage]{})\n}\n\nfunc (c *Client) Ping(ctx context.Context) error {\n\treq, err := c.newRequest(ctx, commandPing, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, &APIResponse[json.RawMessage]{})\n}\n\nfunc (c *Client) do(req *http.Request, result Response) error {\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tif result.GetCode() != codeOk {\n\t\treturn fmt.Errorf(\"error %d: %s\", result.GetCode(), result.GetResult())\n\t}\n\n\treturn err\n}\n\nfunc (c *Client) newRequest(ctx context.Context, command string, payload any) (*http.Request, error) {\n\trequestObject := map[string]any{\n\t\t\"request\": APIRequest{\n\t\t\tUser:    c.username,\n\t\t\tAuth:    authToken(c.username, c.password),\n\t\t\tCommand: command,\n\t\t\tData:    payload,\n\t\t},\n\t}\n\n\tobject, err := json.Marshal(requestObject)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t}\n\n\tform := url.Values{}\n\tform.Add(\"request\", string(object))\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, strings.NewReader(form.Encode()))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Add(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/wedos/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\")\n\t\t\tclient.baseURL = server.URL\n\t\t\tclient.HTTPClient = server.Client()\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().\n\t\t\tWithContentTypeFromURLEncoded())\n}\n\nfunc TestClient_GetRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(commandDNSRowsList+\".json\"),\n\t\t\tcheckFormRequest(`{\"request\":{\"user\":\"user\",\"auth\":\"xxx\",\"command\":\"dns-rows-list\",\"data\":{\"domain\":\"example.com\"}}}`)).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"example.com.\")\n\trequire.NoError(t, err)\n\n\tassert.Len(t, records, 4)\n\n\texpected := []DNSRow{\n\t\t{\n\t\t\tID:   \"911\",\n\t\t\tTTL:  \"1800\",\n\t\t\tType: \"A\",\n\t\t\tData: \"1.2.3.4\",\n\t\t},\n\t\t{\n\t\t\tID:   \"913\",\n\t\t\tTTL:  \"1800\",\n\t\t\tType: \"MX\",\n\t\t\tData: \"1 mail1.wedos.net\",\n\t\t},\n\t\t{\n\t\t\tID:   \"914\",\n\t\t\tTTL:  \"1800\",\n\t\t\tType: \"MX\",\n\t\t\tData: \"10 mailbackup.wedos.net\",\n\t\t},\n\t\t{\n\t\t\tID:   \"912\",\n\t\t\tName: \"*\",\n\t\t\tTTL:  \"1800\",\n\t\t\tType: \"A\",\n\t\t\tData: \"1.2.3.4\",\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(commandDNSRowAdd+\".json\"),\n\t\t\tcheckFormRequest(`{\"request\":{\"user\":\"user\",\"auth\":\"xxx\",\"command\":\"dns-row-add\",\"data\":{\"domain\":\"example.com\",\"name\":\"foo\",\"ttl\":1800,\"type\":\"TXT\",\"rdata\":\"foobar\"}}}`)).\n\t\tBuild(t)\n\n\trecord := DNSRow{\n\t\tID:   \"\",\n\t\tName: \"foo\",\n\t\tTTL:  \"1800\",\n\t\tType: \"TXT\",\n\t\tData: \"foobar\",\n\t}\n\n\terr := client.AddRecord(t.Context(), \"example.com.\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_AddRecord_update(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(commandDNSRowUpdate+\".json\"),\n\t\t\tcheckFormRequest(`{\"request\":{\"user\":\"user\",\"auth\":\"xxx\",\"command\":\"dns-row-update\",\"data\":{\"row_id\":\"1\",\"domain\":\"example.com\",\"ttl\":1800,\"type\":\"TXT\",\"rdata\":\"foobar\"}}}`)).\n\t\tBuild(t)\n\n\trecord := DNSRow{\n\t\tID:   \"1\",\n\t\tName: \"foo\",\n\t\tTTL:  \"1800\",\n\t\tType: \"TXT\",\n\t\tData: \"foobar\",\n\t}\n\n\terr := client.AddRecord(t.Context(), \"example.com.\", record)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(commandDNSRowDelete+\".json\"),\n\t\t\tcheckFormRequest(`{\"request\":{\"user\":\"user\",\"auth\":\"xxx\",\"command\":\"dns-row-delete\",\"data\":{\"row_id\":\"1\",\"domain\":\"example.com\",\"rdata\":\"\"}}}`)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com.\", \"1\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_Commit(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /\",\n\t\t\tservermock.ResponseFromFixture(commandDNSDomainCommit+\".json\"),\n\t\t\tcheckFormRequest(`{\"request\":{\"user\":\"user\",\"auth\":\"xxx\",\"command\":\"dns-domain-commit\",\"data\":{\"name\":\"example.com\"}}}`)).\n\t\tBuild(t)\n\n\terr := client.Commit(t.Context(), \"example.com.\")\n\trequire.NoError(t, err)\n}\n\nfunc checkFormRequest(data string) servermock.LinkFunc {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\terr := req.ParseForm()\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(rw, err.Error(), http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tform := regexp.MustCompile(`\"auth\":\"\\w+\",`).\n\t\t\t\tReplaceAllString(req.PostForm.Get(\"request\"), `\"auth\":\"xxx\",`)\n\n\t\t\tif form != data {\n\t\t\t\thttp.Error(rw, fmt.Sprintf(\"invalid form data: %s\", req.PostForm.Get(\"request\")), http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tnext.ServeHTTP(rw, req)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/wedos/internal/fixtures/dns-domain-commit.json",
    "content": "{\n  \"response\": {\n    \"code\": 1000,\n    \"result\": \"OK\",\n    \"timestamp\": 1291192534,\n    \"svTRID\": \"1291192534.6326.32542.1\",\n    \"command\": \"dns-domain-commit\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/wedos/internal/fixtures/dns-row-add.json",
    "content": "{\n  \"response\": {\n    \"code\": 1000,\n    \"result\": \"OK\",\n    \"timestamp\": 1291210501,\n    \"svTRID\": \"1291210501.7672.19698.1\",\n    \"command\": \"dns-row-add\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/wedos/internal/fixtures/dns-row-delete.json",
    "content": "{\n  \"response\": {\n    \"code\": 1000,\n    \"result\": \"OK\",\n    \"timestamp\": 1291370821,\n    \"svTRID\": \"1291370821.1702.7371.1\",\n    \"command\": \"dns-row-delete\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/wedos/internal/fixtures/dns-row-update.json",
    "content": "{\n  \"response\": {\n    \"code\": 1000,\n    \"result\": \"OK\",\n    \"timestamp\": 1291370821,\n    \"svTRID\": \"1291370821.1702.7371.1\",\n    \"command\": \"dns-row-update\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/wedos/internal/fixtures/dns-rows-list.json",
    "content": "{\n  \"response\": {\n    \"code\": 1000,\n    \"result\": \"OK\",\n    \"timestamp\": 1291194425,\n    \"svTRID\": \"1291194425.9562.9881.1\",\n    \"command\": \"dns-rows-list\",\n    \"data\": {\n      \"row\": [\n        {\n          \"ID\": \"911\",\n          \"name\": \"\",\n          \"ttl\": \"1800\",\n          \"rdtype\": \"A\",\n          \"rdata\": \"1.2.3.4\",\n          \"changed_date\": \"2010-12-01 09:54:41\",\n          \"author_comment\": \"\"\n        },\n        {\n          \"ID\": \"913\",\n          \"name\": \"\",\n          \"ttl\": \"1800\",\n          \"rdtype\": \"MX\",\n          \"rdata\": \"1 mail1.wedos.net\",\n          \"changed_date\": \"2010-12-01 09:54:54\",\n          \"author_comment\": \"\"\n        },\n        {\n          \"ID\": \"914\",\n          \"name\": \"\",\n          \"ttl\": \"1800\",\n          \"rdtype\": \"MX\",\n          \"rdata\": \"10 mailbackup.wedos.net\",\n          \"changed_date\": \"2010-12-01 09:55:07\",\n          \"author_comment\": \"\"\n        },\n        {\n          \"ID\": \"912\",\n          \"name\": \"*\",\n          \"ttl\": \"1800\",\n          \"rdtype\": \"A\",\n          \"rdata\": \"1.2.3.4\",\n          \"changed_date\": \"2010-12-01 09:54:46\",\n          \"author_comment\": \"\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "providers/dns/wedos/internal/token.go",
    "content": "package internal\n\nimport (\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"time\"\n)\n\nfunc authToken(userName, wapiPass string) string {\n\treturn sha1string(userName + sha1string(wapiPass) + czechHourString())\n}\n\nfunc sha1string(txt string) string {\n\th := sha1.New()\n\t_, _ = io.WriteString(h, txt)\n\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n\nfunc czechHourString() string {\n\treturn formatHour(czechHour())\n}\n\nfunc czechHour() int {\n\ttryZones := []string{\"Europe/Prague\", \"Europe/Paris\", \"CET\"}\n\n\tfor _, zoneName := range tryZones {\n\t\tloc, err := time.LoadLocation(zoneName)\n\t\tif err == nil {\n\t\t\treturn time.Now().In(loc).Hour()\n\t\t}\n\t}\n\n\t// hopefully this will never be used\n\t// this is fallback for containers without tzdata installed\n\treturn utcToCet(time.Now().UTC()).Hour()\n}\n\nfunc utcToCet(utc time.Time) time.Time {\n\t// https://en.wikipedia.org/wiki/Central_European_Time\n\t// As of 2011, all member states of the European Union observe Summer Time (daylight saving time),\n\t// from the last Sunday in March to the last Sunday in October.\n\t// States within the CET area switch to Central European Summer Time (CEST -- UTC+02:00) for the summer.[1]\n\tutcMonth := utc.Month()\n\tif utcMonth < time.March || utcMonth > time.October {\n\t\treturn utc.Add(time.Hour)\n\t}\n\n\tif utcMonth > time.March && utcMonth < time.October {\n\t\treturn utc.Add(time.Hour * 2)\n\t}\n\n\tdayOff := 0\n\n\tbreaking := time.Date(utc.Year(), utcMonth+1, dayOff, 1, 0, 0, 0, time.UTC)\n\tfor breaking.Weekday() != time.Sunday {\n\t\tdayOff--\n\n\t\tbreaking = time.Date(utc.Year(), utcMonth+1, dayOff, 1, 0, 0, 0, time.UTC)\n\n\t\tif dayOff < -7 {\n\t\t\tpanic(\"safety exit to avoid infinite loop\")\n\t\t}\n\t}\n\n\tif (utcMonth == time.March && utc.Before(breaking)) || (utcMonth == time.October && utc.After(breaking)) {\n\t\treturn utc.Add(time.Hour)\n\t}\n\n\treturn utc.Add(time.Hour * 2)\n}\n\nfunc formatHour(hour int) string {\n\treturn fmt.Sprintf(\"%02d\", hour)\n}\n"
  },
  {
    "path": "providers/dns/wedos/internal/types.go",
    "content": "package internal\n\nimport \"encoding/json\"\n\nconst codeOk = 1000\n\nconst (\n\tcommandPing            = \"ping\"\n\tcommandDNSDomainCommit = \"dns-domain-commit\"\n\tcommandDNSRowsList     = \"dns-rows-list\"\n\tcommandDNSRowDelete    = \"dns-row-delete\"\n\tcommandDNSRowAdd       = \"dns-row-add\"\n\tcommandDNSRowUpdate    = \"dns-row-update\"\n)\n\ntype Response interface {\n\tGetCode() int\n\tGetResult() string\n}\n\ntype APIResponse[D any] struct {\n\tResponse ResponsePayload[D] `json:\"response\"`\n}\n\nfunc (a APIResponse[D]) GetCode() int {\n\treturn a.Response.Code\n}\n\nfunc (a APIResponse[D]) GetResult() string {\n\treturn a.Response.Result\n}\n\ntype ResponsePayload[D any] struct {\n\tCode      int    `json:\"code,omitempty\"`\n\tResult    string `json:\"result,omitempty\"`\n\tTimestamp int    `json:\"timestamp,omitempty\"`\n\tSvTRID    string `json:\"svTRID,omitempty\"`\n\tCommand   string `json:\"command,omitempty\"`\n\tData      D      `json:\"data\"`\n}\n\ntype Rows struct {\n\tRows []DNSRow `json:\"row\"`\n}\n\ntype DNSRow struct {\n\tID   string      `json:\"ID,omitempty\"`\n\tName string      `json:\"name,omitempty\"`\n\tTTL  json.Number `json:\"ttl,omitempty\"`\n\tType string      `json:\"rdtype,omitempty\"`\n\tData string      `json:\"rdata\"`\n}\n\ntype DNSRowRequest struct {\n\tID     string      `json:\"row_id,omitempty\"`\n\tDomain string      `json:\"domain,omitempty\"`\n\tName   string      `json:\"name,omitempty\"`\n\tTTL    json.Number `json:\"ttl,omitempty\"`\n\tType   string      `json:\"type,omitempty\"`\n\tData   string      `json:\"rdata\"`\n}\n\ntype APIRequest struct {\n\tUser    string `json:\"user,omitempty\"`\n\tAuth    string `json:\"auth,omitempty\"`\n\tCommand string `json:\"command,omitempty\"`\n\tData    any    `json:\"data,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/wedos/wedos.go",
    "content": "package wedos\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/wedos/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"WEDOS_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"WAPI_PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst minTTL = 5 * 60 // 5 minutes\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUsername           string\n\tPassword           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, minTTL),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"wedos: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"wedos: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Username == \"\" || config.Password == \"\" {\n\t\treturn nil, errors.New(\"wedos: some credentials information are missing\")\n\t}\n\n\tif config.TTL < minTTL {\n\t\treturn nil, fmt.Errorf(\"wedos: invalid TTL, TTL (%d) must be greater than %d\", config.TTL, minTTL)\n\t}\n\n\tclient := internal.NewClient(config.Username, config.Password)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wedos: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wedos: %w\", err)\n\t}\n\n\trecord := internal.DNSRow{\n\t\tName: subDomain,\n\t\tTTL:  json.Number(strconv.Itoa(d.config.TTL)),\n\t\tType: \"TXT\",\n\t\tData: info.Value,\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wedos: could not get records for domain %q: %w\", domain, err)\n\t}\n\n\tfor _, candidate := range records {\n\t\tif candidate.Type == \"TXT\" && candidate.Name == subDomain && candidate.Data == info.Value {\n\t\t\trecord.ID = candidate.ID\n\t\t\tbreak\n\t\t}\n\t}\n\n\terr = d.client.AddRecord(ctx, authZone, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wedos: could not add TXT record for domain %q: %w\", domain, err)\n\t}\n\n\terr = d.client.Commit(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wedos: could not commit TXT record for domain %q: %w\", domain, err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wedos: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wedos: %w\", err)\n\t}\n\n\trecords, err := d.client.GetRecords(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wedos: could not get records for domain %q: %w\", domain, err)\n\t}\n\n\tfor _, candidate := range records {\n\t\tif candidate.Type != \"TXT\" || candidate.Name != subDomain || candidate.Data != info.Value {\n\t\t\tcontinue\n\t\t}\n\n\t\terr = d.client.DeleteRecord(ctx, authZone, candidate.ID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"wedos: could not remove TXT record for domain %q: %w\", domain, err)\n\t\t}\n\n\t\terr = d.client.Commit(ctx, authZone)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"wedos: could not commit TXT record for domain %q: %w\", domain, err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/wedos/wedos.toml",
    "content": "Name = \"WEDOS\"\nDescription = ''''''\nURL = \"https://www.wedos.com\"\nCode = \"wedos\"\nSince = \"v4.4.0\"\n\nExample = '''\nWEDOS_USERNAME=xxxxxxxx \\\nWEDOS_WAPI_PASSWORD=xxxxxxxx \\\nlego --dns wedos -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    WEDOS_USERNAME = \"Username is the same as for the admin account\"\n    WEDOS_WAPI_PASSWORD = \"Password needs to be generated and IP allowed in the admin interface\"\n  [Configuration.Additional]\n    WEDOS_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    WEDOS_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 600)\"\n    WEDOS_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)\"\n    WEDOS_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://kb.wedos.com/en/kategorie/wapi-api-interface/wdns-en/\"\n"
  },
  {
    "path": "providers/dns/wedos/wedos_test.go",
    "content": "package wedos\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvUsername, EnvPassword).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"admin@example.com\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials: username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"wedos: some credentials information are missing: WEDOS_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials: password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"admin@example.com\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"wedos: some credentials information are missing: WEDOS_WAPI_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials: all\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"wedos: some credentials information are missing: WEDOS_USERNAME,WEDOS_WAPI_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"admin@example.com\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"wedos: some credentials information are missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing WAPI password\",\n\t\t\tusername: \"admin@example.com\",\n\t\t\texpected: \"wedos: some credentials information are missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/westcn/westcn.go",
    "content": "// Package westcn implements a DNS provider for solving the DNS-01 challenge using West.cn/西部数码.\npackage westcn\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/westcn\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"WESTCN_\"\n\n\tEnvUsername = envNamespace + \"USERNAME\"\n\tEnvPassword = envNamespace + \"PASSWORD\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultBaseURL = \"https://api.west.cn/api/v2\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config = westcn.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 60),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for West.cn/西部数码.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUsername, EnvPassword)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"westcn: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvUsername]\n\tconfig.Password = values[EnvPassword]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for West.cn/西部数码.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"westcn: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := westcn.NewDNSProviderConfig(config, defaultBaseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"westcn: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"westcn: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"westcn: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n"
  },
  {
    "path": "providers/dns/westcn/westcn.toml",
    "content": "Name = \"West.cn/西部数码\"\nDescription = ''''''\nURL = \"https://www.west.cn\"\nCode = \"westcn\"\nSince = \"v4.21.0\"\n\nExample = '''\nWESTCN_USERNAME=\"xxx\" \\\nWESTCN_PASSWORD=\"yyy\" \\\nlego --dns westcn -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    WESTCN_USERNAME = \"Username\"\n    WESTCN_PASSWORD = \"API password\"\n  [Configuration.Additional]\n    WESTCN_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 10)\"\n    WESTCN_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 120)\"\n    WESTCN_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n    WESTCN_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://www.west.cn/CustomerCenter/doc/domain_v2.html\"\n"
  },
  {
    "path": "providers/dns/westcn/westcn_test.go",
    "content": "package westcn\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvUsername, EnvPassword).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"\",\n\t\t\t\tEnvPassword: \"secret\",\n\t\t\t},\n\t\t\texpected: \"westcn: some credentials information are missing: WESTCN_USERNAME\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing password\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUsername: \"user\",\n\t\t\t\tEnvPassword: \"\",\n\t\t\t},\n\t\t\texpected: \"westcn: some credentials information are missing: WESTCN_PASSWORD\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"westcn: some credentials information are missing: WESTCN_USERNAME,WESTCN_PASSWORD\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tusername string\n\t\tpassword string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:     \"success\",\n\t\t\tusername: \"user\",\n\t\t\tpassword: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tpassword: \"secret\",\n\t\t\texpected: \"westcn: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing password\",\n\t\t\tusername: \"user\",\n\t\t\texpected: \"westcn: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"westcn: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.Username = test.username\n\t\t\tconfig.Password = test.password\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/yandex/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n\tquerystring \"github.com/google/go-querystring/query\"\n)\n\nconst defaultBaseURL = \"https://pddimp.yandex.ru/api2/admin/dns\"\n\nconst successCode = \"ok\"\n\nconst pddTokenHeader = \"PddToken\"\n\ntype Client struct {\n\tpddToken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(pddToken string) (*Client, error) {\n\tif pddToken == \"\" {\n\t\treturn nil, errors.New(\"PDD token is required\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tpddToken:   pddToken,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) AddRecord(ctx context.Context, payload Record) (*Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"add\")\n\n\treq, err := newRequest(ctx, http.MethodPost, endpoint, payload)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tr := AddResponse{}\n\n\terr = c.do(req, &r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn r.Record, nil\n}\n\nfunc (c *Client) RemoveRecord(ctx context.Context, payload Record) (int, error) {\n\tendpoint := c.baseURL.JoinPath(\"del\")\n\n\treq, err := newRequest(ctx, http.MethodPost, endpoint, payload)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tr := RemoveResponse{}\n\n\terr = c.do(req, &r)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn r.RecordID, nil\n}\n\nfunc (c *Client) GetRecords(ctx context.Context, domain string) ([]Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"list\")\n\n\tpayload := struct {\n\t\tDomain string `url:\"domain\"`\n\t}{Domain: domain}\n\n\treq, err := newRequest(ctx, http.MethodGet, endpoint, payload)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tr := ListResponse{}\n\n\terr = c.do(req, &r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn r.Records, nil\n}\n\nfunc (c *Client) do(req *http.Request, result Response) error {\n\treq.Header.Set(pddTokenHeader, c.pddToken)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\tif result.GetSuccess() != successCode {\n\t\treturn fmt.Errorf(\"error during operation: %s %s\", result.GetSuccess(), result.GetError())\n\t}\n\n\treturn nil\n}\n\nfunc newRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\tswitch method {\n\t\tcase http.MethodPost:\n\t\t\tvalues, err := querystring.Values(payload)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tbuf.WriteString(values.Encode())\n\n\t\tcase http.MethodGet:\n\t\t\tvalues, err := querystring.Values(payload)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tendpoint.RawQuery = values.Encode()\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\tif method == http.MethodPost {\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/yandex/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setupClient(server *httptest.Server) (*Client, error) {\n\tclient, err := NewClient(\"lego\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.HTTPClient = server.Client()\n\tclient.baseURL, _ = url.Parse(server.URL)\n\n\treturn client, nil\n}\n\nfunc TestAddRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /add\",\n\t\t\tservermock.ResponseFromFixture(\"add_record.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentTypeFromURLEncoded(),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\tWith(\"subdomain\", \"foo\").\n\t\t\t\tWith(\"ttl\", \"300\").\n\t\t\t\tWith(\"content\", \"txtTXTtxtTXTtxtTXT\").\n\t\t\t\tWith(\"type\", \"TXT\")).\n\t\tBuild(t)\n\n\tdata := Record{\n\t\tDomain:    \"example.com\",\n\t\tType:      \"TXT\",\n\t\tContent:   \"txtTXTtxtTXTtxtTXT\",\n\t\tSubDomain: \"foo\",\n\t\tTTL:       300,\n\t}\n\n\trecord, err := client.AddRecord(t.Context(), data)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, record)\n}\n\nfunc TestAddRecord_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /add\",\n\t\t\tservermock.ResponseFromFixture(\"add_record_error.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentTypeFromURLEncoded()).\n\t\tBuild(t)\n\n\tdata := Record{\n\t\tDomain:    \"example.com\",\n\t\tType:      \"TXT\",\n\t\tContent:   \"txtTXTtxtTXTtxtTXT\",\n\t\tSubDomain: \"foo\",\n\t\tTTL:       300,\n\t}\n\n\t_, err := client.AddRecord(t.Context(), data)\n\trequire.EqualError(t, err, \"error during operation: error bad things\")\n}\n\nfunc TestRemoveRecord(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /del\",\n\t\t\tservermock.ResponseFromFixture(\"remove_record.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentTypeFromURLEncoded(),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"domain\", \"example.com\").\n\t\t\t\tWith(\"record_id\", \"6\")).\n\t\tBuild(t)\n\n\tdata := Record{\n\t\tID:     6,\n\t\tDomain: \"example.com\",\n\t}\n\n\tid, err := client.RemoveRecord(t.Context(), data)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 6, id)\n}\n\nfunc TestRemoveRecord_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"POST /del\",\n\t\t\tservermock.ResponseFromFixture(\"remove_record_error.json\"),\n\t\t\tservermock.CheckHeader().\n\t\t\t\tWithContentTypeFromURLEncoded()).\n\t\tBuild(t)\n\n\tdata := Record{\n\t\tID:     6,\n\t\tDomain: \"example.com\",\n\t}\n\n\t_, err := client.RemoveRecord(t.Context(), data)\n\trequire.EqualError(t, err, \"error during operation: error bad things\")\n}\n\nfunc TestGetRecords(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /list\",\n\t\t\tservermock.ResponseFromFixture(\"get_records.json\"),\n\t\t\tservermock.CheckForm().Strict().\n\t\t\t\tWith(\"domain\", \"example.com\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\trequire.Len(t, records, 2)\n}\n\nfunc TestGetRecords_error(t *testing.T) {\n\tclient := servermock.NewBuilder[*Client](setupClient).\n\t\tRoute(\"GET /list\",\n\t\t\tservermock.ResponseFromFixture(\"get_records_error.json\")).\n\t\tBuild(t)\n\n\t_, err := client.GetRecords(t.Context(), \"example.com\")\n\trequire.EqualError(t, err, \"error during operation: error bad things\")\n}\n"
  },
  {
    "path": "providers/dns/yandex/internal/fixtures/add_record.json",
    "content": "{\n  \"success\": \"ok\",\n  \"domain\": \"example.com\",\n  \"record\": {\n    \"record_id\": 1,\n    \"domain\": \"example.com\",\n    \"subdomain\": \"foo\",\n    \"fqdn\": \"foo.example.com.\",\n    \"ttl\": 300,\n    \"type\": \"TXT\",\n    \"content\": \"txtTXTtxtTXTtxtTXT\"\n  }\n}\n"
  },
  {
    "path": "providers/dns/yandex/internal/fixtures/add_record_error.json",
    "content": "{\n  \"success\": \"error\",\n  \"error\": \"bad things\",\n  \"domain\": \"example.com\"\n}\n"
  },
  {
    "path": "providers/dns/yandex/internal/fixtures/get_records.json",
    "content": "{\n  \"success\": \"ok\",\n  \"domain\": \"example.com\",\n  \"records\": [\n    {\n      \"record_id\": 1,\n      \"domain\": \"example.com\",\n      \"subdomain\": \"foo\",\n      \"fqdn\": \"foo.example.com.\",\n      \"ttl\": 300,\n      \"type\": \"TXT\",\n      \"content\": \"txtTXTtxtTXTtxtTXT\"\n    },\n    {\n      \"record_id\": 2,\n      \"domain\": \"example.com\",\n      \"subdomain\": \"foo\",\n      \"fqdn\": \"foo.example.com.\",\n      \"ttl\": 300,\n      \"type\": \"NS\",\n      \"content\": \"bar\"\n    }\n  ]\n}\n"
  },
  {
    "path": "providers/dns/yandex/internal/fixtures/get_records_error.json",
    "content": "{\n  \"success\": \"error\",\n  \"error\": \"bad things\",\n  \"domain\": \"example.com\"\n}\n"
  },
  {
    "path": "providers/dns/yandex/internal/fixtures/remove_record.json",
    "content": "{\n  \"success\": \"ok\",\n  \"domain\": \"example.com\",\n  \"record_id\": 6\n}\n"
  },
  {
    "path": "providers/dns/yandex/internal/fixtures/remove_record_error.json",
    "content": "{\n  \"success\": \"error\",\n  \"error\": \"bad things\",\n  \"domain\": \"example.com\",\n  \"record_id\": 6\n}\n"
  },
  {
    "path": "providers/dns/yandex/internal/types.go",
    "content": "package internal\n\ntype Record struct {\n\tID        int    `json:\"record_id,omitempty\" url:\"record_id,omitempty\"`\n\tDomain    string `json:\"domain,omitempty\" url:\"domain,omitempty\"`\n\tSubDomain string `json:\"subdomain,omitempty\" url:\"subdomain,omitempty\"`\n\tFQDN      string `json:\"fqdn,omitempty\" url:\"fqdn,omitempty\"`\n\tTTL       int    `json:\"ttl,omitempty\" url:\"ttl,omitempty\"`\n\tType      string `json:\"type,omitempty\" url:\"type,omitempty\"`\n\tContent   string `json:\"content,omitempty\" url:\"content,omitempty\"`\n}\n\ntype Response interface {\n\tGetSuccess() string\n\tGetError() string\n}\n\ntype BaseResponse struct {\n\tSuccess string `json:\"success\"`\n\tError   string `json:\"error,omitempty\"`\n}\n\nfunc (r BaseResponse) GetSuccess() string {\n\treturn r.Success\n}\n\nfunc (r BaseResponse) GetError() string {\n\treturn r.Error\n}\n\ntype AddResponse struct {\n\tBaseResponse\n\n\tDomain string  `json:\"domain,omitempty\"`\n\tRecord *Record `json:\"record,omitempty\"`\n}\n\ntype RemoveResponse struct {\n\tBaseResponse\n\n\tDomain   string `json:\"domain,omitempty\"`\n\tRecordID int    `json:\"record_id,omitempty\"`\n}\n\ntype ListResponse struct {\n\tBaseResponse\n\n\tDomain  string   `json:\"domain,omitempty\"`\n\tRecords []Record `json:\"records,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/yandex/yandex.go",
    "content": "// Package yandex implements a DNS provider for solving the DNS-01 challenge using Yandex PDD.\npackage yandex\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/yandex/internal\"\n\t\"github.com/miekg/dns\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"YANDEX_\"\n\n\tEnvPddToken = envNamespace + \"PDD_TOKEN\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tPddToken           string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 21600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *internal.Client\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Yandex.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvPddToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"yandex: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.PddToken = values[EnvPddToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Yandex.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"yandex: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.PddToken == \"\" {\n\t\treturn nil, errors.New(\"yandex: credentials missing\")\n\t}\n\n\tclient, err := internal.NewClient(config.PddToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"yandex: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{client: client, config: config}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trootDomain, subDomain, err := splitDomain(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandex: %w\", err)\n\t}\n\n\tdata := internal.Record{\n\t\tDomain:    rootDomain,\n\t\tSubDomain: subDomain,\n\t\tType:      \"TXT\",\n\t\tTTL:       d.config.TTL,\n\t\tContent:   info.Value,\n\t}\n\n\t_, err = d.client.AddRecord(context.Background(), data)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandex: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\trootDomain, subDomain, err := splitDomain(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandex: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\trecords, err := d.client.GetRecords(ctx, rootDomain)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandex: %w\", err)\n\t}\n\n\tvar record *internal.Record\n\n\tfor _, rcd := range records {\n\t\tif rcd.Type == \"TXT\" && rcd.SubDomain == subDomain && rcd.Content == info.Value {\n\t\t\trecord = &rcd\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif record == nil {\n\t\treturn fmt.Errorf(\"yandex: TXT record not found for domain: %s\", domain)\n\t}\n\n\tdata := internal.Record{\n\t\tID:     record.ID,\n\t\tDomain: rootDomain,\n\t}\n\n\t_, err = d.client.RemoveRecord(ctx, data)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandex: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\nfunc splitDomain(full string) (string, string, error) {\n\tsplit := dns.Split(full)\n\tif len(split) < 2 {\n\t\treturn \"\", \"\", fmt.Errorf(\"unsupported domain: %s\", full)\n\t}\n\n\tif len(split) == 2 {\n\t\treturn full, \"\", nil\n\t}\n\n\tdomain := full[split[len(split)-2]:]\n\tsubDomain := full[:split[len(split)-2]-1]\n\n\treturn domain, subDomain, nil\n}\n"
  },
  {
    "path": "providers/dns/yandex/yandex.toml",
    "content": "Name = \"Yandex PDD\"\nDescription = '''\n'''\nURL = \"https://pdd.yandex.com\"\nCode = \"yandex\"\nSince = \"v3.7.0\"\n\nExample = '''\nYANDEX_PDD_TOKEN=<your PDD Token> \\\nlego --dns yandex -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    YANDEX_PDD_TOKEN = \"Basic authentication username\"\n  [Configuration.Additional]\n    YANDEX_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    YANDEX_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    YANDEX_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 21600)\"\n    YANDEX_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://yandex.com/dev/domain/doc/concepts/api-dns.html\"\n"
  },
  {
    "path": "providers/dns/yandex/yandex_test.go",
    "content": "package yandex\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvPddToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvPddToken: \"SECRET\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing token\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"yandex: some credentials information are missing: YANDEX_PDD_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tconfig   *Config\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tconfig: &Config{\n\t\t\t\tPddToken: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"nil config\",\n\t\t\tconfig:   nil,\n\t\t\texpected: \"yandex: the configuration of the DNS provider is nil\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing token\",\n\t\t\tconfig:   &Config{},\n\t\t\texpected: \"yandex: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tp, err := NewDNSProviderConfig(test.config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/yandex360/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://api360.yandex.net/\"\n\ntype Client struct {\n\toauthToken string\n\torgID      int64\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\nfunc NewClient(oauthToken string, orgID int64) (*Client, error) {\n\tif oauthToken == \"\" {\n\t\treturn nil, errors.New(\"OAuth token is required\")\n\t}\n\n\tif orgID == 0 {\n\t\treturn nil, errors.New(\"orgID is required\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\toauthToken: oauthToken,\n\t\torgID:      orgID,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\n// AddRecord Adds a DNS record.\n// POST https://api30.yandex.net/directory/v1/org/{orgId}/domains/{domain}/dns\n// https://yandex.ru/dev/api360/doc/ref/DomainDNSService/DomainDNSService_Create.html\nfunc (c *Client) AddRecord(ctx context.Context, domain string, record Record) (*Record, error) {\n\tendpoint := c.baseURL.JoinPath(\"directory\", \"v1\", \"org\", strconv.FormatInt(c.orgID, 10), \"domains\", domain, \"dns\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar newRecord Record\n\n\terr = c.do(req, &newRecord)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &newRecord, nil\n}\n\n// DeleteRecord Deletes a DNS record.\n// DELETE https://api360.yandex.net/directory/v1/org/{orgId}/domains/{domain}/dns/{recordId}\n// https://yandex.ru/dev/api360/doc/ref/DomainDNSService/DomainDNSService_Delete.html\nfunc (c *Client) DeleteRecord(ctx context.Context, domain string, recordID int64) error {\n\tendpoint := c.baseURL.JoinPath(\"directory\", \"v1\", \"org\", strconv.FormatInt(c.orgID, 10), \"domains\", domain, \"dns\", strconv.FormatInt(recordID, 10))\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.Header.Set(\"Authorization\", \"OAuth \"+c.oauthToken)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn parseError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n\nfunc parseError(req *http.Request, resp *http.Response) error {\n\traw, _ := io.ReadAll(resp.Body)\n\n\tvar apiErr APIError\n\n\terr := json.Unmarshal(raw, &apiErr)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code: %d] %w\", resp.StatusCode, apiErr)\n}\n"
  },
  {
    "path": "providers/dns/yandex360/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient, err := NewClient(\"secret\", 123456)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.baseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithAuthorization(\"OAuth secret\"))\n}\n\nfunc TestClient_AddRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /directory/v1/org/123456/domains/example.com/dns\",\n\t\t\tservermock.ResponseFromFixture(\"add-record.json\"),\n\t\t\tservermock.CheckRequestJSONBody(`{\"name\":\"_acme-challenge\",\"text\":\"txtxtxt\",\"ttl\":60,\"type\":\"TXT\"}`)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName: \"_acme-challenge\",\n\t\tText: \"txtxtxt\",\n\t\tTTL:  60,\n\t\tType: \"TXT\",\n\t}\n\n\tnewRecord, err := client.AddRecord(t.Context(), \"example.com\", record)\n\trequire.NoError(t, err)\n\n\texpected := &Record{\n\t\tID:   789465,\n\t\tName: \"foo\",\n\t\tText: \"_acme-challenge\",\n\t\tTTL:  60,\n\t\tType: \"txtxtxt\",\n\t}\n\n\tassert.Equal(t, expected, newRecord)\n}\n\nfunc TestClient_AddRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /directory/v1/org/123456/domains/example.com/dns\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\trecord := Record{\n\t\tName: \"_acme-challenge\",\n\t\tText: \"txtxtxt\",\n\t\tTTL:  60,\n\t\tType: \"TXT\",\n\t}\n\n\tnewRecord, err := client.AddRecord(t.Context(), \"example.com\", record)\n\trequire.Error(t, err)\n\n\tassert.Nil(t, newRecord)\n}\n\nfunc TestClient_DeleteRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /directory/v1/org/123456/domains/example.com/dns/789456\",\n\t\t\tservermock.ResponseFromFixture(\"delete-record.json\")).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", 789456)\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /directory/v1/org/123456/domains/example.com/dns/789456\",\n\t\t\tservermock.ResponseFromFixture(\"error.json\").\n\t\t\t\tWithStatusCode(http.StatusUnauthorized)).\n\t\tBuild(t)\n\n\terr := client.DeleteRecord(t.Context(), \"example.com\", 789456)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "providers/dns/yandex360/internal/fixtures/add-record.json",
    "content": "{\n  \"recordID\": 789465,\n  \"name\": \"foo\",\n  \"text\": \"_acme-challenge\",\n  \"ttl\": 60,\n  \"type\": \"txtxtxt\"\n}\n"
  },
  {
    "path": "providers/dns/yandex360/internal/fixtures/delete-record.json",
    "content": "{}\n"
  },
  {
    "path": "providers/dns/yandex360/internal/fixtures/error.json",
    "content": "{\n  \"code\": 123,\n  \"details\": [\n    {\n      \"@type\": \"foo\"\n    }\n  ],\n  \"message\": \"bar\"\n}\n"
  },
  {
    "path": "providers/dns/yandex360/internal/types.go",
    "content": "package internal\n\nimport \"fmt\"\n\ntype Record struct {\n\tID         int64  `json:\"recordId,omitempty\"`\n\tAddress    string `json:\"address,omitempty\"`\n\tExchange   string `json:\"exchange,omitempty\"`\n\tFlag       int64  `json:\"flag,omitempty\"`\n\tName       string `json:\"name,omitempty\"`\n\tPort       int64  `json:\"port,omitempty\"`\n\tPreference int64  `json:\"preference,omitempty\"`\n\tPriority   int64  `json:\"priority,omitempty\"`\n\tTag        string `json:\"tag,omitempty\"`\n\tTarget     string `json:\"target,omitempty\"`\n\tText       string `json:\"text,omitempty\"`\n\tTTL        int    `json:\"ttl,omitempty\"`\n\tType       string `json:\"type,omitempty\"`\n\tValue      string `json:\"value,omitempty\"`\n\tWeight     int64  `json:\"weight,omitempty\"`\n}\n\ntype APIError struct {\n\tCode    int32    `json:\"code\"`\n\tDetails []Detail `json:\"details\"`\n\tMessage string   `json:\"message\"`\n}\n\nfunc (a APIError) Error() string {\n\treturn fmt.Sprintf(\"%d: %s: %v\", a.Code, a.Message, a.Details)\n}\n\ntype Detail struct {\n\tType string `json:\"@type\"`\n}\n\nfunc (d Detail) String() string {\n\treturn d.Type\n}\n"
  },
  {
    "path": "providers/dns/yandex360/yandex360.go",
    "content": "// Package yandex360 implements a DNS provider for solving the DNS-01 challenge using Yandex 360.\npackage yandex360\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/yandex360/internal\"\n\t\"github.com/miekg/dns\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"YANDEX360_\"\n\n\tEnvOAuthToken = envNamespace + \"OAUTH_TOKEN\"\n\tEnvOrgID      = envNamespace + \"ORG_ID\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tOAuthToken         string\n\tOrgID              int64\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 21600),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient *internal.Client\n\tconfig *Config\n\n\trecordIDs   map[string]int64\n\trecordIDsMu sync.Mutex\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Yandex 360.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvOAuthToken, EnvOrgID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"yandex360: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.OAuthToken = values[EnvOAuthToken]\n\n\torgID, err := strconv.ParseInt(values[EnvOrgID], 10, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"yandex360: %w\", err)\n\t}\n\n\tconfig.OrgID = orgID\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Yandex 360.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"yandex360: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.OAuthToken, config.OrgID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"yandex360: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tclient:    client,\n\t\tconfig:    config,\n\t\trecordIDs: make(map[string]int64),\n\t}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(dns.Fqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandex360: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandex360: %w\", err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\trecord := internal.Record{\n\t\tName: subDomain,\n\t\tTTL:  d.config.TTL,\n\t\tText: info.Value,\n\t\tType: \"TXT\",\n\t}\n\n\tnewRecord, err := d.client.AddRecord(context.Background(), authZone, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandex360: add DNS record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\td.recordIDs[token] = newRecord.ID\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(dns.Fqdn(info.EffectiveFQDN))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandex360: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\td.recordIDsMu.Lock()\n\trecordID, ok := d.recordIDs[token]\n\td.recordIDsMu.Unlock()\n\n\tif !ok {\n\t\treturn fmt.Errorf(\"yandex360: unknown recordID for %q\", info.EffectiveFQDN)\n\t}\n\n\terr = d.client.DeleteRecord(context.Background(), authZone, recordID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandex360: delete DNS record: %w\", err)\n\t}\n\n\td.recordIDsMu.Lock()\n\tdelete(d.recordIDs, token)\n\td.recordIDsMu.Unlock()\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/yandex360/yandex360.toml",
    "content": "Name = \"Yandex 360\"\nDescription = '''\n'''\nURL = \"https://360.yandex.ru\"\nCode = \"yandex360\"\nSince = \"v4.14.0\"\n\nExample = '''\nYANDEX360_OAUTH_TOKEN=<your OAuth Token> \\\nYANDEX360_ORG_ID=<your organization ID> \\\nlego --dns yandex360 -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    YANDEX360_OAUTH_TOKEN = \"The OAuth Token\"\n    YANDEX360_ORG_ID = \"The organization ID\"\n  [Configuration.Additional]\n    YANDEX360_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    YANDEX360_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    YANDEX360_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 21600)\"\n    YANDEX360_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://yandex.ru/dev/api360/doc/ref/DomainDNSService.html\"\n"
  },
  {
    "path": "providers/dns/yandex360/yandex360_test.go",
    "content": "package yandex360\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvOAuthToken, EnvOrgID).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvOAuthToken: \"secret\",\n\t\t\t\tEnvOrgID:      \"123456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing org ID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvOAuthToken: \"secret\",\n\t\t\t},\n\t\t\texpected: \"yandex360: some credentials information are missing: YANDEX360_ORG_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvOrgID: \"123456\",\n\t\t\t},\n\t\t\texpected: \"yandex360: some credentials information are missing: YANDEX360_OAUTH_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc       string\n\t\toauthToken string\n\t\torgID      int64\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tdesc:       \"success\",\n\t\t\toauthToken: \"secret\",\n\t\t\torgID:      123456,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"missing org ID\",\n\t\t\toauthToken: \"secret\",\n\t\t\texpected:   \"yandex360: orgID is required\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing token\",\n\t\t\torgID:    123456,\n\t\t\texpected: \"yandex360: OAuth token is required\",\n\t\t},\n\t}\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.OAuthToken = test.oauthToken\n\t\t\tconfig.OrgID = test.orgID\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/yandexcloud/yandexcloud.go",
    "content": "// Package yandexcloud implements a DNS provider for solving the DNS-01 challenge using Yandex Cloud.\npackage yandexcloud\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\tycdnsproto \"github.com/yandex-cloud/go-genproto/yandex/cloud/dns/v1\"\n\tycdns \"github.com/yandex-cloud/go-sdk/services/dns/v1\"\n\tycsdk \"github.com/yandex-cloud/go-sdk/v2\"\n\t\"github.com/yandex-cloud/go-sdk/v2/credentials\"\n\t\"github.com/yandex-cloud/go-sdk/v2/pkg/iamkey\"\n\t\"github.com/yandex-cloud/go-sdk/v2/pkg/options\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"YANDEX_CLOUD_\"\n\n\tEnvIamToken = envNamespace + \"IAM_TOKEN\"\n\tEnvFolderID = envNamespace + \"FOLDER_ID\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tIamToken string\n\tFolderID string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tTTL                int\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, 60),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tclient ycdns.DnsZoneClient\n\tconfig *Config\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Yandex Cloud.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvIamToken, EnvFolderID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"yandexcloud: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.IamToken = values[EnvIamToken]\n\tconfig.FolderID = values[EnvFolderID]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Yandex Cloud.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"yandexcloud: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.IamToken == \"\" {\n\t\treturn nil, errors.New(\"yandexcloud: some credentials information are missing IAM token\")\n\t}\n\n\tif config.FolderID == \"\" {\n\t\treturn nil, errors.New(\"yandexcloud: some credentials information are missing folder id\")\n\t}\n\n\tcreds, err := decodeCredentials(config.IamToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"yandexcloud: iam token is malformed: %w\", err)\n\t}\n\n\tsdk, err := ycsdk.Build(context.Background(), options.WithCredentials(creds))\n\tif err != nil {\n\t\treturn nil, errors.New(\"yandexcloud: unable to build yandex cloud sdk\")\n\t}\n\n\treturn &DNSProvider{\n\t\tclient: ycdns.NewDnsZoneClient(sdk),\n\t\tconfig: config,\n\t}, nil\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandexcloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tzones, err := d.getZones(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandexcloud: %w\", err)\n\t}\n\n\tvar zoneID string\n\n\tfor _, zone := range zones {\n\t\tif zone.GetZone() == authZone {\n\t\t\tzoneID = zone.GetId()\n\t\t}\n\t}\n\n\tif zoneID == \"\" {\n\t\treturn fmt.Errorf(\"yandexcloud: cant find dns zone %s in yandex cloud\", authZone)\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandexcloud: %w\", err)\n\t}\n\n\terr = d.upsertRecordSetData(ctx, zoneID, subDomain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandexcloud: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandexcloud: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tctx := context.Background()\n\n\tzones, err := d.getZones(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandexcloud: %w\", err)\n\t}\n\n\tvar zoneID string\n\n\tfor _, zone := range zones {\n\t\tif zone.GetZone() == authZone {\n\t\t\tzoneID = zone.GetId()\n\t\t}\n\t}\n\n\tif zoneID == \"\" {\n\t\treturn nil\n\t}\n\n\tsubDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandexcloud: %w\", err)\n\t}\n\n\terr = d.removeRecordSetData(ctx, zoneID, subDomain, info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"yandexcloud: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// getZones retrieves available zones from yandex cloud.\nfunc (d *DNSProvider) getZones(ctx context.Context) ([]*ycdnsproto.DnsZone, error) {\n\tlist := &ycdnsproto.ListDnsZonesRequest{\n\t\tFolderId: d.config.FolderID,\n\t}\n\n\tresponse, err := d.client.List(ctx, list)\n\tif err != nil {\n\t\treturn nil, errors.New(\"unable to fetch dns zones\")\n\t}\n\n\treturn response.GetDnsZones(), nil\n}\n\nfunc (d *DNSProvider) upsertRecordSetData(ctx context.Context, zoneID, name, value string) error {\n\tget := &ycdnsproto.GetDnsZoneRecordSetRequest{\n\t\tDnsZoneId: zoneID,\n\t\tName:      name,\n\t\tType:      \"TXT\",\n\t}\n\n\texist, err := d.client.GetRecordSet(ctx, get)\n\tif err != nil {\n\t\tif !strings.Contains(err.Error(), \"RecordSet not found\") {\n\t\t\treturn err\n\t\t}\n\t}\n\n\trecord := &ycdnsproto.RecordSet{\n\t\tName: name,\n\t\tType: \"TXT\",\n\t\tTtl:  int64(d.config.TTL),\n\t\tData: []string{},\n\t}\n\n\tvar deletions []*ycdnsproto.RecordSet\n\n\tif exist != nil {\n\t\trecord.SetData(append(record.GetData(), exist.GetData()...))\n\t\tdeletions = append(deletions, exist)\n\t}\n\n\tappended := appendRecordSetData(record, value)\n\tif !appended {\n\t\t// The value already present in RecordSet, nothing to do\n\t\treturn nil\n\t}\n\n\tupdate := &ycdnsproto.UpdateRecordSetsRequest{\n\t\tDnsZoneId: zoneID,\n\t\tDeletions: deletions,\n\t\tAdditions: []*ycdnsproto.RecordSet{record},\n\t}\n\n\t_, err = d.client.UpdateRecordSets(ctx, update)\n\n\treturn err\n}\n\nfunc (d *DNSProvider) removeRecordSetData(ctx context.Context, zoneID, name, value string) error {\n\tget := &ycdnsproto.GetDnsZoneRecordSetRequest{\n\t\tDnsZoneId: zoneID,\n\t\tName:      name,\n\t\tType:      \"TXT\",\n\t}\n\n\tpreviousRecord, err := d.client.GetRecordSet(ctx, get)\n\tif err != nil {\n\t\tif strings.Contains(err.Error(), \"RecordSet not found\") {\n\t\t\t// RecordSet is not present, nothing to do\n\t\t\treturn nil\n\t\t}\n\n\t\treturn err\n\t}\n\n\tvar additions []*ycdnsproto.RecordSet\n\n\tif len(previousRecord.GetData()) > 1 {\n\t\t// RecordSet is not empty we should update it\n\t\trecord := &ycdnsproto.RecordSet{\n\t\t\tName: name,\n\t\t\tType: \"TXT\",\n\t\t\tTtl:  int64(d.config.TTL),\n\t\t\tData: []string{},\n\t\t}\n\n\t\tfor _, data := range previousRecord.GetData() {\n\t\t\tif data != value {\n\t\t\t\trecord.SetData(append(record.GetData(), data))\n\t\t\t}\n\t\t}\n\n\t\tadditions = append(additions, record)\n\t}\n\n\tupdate := &ycdnsproto.UpdateRecordSetsRequest{\n\t\tDnsZoneId: zoneID,\n\t\tDeletions: []*ycdnsproto.RecordSet{previousRecord},\n\t\tAdditions: additions,\n\t}\n\n\t_, err = d.client.UpdateRecordSets(ctx, update)\n\n\treturn err\n}\n\n// decodeCredentials converts base64 encoded json of iam token to struct.\nfunc decodeCredentials(accountB64 string) (credentials.Credentials, error) {\n\taccount, err := base64.StdEncoding.DecodeString(accountB64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tkey := &iamkey.Key{}\n\n\terr = json.Unmarshal(account, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn credentials.ServiceAccountKey(key)\n}\n\nfunc appendRecordSetData(record *ycdnsproto.RecordSet, value string) bool {\n\tif slices.Contains(record.GetData(), value) {\n\t\treturn false\n\t}\n\n\trecord.SetData(append(record.GetData(), value))\n\n\treturn true\n}\n"
  },
  {
    "path": "providers/dns/yandexcloud/yandexcloud.toml",
    "content": "Name = \"Yandex Cloud\"\nDescription = ''''''\nURL = \"https://cloud.yandex.com\"\nCode = \"yandexcloud\"\nSince = \"v4.9.0\"\n\nExample = '''\nYANDEX_CLOUD_IAM_TOKEN=<base64_IAM_token> \\\nYANDEX_CLOUD_FOLDER_ID=<folder/project_id> \\\nlego --dns yandexcloud -d '*.example.com' -d example.com run\n\n# ---\n\nYANDEX_CLOUD_IAM_TOKEN=$(echo '{ \\\n  \"id\": \"<string id>\", \\\n  \"service_account_id\": \"<string id>\", \\\n  \"created_at\": \"<datetime>\", \\\n  \"key_algorithm\": \"RSA_2048\", \\\n  \"public_key\": \"-----BEGIN PUBLIC KEY-----<rsa public key>-----END PUBLIC KEY-----\", \\\n  \"private_key\": \"-----BEGIN PRIVATE KEY-----<rsa private key>-----END PRIVATE KEY-----\" \\\n}' | base64) \\\nYANDEX_CLOUD_FOLDER_ID=<yandex cloud folder(project) id> \\\nlego --dns yandexcloud -d '*.example.com' -d example.com run\n'''\n\nAdditional = '''\n## IAM Token\n\nThe simplest way to retrieve IAM access token is usage of yc-cli,\nfollow [docs](https://cloud.yandex.ru/docs/iam/operations/iam-token/create-for-sa) to get it\n\n```bash\nyc iam key create --service-account-name my-robot --output key.json\ncat key.json | base64\n```\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    YANDEX_CLOUD_IAM_TOKEN = \"The base64 encoded json which contains information about iam token of service account with `dns.admin` permissions\"\n    YANDEX_CLOUD_FOLDER_ID = \"The string id of folder (aka project) in Yandex Cloud\"\n  [Configuration.Additional]\n    YANDEX_CLOUD_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    YANDEX_CLOUD_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    YANDEX_CLOUD_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 60)\"\n\n[Links]\n  API = \"https://cloud.yandex.com/en/docs/dns/quickstart\"\n"
  },
  {
    "path": "providers/dns/yandexcloud/yandexcloud_test.go",
    "content": "package yandexcloud\n\nimport (\n\t\"encoding/base64\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nconst fakeIAMToken = `\n{\n  \"id\": \"abcdefghijklmnopqrst\",\n  \"service_account_id\": \"abcdefghijklmnopqrst\",\n  \"created_at\": \"2000-01-01T00:00:00.000000000Z\",\n  \"key_algorithm\": \"RSA_2048\",\n  \"public_key\": \"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkVF2HjTx4v9rGof5OHGO\\nGka+5XJc+px2KkzG0kG2H0ftal8n1LaY2rARmGp1T1/px80rR3amJ9mhnmB+jH5+\\ntwxWr+qVwVnJrklBozgEtl6wXzB7zNqC3kV5rXZ4Omvn6daKuiczfgLL7N/yYQzk\\nSKRYOCygBbPoxVGS50ZLVdCWWtz1iFbNmElnsM4KQjnxWBVRDwR2H5OIU84NonUz\\nNcHDkVBX/d8pkSg7iB4NyD1AqvJtF1pS03NQm32n69bsfRsJxrqR6LK/aql379rk\\nhgA7SyzMLJcLckKug+KfTCpktrwzi2AppUPD7keKJilOfhSrCGQglMr6Q3ao03SZ\\ncQIDAQAB\\n-----END PUBLIC KEY-----\",\n  \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCRUXYeNPHi/2sa\\nh/k4cY4aRr7lclz6nHYqTMbSQbYfR+1qXyfUtpjasBGYanVPX+nHzStHdqYn2aGe\\nYH6Mfn63DFav6pXBWcmuSUGjOAS2XrBfMHvM2oLeRXmtdng6a+fp1oq6JzN+Asvs\\n3/JhDORIpFg4LKAFs+jFUZLnRktV0JZa3PWIVs2YSWewzgpCOfFYFVEPBHYfk4hT\\nzg2idTM1wcORUFf93ymRKDuIHg3IPUCq8m0XWlLTc1Cbfafr1ux9GwnGupHosr9q\\nqXfv2uSGADtLLMwslwtyQq6D4p9MKmS2vDOLYCmlQ8PuR4omKU5+FKsIZCCUyvpD\\ndqjTdJlxAgMBAAECggEAOzG7s8JNZfI1ZrFMy7k18W4wBLb5OPzTBZgQxUUPMt7R\\nzyrDxto6mZpvEG8NKjAfwsvIfWvPcxwrwZ/87K36YAYeqbodFo3EocIlgp8nDEK2\\nBZByXZgFBxW14vsHLoUWCyLhj8K4LvRkrTDsQqxFsXGAniFPbgNDJl18QclYlrOr\\nnn9ZF7W0t2d0jnuzwB9k8L18RqRYWovCAjnFCS0tX5uQKtjSYD0JRG7CiKqd4ruv\\ntJ1Go4bo+rRcaEbFgDyf8BEVa6t9VJX1MVjL2xm0toQUjtA+ZTuAAg4hCibEoru8\\nYo55+R65HHI9B8nZxfp0kEVyzAhQWov91JbHzhRiAQKBgQDM8yuJ4tDAQ53RDmDF\\nX5er2F9TeJo2ARiFB2C+4h9I88jC1LJ3Kgd161MO1mY3SVfNMHXZc0tpRDr+5xdn\\nUNKuV8AS+O80Fan5eJX245bJiXr7Q73tV1PjVwJmXkMT+GaITqKsGyOZp1ms61Ed\\nP/YaDfS7az1KeIGKWmkO5xDc2QKBgQC1g9G4wTrAaaZ8uXBkm982Oy47iMDy4IgW\\na4mLyedhvBhOFNSGwNKfw6zBX+PPT1FKM9xJX1g1kbNNhH+W/y/Qx/uNz7QcsSvQ\\nsUVRwPRmUarPsIuDGvqIj7kn7HjQgqJ/hTlmOXR3fTrvGZq8OYyhgF6BqowPFS/2\\nxVYOLXsiWQKBgQCpmxdNzZlJcut4ZTiqPfiLas1Ai4664F9FP5zNet2/Bpf+u/xQ\\n50QzTqJ2pfEDEbwKf28Xm/UtURytc9qHUnh3dQDr8nwqEz+Nxz/7h85yTEatBxt2\\n/Yzbl1bSFnHWZfucE89FNFRaxQZONpLy7MqiNyhvrUiUh3NUZouInKn0yQKBgEAv\\nGougGCxNr4dO80VAMM+2YYS/uKqpZrW21O5POLhAkL+bcgMsT84anQ3L4Hw/6di5\\nOd3gDwryOFrizVMRbVEARh1BIsk6hOnIpWBhQIqluiayoMJ9WbXMTIangZkJeHhr\\nHX7eNibCa4J8pVCFcQryn3huXBRBQ7KY2PMudeoRAoGBAJ1vdBQSuai3RIfyj8Yr\\n4ArtCU1T5bicp13+mJODSeRhHMnlKkmI64vwrW5POFXWyJKPYLkuDk9bEYOyNBOA\\nBTsUyaJp3jx/942oEwURc4Tb9az7CqEHaCrWHVHCj1CjCEX/FsRfd+wYyuGLwwly\\nwdpqBWBl5iH74tRD6c+rguma\\n-----END PRIVATE KEY-----\"\n}\n`\n\nvar envTest = tester.NewEnvTest(EnvIamToken, EnvFolderID).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvIamToken: base64.StdEncoding.EncodeToString([]byte(fakeIAMToken)),\n\t\t\t\tEnvFolderID: \"folder_id\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing iam token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvFolderID: \"folder_id\",\n\t\t\t},\n\t\t\texpected: \"yandexcloud: some credentials information are missing: YANDEX_CLOUD_IAM_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing folder_id\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvIamToken: base64.StdEncoding.EncodeToString([]byte(fakeIAMToken)),\n\t\t\t},\n\t\t\texpected: \"yandexcloud: some credentials information are missing: YANDEX_CLOUD_FOLDER_ID\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"malformed token (not base64)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvIamToken: fakeIAMToken,\n\t\t\t\tEnvFolderID: \"folder_id\",\n\t\t\t},\n\t\t\texpected: \"yandexcloud: iam token is malformed: illegal base64 data at input byte 1\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"malformed token (invalid json in bas64)\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvIamToken: \"aW52YWxpZCBqc29u\",\n\t\t\t\tEnvFolderID: \"folder_id\",\n\t\t\t},\n\t\t\texpected: \"yandexcloud: iam token is malformed: invalid character 'i' looking for beginning of value\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tconfig   *Config\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tconfig: &Config{\n\t\t\t\tIamToken: base64.StdEncoding.EncodeToString([]byte(fakeIAMToken)),\n\t\t\t\tFolderID: \"folder_id\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:     \"nil config\",\n\t\t\tconfig:   nil,\n\t\t\texpected: \"yandexcloud: the configuration of the DNS provider is nil\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing token\",\n\t\t\tconfig: &Config{\n\t\t\t\tFolderID: \"folder_id\",\n\t\t\t},\n\t\t\texpected: \"yandexcloud: some credentials information are missing IAM token\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing folder id\",\n\t\t\tconfig: &Config{\n\t\t\t\tIamToken: base64.StdEncoding.EncodeToString([]byte(fakeIAMToken)),\n\t\t\t},\n\t\t\texpected: \"yandexcloud: some credentials information are missing folder id\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tp, err := NewDNSProviderConfig(test.config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/zoneedit/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\nconst defaultBaseURL = \"https://dynamic.zoneedit.com\"\n\n// Client the ZoneEdit API client.\ntype Client struct {\n\tuser      string\n\tauthToken string\n\n\tbaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(user, authToken string) (*Client, error) {\n\tif user == \"\" || authToken == \"\" {\n\t\treturn nil, errors.New(\"credentials missing\")\n\t}\n\n\tbaseURL, _ := url.Parse(defaultBaseURL)\n\n\treturn &Client{\n\t\tuser:       user,\n\t\tauthToken:  authToken,\n\t\tbaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 10 * time.Second},\n\t}, nil\n}\n\nfunc (c *Client) CreateTXTRecord(domain, rdata string) error {\n\treturn c.perform(\"txt-create.php\", domain, rdata)\n}\n\nfunc (c *Client) DeleteTXTRecord(domain, rdata string) error {\n\treturn c.perform(\"txt-delete.php\", domain, rdata)\n}\n\nfunc (c *Client) perform(actionPath, domain, rdata string) error {\n\tendpoint := c.baseURL.JoinPath(actionPath)\n\n\tquery := endpoint.Query()\n\tquery.Set(\"host\", domain)\n\tquery.Set(\"rdata\", rdata)\n\tendpoint.RawQuery = query.Encode()\n\n\treq, err := http.NewRequest(http.MethodGet, endpoint.String(), http.NoBody)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req)\n}\n\nfunc (c *Client) do(req *http.Request) error {\n\treq.SetBasicAuth(c.user, c.authToken)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\traw, _ := io.ReadAll(resp.Body)\n\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\tif bytes.Contains(raw, []byte(\"SUCCESS CODE\")) {\n\t\treturn nil\n\t}\n\n\traw = bytes.TrimSpace(raw)\n\n\t// The answer is not an XML valid (missing closing), so I fix it to parse it.\n\tif bytes.HasSuffix(raw, []byte(\">\")) {\n\t\traw = slices.Concat(raw[:len(raw)-1], []byte(\"/>\"))\n\t}\n\n\tvar apiErr APIError\n\n\terr = xml.Unmarshal(raw, &apiErr)\n\tif err != nil {\n\t\treturn errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)\n\t}\n\n\treturn fmt.Errorf(\"[status code: %d] %w\", resp.StatusCode, apiErr)\n}\n"
  },
  {
    "path": "providers/dns/zoneedit/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder(func(server *httptest.Server) (*Client, error) {\n\t\tclient, err := NewClient(\"user\", \"secret\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tclient.baseURL, _ = url.Parse(server.URL)\n\t\tclient.HTTPClient = server.Client()\n\n\t\treturn client, nil\n\t})\n}\n\nfunc TestClient_CreateTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /txt-create.php\",\n\t\t\tservermock.ResponseFromFixture(\"success.xml\")).\n\t\tBuild(t)\n\n\terr := client.CreateTXTRecord(\"_acme-challenge.example.com\", \"value\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_CreateTXTRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /txt-create.php\",\n\t\t\tservermock.ResponseFromFixture(\"error.xml\")).\n\t\tBuild(t)\n\n\terr := client.CreateTXTRecord(\"_acme-challenge.example.com\", \"value\")\n\trequire.EqualError(t, err, \"[status code: 200] 708: Failed Login: user (_acme-challenge.example.com)\")\n}\n\nfunc TestClient_DeleteTXTRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /txt-delete.php\",\n\t\t\tservermock.ResponseFromFixture(\"success.xml\")).\n\t\tBuild(t)\n\n\terr := client.DeleteTXTRecord(\"_acme-challenge.example.com\", \"value\")\n\trequire.NoError(t, err)\n}\n\nfunc TestClient_DeleteTXTRecord_error(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /txt-delete.php\",\n\t\t\tservermock.ResponseFromFixture(\"error.xml\")).\n\t\tBuild(t)\n\n\terr := client.DeleteTXTRecord(\"_acme-challenge.example.com\", \"value\")\n\trequire.EqualError(t, err, \"[status code: 200] 708: Failed Login: user (_acme-challenge.example.com)\")\n}\n"
  },
  {
    "path": "providers/dns/zoneedit/internal/fixtures/error.xml",
    "content": "<ERROR CODE=\"708\" TEXT=\"Failed Login: user\" ZONE=\"_acme-challenge.example.com\">\n"
  },
  {
    "path": "providers/dns/zoneedit/internal/fixtures/success.xml",
    "content": "<SUCCESS CODE=\"200\" TEXT=\"_acme-challenge.example.ca TXT with rdata yaZy0O9QYEKtqBWeJqq7vJYjFuUoB0c0dzjo7UaJcMs deleted\" ZONE=\"example.com\">\n"
  },
  {
    "path": "providers/dns/zoneedit/internal/types.go",
    "content": "package internal\n\nimport (\n\t\"encoding/xml\"\n\t\"fmt\"\n)\n\ntype APIError struct {\n\tXMLName xml.Name `xml:\"ERROR\"`\n\tText    string   `xml:\",chardata\"`\n\tCode    string   `xml:\"CODE,attr\"`\n\tMessage string   `xml:\"TEXT,attr\"`\n\tZone    string   `xml:\"ZONE,attr\"`\n}\n\nfunc (a APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s (%s)\", a.Code, a.Message, a.Zone)\n}\n"
  },
  {
    "path": "providers/dns/zoneedit/zoneedit.go",
    "content": "// Package zoneedit implements a DNS provider for solving the DNS-01 challenge using ZoneEdit.\npackage zoneedit\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/zoneedit/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"ZONEEDIT_\"\n\n\tEnvUser     = envNamespace + \"USER\"\n\tEnAuthToken = envNamespace + \"AUTH_TOKEN\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tUser      string\n\tAuthToken string\n\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for ZoneEdit.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvUser, EnAuthToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"zoneedit: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.User = values[EnvUser]\n\tconfig.AuthToken = values[EnAuthToken]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for ZoneEdit.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"zoneedit: the configuration of the DNS provider is nil\")\n\t}\n\n\tclient, err := internal.NewClient(config.User, config.AuthToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"zoneedit: %w\", err)\n\t}\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\treturn &DNSProvider{\n\t\tconfig: config,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.client.CreateTXTRecord(dns01.UnFqdn(info.EffectiveFQDN), info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"zoneedit: create TXT record: %w\", err)\n\t}\n\n\t// ERROR CODE=\"702\" TEXT=\"Minimum 10 seconds between requests\"\n\ttime.Sleep(11 * time.Second)\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\terr := d.client.DeleteTXTRecord(dns01.UnFqdn(info.EffectiveFQDN), info.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"zoneedit: delete TXT record: %w\", err)\n\t}\n\n\t// ERROR CODE=\"702\" TEXT=\"Minimum 10 seconds between requests\"\n\ttime.Sleep(11 * time.Second)\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n"
  },
  {
    "path": "providers/dns/zoneedit/zoneedit.toml",
    "content": "Name = \"ZoneEdit\"\nDescription = ''''''\nURL = \"https://www.zoneedit.com\"\nCode = \"zoneedit\"\nSince = \"v4.25.0\"\n\nExample = '''\nZONEEDIT_USER=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nZONEEDIT_AUTH_TOKEN=\"xxxxxxxxxxxxxxxxxxxxx\" \\\nlego --dns zoneedit -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    ZONEEDIT_USER = \"User ID\"\n    ZONEEDIT_AUTH_TOKEN = \"Authentication token\"\n  [Configuration.Additional]\n    ZONEEDIT_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    ZONEEDIT_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    ZONEEDIT_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://support.zoneedit.com/en/knowledgebase/article/changes-to-dynamic-dns\"\n"
  },
  {
    "path": "providers/dns/zoneedit/zoneedit_test.go",
    "content": "package zoneedit\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvUser, EnAuthToken).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUser:     \"user\",\n\t\t\t\tEnAuthToken: \"secret\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing user ID\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUser:     \"\",\n\t\t\t\tEnAuthToken: \"secret\",\n\t\t\t},\n\t\t\texpected: \"zoneedit: some credentials information are missing: ZONEEDIT_USER\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing auth token\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvUser:     \"user\",\n\t\t\t\tEnAuthToken: \"\",\n\t\t\t},\n\t\t\texpected: \"zoneedit: some credentials information are missing: ZONEEDIT_AUTH_TOKEN\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\tenvVars:  map[string]string{},\n\t\t\texpected: \"zoneedit: some credentials information are missing: ZONEEDIT_USER,ZONEEDIT_AUTH_TOKEN\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\tuser      string\n\t\tauthToken string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tuser:      \"user\",\n\t\t\tauthToken: \"secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing user ID\",\n\t\t\tauthToken: \"secret\",\n\t\t\texpected:  \"zoneedit: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing auth token\",\n\t\t\tuser:     \"user\",\n\t\t\texpected: \"zoneedit: credentials missing\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"zoneedit: credentials missing\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.User = test.user\n\t\t\tconfig.AuthToken = test.authToken\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t\trequire.NotNil(t, p.client)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/zoneee/internal/client.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/errutils\"\n)\n\n// DefaultEndpoint the default API endpoint.\nconst DefaultEndpoint = \"https://api.zone.eu/v2/\"\n\n// Client the API client for Zoneee.\ntype Client struct {\n\tusername string\n\tapiKey   string\n\n\tBaseURL    *url.URL\n\tHTTPClient *http.Client\n}\n\n// NewClient creates a new Client.\nfunc NewClient(username, apiKey string) *Client {\n\tbaseURL, _ := url.Parse(DefaultEndpoint)\n\n\treturn &Client{\n\t\tusername:   username,\n\t\tapiKey:     apiKey,\n\t\tBaseURL:    baseURL,\n\t\tHTTPClient: &http.Client{Timeout: 5 * time.Second},\n\t}\n}\n\n// GetTxtRecords get TXT records.\n// https://api.zone.eu/v2#operation/getdnstxtrecords\nfunc (c *Client) GetTxtRecords(ctx context.Context, domain string) ([]TXTRecord, error) {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", domain, \"txt\")\n\n\treq, err := newJSONRequest(ctx, http.MethodGet, endpoint, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar records []TXTRecord\n\tif err := c.do(req, &records); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn records, nil\n}\n\n// AddTxtRecord creates a TXT records.\n// https://api.zone.eu/v2#operation/creatednstxtrecord\nfunc (c *Client) AddTxtRecord(ctx context.Context, domain string, record TXTRecord) ([]TXTRecord, error) {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", domain, \"txt\")\n\n\treq, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar records []TXTRecord\n\tif err := c.do(req, &records); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn records, nil\n}\n\n// RemoveTxtRecord deletes a TXT record.\n// https://api.zone.eu/v2#operation/deletednstxtrecord\nfunc (c *Client) RemoveTxtRecord(ctx context.Context, domain, id string) error {\n\tendpoint := c.BaseURL.JoinPath(\"dns\", domain, \"txt\", id)\n\n\treq, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.do(req, nil)\n}\n\nfunc (c *Client) do(req *http.Request, result any) error {\n\treq.SetBasicAuth(c.username, c.apiKey)\n\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn errutils.NewHTTPDoError(req, err)\n\t}\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode/100 != 2 {\n\t\treturn errutils.NewUnexpectedResponseStatusCodeError(req, resp)\n\t}\n\n\tif result == nil {\n\t\treturn nil\n\t}\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errutils.NewReadResponseError(req, resp.StatusCode, err)\n\t}\n\n\terr = json.Unmarshal(raw, result)\n\tif err != nil {\n\t\treturn errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)\n\t}\n\n\treturn nil\n}\n\nfunc newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {\n\tbuf := new(bytes.Buffer)\n\n\tif payload != nil {\n\t\terr := json.NewEncoder(buf).Encode(payload)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create request JSON body: %w\", err)\n\t\t}\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tif payload != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "providers/dns/zoneee/internal/client_test.go",
    "content": "package internal\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockBuilder() *servermock.Builder[*Client] {\n\treturn servermock.NewBuilder[*Client](\n\t\tfunc(server *httptest.Server) (*Client, error) {\n\t\t\tclient := NewClient(\"user\", \"secret\")\n\t\t\tclient.HTTPClient = server.Client()\n\t\t\tclient.BaseURL, _ = url.Parse(server.URL)\n\n\t\t\treturn client, nil\n\t\t},\n\t\tservermock.CheckHeader().WithJSONHeaders().\n\t\t\tWithBasicAuth(\"user\", \"secret\"),\n\t)\n}\n\nfunc TestClient_GetTxtRecords(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"GET /dns/example.com/txt\", servermock.ResponseFromFixture(\"get-txt-records.json\")).\n\t\tBuild(t)\n\n\trecords, err := client.GetTxtRecords(t.Context(), \"example.com\")\n\trequire.NoError(t, err)\n\n\texpected := []TXTRecord{\n\t\t{ID: \"123\", Name: \"prefix.example.com\", Destination: \"server.example.com\", Delete: true, Modify: true, ResourceURL: \"string\"},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_AddTxtRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"POST /dns/example.com/txt\",\n\t\t\tservermock.ResponseFromFixture(\"create-txt-record.json\").\n\t\t\t\tWithStatusCode(http.StatusCreated),\n\t\t\tservermock.CheckRequestJSONBody(`{\"name\":\"prefix.example.com\",\"destination\":\"server.example.com\"}`)).\n\t\tBuild(t)\n\n\trecords, err := client.AddTxtRecord(t.Context(), \"example.com\", TXTRecord{Name: \"prefix.example.com\", Destination: \"server.example.com\"})\n\trequire.NoError(t, err)\n\n\texpected := []TXTRecord{\n\t\t{ID: \"123\", Name: \"prefix.example.com\", Destination: \"server.example.com\", Delete: true, Modify: true, ResourceURL: \"string\"},\n\t}\n\n\tassert.Equal(t, expected, records)\n}\n\nfunc TestClient_RemoveTxtRecord(t *testing.T) {\n\tclient := mockBuilder().\n\t\tRoute(\"DELETE /dns/example.com/txt/123\",\n\t\t\tservermock.Noop().\n\t\t\t\tWithStatusCode(http.StatusNoContent)).\n\t\tBuild(t)\n\n\terr := client.RemoveTxtRecord(t.Context(), \"example.com\", \"123\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/zoneee/internal/fixtures/create-txt-record.json",
    "content": "[\n  {\n    \"resource_url\": \"string\",\n    \"destination\": \"server.example.com\",\n    \"id\": \"123\",\n    \"name\": \"prefix.example.com\",\n    \"delete\": true,\n    \"modify\": true\n  }\n]\n"
  },
  {
    "path": "providers/dns/zoneee/internal/fixtures/get-txt-records.json",
    "content": "[\n  {\n    \"resource_url\": \"string\",\n    \"destination\": \"server.example.com\",\n    \"id\": \"123\",\n    \"name\": \"prefix.example.com\",\n    \"delete\": true,\n    \"modify\": true\n  }\n]\n"
  },
  {
    "path": "providers/dns/zoneee/internal/types.go",
    "content": "package internal\n\ntype TXTRecord struct {\n\t// Identifier (identificator)\n\tID string `json:\"id,omitempty\"`\n\t// Hostname\n\tName string `json:\"name\"`\n\t// TXT content value\n\tDestination string `json:\"destination\"`\n\t// Can this record be deleted\n\tDelete bool `json:\"delete,omitempty\"`\n\t// Can this record be modified\n\tModify bool `json:\"modify,omitempty\"`\n\t// API url to get this entity\n\tResourceURL string `json:\"resource_url,omitempty\"`\n}\n"
  },
  {
    "path": "providers/dns/zoneee/zoneee.go",
    "content": "// Package zoneee implements a DNS provider for solving the DNS-01 challenge through zone.ee.\npackage zoneee\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug\"\n\t\"github.com/go-acme/lego/v4/providers/dns/zoneee/internal\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"ZONEEE_\"\n\n\tEnvEndpoint = envNamespace + \"ENDPOINT\"\n\tEnvAPIUser  = envNamespace + \"API_USER\"\n\tEnvAPIKey   = envNamespace + \"API_KEY\"\n\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config struct {\n\tEndpoint           *url.URL\n\tUsername           string\n\tAPIKey             string\n\tPropagationTimeout time.Duration\n\tPollingInterval    time.Duration\n\tHTTPClient         *http.Client\n}\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\tendpoint, _ := url.Parse(internal.DefaultEndpoint)\n\n\treturn &Config{\n\t\tEndpoint: endpoint,\n\t\t// zone.ee can take up to 5min to propagate according to the support\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tconfig *Config\n\tclient *internal.Client\n}\n\n// NewDNSProvider returns a DNSProvider instance.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIUser, EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"zoneee: %w\", err)\n\t}\n\n\trawEndpoint := env.GetOrDefaultString(EnvEndpoint, internal.DefaultEndpoint)\n\n\tendpoint, err := url.Parse(rawEndpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"zoneee: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.Username = values[EnvAPIUser]\n\tconfig.APIKey = values[EnvAPIKey]\n\tconfig.Endpoint = endpoint\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Zone.ee.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"zoneee: the configuration of the DNS provider is nil\")\n\t}\n\n\tif config.Username == \"\" {\n\t\treturn nil, errors.New(\"zoneee: credentials missing: username\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn nil, errors.New(\"zoneee: credentials missing: API key\")\n\t}\n\n\tif config.Endpoint == nil {\n\t\treturn nil, errors.New(\"zoneee: the endpoint is missing\")\n\t}\n\n\tclient := internal.NewClient(config.Username, config.APIKey)\n\n\tif config.HTTPClient != nil {\n\t\tclient.HTTPClient = config.HTTPClient\n\t}\n\n\tclient.HTTPClient = clientdebug.Wrap(client.HTTPClient)\n\n\tif config.Endpoint != nil {\n\t\tclient.BaseURL = config.Endpoint\n\t}\n\n\treturn &DNSProvider{config: config, client: client}, nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.config.PropagationTimeout, d.config.PollingInterval\n}\n\n// Present creates a TXT record to fulfill the dns-01 challenge.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"zoneee: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\trecord := internal.TXTRecord{\n\t\tName:        dns01.UnFqdn(info.EffectiveFQDN),\n\t\tDestination: info.Value,\n\t}\n\n\t_, err = d.client.AddTxtRecord(context.Background(), authZone, record)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"zoneee: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record previously created.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\tinfo := dns01.GetChallengeInfo(domain, keyAuth)\n\n\tauthZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"zoneee: could not find zone for domain %q: %w\", domain, err)\n\t}\n\n\tauthZone = dns01.UnFqdn(authZone)\n\n\tctx := context.Background()\n\n\trecords, err := d.client.GetTxtRecords(ctx, authZone)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"zoneee: %w\", err)\n\t}\n\n\tvar id string\n\n\tfor _, record := range records {\n\t\tif record.Destination == info.Value {\n\t\t\tid = record.ID\n\t\t}\n\t}\n\n\tif id == \"\" {\n\t\treturn fmt.Errorf(\"zoneee: txt record does not exist for %s\", info.Value)\n\t}\n\n\tif err = d.client.RemoveTxtRecord(ctx, authZone, id); err != nil {\n\t\treturn fmt.Errorf(\"zoneee: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/dns/zoneee/zoneee.toml",
    "content": "Name = \"Zone.ee\"\nDescription = ''''''\nURL = \"https://www.zone.ee/\"\nCode = \"zoneee\"\nSince = \"v2.1.0\"\n\nExample = '''\nZONEEE_API_USER=xxxxx \\\nZONEEE_API_KEY=yyyyy \\\nlego --dns zoneee -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    ZONEEE_API_USER = \"API user\"\n    ZONEEE_API_KEY = \"API key\"\n  [Configuration.Additional]\n    ZONEEE_ENDPOINT = \"API endpoint URL\"\n    ZONEEE_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 5)\"\n    ZONEEE_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 300)\"\n    ZONEEE_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://api.zone.eu/v2\"\n"
  },
  {
    "path": "providers/dns/zoneee/zoneee_test.go",
    "content": "package zoneee\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/go-acme/lego/v4/providers/dns/zoneee/internal\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nconst (\n\tfakeUsername = \"user\"\n\tfakeAPIKey   = \"secret\"\n)\n\nvar envTest = tester.NewEnvTest(EnvEndpoint, EnvAPIUser, EnvAPIKey).\n\tWithLiveTestRequirements(EnvAPIUser, EnvAPIKey).\n\tWithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser: \"123\",\n\t\t\t\tEnvAPIKey:  \"456\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing credentials\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser: \"\",\n\t\t\t\tEnvAPIKey:  \"\",\n\t\t\t},\n\t\t\texpected: \"zoneee: some credentials information are missing: ZONEEE_API_USER,ZONEEE_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing username\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser: \"\",\n\t\t\t\tEnvAPIKey:  \"456\",\n\t\t\t},\n\t\t\texpected: \"zoneee: some credentials information are missing: ZONEEE_API_USER\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing API key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser: \"123\",\n\t\t\t\tEnvAPIKey:  \"\",\n\t\t\t},\n\t\t\texpected: \"zoneee: some credentials information are missing: ZONEEE_API_KEY\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid URL\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIUser:  \"123\",\n\t\t\t\tEnvAPIKey:   \"456\",\n\t\t\t\tEnvEndpoint: \":\",\n\t\t\t},\n\t\t\texpected: `zoneee: parse \":\": missing protocol scheme`,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tapiUser  string\n\t\tapiKey   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc:    \"success\",\n\t\t\tapiKey:  \"123\",\n\t\t\tapiUser: \"456\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing credentials\",\n\t\t\texpected: \"zoneee: credentials missing: username\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing api key\",\n\t\t\tapiUser:  \"456\",\n\t\t\texpected: \"zoneee: credentials missing: API key\",\n\t\t},\n\t\t{\n\t\t\tdesc:     \"missing username\",\n\t\t\tapiKey:   \"123\",\n\t\t\texpected: \"zoneee: credentials missing: username\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\t\t\tconfig.Username = test.apiUser\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.config)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_Present(t *testing.T) {\n\thostedZone := \"example.com\"\n\tdomain := \"prefix.\" + hostedZone\n\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tbuilder       *servermock.Builder[*DNSProvider]\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tbuilder: mockBuilder(fakeUsername, fakeAPIKey).\n\t\t\t\tRoute(\"POST /dns/\"+hostedZone+\"/txt\",\n\t\t\t\t\tmockHandlerCreateRecord()),\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid auth\",\n\t\t\tbuilder: mockBuilder(\"nope\", \"nope\").\n\t\t\t\tRoute(\"POST /dns/\"+hostedZone+\"/txt\", nil),\n\t\t\texpectedError: \"zoneee: unexpected status code: [status code: 401] body: Unauthorized\",\n\t\t},\n\t\t{\n\t\t\tdesc:          \"error\",\n\t\t\tbuilder:       mockBuilder(fakeUsername, fakeAPIKey),\n\t\t\texpectedError: \"zoneee: unexpected status code: [status code: 404] body: 404 page not found\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tprovider := test.builder.Build(t)\n\n\t\t\terr := provider.Present(domain, \"token\", \"key\")\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNSProvider_Cleanup(t *testing.T) {\n\thostedZone := \"example.com\"\n\tdomain := \"prefix.\" + hostedZone\n\n\ttestCases := []struct {\n\t\tdesc          string\n\t\tbuilder       *servermock.Builder[*DNSProvider]\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tbuilder: mockBuilder(fakeUsername, fakeAPIKey).\n\t\t\t\tRoute(\"GET /dns/\"+hostedZone+\"/txt\",\n\t\t\t\t\tmockHandlerGetRecords([]internal.TXTRecord{{\n\t\t\t\t\t\tID:          \"1234\",\n\t\t\t\t\t\tName:        domain,\n\t\t\t\t\t\tDestination: \"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM\",\n\t\t\t\t\t\tDelete:      true,\n\t\t\t\t\t\tModify:      true,\n\t\t\t\t\t}})).\n\t\t\t\tRoute(\"DELETE /dns/\"+hostedZone+\"/txt/1234\",\n\t\t\t\t\tservermock.Noop().\n\t\t\t\t\t\tWithStatusCode(http.StatusNoContent)),\n\t\t},\n\t\t{\n\t\t\tdesc: \"no txt records\",\n\t\t\tbuilder: mockBuilder(fakeUsername, fakeAPIKey).\n\t\t\t\tRoute(\"GET /dns/\"+hostedZone+\"/txt\",\n\t\t\t\t\tmockHandlerGetRecords([]internal.TXTRecord{})),\n\t\t\texpectedError: \"zoneee: txt record does not exist for LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid auth\",\n\t\t\tbuilder: mockBuilder(\"nope\", \"nope\").\n\t\t\t\tRoute(\"GET /dns/\"+hostedZone+\"/txt\", nil),\n\t\t\texpectedError: \"zoneee: unexpected status code: [status code: 401] body: Unauthorized\",\n\t\t},\n\t\t{\n\t\t\tdesc:          \"error\",\n\t\t\tbuilder:       mockBuilder(fakeUsername, fakeAPIKey),\n\t\t\texpectedError: \"zoneee: unexpected status code: [status code: 404] body: 404 page not found\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tprovider := test.builder.Build(t)\n\n\t\t\terr := provider.CleanUp(domain, \"token\", \"key\")\n\t\t\tif test.expectedError == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expectedError)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(2 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc mockBuilder(username, apiKey string) *servermock.Builder[*DNSProvider] {\n\treturn servermock.NewBuilder(\n\t\tfunc(server *httptest.Server) (*DNSProvider, error) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.HTTPClient = server.Client()\n\t\t\tconfig.Endpoint, _ = url.Parse(server.URL)\n\t\t\tconfig.Username = username\n\t\t\tconfig.APIKey = apiKey\n\n\t\t\treturn NewDNSProviderConfig(config)\n\t\t},\n\t\tcheckBasicAuth())\n}\n\nfunc mockHandlerCreateRecord() http.HandlerFunc {\n\treturn encodeJSONHandler(func(req *http.Request, rw http.ResponseWriter) (any, error) {\n\t\trecord := internal.TXTRecord{}\n\n\t\terr := json.NewDecoder(req.Body).Decode(&record)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\trecord.ID = \"1234\"\n\t\trecord.Delete = true\n\t\trecord.Modify = true\n\t\trecord.ResourceURL = req.URL.String() + \"/1234\"\n\n\t\treturn []internal.TXTRecord{record}, nil\n\t})\n}\n\nfunc mockHandlerGetRecords(records []internal.TXTRecord) http.HandlerFunc {\n\treturn encodeJSONHandler(func(req *http.Request, rw http.ResponseWriter) (any, error) {\n\t\tfor _, record := range records {\n\t\t\tif record.ResourceURL == \"\" {\n\t\t\t\trecord.ResourceURL = req.URL.String() + \"/\" + record.ID\n\t\t\t}\n\t\t}\n\n\t\treturn records, nil\n\t})\n}\n\nfunc encodeJSONHandler(build func(req *http.Request, rw http.ResponseWriter) (any, error)) http.HandlerFunc {\n\treturn func(rw http.ResponseWriter, req *http.Request) {\n\t\tdata, err := build(req, rw)\n\t\tif err != nil {\n\t\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tbytes, err := json.Marshal(data)\n\t\tif err != nil {\n\t\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif _, err = rw.Write(bytes); err != nil {\n\t\t\thttp.Error(rw, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc checkBasicAuth() servermock.LinkFunc {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\tusername, apiKey, ok := req.BasicAuth()\n\t\t\tif username != fakeUsername || apiKey != fakeAPIKey || !ok {\n\t\t\t\trw.Header().Set(\"WWW-Authenticate\", fmt.Sprintf(`Basic realm=%q`, \"Please enter your username and API key.\"))\n\t\t\t\thttp.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tnext.ServeHTTP(rw, req)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "providers/dns/zonomi/zonomi.go",
    "content": "// Package zonomi implements a DNS provider for solving the DNS-01 challenge using Zonomi DNS.\npackage zonomi\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/challenge/dns01\"\n\t\"github.com/go-acme/lego/v4/platform/config/env\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internal/rimuhosting\"\n)\n\n// Environment variables names.\nconst (\n\tenvNamespace = \"ZONOMI_\"\n\n\tEnvAPIKey = envNamespace + \"API_KEY\"\n\n\tEnvTTL                = envNamespace + \"TTL\"\n\tEnvPropagationTimeout = envNamespace + \"PROPAGATION_TIMEOUT\"\n\tEnvPollingInterval    = envNamespace + \"POLLING_INTERVAL\"\n\tEnvHTTPTimeout        = envNamespace + \"HTTP_TIMEOUT\"\n)\n\nconst defaultBaseURL = \"https://zonomi.com/app/dns/dyndns.jsp\"\n\nvar _ challenge.ProviderTimeout = (*DNSProvider)(nil)\n\n// Config is used to configure the creation of the DNSProvider.\ntype Config = rimuhosting.Config\n\n// NewDefaultConfig returns a default configuration for the DNSProvider.\nfunc NewDefaultConfig() *Config {\n\treturn &Config{\n\t\tTTL:                env.GetOrDefaultInt(EnvTTL, rimuhosting.DefaultTTL),\n\t\tPropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),\n\t\tPollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),\n\t\t},\n\t}\n}\n\n// DNSProvider implements the challenge.Provider interface.\ntype DNSProvider struct {\n\tprv challenge.ProviderTimeout\n}\n\n// NewDNSProvider returns a DNSProvider instance configured for Zonomi.\n// Credentials must be passed in the environment variables.\nfunc NewDNSProvider() (*DNSProvider, error) {\n\tvalues, err := env.Get(EnvAPIKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"zonomi: %w\", err)\n\t}\n\n\tconfig := NewDefaultConfig()\n\tconfig.APIKey = values[EnvAPIKey]\n\n\treturn NewDNSProviderConfig(config)\n}\n\n// NewDNSProviderConfig return a DNSProvider instance configured for Zonomi.\nfunc NewDNSProviderConfig(config *Config) (*DNSProvider, error) {\n\tif config == nil {\n\t\treturn nil, errors.New(\"zonomi: the configuration of the DNS provider is nil\")\n\t}\n\n\tprovider, err := rimuhosting.NewDNSProviderConfig(config, defaultBaseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"zonomi: %w\", err)\n\t}\n\n\treturn &DNSProvider{prv: provider}, nil\n}\n\n// Present creates a TXT record using the specified parameters.\nfunc (d *DNSProvider) Present(domain, token, keyAuth string) error {\n\terr := d.prv.Present(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"zonomi: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the TXT record matching the specified parameters.\nfunc (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := d.prv.CleanUp(domain, token, keyAuth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"zonomi: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Timeout returns the timeout and interval to use when checking for DNS propagation.\n// Adjusting here to cope with spikes in propagation times.\nfunc (d *DNSProvider) Timeout() (timeout, interval time.Duration) {\n\treturn d.prv.Timeout()\n}\n"
  },
  {
    "path": "providers/dns/zonomi/zonomi.toml",
    "content": "Name = \"Zonomi\"\nDescription = ''''''\nURL = \"https://zonomi.com\"\nCode = \"zonomi\"\nSince = \"v3.5.0\"\n\nExample = '''\nZONOMI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \\\nlego --dns zonomi -d '*.example.com' -d example.com run\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    ZONOMI_API_KEY = \"User API key\"\n  [Configuration.Additional]\n    ZONOMI_POLLING_INTERVAL = \"Time between DNS propagation check in seconds (Default: 2)\"\n    ZONOMI_PROPAGATION_TIMEOUT = \"Maximum waiting time for DNS propagation in seconds (Default: 60)\"\n    ZONOMI_TTL = \"The TTL of the TXT record used for the DNS challenge in seconds (Default: 3600)\"\n    ZONOMI_HTTP_TIMEOUT = \"API request timeout in seconds (Default: 30)\"\n\n[Links]\n  API = \"https://zonomi.com/app/dns/dyndns.jsp\"\n"
  },
  {
    "path": "providers/dns/zonomi/zonomi_test.go",
    "content": "package zonomi\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst envDomain = envNamespace + \"DOMAIN\"\n\nvar envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)\n\nfunc TestNewDNSProvider(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tdesc: \"success\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"missing api key\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\tEnvAPIKey: \"\",\n\t\t\t},\n\t\t\texpected: \"zonomi: some credentials information are missing: ZONOMI_API_KEY\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tdefer envTest.RestoreEnv()\n\n\t\t\tenvTest.ClearEnv()\n\n\t\t\tenvTest.Apply(test.envVars)\n\n\t\t\tp, err := NewDNSProvider()\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewDNSProviderConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc      string\n\t\texpected  string\n\t\tapiKey    string\n\t\tsecretKey string\n\t}{\n\t\t{\n\t\t\tdesc:      \"success\",\n\t\t\tapiKey:    \"api_key\",\n\t\t\tsecretKey: \"api_secret\",\n\t\t},\n\t\t{\n\t\t\tdesc:      \"missing api key\",\n\t\t\tapiKey:    \"\",\n\t\t\tsecretKey: \"api_secret\",\n\t\t\texpected:  \"zonomi: incomplete credentials, missing API key\",\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tconfig := NewDefaultConfig()\n\t\t\tconfig.APIKey = test.apiKey\n\n\t\t\tp, err := NewDNSProviderConfig(config)\n\n\t\t\tif test.expected == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, p)\n\t\t\t\trequire.NotNil(t, p.prv)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLivePresent(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\terr = provider.Present(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveCleanUp(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\tprovider, err := NewDNSProvider()\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = provider.CleanUp(envTest.GetDomain(), \"\", \"123d==\")\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "providers/dns/zz_gen_dns_providers.go",
    "content": "// Code generated by 'make generate-dns'; DO NOT EDIT.\n\npackage dns\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-acme/lego/v4/challenge\"\n\t\"github.com/go-acme/lego/v4/providers/dns/acmedns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/active24\"\n\t\"github.com/go-acme/lego/v4/providers/dns/alidns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/aliesa\"\n\t\"github.com/go-acme/lego/v4/providers/dns/allinkl\"\n\t\"github.com/go-acme/lego/v4/providers/dns/alwaysdata\"\n\t\"github.com/go-acme/lego/v4/providers/dns/anexia\"\n\t\"github.com/go-acme/lego/v4/providers/dns/artfiles\"\n\t\"github.com/go-acme/lego/v4/providers/dns/arvancloud\"\n\t\"github.com/go-acme/lego/v4/providers/dns/auroradns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/autodns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/axelname\"\n\t\"github.com/go-acme/lego/v4/providers/dns/azion\"\n\t\"github.com/go-acme/lego/v4/providers/dns/azure\"\n\t\"github.com/go-acme/lego/v4/providers/dns/azuredns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/baiducloud\"\n\t\"github.com/go-acme/lego/v4/providers/dns/beget\"\n\t\"github.com/go-acme/lego/v4/providers/dns/binarylane\"\n\t\"github.com/go-acme/lego/v4/providers/dns/bindman\"\n\t\"github.com/go-acme/lego/v4/providers/dns/bluecat\"\n\t\"github.com/go-acme/lego/v4/providers/dns/bluecatv2\"\n\t\"github.com/go-acme/lego/v4/providers/dns/bookmyname\"\n\t\"github.com/go-acme/lego/v4/providers/dns/brandit\"\n\t\"github.com/go-acme/lego/v4/providers/dns/bunny\"\n\t\"github.com/go-acme/lego/v4/providers/dns/checkdomain\"\n\t\"github.com/go-acme/lego/v4/providers/dns/civo\"\n\t\"github.com/go-acme/lego/v4/providers/dns/clouddns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cloudflare\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cloudns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cloudru\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cloudxns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/com35\"\n\t\"github.com/go-acme/lego/v4/providers/dns/conoha\"\n\t\"github.com/go-acme/lego/v4/providers/dns/conohav3\"\n\t\"github.com/go-acme/lego/v4/providers/dns/constellix\"\n\t\"github.com/go-acme/lego/v4/providers/dns/corenetworks\"\n\t\"github.com/go-acme/lego/v4/providers/dns/cpanel\"\n\t\"github.com/go-acme/lego/v4/providers/dns/czechia\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ddnss\"\n\t\"github.com/go-acme/lego/v4/providers/dns/derak\"\n\t\"github.com/go-acme/lego/v4/providers/dns/desec\"\n\t\"github.com/go-acme/lego/v4/providers/dns/designate\"\n\t\"github.com/go-acme/lego/v4/providers/dns/digitalocean\"\n\t\"github.com/go-acme/lego/v4/providers/dns/directadmin\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dnsexit\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dnshomede\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dnsimple\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dnspod\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dode\"\n\t\"github.com/go-acme/lego/v4/providers/dns/domeneshop\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dreamhost\"\n\t\"github.com/go-acme/lego/v4/providers/dns/duckdns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dyn\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dyndnsfree\"\n\t\"github.com/go-acme/lego/v4/providers/dns/dynu\"\n\t\"github.com/go-acme/lego/v4/providers/dns/easydns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/edgecenter\"\n\t\"github.com/go-acme/lego/v4/providers/dns/edgedns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/edgeone\"\n\t\"github.com/go-acme/lego/v4/providers/dns/efficientip\"\n\t\"github.com/go-acme/lego/v4/providers/dns/epik\"\n\t\"github.com/go-acme/lego/v4/providers/dns/eurodns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/excedo\"\n\t\"github.com/go-acme/lego/v4/providers/dns/exec\"\n\t\"github.com/go-acme/lego/v4/providers/dns/exoscale\"\n\t\"github.com/go-acme/lego/v4/providers/dns/f5xc\"\n\t\"github.com/go-acme/lego/v4/providers/dns/freemyip\"\n\t\"github.com/go-acme/lego/v4/providers/dns/gandi\"\n\t\"github.com/go-acme/lego/v4/providers/dns/gandiv5\"\n\t\"github.com/go-acme/lego/v4/providers/dns/gcloud\"\n\t\"github.com/go-acme/lego/v4/providers/dns/gcore\"\n\t\"github.com/go-acme/lego/v4/providers/dns/gigahostno\"\n\t\"github.com/go-acme/lego/v4/providers/dns/glesys\"\n\t\"github.com/go-acme/lego/v4/providers/dns/godaddy\"\n\t\"github.com/go-acme/lego/v4/providers/dns/googledomains\"\n\t\"github.com/go-acme/lego/v4/providers/dns/gravity\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hetzner\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hostingde\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hostinger\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hostingnl\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hosttech\"\n\t\"github.com/go-acme/lego/v4/providers/dns/httpnet\"\n\t\"github.com/go-acme/lego/v4/providers/dns/httpreq\"\n\t\"github.com/go-acme/lego/v4/providers/dns/huaweicloud\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hurricane\"\n\t\"github.com/go-acme/lego/v4/providers/dns/hyperone\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ibmcloud\"\n\t\"github.com/go-acme/lego/v4/providers/dns/iij\"\n\t\"github.com/go-acme/lego/v4/providers/dns/iijdpf\"\n\t\"github.com/go-acme/lego/v4/providers/dns/infoblox\"\n\t\"github.com/go-acme/lego/v4/providers/dns/infomaniak\"\n\t\"github.com/go-acme/lego/v4/providers/dns/internetbs\"\n\t\"github.com/go-acme/lego/v4/providers/dns/inwx\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ionos\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ionoscloud\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ipv64\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ispconfig\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ispconfigddns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/iwantmyname\"\n\t\"github.com/go-acme/lego/v4/providers/dns/jdcloud\"\n\t\"github.com/go-acme/lego/v4/providers/dns/joker\"\n\t\"github.com/go-acme/lego/v4/providers/dns/keyhelp\"\n\t\"github.com/go-acme/lego/v4/providers/dns/leaseweb\"\n\t\"github.com/go-acme/lego/v4/providers/dns/liara\"\n\t\"github.com/go-acme/lego/v4/providers/dns/lightsail\"\n\t\"github.com/go-acme/lego/v4/providers/dns/limacity\"\n\t\"github.com/go-acme/lego/v4/providers/dns/linode\"\n\t\"github.com/go-acme/lego/v4/providers/dns/liquidweb\"\n\t\"github.com/go-acme/lego/v4/providers/dns/loopia\"\n\t\"github.com/go-acme/lego/v4/providers/dns/luadns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/mailinabox\"\n\t\"github.com/go-acme/lego/v4/providers/dns/manageengine\"\n\t\"github.com/go-acme/lego/v4/providers/dns/manual\"\n\t\"github.com/go-acme/lego/v4/providers/dns/metaname\"\n\t\"github.com/go-acme/lego/v4/providers/dns/metaregistrar\"\n\t\"github.com/go-acme/lego/v4/providers/dns/mijnhost\"\n\t\"github.com/go-acme/lego/v4/providers/dns/mittwald\"\n\t\"github.com/go-acme/lego/v4/providers/dns/myaddr\"\n\t\"github.com/go-acme/lego/v4/providers/dns/mydnsjp\"\n\t\"github.com/go-acme/lego/v4/providers/dns/mythicbeasts\"\n\t\"github.com/go-acme/lego/v4/providers/dns/namecheap\"\n\t\"github.com/go-acme/lego/v4/providers/dns/namedotcom\"\n\t\"github.com/go-acme/lego/v4/providers/dns/namesilo\"\n\t\"github.com/go-acme/lego/v4/providers/dns/namesurfer\"\n\t\"github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech\"\n\t\"github.com/go-acme/lego/v4/providers/dns/neodigit\"\n\t\"github.com/go-acme/lego/v4/providers/dns/netcup\"\n\t\"github.com/go-acme/lego/v4/providers/dns/netlify\"\n\t\"github.com/go-acme/lego/v4/providers/dns/nicmanager\"\n\t\"github.com/go-acme/lego/v4/providers/dns/nicru\"\n\t\"github.com/go-acme/lego/v4/providers/dns/nifcloud\"\n\t\"github.com/go-acme/lego/v4/providers/dns/njalla\"\n\t\"github.com/go-acme/lego/v4/providers/dns/nodion\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ns1\"\n\t\"github.com/go-acme/lego/v4/providers/dns/octenium\"\n\t\"github.com/go-acme/lego/v4/providers/dns/oraclecloud\"\n\t\"github.com/go-acme/lego/v4/providers/dns/otc\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ovh\"\n\t\"github.com/go-acme/lego/v4/providers/dns/pdns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/plesk\"\n\t\"github.com/go-acme/lego/v4/providers/dns/porkbun\"\n\t\"github.com/go-acme/lego/v4/providers/dns/rackspace\"\n\t\"github.com/go-acme/lego/v4/providers/dns/rainyun\"\n\t\"github.com/go-acme/lego/v4/providers/dns/rcodezero\"\n\t\"github.com/go-acme/lego/v4/providers/dns/regfish\"\n\t\"github.com/go-acme/lego/v4/providers/dns/regru\"\n\t\"github.com/go-acme/lego/v4/providers/dns/rfc2136\"\n\t\"github.com/go-acme/lego/v4/providers/dns/rimuhosting\"\n\t\"github.com/go-acme/lego/v4/providers/dns/route53\"\n\t\"github.com/go-acme/lego/v4/providers/dns/safedns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/sakuracloud\"\n\t\"github.com/go-acme/lego/v4/providers/dns/scaleway\"\n\t\"github.com/go-acme/lego/v4/providers/dns/selectel\"\n\t\"github.com/go-acme/lego/v4/providers/dns/selectelv2\"\n\t\"github.com/go-acme/lego/v4/providers/dns/selfhostde\"\n\t\"github.com/go-acme/lego/v4/providers/dns/servercow\"\n\t\"github.com/go-acme/lego/v4/providers/dns/shellrent\"\n\t\"github.com/go-acme/lego/v4/providers/dns/simply\"\n\t\"github.com/go-acme/lego/v4/providers/dns/sonic\"\n\t\"github.com/go-acme/lego/v4/providers/dns/spaceship\"\n\t\"github.com/go-acme/lego/v4/providers/dns/stackpath\"\n\t\"github.com/go-acme/lego/v4/providers/dns/syse\"\n\t\"github.com/go-acme/lego/v4/providers/dns/technitium\"\n\t\"github.com/go-acme/lego/v4/providers/dns/tencentcloud\"\n\t\"github.com/go-acme/lego/v4/providers/dns/timewebcloud\"\n\t\"github.com/go-acme/lego/v4/providers/dns/todaynic\"\n\t\"github.com/go-acme/lego/v4/providers/dns/transip\"\n\t\"github.com/go-acme/lego/v4/providers/dns/ultradns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/uniteddomains\"\n\t\"github.com/go-acme/lego/v4/providers/dns/variomedia\"\n\t\"github.com/go-acme/lego/v4/providers/dns/vegadns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/vercel\"\n\t\"github.com/go-acme/lego/v4/providers/dns/versio\"\n\t\"github.com/go-acme/lego/v4/providers/dns/vinyldns\"\n\t\"github.com/go-acme/lego/v4/providers/dns/virtualname\"\n\t\"github.com/go-acme/lego/v4/providers/dns/vkcloud\"\n\t\"github.com/go-acme/lego/v4/providers/dns/volcengine\"\n\t\"github.com/go-acme/lego/v4/providers/dns/vscale\"\n\t\"github.com/go-acme/lego/v4/providers/dns/vultr\"\n\t\"github.com/go-acme/lego/v4/providers/dns/webnames\"\n\t\"github.com/go-acme/lego/v4/providers/dns/webnamesca\"\n\t\"github.com/go-acme/lego/v4/providers/dns/websupport\"\n\t\"github.com/go-acme/lego/v4/providers/dns/wedos\"\n\t\"github.com/go-acme/lego/v4/providers/dns/westcn\"\n\t\"github.com/go-acme/lego/v4/providers/dns/yandex\"\n\t\"github.com/go-acme/lego/v4/providers/dns/yandex360\"\n\t\"github.com/go-acme/lego/v4/providers/dns/yandexcloud\"\n\t\"github.com/go-acme/lego/v4/providers/dns/zoneedit\"\n\t\"github.com/go-acme/lego/v4/providers/dns/zoneee\"\n\t\"github.com/go-acme/lego/v4/providers/dns/zonomi\"\n)\n\n// NewDNSChallengeProviderByName Factory for DNS providers.\nfunc NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {\n\tswitch name {\n\tcase \"acme-dns\", \"acmedns\":\n\t\treturn acmedns.NewDNSProvider()\n\tcase \"active24\":\n\t\treturn active24.NewDNSProvider()\n\tcase \"alidns\":\n\t\treturn alidns.NewDNSProvider()\n\tcase \"aliesa\":\n\t\treturn aliesa.NewDNSProvider()\n\tcase \"allinkl\":\n\t\treturn allinkl.NewDNSProvider()\n\tcase \"alwaysdata\":\n\t\treturn alwaysdata.NewDNSProvider()\n\tcase \"anexia\":\n\t\treturn anexia.NewDNSProvider()\n\tcase \"artfiles\":\n\t\treturn artfiles.NewDNSProvider()\n\tcase \"arvancloud\":\n\t\treturn arvancloud.NewDNSProvider()\n\tcase \"auroradns\":\n\t\treturn auroradns.NewDNSProvider()\n\tcase \"autodns\":\n\t\treturn autodns.NewDNSProvider()\n\tcase \"axelname\":\n\t\treturn axelname.NewDNSProvider()\n\tcase \"azion\":\n\t\treturn azion.NewDNSProvider()\n\tcase \"azure\":\n\t\treturn azure.NewDNSProvider()\n\tcase \"azuredns\":\n\t\treturn azuredns.NewDNSProvider()\n\tcase \"baiducloud\":\n\t\treturn baiducloud.NewDNSProvider()\n\tcase \"beget\":\n\t\treturn beget.NewDNSProvider()\n\tcase \"binarylane\":\n\t\treturn binarylane.NewDNSProvider()\n\tcase \"bindman\":\n\t\treturn bindman.NewDNSProvider()\n\tcase \"bluecat\":\n\t\treturn bluecat.NewDNSProvider()\n\tcase \"bluecatv2\":\n\t\treturn bluecatv2.NewDNSProvider()\n\tcase \"bookmyname\":\n\t\treturn bookmyname.NewDNSProvider()\n\tcase \"brandit\":\n\t\treturn brandit.NewDNSProvider()\n\tcase \"bunny\":\n\t\treturn bunny.NewDNSProvider()\n\tcase \"checkdomain\":\n\t\treturn checkdomain.NewDNSProvider()\n\tcase \"civo\":\n\t\treturn civo.NewDNSProvider()\n\tcase \"clouddns\":\n\t\treturn clouddns.NewDNSProvider()\n\tcase \"cloudflare\":\n\t\treturn cloudflare.NewDNSProvider()\n\tcase \"cloudns\":\n\t\treturn cloudns.NewDNSProvider()\n\tcase \"cloudru\":\n\t\treturn cloudru.NewDNSProvider()\n\tcase \"cloudxns\":\n\t\treturn cloudxns.NewDNSProvider()\n\tcase \"com35\":\n\t\treturn com35.NewDNSProvider()\n\tcase \"conoha\":\n\t\treturn conoha.NewDNSProvider()\n\tcase \"conohav3\":\n\t\treturn conohav3.NewDNSProvider()\n\tcase \"constellix\":\n\t\treturn constellix.NewDNSProvider()\n\tcase \"corenetworks\":\n\t\treturn corenetworks.NewDNSProvider()\n\tcase \"cpanel\":\n\t\treturn cpanel.NewDNSProvider()\n\tcase \"czechia\":\n\t\treturn czechia.NewDNSProvider()\n\tcase \"ddnss\":\n\t\treturn ddnss.NewDNSProvider()\n\tcase \"derak\":\n\t\treturn derak.NewDNSProvider()\n\tcase \"desec\":\n\t\treturn desec.NewDNSProvider()\n\tcase \"designate\":\n\t\treturn designate.NewDNSProvider()\n\tcase \"digitalocean\":\n\t\treturn digitalocean.NewDNSProvider()\n\tcase \"directadmin\":\n\t\treturn directadmin.NewDNSProvider()\n\tcase \"dnsexit\":\n\t\treturn dnsexit.NewDNSProvider()\n\tcase \"dnshomede\":\n\t\treturn dnshomede.NewDNSProvider()\n\tcase \"dnsimple\":\n\t\treturn dnsimple.NewDNSProvider()\n\tcase \"dnsmadeeasy\":\n\t\treturn dnsmadeeasy.NewDNSProvider()\n\tcase \"dnspod\":\n\t\treturn dnspod.NewDNSProvider()\n\tcase \"dode\":\n\t\treturn dode.NewDNSProvider()\n\tcase \"domeneshop\", \"domainnameshop\":\n\t\treturn domeneshop.NewDNSProvider()\n\tcase \"dreamhost\":\n\t\treturn dreamhost.NewDNSProvider()\n\tcase \"duckdns\":\n\t\treturn duckdns.NewDNSProvider()\n\tcase \"dyn\":\n\t\treturn dyn.NewDNSProvider()\n\tcase \"dyndnsfree\":\n\t\treturn dyndnsfree.NewDNSProvider()\n\tcase \"dynu\":\n\t\treturn dynu.NewDNSProvider()\n\tcase \"easydns\":\n\t\treturn easydns.NewDNSProvider()\n\tcase \"edgecenter\":\n\t\treturn edgecenter.NewDNSProvider()\n\tcase \"edgedns\", \"fastdns\":\n\t\treturn edgedns.NewDNSProvider()\n\tcase \"edgeone\":\n\t\treturn edgeone.NewDNSProvider()\n\tcase \"efficientip\":\n\t\treturn efficientip.NewDNSProvider()\n\tcase \"epik\":\n\t\treturn epik.NewDNSProvider()\n\tcase \"eurodns\":\n\t\treturn eurodns.NewDNSProvider()\n\tcase \"excedo\":\n\t\treturn excedo.NewDNSProvider()\n\tcase \"exec\":\n\t\treturn exec.NewDNSProvider()\n\tcase \"exoscale\":\n\t\treturn exoscale.NewDNSProvider()\n\tcase \"f5xc\":\n\t\treturn f5xc.NewDNSProvider()\n\tcase \"freemyip\":\n\t\treturn freemyip.NewDNSProvider()\n\tcase \"gandi\":\n\t\treturn gandi.NewDNSProvider()\n\tcase \"gandiv5\":\n\t\treturn gandiv5.NewDNSProvider()\n\tcase \"gcloud\":\n\t\treturn gcloud.NewDNSProvider()\n\tcase \"gcore\":\n\t\treturn gcore.NewDNSProvider()\n\tcase \"gigahostno\":\n\t\treturn gigahostno.NewDNSProvider()\n\tcase \"glesys\":\n\t\treturn glesys.NewDNSProvider()\n\tcase \"godaddy\":\n\t\treturn godaddy.NewDNSProvider()\n\tcase \"googledomains\":\n\t\treturn googledomains.NewDNSProvider()\n\tcase \"gravity\":\n\t\treturn gravity.NewDNSProvider()\n\tcase \"hetzner\":\n\t\treturn hetzner.NewDNSProvider()\n\tcase \"hostingde\":\n\t\treturn hostingde.NewDNSProvider()\n\tcase \"hostinger\":\n\t\treturn hostinger.NewDNSProvider()\n\tcase \"hostingnl\":\n\t\treturn hostingnl.NewDNSProvider()\n\tcase \"hosttech\":\n\t\treturn hosttech.NewDNSProvider()\n\tcase \"httpnet\":\n\t\treturn httpnet.NewDNSProvider()\n\tcase \"httpreq\":\n\t\treturn httpreq.NewDNSProvider()\n\tcase \"huaweicloud\":\n\t\treturn huaweicloud.NewDNSProvider()\n\tcase \"hurricane\":\n\t\treturn hurricane.NewDNSProvider()\n\tcase \"hyperone\":\n\t\treturn hyperone.NewDNSProvider()\n\tcase \"ibmcloud\":\n\t\treturn ibmcloud.NewDNSProvider()\n\tcase \"iij\":\n\t\treturn iij.NewDNSProvider()\n\tcase \"iijdpf\":\n\t\treturn iijdpf.NewDNSProvider()\n\tcase \"infoblox\":\n\t\treturn infoblox.NewDNSProvider()\n\tcase \"infomaniak\":\n\t\treturn infomaniak.NewDNSProvider()\n\tcase \"internetbs\":\n\t\treturn internetbs.NewDNSProvider()\n\tcase \"inwx\":\n\t\treturn inwx.NewDNSProvider()\n\tcase \"ionos\":\n\t\treturn ionos.NewDNSProvider()\n\tcase \"ionoscloud\":\n\t\treturn ionoscloud.NewDNSProvider()\n\tcase \"ipv64\":\n\t\treturn ipv64.NewDNSProvider()\n\tcase \"ispconfig\":\n\t\treturn ispconfig.NewDNSProvider()\n\tcase \"ispconfigddns\":\n\t\treturn ispconfigddns.NewDNSProvider()\n\tcase \"iwantmyname\":\n\t\treturn iwantmyname.NewDNSProvider()\n\tcase \"jdcloud\":\n\t\treturn jdcloud.NewDNSProvider()\n\tcase \"joker\":\n\t\treturn joker.NewDNSProvider()\n\tcase \"keyhelp\":\n\t\treturn keyhelp.NewDNSProvider()\n\tcase \"leaseweb\":\n\t\treturn leaseweb.NewDNSProvider()\n\tcase \"liara\":\n\t\treturn liara.NewDNSProvider()\n\tcase \"lightsail\":\n\t\treturn lightsail.NewDNSProvider()\n\tcase \"limacity\":\n\t\treturn limacity.NewDNSProvider()\n\tcase \"linode\", \"linodev4\":\n\t\treturn linode.NewDNSProvider()\n\tcase \"liquidweb\":\n\t\treturn liquidweb.NewDNSProvider()\n\tcase \"loopia\":\n\t\treturn loopia.NewDNSProvider()\n\tcase \"luadns\":\n\t\treturn luadns.NewDNSProvider()\n\tcase \"mailinabox\":\n\t\treturn mailinabox.NewDNSProvider()\n\tcase \"manageengine\":\n\t\treturn manageengine.NewDNSProvider()\n\tcase \"manual\":\n\t\treturn manual.NewDNSProvider()\n\tcase \"metaname\":\n\t\treturn metaname.NewDNSProvider()\n\tcase \"metaregistrar\":\n\t\treturn metaregistrar.NewDNSProvider()\n\tcase \"mijnhost\":\n\t\treturn mijnhost.NewDNSProvider()\n\tcase \"mittwald\":\n\t\treturn mittwald.NewDNSProvider()\n\tcase \"myaddr\":\n\t\treturn myaddr.NewDNSProvider()\n\tcase \"mydnsjp\":\n\t\treturn mydnsjp.NewDNSProvider()\n\tcase \"mythicbeasts\":\n\t\treturn mythicbeasts.NewDNSProvider()\n\tcase \"namecheap\":\n\t\treturn namecheap.NewDNSProvider()\n\tcase \"namedotcom\":\n\t\treturn namedotcom.NewDNSProvider()\n\tcase \"namesilo\":\n\t\treturn namesilo.NewDNSProvider()\n\tcase \"namesurfer\":\n\t\treturn namesurfer.NewDNSProvider()\n\tcase \"nearlyfreespeech\":\n\t\treturn nearlyfreespeech.NewDNSProvider()\n\tcase \"neodigit\":\n\t\treturn neodigit.NewDNSProvider()\n\tcase \"netcup\":\n\t\treturn netcup.NewDNSProvider()\n\tcase \"netlify\":\n\t\treturn netlify.NewDNSProvider()\n\tcase \"nicmanager\":\n\t\treturn nicmanager.NewDNSProvider()\n\tcase \"nicru\":\n\t\treturn nicru.NewDNSProvider()\n\tcase \"nifcloud\":\n\t\treturn nifcloud.NewDNSProvider()\n\tcase \"njalla\":\n\t\treturn njalla.NewDNSProvider()\n\tcase \"nodion\":\n\t\treturn nodion.NewDNSProvider()\n\tcase \"ns1\":\n\t\treturn ns1.NewDNSProvider()\n\tcase \"octenium\":\n\t\treturn octenium.NewDNSProvider()\n\tcase \"oraclecloud\":\n\t\treturn oraclecloud.NewDNSProvider()\n\tcase \"otc\":\n\t\treturn otc.NewDNSProvider()\n\tcase \"ovh\":\n\t\treturn ovh.NewDNSProvider()\n\tcase \"pdns\":\n\t\treturn pdns.NewDNSProvider()\n\tcase \"plesk\":\n\t\treturn plesk.NewDNSProvider()\n\tcase \"porkbun\":\n\t\treturn porkbun.NewDNSProvider()\n\tcase \"rackspace\":\n\t\treturn rackspace.NewDNSProvider()\n\tcase \"rainyun\":\n\t\treturn rainyun.NewDNSProvider()\n\tcase \"rcodezero\":\n\t\treturn rcodezero.NewDNSProvider()\n\tcase \"regfish\":\n\t\treturn regfish.NewDNSProvider()\n\tcase \"regru\":\n\t\treturn regru.NewDNSProvider()\n\tcase \"rfc2136\":\n\t\treturn rfc2136.NewDNSProvider()\n\tcase \"rimuhosting\":\n\t\treturn rimuhosting.NewDNSProvider()\n\tcase \"route53\":\n\t\treturn route53.NewDNSProvider()\n\tcase \"safedns\":\n\t\treturn safedns.NewDNSProvider()\n\tcase \"sakuracloud\":\n\t\treturn sakuracloud.NewDNSProvider()\n\tcase \"scaleway\":\n\t\treturn scaleway.NewDNSProvider()\n\tcase \"selectel\":\n\t\treturn selectel.NewDNSProvider()\n\tcase \"selectelv2\":\n\t\treturn selectelv2.NewDNSProvider()\n\tcase \"selfhostde\":\n\t\treturn selfhostde.NewDNSProvider()\n\tcase \"servercow\":\n\t\treturn servercow.NewDNSProvider()\n\tcase \"shellrent\":\n\t\treturn shellrent.NewDNSProvider()\n\tcase \"simply\":\n\t\treturn simply.NewDNSProvider()\n\tcase \"sonic\":\n\t\treturn sonic.NewDNSProvider()\n\tcase \"spaceship\":\n\t\treturn spaceship.NewDNSProvider()\n\tcase \"stackpath\":\n\t\treturn stackpath.NewDNSProvider()\n\tcase \"syse\":\n\t\treturn syse.NewDNSProvider()\n\tcase \"technitium\":\n\t\treturn technitium.NewDNSProvider()\n\tcase \"tencentcloud\":\n\t\treturn tencentcloud.NewDNSProvider()\n\tcase \"timewebcloud\":\n\t\treturn timewebcloud.NewDNSProvider()\n\tcase \"todaynic\":\n\t\treturn todaynic.NewDNSProvider()\n\tcase \"transip\":\n\t\treturn transip.NewDNSProvider()\n\tcase \"ultradns\":\n\t\treturn ultradns.NewDNSProvider()\n\tcase \"uniteddomains\":\n\t\treturn uniteddomains.NewDNSProvider()\n\tcase \"variomedia\":\n\t\treturn variomedia.NewDNSProvider()\n\tcase \"vegadns\":\n\t\treturn vegadns.NewDNSProvider()\n\tcase \"vercel\":\n\t\treturn vercel.NewDNSProvider()\n\tcase \"versio\":\n\t\treturn versio.NewDNSProvider()\n\tcase \"vinyldns\":\n\t\treturn vinyldns.NewDNSProvider()\n\tcase \"virtualname\":\n\t\treturn virtualname.NewDNSProvider()\n\tcase \"vkcloud\":\n\t\treturn vkcloud.NewDNSProvider()\n\tcase \"volcengine\":\n\t\treturn volcengine.NewDNSProvider()\n\tcase \"vscale\":\n\t\treturn vscale.NewDNSProvider()\n\tcase \"vultr\":\n\t\treturn vultr.NewDNSProvider()\n\tcase \"webnames\", \"webnamesru\":\n\t\treturn webnames.NewDNSProvider()\n\tcase \"webnamesca\":\n\t\treturn webnamesca.NewDNSProvider()\n\tcase \"websupport\":\n\t\treturn websupport.NewDNSProvider()\n\tcase \"wedos\":\n\t\treturn wedos.NewDNSProvider()\n\tcase \"westcn\":\n\t\treturn westcn.NewDNSProvider()\n\tcase \"yandex\":\n\t\treturn yandex.NewDNSProvider()\n\tcase \"yandex360\":\n\t\treturn yandex360.NewDNSProvider()\n\tcase \"yandexcloud\":\n\t\treturn yandexcloud.NewDNSProvider()\n\tcase \"zoneedit\":\n\t\treturn zoneedit.NewDNSProvider()\n\tcase \"zoneee\":\n\t\treturn zoneee.NewDNSProvider()\n\tcase \"zonomi\":\n\t\treturn zonomi.NewDNSProvider()\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unrecognized DNS provider: %s\", name)\n\t}\n}\n"
  },
  {
    "path": "providers/http/memcached/README.md",
    "content": "# Memcached http provider\n\nPublishes challenges into memcached where they can be retrieved by nginx. Allows\nspecifying multiple memcached servers and the responses will be published to all\nof them, making it easier to verify when your domain is hosted on a cluster of\nservers.\n\nExample nginx config:\n\n```\n    location /.well-known/acme-challenge/ {\n        set $memcached_key \"$uri\";\n        memcached_pass 127.0.0.1:11211;\n    }\n```\n"
  },
  {
    "path": "providers/http/memcached/memcached.go",
    "content": "// Package memcached implements an HTTP provider for solving the HTTP-01 challenge using memcached in combination with a webserver.\npackage memcached\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"path\"\n\n\t\"github.com/go-acme/lego/v4/challenge/http01\"\n\t\"github.com/rainycape/memcache\"\n)\n\n// HTTPProvider implements HTTPProvider for `http-01` challenge.\ntype HTTPProvider struct {\n\thosts []string\n}\n\n// NewMemcachedProvider returns a HTTPProvider instance with a configured webroot path.\nfunc NewMemcachedProvider(hosts []string) (*HTTPProvider, error) {\n\tif len(hosts) == 0 {\n\t\treturn nil, errors.New(\"no memcached hosts provided\")\n\t}\n\n\tc := &HTTPProvider{\n\t\thosts: hosts,\n\t}\n\n\treturn c, nil\n}\n\n// Present makes the token available at `HTTP01ChallengePath(token)` by creating a file in the given webroot path.\nfunc (w *HTTPProvider) Present(domain, token, keyAuth string) error {\n\tvar errs []error\n\n\tchallengePath := path.Join(\"/\", http01.ChallengePath(token))\n\n\tfor _, host := range w.hosts {\n\t\tmc, err := memcache.New(host)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t\tcontinue\n\t\t}\n\n\t\t_ = mc.Add(&memcache.Item{\n\t\t\tKey:        challengePath,\n\t\t\tValue:      []byte(keyAuth),\n\t\t\tExpiration: 60,\n\t\t})\n\t}\n\n\tif len(errs) == len(w.hosts) {\n\t\treturn fmt.Errorf(\"unable to store key in any of the memcache hosts: %v\", errs)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the file created for the challenge.\nfunc (w *HTTPProvider) CleanUp(domain, token, keyAuth string) error {\n\t// Memcached will clean up itself, that's what expiration is for.\n\treturn nil\n}\n"
  },
  {
    "path": "providers/http/memcached/memcached_test.go",
    "content": "package memcached\n\nimport (\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/challenge/http01\"\n\t\"github.com/rainycape/memcache\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tdomain  = \"lego.test\"\n\ttoken   = \"foo\"\n\tkeyAuth = \"bar\"\n)\n\nvar memcachedHosts = loadMemcachedHosts()\n\nfunc loadMemcachedHosts() []string {\n\tmemcachedHostsStr := os.Getenv(\"MEMCACHED_HOSTS\")\n\tif memcachedHostsStr != \"\" {\n\t\treturn strings.Split(memcachedHostsStr, \",\")\n\t}\n\n\treturn nil\n}\n\nfunc TestNewMemcachedProviderEmpty(t *testing.T) {\n\temptyHosts := make([]string, 0)\n\t_, err := NewMemcachedProvider(emptyHosts)\n\trequire.EqualError(t, err, \"no memcached hosts provided\")\n}\n\nfunc TestNewMemcachedProviderValid(t *testing.T) {\n\tif len(memcachedHosts) == 0 {\n\t\tt.Skip(\"Skipping memcached tests\")\n\t}\n\n\t_, err := NewMemcachedProvider(memcachedHosts)\n\trequire.NoError(t, err)\n}\n\nfunc TestMemcachedPresentSingleHost(t *testing.T) {\n\tif len(memcachedHosts) == 0 {\n\t\tt.Skip(\"Skipping memcached tests\")\n\t}\n\n\tp, err := NewMemcachedProvider(memcachedHosts[0:1])\n\trequire.NoError(t, err)\n\n\tchallengePath := path.Join(\"/\", http01.ChallengePath(token))\n\n\terr = p.Present(domain, token, keyAuth)\n\trequire.NoError(t, err)\n\tmc, err := memcache.New(memcachedHosts[0])\n\trequire.NoError(t, err)\n\ti, err := mc.Get(challengePath)\n\trequire.NoError(t, err)\n\tassert.Equal(t, i.Value, []byte(keyAuth))\n}\n\nfunc TestMemcachedPresentMultiHost(t *testing.T) {\n\tif len(memcachedHosts) <= 1 {\n\t\tt.Skip(\"Skipping memcached multi-host tests\")\n\t}\n\n\tp, err := NewMemcachedProvider(memcachedHosts)\n\trequire.NoError(t, err)\n\n\tchallengePath := path.Join(\"/\", http01.ChallengePath(token))\n\n\terr = p.Present(domain, token, keyAuth)\n\trequire.NoError(t, err)\n\n\tfor _, host := range memcachedHosts {\n\t\tmc, err := memcache.New(host)\n\t\trequire.NoError(t, err)\n\t\ti, err := mc.Get(challengePath)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, i.Value, []byte(keyAuth))\n\t}\n}\n\nfunc TestMemcachedPresentPartialFailureMultiHost(t *testing.T) {\n\tif len(memcachedHosts) == 0 {\n\t\tt.Skip(\"Skipping memcached tests\")\n\t}\n\n\thosts := append(memcachedHosts, \"5.5.5.5:11211\")\n\tp, err := NewMemcachedProvider(hosts)\n\trequire.NoError(t, err)\n\n\tchallengePath := path.Join(\"/\", http01.ChallengePath(token))\n\n\terr = p.Present(domain, token, keyAuth)\n\trequire.NoError(t, err)\n\n\tfor _, host := range memcachedHosts {\n\t\tmc, err := memcache.New(host)\n\t\trequire.NoError(t, err)\n\t\ti, err := mc.Get(challengePath)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, i.Value, []byte(keyAuth))\n\t}\n}\n\nfunc TestMemcachedCleanup(t *testing.T) {\n\tif len(memcachedHosts) == 0 {\n\t\tt.Skip(\"Skipping memcached tests\")\n\t}\n\n\tp, err := NewMemcachedProvider(memcachedHosts)\n\trequire.NoError(t, err)\n\trequire.NoError(t, p.CleanUp(domain, token, keyAuth))\n}\n"
  },
  {
    "path": "providers/http/s3/s3.go",
    "content": "// Package s3 implements an HTTP provider for solving the HTTP-01 challenge using AWS S3.\npackage s3\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/go-acme/lego/v4/challenge/http01\"\n)\n\n// HTTPProvider implements ChallengeProvider for `http-01` challenge.\ntype HTTPProvider struct {\n\tbucket string\n\tclient *s3.Client\n}\n\n// NewHTTPProvider returns a HTTPProvider instance with a configured s3 bucket and aws session.\n// Credentials must be passed in the environment variables.\nfunc NewHTTPProvider(bucket string) (*HTTPProvider, error) {\n\tif bucket == \"\" {\n\t\treturn nil, errors.New(\"s3: bucket name missing\")\n\t}\n\n\tctx := context.Background()\n\n\tcfg, err := config.LoadDefaultConfig(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"s3: unable to create AWS config: %w\", err)\n\t}\n\n\tclient := s3.NewFromConfig(cfg)\n\n\treturn &HTTPProvider{\n\t\tbucket: bucket,\n\t\tclient: client,\n\t}, nil\n}\n\n// Present makes the token available at `HTTP01ChallengePath(token)` by creating a file in the given s3 bucket.\nfunc (s *HTTPProvider) Present(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tparams := &s3.PutObjectInput{\n\t\tACL:    \"public-read\",\n\t\tBucket: aws.String(s.bucket),\n\t\tKey:    aws.String(strings.Trim(http01.ChallengePath(token), \"/\")),\n\t\tBody:   bytes.NewReader([]byte(keyAuth)),\n\t}\n\n\t_, err := s.client.PutObject(ctx, params)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"s3: failed to upload token to s3: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the file created for the challenge.\nfunc (s *HTTPProvider) CleanUp(domain, token, keyAuth string) error {\n\tctx := context.Background()\n\n\tparams := &s3.DeleteObjectInput{\n\t\tBucket: aws.String(s.bucket),\n\t\tKey:    aws.String(strings.Trim(http01.ChallengePath(token), \"/\")),\n\t}\n\n\t_, err := s.client.DeleteObject(ctx, params)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"s3: could not remove file in s3 bucket after HTTP challenge: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/http/s3/s3.toml",
    "content": "Name = \"Amazon S3\"\nDescription = ''''''\nURL = \"https://aws.amazon.com/s3/\"\nCode = \"s3\"\nSince = \"v4.14.0\"\n\nExample = '''\nAWS_ACCESS_KEY_ID=your_key_id \\\nAWS_SECRET_ACCESS_KEY=your_secret_access_key \\\nAWS_REGION=aws-region \\\nlego --domains example.com --email your_example@email.com --http --http.s3-bucket your_s3_bucket --accept-tos=true run\n'''\n\nAdditional = '''\n## Description\n\nAWS Credentials are automatically detected in the following locations and prioritized in the following order:\n\n1. Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, [`AWS_SESSION_TOKEN`]\n2. Shared credentials file (defaults to `~/.aws/credentials`, profiles can be specified using `AWS_PROFILE`)\n3. Amazon EC2 IAM role\n\nThe AWS Region is automatically detected in the following locations and prioritized in the following order:\n\n1. Environment variables: `AWS_REGION`\n2. Shared configuration file if `AWS_SDK_LOAD_CONFIG` is set (defaults to `~/.aws/config`, profiles can be specified using `AWS_PROFILE`)\n\nSee also: https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/\n\n### Broad privileges for testing purposes\n\nWill need to create an S3 bucket which has read permissions set for Everyone (public access).\nThe S3 bucket doesn't require static website hosting to be enabled.\nAWS_REGION must match the region where the s3 bucket is hosted.\n'''\n\n[Configuration]\n  [Configuration.Credentials]\n    AWS_ACCESS_KEY_ID = \"Managed by the AWS client. Access key ID (`AWS_ACCESS_KEY_ID_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead)\"\n    AWS_SECRET_ACCESS_KEY = \"Managed by the AWS client. Secret access key (`AWS_SECRET_ACCESS_KEY_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead)\"\n    AWS_REGION = \"Managed by the AWS client (`AWS_REGION_FILE` is not supported)\"\n    S3_BUCKET = \"Name of the s3 bucket\"\n    AWS_PROFILE = \"Managed by the AWS client (`AWS_PROFILE_FILE` is not supported)\"\n    AWS_SDK_LOAD_CONFIG = \"Managed by the AWS client. Retrieve the region from the CLI config file (`AWS_SDK_LOAD_CONFIG_FILE` is not supported)\"\n    AWS_ASSUME_ROLE_ARN = \"Managed by the AWS Role ARN (`AWS_ASSUME_ROLE_ARN_FILE` is not supported)\"\n    AWS_EXTERNAL_ID = \"Managed by STS AssumeRole API operation (`AWS_EXTERNAL_ID_FILE` is not supported)\"\n  [Configuration.Additional]\n    AWS_SHARED_CREDENTIALS_FILE = \"Managed by the AWS client. Shared credentials file.\"\n    AWS_MAX_RETRIES = \"The number of maximum returns the service will use to make an individual API request\"\n\n[Links]\n  API = \"https://docs.aws.amazon.com/AmazonS3/latest/userguide//Welcome.html\"\n  GoClient = \"https://docs.aws.amazon.com/sdk-for-go/\"\n\n"
  },
  {
    "path": "providers/http/s3/s3_test.go",
    "content": "package s3\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/challenge/http01\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tdomain  = \"example.com\"\n\ttoken   = \"foo\"\n\tkeyAuth = \"bar\"\n)\n\nvar envTest = tester.NewEnvTest(\n\t\"AWS_ACCESS_KEY_ID\",\n\t\"AWS_SECRET_ACCESS_KEY\",\n\t\"AWS_REGION\",\n\t\"S3_BUCKET\")\n\nfunc TestLiveNewHTTPProvider_Valid(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\t_, err := NewHTTPProvider(envTest.GetValue(\"S3_BUCKET\"))\n\trequire.NoError(t, err)\n}\n\nfunc TestLiveNewHTTPProvider(t *testing.T) {\n\tif !envTest.IsLiveTest() {\n\t\tt.Skip(\"skipping live test\")\n\t}\n\n\tenvTest.RestoreEnv()\n\n\ts3Bucket := os.Getenv(\"S3_BUCKET\")\n\n\tprovider, err := NewHTTPProvider(s3Bucket)\n\trequire.NoError(t, err)\n\n\t// Present\n\n\terr = provider.Present(domain, token, keyAuth)\n\trequire.NoError(t, err)\n\n\tchlgPath := fmt.Sprintf(\"http://%s.s3.%s.amazonaws.com%s\",\n\t\ts3Bucket, envTest.GetValue(\"AWS_REGION\"), http01.ChallengePath(token))\n\n\tresp, err := http.Get(chlgPath)\n\trequire.NoError(t, err)\n\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tdata, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, []byte(keyAuth), data)\n\n\t// CleanUp\n\n\terr = provider.CleanUp(domain, token, keyAuth)\n\trequire.NoError(t, err)\n\n\tcleanupResp, err := http.Get(chlgPath)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 403, cleanupResp.StatusCode)\n}\n"
  },
  {
    "path": "providers/http/webroot/webroot.go",
    "content": "// Package webroot implements an HTTP provider for solving the HTTP-01 challenge using web server's root path.\npackage webroot\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/go-acme/lego/v4/challenge/http01\"\n)\n\n// HTTPProvider implements ChallengeProvider for `http-01` challenge.\ntype HTTPProvider struct {\n\tpath string\n}\n\n// NewHTTPProvider returns a HTTPProvider instance with a configured webroot path.\nfunc NewHTTPProvider(path string) (*HTTPProvider, error) {\n\tif _, err := os.Stat(path); os.IsNotExist(err) {\n\t\treturn nil, errors.New(\"webroot path does not exist\")\n\t}\n\n\treturn &HTTPProvider{path: path}, nil\n}\n\n// Present makes the token available at `HTTP01ChallengePath(token)` by creating a file in the given webroot path.\nfunc (w *HTTPProvider) Present(domain, token, keyAuth string) error {\n\tvar err error\n\n\tchallengeFilePath := filepath.Join(w.path, http01.ChallengePath(token))\n\n\terr = os.MkdirAll(filepath.Dir(challengeFilePath), 0o755)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not create required directories in webroot for HTTP challenge: %w\", err)\n\t}\n\n\terr = os.WriteFile(challengeFilePath, []byte(keyAuth), 0o644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not write file in webroot for HTTP challenge: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// CleanUp removes the file created for the challenge.\nfunc (w *HTTPProvider) CleanUp(domain, token, keyAuth string) error {\n\terr := os.Remove(filepath.Join(w.path, http01.ChallengePath(token)))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not remove file in webroot after HTTP challenge: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "providers/http/webroot/webroot_test.go",
    "content": "package webroot\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHTTPProvider(t *testing.T) {\n\twebroot := \"webroot\"\n\tdomain := \"domain\"\n\ttoken := \"token\"\n\tkeyAuth := \"keyAuth\"\n\tchallengeFilePath := webroot + \"/.well-known/acme-challenge/\" + token\n\n\trequire.NoError(t, os.MkdirAll(webroot+\"/.well-known/acme-challenge\", 0o777))\n\tdefer os.RemoveAll(webroot)\n\n\tprovider, err := NewHTTPProvider(webroot)\n\trequire.NoError(t, err)\n\n\terr = provider.Present(domain, token, keyAuth)\n\trequire.NoError(t, err)\n\n\tif _, err = os.Stat(challengeFilePath); os.IsNotExist(err) {\n\t\tt.Error(\"Challenge file was not created in webroot\")\n\t}\n\n\tvar data []byte\n\n\tdata, err = os.ReadFile(challengeFilePath)\n\trequire.NoError(t, err)\n\n\tdataStr := string(data)\n\tassert.Equal(t, keyAuth, dataStr)\n\n\terr = provider.CleanUp(domain, token, keyAuth)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "registration/registar.go",
    "content": "package registration\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/log\"\n)\n\nconst mailTo = \"mailto:\"\n\n// Resource represents all important information about a registration\n// of which the client needs to keep track itself.\n// WARNING: will be removed in the future (acme.ExtendedAccount), https://github.com/go-acme/lego/issues/855.\ntype Resource struct {\n\tBody acme.Account `json:\"body\"`\n\tURI  string       `json:\"uri,omitempty\"`\n}\n\ntype RegisterOptions struct {\n\tTermsOfServiceAgreed bool\n}\n\ntype RegisterEABOptions struct {\n\tTermsOfServiceAgreed bool\n\tKid                  string\n\tHmacEncoded          string\n}\n\ntype Registrar struct {\n\tcore *api.Core\n\tuser User\n}\n\nfunc NewRegistrar(core *api.Core, user User) *Registrar {\n\treturn &Registrar{\n\t\tcore: core,\n\t\tuser: user,\n\t}\n}\n\n// Register the current account to the ACME server.\nfunc (r *Registrar) Register(options RegisterOptions) (*Resource, error) {\n\tif r == nil || r.user == nil {\n\t\treturn nil, errors.New(\"acme: cannot register a nil client or user\")\n\t}\n\n\taccMsg := acme.Account{\n\t\tTermsOfServiceAgreed: options.TermsOfServiceAgreed,\n\t\tContact:              []string{},\n\t}\n\n\tif r.user.GetEmail() != \"\" {\n\t\tlog.Infof(\"acme: Registering account for %s\", r.user.GetEmail())\n\t\taccMsg.Contact = []string{mailTo + r.user.GetEmail()}\n\t}\n\n\taccount, err := r.core.Accounts.New(accMsg)\n\tif err != nil {\n\t\t// seems impossible\n\t\terrorDetails := &acme.ProblemDetails{}\n\t\tif !errors.As(err, &errorDetails) || errorDetails.HTTPStatus != http.StatusConflict {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &Resource{URI: account.Location, Body: account.Account}, nil\n}\n\n// RegisterWithExternalAccountBinding Register the current account to the ACME server.\nfunc (r *Registrar) RegisterWithExternalAccountBinding(options RegisterEABOptions) (*Resource, error) {\n\taccMsg := acme.Account{\n\t\tTermsOfServiceAgreed: options.TermsOfServiceAgreed,\n\t\tContact:              []string{},\n\t}\n\n\tif r.user.GetEmail() != \"\" {\n\t\tlog.Infof(\"acme: Registering account for %s\", r.user.GetEmail())\n\t\taccMsg.Contact = []string{mailTo + r.user.GetEmail()}\n\t}\n\n\taccount, err := r.core.Accounts.NewEAB(accMsg, options.Kid, options.HmacEncoded)\n\tif err != nil {\n\t\t// seems impossible\n\t\terrorDetails := &acme.ProblemDetails{}\n\t\tif !errors.As(err, &errorDetails) || errorDetails.HTTPStatus != http.StatusConflict {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &Resource{URI: account.Location, Body: account.Account}, nil\n}\n\n// QueryRegistration runs a POST request on the client's registration and returns the result.\n//\n// This is similar to the Register function,\n// but acting on an existing registration link and resource.\nfunc (r *Registrar) QueryRegistration() (*Resource, error) {\n\tif r == nil || r.user == nil || r.user.GetRegistration() == nil {\n\t\treturn nil, errors.New(\"acme: cannot query the registration of a nil client or user\")\n\t}\n\n\t// Log the URL here instead of the email as the email may not be set\n\tlog.Infof(\"acme: Querying account for %s\", r.user.GetRegistration().URI)\n\n\taccount, err := r.core.Accounts.Get(r.user.GetRegistration().URI)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Resource{\n\t\tBody: account,\n\t\t// Location: header is not returned so this needs to be populated off of existing URI\n\t\tURI: r.user.GetRegistration().URI,\n\t}, nil\n}\n\n// UpdateRegistration update the user registration on the ACME server.\nfunc (r *Registrar) UpdateRegistration(options RegisterOptions) (*Resource, error) {\n\tif r == nil || r.user == nil {\n\t\treturn nil, errors.New(\"acme: cannot update a nil client or user\")\n\t}\n\n\taccMsg := acme.Account{\n\t\tTermsOfServiceAgreed: options.TermsOfServiceAgreed,\n\t\tContact:              []string{},\n\t}\n\n\tif r.user.GetEmail() != \"\" {\n\t\tlog.Infof(\"acme: Registering account for %s\", r.user.GetEmail())\n\t\taccMsg.Contact = []string{mailTo + r.user.GetEmail()}\n\t}\n\n\taccountURL := r.user.GetRegistration().URI\n\n\taccount, err := r.core.Accounts.Update(accountURL, accMsg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Resource{URI: accountURL, Body: account}, nil\n}\n\n// DeleteRegistration deletes the client's user registration from the ACME server.\nfunc (r *Registrar) DeleteRegistration() error {\n\tif r == nil || r.user == nil {\n\t\treturn errors.New(\"acme: cannot unregister a nil client or user\")\n\t}\n\n\tlog.Infof(\"acme: Deleting account for %s\", r.user.GetEmail())\n\n\treturn r.core.Accounts.Deactivate(r.user.GetRegistration().URI)\n}\n\n// ResolveAccountByKey will attempt to look up an account using the given account key\n// and return its registration resource.\nfunc (r *Registrar) ResolveAccountByKey() (*Resource, error) {\n\tlog.Infof(\"acme: Trying to resolve account by key\")\n\n\taccMsg := acme.Account{OnlyReturnExisting: true}\n\n\taccount, err := r.core.Accounts.New(accMsg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Resource{URI: account.Location, Body: account.Account}, nil\n}\n"
  },
  {
    "path": "registration/registar_test.go",
    "content": "package registration\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/go-acme/lego/v4/acme\"\n\t\"github.com/go-acme/lego/v4/acme/api\"\n\t\"github.com/go-acme/lego/v4/platform/tester\"\n\t\"github.com/go-acme/lego/v4/platform/tester/servermock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRegistrar_ResolveAccountByKey(t *testing.T) {\n\tserver := tester.MockACMEServer().\n\t\tRoute(\"/account\",\n\t\t\thttp.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\t\t\trw.Header().Set(\"Location\",\n\t\t\t\t\tfmt.Sprintf(\"http://%s/account\", req.Context().Value(http.LocalAddrContextKey)))\n\n\t\t\t\tservermock.JSONEncode(acme.Account{Status: \"valid\"}).ServeHTTP(rw, req)\n\t\t\t})).\n\t\tBuildHTTPS(t)\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err, \"Could not generate test key\")\n\n\tuser := mockUser{\n\t\temail:      \"test@test.com\",\n\t\tregres:     &Resource{},\n\t\tprivatekey: key,\n\t}\n\n\tcore, err := api.New(server.Client(), \"lego-test\", server.URL+\"/dir\", \"\", key)\n\trequire.NoError(t, err)\n\n\tregistrar := NewRegistrar(core, user)\n\n\tres, err := registrar.ResolveAccountByKey()\n\trequire.NoError(t, err, \"Unexpected error resolving account by key\")\n\n\tassert.Equal(t, \"valid\", res.Body.Status, \"Unexpected account status\")\n}\n"
  },
  {
    "path": "registration/user.go",
    "content": "package registration\n\nimport (\n\t\"crypto\"\n)\n\n// User interface is to be implemented by users of this library.\n// It is used by the client type to get user specific information.\ntype User interface {\n\tGetEmail() string\n\tGetRegistration() *Resource\n\tGetPrivateKey() crypto.PrivateKey\n}\n"
  },
  {
    "path": "registration/user_test.go",
    "content": "package registration\n\nimport (\n\t\"crypto\"\n\t\"crypto/rsa\"\n)\n\ntype mockUser struct {\n\temail      string\n\tregres     *Resource\n\tprivatekey *rsa.PrivateKey\n}\n\nfunc (u mockUser) GetEmail() string                 { return u.email }\nfunc (u mockUser) GetRegistration() *Resource       { return u.regres }\nfunc (u mockUser) GetPrivateKey() crypto.PrivateKey { return u.privatekey }\n"
  }
]