pax_global_header00006660000000000000000000000064143402046350014513gustar00rootroot0000000000000052 comment=df79f1302276a36da386c7fdf70cbacc284efab9 lego-4.9.1/000077500000000000000000000000001434020463500124545ustar00rootroot00000000000000lego-4.9.1/.dockerignore000066400000000000000000000000761434020463500151330ustar00rootroot00000000000000lego.exe .lego .gitcookies .idea .vscode/ dist/ builds/ docs/ lego-4.9.1/.gitcookies.enc000066400000000000000000000007401434020463500153620ustar00rootroot00000000000000[{<9^mUmm喨LF7 o*/+G0R"47 Ie >rW ND Ho` l6xe p:Ņ>Qn|_"QeYXCBN# '= Ȗ(FKpwx\SbTz$u dN(?$)z,a?d:MB_44@ Nkq%p7,UM(t:D2}Wy9GO6ȡcSGIMLEwȘ:}Hj|{^"W]b;ԬR!UdhP݌g/Qtƃ~~îaCB,|}C%֛V=ES("E5-73#"î ) %qf^6&,"=zAUR?CNlego-4.9.1/.github/000077500000000000000000000000001434020463500140145ustar00rootroot00000000000000lego-4.9.1/.github/FUNDING.yml000066400000000000000000000000151434020463500156250ustar00rootroot00000000000000github: ldez lego-4.9.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001434020463500161775ustar00rootroot00000000000000lego-4.9.1/.github/ISSUE_TEMPLATE/bug_report.yml000066400000000000000000000041021434020463500210670ustar00rootroot00000000000000name: Bug Report description: Create a report to help us improve. labels: [bug] body: - type: checkboxes id: terms attributes: label: Welcome options: - label: Yes, I'm using a binary release within 2 latest releases. required: true - label: Yes, I've searched similar issues on GitHub and didn't find any. required: true - label: Yes, I've included all information below (version, config, etc). required: true - type: textarea id: expected attributes: label: What did you expect to see? placeholder: Description. validations: required: true - type: textarea id: current attributes: label: What did you see instead? placeholder: Description. validations: required: true - type: dropdown id: type attributes: label: How do you use lego? options: - Library - Binary - Docker image - Through Traefik - Through Caddy - Other validations: required: true - type: textarea id: steps attributes: label: Reproduction steps description: "How do you trigger this bug? Please walk us through it step by step." placeholder: | 1. ... 2. ... 3. ... ... validations: required: true - type: textarea id: version attributes: label: Version of lego description: |- ```console $ lego --version ``` placeholder: Paste output here render: console validations: required: true - type: textarea id: logs attributes: label: Logs value: |-
```console # paste output here ```
validations: required: true - type: textarea id: go-env attributes: label: Go environment (if applicable) value: |-
```console $ go version && go env # paste output here ```
validations: required: false lego-4.9.1/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000005241434020463500201700ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Questions url: https://github.com/go-acme/lego/discussions about: If you have a question, or are looking for advice, please post on our Discussions section! - name: lego documentation url: https://go-acme.github.io/lego/ about: Please take a look to our documentation. lego-4.9.1/.github/ISSUE_TEMPLATE/feature_request.yml000066400000000000000000000012611434020463500221250ustar00rootroot00000000000000name: Feature request description: Suggest an idea for this project. body: - type: checkboxes id: terms attributes: label: Welcome options: - label: Yes, I've searched similar issues on GitHub and didn't find any. required: true - type: dropdown id: type attributes: label: How do you use lego? options: - Library - Binary - Docker image - Through Traefik - Through Caddy - Other validations: required: true - type: textarea id: description attributes: label: Detailed Description placeholder: Description. validations: required: true lego-4.9.1/.github/ISSUE_TEMPLATE/new_dns_provider.yml000066400000000000000000000031241434020463500222710ustar00rootroot00000000000000name: New DNS provider support description: Request for the support of a new DNS provider. title: "Support for provider: " labels: [enhancement, new-provider] body: - type: checkboxes id: terms attributes: label: Welcome options: - label: Yes, I've searched similar issues on GitHub and didn't find any. required: true - label: Yes, the DNS provider exposes a public API. required: true - label: Yes, I know that the lego maintainers don't have an account in all DNS providers in the world. required: true - label: Yes, I'm able to create a pull request and be able to maintain the implementation. required: false - label: Yes, I'm able to test an implementation if someone creates a pull request to add the support of this DNS provider. required: false - type: dropdown id: type attributes: label: How do you use lego? options: - Library - Binary - Docker image - Through Traefik - Through Caddy - Other validations: required: true - type: input id: provider-link attributes: label: Link to the DNS provider placeholder: Put your link here. validations: required: true - type: input id: api-link attributes: label: Link to the API documentation placeholder: Put your link here. validations: required: true - type: textarea id: expected attributes: label: Additional Notes placeholder: Your notes. validations: required: false lego-4.9.1/.github/workflows/000077500000000000000000000000001434020463500160515ustar00rootroot00000000000000lego-4.9.1/.github/workflows/documentation.yml000066400000000000000000000023321434020463500214450ustar00rootroot00000000000000name: Documentation on: push: branches: - master jobs: doc: name: Build and deploy documentation runs-on: ubuntu-latest env: GO_VERSION: 1.19 HUGO_VERSION: 0.101.0 CGO_ENABLED: 0 steps: # https://github.com/marketplace/actions/setup-go-environment - name: Set up Go ${{ env.GO_VERSION }} uses: actions/setup-go@v2 with: go-version: ${{ env.GO_VERSION }} # https://github.com/marketplace/actions/checkout - name: Check out code uses: actions/checkout@v2 with: fetch-depth: 0 - name: Generate DNS docs run: make generate-dns - name: Install Hugo run: | wget -O /tmp/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_Linux-64bit.deb sudo dpkg -i /tmp/hugo.deb - name: Build Documentation run: make docs-build # https://github.com/marketplace/actions/github-pages - name: Deploy to GitHub Pages uses: crazy-max/ghaction-github-pages@v2 with: target_branch: gh-pages build_dir: docs/public env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} lego-4.9.1/.github/workflows/go-cross.yml000066400000000000000000000025721434020463500203360ustar00rootroot00000000000000name: Go Matrix on: push: branches: - master pull_request: jobs: cross: name: Go runs-on: ${{ matrix.os }} env: CGO_ENABLED: 0 strategy: matrix: go-version: [ 1.18, 1.19, 1.x ] os: [ubuntu-latest, macos-latest, windows-latest] steps: # https://github.com/marketplace/actions/setup-go-environment - name: Set up Go ${{ matrix.go-version }} uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} # https://github.com/marketplace/actions/checkout - name: Checkout code uses: actions/checkout@v2 # https://github.com/marketplace/actions/cache - name: Cache Go modules uses: actions/cache@v3 with: # In order: # * Module download cache # * Build cache (Linux) # * Build cache (Mac) # * Build cache (Windows) path: | ~/go/pkg/mod ~/.cache/go-build ~/Library/Caches/go-build %LocalAppData%\go-build key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-${{ matrix.go-version }}-go- - name: Test run: go test -v -cover ./... - name: Build run: go build -v -ldflags "-s -w" -trimpath -o ./dist/lego ./cmd/lego/ lego-4.9.1/.github/workflows/pr.yml000066400000000000000000000045211434020463500172170ustar00rootroot00000000000000name: Main on: push: branches: - master pull_request: jobs: main: name: Main Process runs-on: ubuntu-latest env: GO_VERSION: 1.19 GOLANGCI_LINT_VERSION: v1.49.0 HUGO_VERSION: 0.54.0 CGO_ENABLED: 0 LEGO_E2E_TESTS: CI MEMCACHED_HOSTS: localhost:11211 steps: # https://github.com/marketplace/actions/setup-go-environment - name: Set up Go ${{ env.GO_VERSION }} uses: actions/setup-go@v2 with: go-version: ${{ env.GO_VERSION }} # https://github.com/marketplace/actions/checkout - name: Check out code uses: actions/checkout@v2 with: fetch-depth: 0 # https://github.com/marketplace/actions/cache - name: Cache Go modules uses: actions/cache@v3 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Check and get dependencies run: | go mod tidy git diff --exit-code go.mod git diff --exit-code go.sum # https://golangci-lint.run/usage/install#other-ci - name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }} run: | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION} golangci-lint --version - name: Install Pebble and challtestsrv run: GO111MODULE=off go get -u github.com/letsencrypt/pebble/... - name: Set up a Memcached server uses: niden/actions-memcached@v7 - name: Setup /etc/hosts run: | echo "127.0.0.1 acme.wtf" | sudo tee -a /etc/hosts echo "127.0.0.1 lego.wtf" | sudo tee -a /etc/hosts echo "127.0.0.1 acme.lego.wtf" | sudo tee -a /etc/hosts echo "127.0.0.1 légô.wtf" | sudo tee -a /etc/hosts echo "127.0.0.1 xn--lg-bja9b.wtf" | sudo tee -a /etc/hosts - name: Make run: | make make clean - name: Install Hugo run: | wget -O /tmp/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_Linux-64bit.deb sudo dpkg -i /tmp/hugo.deb - name: Build Documentation run: make docs-build lego-4.9.1/.github/workflows/release.yml000066400000000000000000000031441434020463500202160ustar00rootroot00000000000000name: Release on: push: tags: - v* jobs: release: name: Release version runs-on: ubuntu-latest env: GO_VERSION: 1.19 SEIHON_VERSION: v0.8.3 CGO_ENABLED: 0 steps: - name: Set up Go ${{ env.GO_VERSION }} uses: actions/setup-go@v2 with: go-version: ${{ env.GO_VERSION }} - name: Check out code uses: actions/checkout@v2 with: fetch-depth: 0 - name: Cache Go modules uses: actions/cache@v3 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- # https://goreleaser.com/ci/actions/ - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: version: latest args: release --rm-dist --timeout=60m env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN_REPO }} # Install Docker image multi-arch builder - name: Install Seihon ${{ env.SEIHON_VERSION }} if: startsWith(github.ref, 'refs/tags/v') run: | curl -sSfL https://raw.githubusercontent.com/ldez/seihon/master/godownloader.sh | sh -s -- -b $(go env GOPATH)/bin ${SEIHON_VERSION} seihon --version - name: Docker Login env: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} run: echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin - name: Publish Docker Images (Seihon) run: make publish-images lego-4.9.1/.gitignore000066400000000000000000000000571434020463500144460ustar00rootroot00000000000000.lego .gitcookies .idea .vscode/ dist/ builds/ lego-4.9.1/.golangci.toml000066400000000000000000000156371434020463500152260ustar00rootroot00000000000000[run] timeout = "7m" skip-files = [] [linters-settings] [linters-settings.govet] check-shadowing = true [linters-settings.gocyclo] min-complexity = 12.0 [linters-settings.maligned] suggest-new = true [linters-settings.goconst] min-len = 3.0 min-occurrences = 3.0 [linters-settings.funlen] lines = -1 statements = 50 [linters-settings.misspell] locale = "US" ignore-words = ["internetbs"] [linters-settings.depguard] list-type = "denylist" include-go-root = false packages = ["github.com/pkg/errors"] [linters-settings.godox] keywords = ["FIXME"] [linters-settings.gocritic] enabled-tags = ["diagnostic", "style", "performance"] disabled-checks= [ "paramTypeCombine", # already handle by gofumpt.extra-rules "whyNoLint", # already handle by nonolint "unnamedResult", "hugeParam", "sloppyReassign", "rangeValCopy", "octalLiteral", "ptrToRefParam", "appendAssign", "ruleguard", "httpNoBody", "exposedSyncMutex", ] [linters] enable-all = true disable = [ "deadcode", # deprecated "exhaustivestruct", # deprecated "golint", # deprecated "ifshort", # deprecated "interfacer", # deprecated "maligned", # deprecated "nosnakecase", # deprecated "scopelint", # deprecated "structcheck", # deprecated "varcheck", # deprecated "cyclop", # duplicate of gocyclo "sqlclosecheck", # not relevant (SQL) "rowserrcheck", # not relevant (SQL) "execinquery", # not relevant (SQL) "lll", "gosec", "dupl", # not relevant "prealloc", # too many false-positive "bodyclose", # too many false-positive "gomnd", "testpackage", # not relevant "tparallel", # not relevant "paralleltest", # not relevant "nestif", # too many false-positive "wrapcheck", "goerr113", # not relevant "nlreturn", # not relevant "wsl", # not relevant "exhaustive", # not relevant "exhaustruct", # not relevant "makezero", # not relevant "forbidigo", # not relevant "varnamelen", # not relevant "nilnil", # not relevant "ireturn", # not relevant "contextcheck", # too many false-positive "tenv", # we already have a test "framework" to handle env vars "noctx", "forcetypeassert", "tagliatelle", "errname", "errchkjson", "nonamedreturns", ] [issues] exclude-use-default = false max-per-linter = 0 max-same-issues = 0 exclude = [ "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*printf?|os\\.(Un)?Setenv). is not checked", "exported (type|method|function) (.+) should have comment or be unexported", "ST1000: at least one file in a package should have a package comment", "package-comments: should have a package comment", ] [[issues.exclude-rules]] path = "(.+)_test.go" linters = ["funlen", "goconst", "maintidx"] [[issues.exclude-rules]] path = "providers/dns/dns_providers.go" linters = ["gocyclo"] [[issues.exclude-rules]] path = "providers/dns/gcloud/googlecloud_test.go" text = "string `(lego\\.wtf|manhattan)` has (\\d+) occurrences, make it a constant" [[issues.exclude-rules]] path = "providers/dns/zoneee/zoneee_test.go" text = "string `(bar|foo)` has (\\d+) occurrences, make it a constant" [[issues.exclude-rules]] path = "certcrypto/crypto.go" text = "(tlsFeatureExtensionOID|ocspMustStapleFeature) is a global variable" [[issues.exclude-rules]] path = "challenge/dns01/nameserver.go" text = "(defaultNameservers|recursiveNameservers|fqdnSoaCache|muFqdnSoaCache) is a global variable" [[issues.exclude-rules]] path = "challenge/dns01/nameserver_.+.go" text = "dnsTimeout is a global variable" [[issues.exclude-rules]] path = "challenge/dns01/nameserver_test.go" text = "findXByFqdnTestCases is a global variable" [[issues.exclude-rules]] path = "challenge/http01/domain_matcher.go" text = "string `Host` has \\d occurrences, make it a constant" [[issues.exclude-rules]] path = "challenge/http01/domain_matcher.go" text = "cyclomatic complexity \\d+ of func `parseForwardedHeader` is high" [[issues.exclude-rules]] path = "challenge/http01/domain_matcher.go" text = "Function 'parseForwardedHeader' has too many statements" [[issues.exclude-rules]] path = "challenge/tlsalpn01/tls_alpn_challenge.go" text = "idPeAcmeIdentifierV1 is a global variable" [[issues.exclude-rules]] path = "log/logger.go" text = "Logger is a global variable" [[issues.exclude-rules]] path = "e2e/(dnschallenge/)?[\\d\\w]+_test.go" text = "load is a global variable" [[issues.exclude-rules]] path = "providers/dns/([\\d\\w]+/)*[\\d\\w]+_test.go" text = "envTest is a global variable" [[issues.exclude-rules]] path = "providers/dns/namecheap/namecheap_test.go" text = "testCases is a global variable" [[issues.exclude-rules]] path = "providers/dns/acmedns/acmedns_test.go" text = "egTestAccount is a global variable" [[issues.exclude-rules]] path = "providers/http/memcached/memcached_test.go" text = "memcachedHosts is a global variable" [[issues.exclude-rules]] path = "providers/dns/sakuracloud/client_test.go" text = "cyclomatic complexity 13 of func `(TestDNSProvider_cleanupTXTRecord_concurrent|TestDNSProvider_addTXTRecord_concurrent)` is high" [[issues.exclude-rules]] path = "providers/dns/dns_providers.go" text = "Function 'NewDNSChallengeProviderByName' has too many statements" [[issues.exclude-rules]] path = "cmd/flags.go" text = "Function 'CreateFlags' is too long" [[issues.exclude-rules]] path = "certificate/certificates.go" text = "Function 'GetOCSP' is too long" [[issues.exclude-rules]] path = "providers/dns/otc/client.go" text = "Function 'loginRequest' is too long" [[issues.exclude-rules]] path = "providers/dns/gandi/gandi.go" text = "Function 'Present' is too long" [[issues.exclude-rules]] path = "cmd/zz_gen_cmd_dnshelp.go" linters = ["gocyclo", "funlen"] [[issues.exclude-rules]] path = "providers/dns/checkdomain/client.go" text = "`payed` is a misspelling of `paid`" [[issues.exclude-rules]] path = "providers/dns/namecheap/namecheap_test.go" text = "cognitive complexity (\\d+) of func `TestDNSProvider_getHosts` is high" [[issues.exclude-rules]] path = "platform/tester/env_test.go" linters = ["thelper"] [[issues.exclude-rules]] path = "providers/dns/oraclecloud/oraclecloud_test.go" text = "SA1019: x509.EncryptPEMBlock has been deprecated since Go 1.16" [[issues.exclude-rules]] path = "challenge/http01/domain_matcher.go" text = "yodaStyleExpr" [[issues.exclude-rules]] path = "providers/dns/dns_providers.go" text = "Function name: NewDNSChallengeProviderByName," [[issues.exclude-rules]] path = "providers/dns/sakuracloud/client.go" text = "mu is a global variable" lego-4.9.1/.goreleaser.yml000066400000000000000000000016111434020463500154040ustar00rootroot00000000000000project_name: lego builds: - binary: lego main: ./cmd/lego/main.go env: - CGO_ENABLED=0 flags: - -trimpath ldflags: - -s -w -X main.version={{.Version}} goos: - windows - darwin - linux - freebsd - openbsd - solaris goarch: - amd64 - 386 - arm - arm64 - mips - mipsle - mips64 - mips64le goarm: - 7 - 6 - 5 gomips: - hardfloat - softfloat ignore: - goos: darwin goarch: 386 - goos: openbsd goarch: arm archives: - id: lego name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}' format: tar.gz format_overrides: - goos: windows format: zip files: - LICENSE - CHANGELOG.md lego-4.9.1/CHANGELOG.md000066400000000000000000001022531434020463500142700ustar00rootroot00000000000000# Changelog ## [v4.9.1] - 2022-11-25 ### Changed: - - **[lib,cname]** cname: add log about CNAME entries - **[dnsprovider]** regru: improve error handling ### Fixed: - - **[dnsprovider,cname]** fix CNAME support for multiple DNS providers - **[dnsprovider,cname]** duckdns: fix CNAME support - **[dnsprovider,cname]** oraclecloud: use fqdn to resolve zone - **[dnsprovider]** hurricane: fix CNAME support - **[lib,cname]** cname: stop trying to traverse cname if none have been found ## [v4.9.0] - 2022-10-03 ### Added: - **[dnsprovider]** Add DNS provider for CIVO - **[dnsprovider]** Add DNS provider for VK Cloud - **[dnsprovider]** Add DNS provider for YandexCloud - **[dnsprovider]** digitalocean: configurable base URL - **[dnsprovider]** loopia: add configurable API endpoint - **[dnsprovider]** pdns: notify secondary servers after updates ### Changed: - **[dnsprovider]** allinkl: removed deprecated sha1 hashing - **[dnsprovider]** auroradns: update authentification - **[dnsprovider]** dnspod: deprecated. Use Tencent Cloud instead. - **[dnsprovider]** exoscale: migrate to API v2 endpoints - **[dnsprovider]** gcloud: update golang.org/x/oauth2 - **[dnsprovider]** lightsail: cleanup - **[dnsprovider]** sakuracloud: update api client library - **[cname]** take out CNAME support from experimental features - **[lib,cname]** add recursive CNAME lookup support - **[lib]** Remove embedded issuer certificates from issued certificate if bundle is false ### Fixed: - **[dnsprovider]** luadns: fix cname support - **[dnsprovider]** njalla: fix record id unmarshal error - **[dnsprovider]** tencentcloud: fix subdomain error ## [v4.8.0] - 2022-06-30 ### Added - **[dnsprovider]** Add DNS provider for Variomedia - **[dnsprovider]** Add NearlyFreeSpeech DNS Provider - **[cli]** Add a --user-agent flag to lego-cli ### Changed - new logo - **[cli]** feat: sleep at renewal - **[cli]** cli/renew: skip random sleep if stdout is a terminal - **[dnsprovider]** hetzner: set min TTL to 60s - **[docs]** refactoring and cleanup ## [v4.7.0] - 2022-05-27 ### Added: - **[dnsprovider]** Add DNS provider for iwantmyname - **[dnsprovider]** Add DNS Provider for IIJ DNS Platform Service - **[dnsprovider]** Add DNS provider for Vercel - **[dnsprovider]** route53: add assume role ARN - **[dnsprovider]** dnsimple: add debug option - **[cli]** feat: add `LEGO_CERT_PEM_PATH` and `LEGO_CERT_PFX_PATH` ### Changed: - **[dnsprovider]** gcore: change dns api url - **[dnsprovider]** bluecat: rewrite provider implementation ### Fixed: - **[dnsprovider]** rfc2136: fix TSIG secret - **[dnsprovider]** tencentcloud: fix InvalidParameter.DomainInvalid error when using DNS challenges - **[lib]** fix: panic in certcrypto.ParsePEMPrivateKey ## [v4.6.0] - 2022-01-18 ### Added - **[dnsprovider]** Add DNS provider for UKFast SafeDNS - **[dnsprovider]** Add DNS Provider for Tencent Cloud - **[dnsprovider]** azure: add support for Azure Private Zone DNS - **[dnsprovider]** exec: add sequence interval - **[cli]** Add a `--pfx`, and `--pfx.pas`s option to generate a PKCS#12 (`.pfx`) file. - **[lib]** Extended support of cert pool (`LEGO_CA_CERTIFICATES` and `LEGO_CA_SYSTEM_CERT_POOL`) - **[lib,httpprovider]** added uds capability to http challenge server ### Changed - **[lib]** Extend validity of TLS-ALPN-01 certificates to 365 days - **[lib,cli]** Allows defining the reason for the certificate revocation ### Fixed - **[dnsprovider]** mythicbeasts: fix token expiration - **[dnsprovider]** rackspace: change zone ID to string ## [v4.5.3] - 2021-09-06 ### Fixed: - **[lib,cli]** fix: missing preferred chain param for renew request ## [v4.5.2] - 2021-09-01 ### Added: - **[dnsprovider]** Add DNS provider for all-inkl - **[dnsprovider]** Add DNS provider for Epik - **[dnsprovider]** Add DNS provider for freemyip.com - **[dnsprovider]** Add DNS provider for g-core labs - **[dnsprovider]** Add DNS provider for hosttech - **[dnsprovider]** Add DNS Provider for IBM Cloud (SoftLayer) - **[dnsprovider]** Add DNS provider for Internet.bs - **[dnsprovider]** Add DNS provider for nicmanager ### Changed: - **[dnsprovider]** alidns: support ECS instance RAM role - **[dnsprovider]** alidns: support sts token credential - **[dnsprovider]** azure: zone name as environment variable - **[dnsprovider]** ovh: follow cname - **[lib,cli]** Add AlwaysDeactivateAuthorizations flag to ObtainRequest ### Fixed: - **[dnsprovider]** infomaniak: fix subzone support - **[dnsprovider]** edgedns: fix Present and CleanUp logic - **[dnsprovider]** lightsail: wrong Region env var name - **[lib]** lib: fix backoff in SolverManager - **[lib]** lib: use permanent error instead of context cancellation - **[dnsprovider]** desec: bump to v0.6.0 ## [v4.5.1] - 2021-09-01 Cancelled due to a CI issue, replaced by v4.5.2. ## [v4.5.0] - 2021-09-30 Cancelled due to a CI issue, replaced by v4.5.2. ## [v4.4.0] - 2021-06-08 ### Added: - **[dnsprovider]** Add DNS provider for Infoblox - **[dnsprovider]** Add DNS provider for Porkbun - **[dnsprovider]** Add DNS provider for Simply.com - **[dnsprovider]** Add DNS provider for Sonic - **[dnsprovider]** Add DNS provider for VinylDNS - **[dnsprovider]** Add DNS provider for wedos ### Changed: - **[cli]** log: Use stderr instead of stdout. - **[dnsprovider]** hostingde: autodetection of the zone name. - **[dnsprovider]** scaleway: use official SDK - **[dnsprovider]** powerdns: several improvements - **[lib]** lib: improve wait.For returns. ### Fixed: - **[dnsprovider]** hurricane: add API rate limiter. - **[dnsprovider]** hurricane: only treat first word of response body as response code - **[dnsprovider]** exoscale: fix DNS provider debugging - **[dnsprovider]** wedos: fix api call parameters - **[dnsprovider]** nifcloud: Get zone info from dns01.FindZoneByFqdn - **[cli,lib]** csr: Support the type `NEW CERTIFICATE REQUEST` ## [v4.3.1] - 2021-03-12 ### Fixed: - **[dnsprovider]** exoscale: fix dependency version. ## [v4.3.0] - 2021-03-10 ### Added: - **[dnsprovider]** Add DNS provider for Njalla - **[dnsprovider]** Add DNS provider for Domeneshop - **[dnsprovider]** Add DNS provider for Hurricane Electric - **[dnsprovider]** designate: support for Openstack Application Credentials - **[dnsprovider]** edgedns: support for .edgerc file ### Changed: - **[dnsprovider]** infomaniak: Make error message more meaningful - **[dnsprovider]** cloudns: Improve reliability - **[dnsprovider]** rfc2163: Removed support for MD5 algorithm. The default algorithm is now SHA1. ### Fixed: - **[dnsprovider]** desec: fix error with default TTL - **[dnsprovider]** mythicbeasts: implement `ProviderTimeout` - **[dnsprovider]** dnspod: improve search accuracy when a domain have more than 100 records - **[lib]** Increase HTTP client timeouts - **[lib]** preferred chain only match root name ## [v4.2.0] - 2021-01-24 ### Added: - **[dnsprovider]** Add DNS provider for Loopia - **[dnsprovider]** Add DNS provider for Ionos. ### Changed: - **[dnsprovider]** acme-dns: update cpu/goacmedns to v0.1.1. - **[dnsprovider]** inwx: Increase propagation timeout to 360s to improve robustness - **[dnsprovider]** vultr: Update to govultr v2 API - **[dnsprovider]** pdns: get exact zone instead of all zones ### Fixed: - **[dnsprovider]** vult, dnspod: fix default HTTP timeout. - **[dnsprovider]** pdns: URL request creation. - **[lib]** errors: Fix instance not being printed ## [v4.1.3] - 2020-11-25 ### Fixed: - **[dnsprovider]** azure: fix error handling. ## [v4.1.2] - 2020-11-21 ### Fixed: - **[lib]** fix: preferred chain support. ## [v4.1.1] - 2020-11-19 ### Fixed: - **[dnsprovider]** otc: select correct zone if multiple returned - **[dnsprovider]** azure: fix target must be a non-nil pointer ## [v4.1.0] - 2020-11-06 ### Added: - **[dnsprovider]** Add DNS provider for Infomaniak - **[dnsprovider]** joker: add support for SVC API - **[dnsprovider]** gcloud: add an option to allow the use of private zones ### Changed: - **[dnsprovider]** rfc2136: ensure TSIG algorithm is fully qualified - **[dnsprovider]** designate: Deprecate OS_TENANT_NAME as required field ### Fixed: - **[lib]** acme/api: use postAsGet instead of post for AccountService.Get - **[lib]** fix: use http.Header.Set method instead of Add. ## [v4.0.1] - 2020-09-03 ### Fixed: - **[dnsprovider]** exoscale: change dependency version. ## [v4.0.0] - 2020-09-02 ### Added: - **[cli], [lib]** Support "alternate" certificate links for selecting different signing Chains ### Changed: - **[cli]** Replaces `ec384` by `ec256` as default key-type - **[lib]** Changes `ObtainForCSR` method signature ### Removed: - **[dnsprovider]** Replaces FastDNS by EdgeDNS - **[dnsprovider]** Removes old Linode provider - **[lib]** Removes `AddPreCheck` function ## [v3.9.0] - 2020-09-01 ### Added: - **[dnsprovider]** Add Akamai Edgedns. Deprecate FastDNS - **[dnsprovider]** Add DNS provider for HyperOne ### Changed: - **[dnsprovider]** designate: add support for Openstack clouds.yaml - **[dnsprovider]** azure: allow selecting environments - **[dnsprovider]** desec: applies API rate limits. ### Fixed: - **[dnsprovider]** namesilo: fix cleanup. ## [v3.8.0] - 2020-07-02 ### Added: - **[cli]** cli: add hook on the run command. - **[dnsprovider]** inwx: Two-Factor-Authentication - **[dnsprovider]** Add DNS provider for ArvanCloud ### Changed: - **[dnsprovider]** vultr: bumping govultr version - **[dnsprovider]** desec: improve error logs. - **[lib]** Ensures the return of a location during account updates - **[dnsprovider]** route53: Document all AWS credential environment variables ### Fixed: - **[dnsprovider]** stackpath: fix subdomain support. - **[dnsprovider]** arvandcloud: fix record name. - **[dnsprovider]** fix: multi-va. - **[dnsprovider]** constellix: fix search records API call. - **[dnsprovider]** hetzner: fix record name. - **[lib]** Registrar.ResolveAccountByKey: Fix malformed request ## [v3.7.0] - 2020-05-11 ### Added: - **[dnsprovider]** Add DNS provider for Netlify. - **[dnsprovider]** Add DNS provider for deSEC.io - **[dnsprovider]** Add DNS provider for LuaDNS - **[dnsprovider]** Adding Hetzner DNS provider - **[dnsprovider]** Add DNS provider for Mythic beasts DNSv2 - **[dnsprovider]** Add DNS provider for Yandex. ### Changed: - **[dnsprovider]** Upgrade DNSimple client to 0.60.0 - **[dnsprovider]** update aws sdk ### Fixed: - **[dnsprovider]** autodns: removes TXT records during CleanUp. - **[dnsprovider]** Fix exoscale HTTP timeout - **[cli]** fix: renew path information. - **[cli]** Fix account storage location warning message ## [v3.6.0] - 2020-04-24 ### Added: - **[dnsprovider]** Add DNS provider for CloudDNS. - **[dnsprovider]** alicloud: add support for domain with punycode - **[dnsprovider]** cloudns: Add subuser support - **[cli]** Information about renewed certificates are now passed to the renew hook ### Changed: - **[dnsprovider]** acmedns: Update cpu/goacmedns v0.0.1 -> v0.0.2 - **[dnsprovider]** alicloud: update sdk dependency version to v1.61.112 - **[dnsprovider]** azure: Allow for the use of MSI - **[dnsprovider]** constellix: improve challenge. - **[dnsprovider]** godaddy: allow parallel solve. - **[dnsprovider]** namedotcom: get the actual registered domain so we can remove just that from the hostname to be created - **[dnsprovider]** transip: updated the client to v6 ### Fixed: - **[dnsprovider]** ns1: fix missing domain in log - **[dnsprovider]** rimuhosting: use HTTP client from config. ## [v3.5.0] - 2020-03-15 ### Added: - **[dnsprovider]** Add DNS provider for Dynu. - **[dnsprovider]** Add DNS provider for reg.ru - **[dnsprovider]** Add DNS provider for Zonomi and RimuHosting. - **[cli]** Building binaries for arm 6 and 5 - **[cli]** Uses CGO_ENABLED=0 - **[cli]** Multi-arch Docker image. - **[cli]** Adds `--name` flag to list command. ### Changed: - **[lib]** lib: Improve cleanup log messages. - **[lib]** Wrap errors. ### Fixed: - **[dnsprovider]** azure: pass AZURE_CLIENT_SECRET_FILE to autorest.Authorizer - **[dnsprovider]** gcloud: fixes issues when used with GKE Workload Identity - **[dnsprovider]** oraclecloud: fix subdomain support ## [v3.4.0] - 2020-02-25 ### Added: - **[dnsprovider]** Add DNS provider for Constellix - **[dnsprovider]** Add DNS provider for Servercow. - **[dnsprovider]** Add DNS provider for Scaleway - **[cli]** Add "LEGO_PATH" environment variable ### Changed: - **[dnsprovider]** route53: allow custom client to be provided - **[dnsprovider]** namecheap: allow external domains - **[dnsprovider]** namecheap: add sandbox support. - **[dnsprovider]** ovh: Improve provider documentation - **[dnsprovider]** route53: Improve provider documentation ### Fixed: - **[dnsprovider]** zoneee: fix subdomains. - **[dnsprovider]** designate: Don't clean up managed records like SOA and NS - **[dnsprovider]** dnspod: update lib. - **[lib]** crypto: Treat CommonName as optional - **[lib]** chore: update cenkalti/backoff to v4. ## [v3.3.0] - 2020-01-08 ### Added: - **[dnsprovider]** Add DNS provider for Checkdomain - **[lib]** Add support to update account ### Changed: - **[dnsprovider]** gcloud: Auto-detection of the project ID. - **[lib]** Successfully parse private key PEM blocks ### Fixed: - **[dnsprovider]** Update dnspod, because of API breaking changes. ## [v3.2.0] - 2019-11-10 ### Added: - **[dnsprovider]** Add support for autodns ### Changed: - **[dnsprovider]** httpreq: Allow use environment vars from a `_FILE` file - **[lib]** Don't deactivate valid authorizations - **[lib]** Expose more SOA fields found by dns01.FindZoneByFqdn ### Fixed: - **[dnsprovider]** use token as unique ID. ## [v3.1.0] - 2019-10-07 ### Added: - **[dnsprovider]** Add DNS provider for Liquid Web - **[dnsprovider]** cloudflare: add support for API tokens - **[cli]** feat: ease operation behind proxy servers ### Changed: - **[dnsprovider]** cloudflare: update client - **[dnsprovider]** linodev4: propagation timeout configuration. ### Fixed: - **[dnsprovider]** ovh: fix int overflow. - **[dnsprovider]** bindman: fix client version. ## [v3.0.2] - 2019-08-15 ### Fixed: - Invalid pseudo version (related to Cloudflare client). ## [v3.0.1] - 2019-08-14 There was a problem when creating the tag v3.0.1, this tag has been invalidate. ## [v3.0.0] - 2019-08-05 ### Changed: - migrate to go module (new import github.com/go-acme/lego/v3/) - update DNS clients ## [v2.7.2] - 2019-07-30 ### Fixed: - **[dnsprovider]** vultr: quote TXT record ## [v2.7.1] - 2019-07-22 ### Fixed: - **[dnsprovider]** vultr: invalid record type. ## [v2.7.0] - 2019-07-17 ### Added: - **[dnsprovider]** Add DNS provider for namesilo - **[dnsprovider]** Add DNS provider for versio.nl ### Changed: - **[dnsprovider]** Update DNS providers libs. - **[dnsprovider]** joker: support username and password. - **[dnsprovider]** Vultr: Switch to official client ### Fixed: - **[dnsprovider]** otc: Prevent sending empty body. ## [v2.6.0] - 2019-05-27 ### Added: - **[dnsprovider]** Add support for Joker.com DMAPI - **[dnsprovider]** Add support for Bindman DNS provider - **[dnsprovider]** Add support for EasyDNS - **[lib]** Get an existing certificate by URL ### Changed: - **[dnsprovider]** digitalocean: LEGO_EXPERIMENTAL_CNAME_SUPPORT support - **[dnsprovider]** gcloud: Use fqdn to get zone Present/CleanUp - **[dnsprovider]** exec: serial behavior - **[dnsprovider]** manual: serial behavior. - **[dnsprovider]** Strip newlines when reading environment variables from `_FILE` suffixed files. ### Fixed: - **[cli]** fix: cli disable-cp option. - **[dnsprovider]** gcloud: fix zone visibility. ## [v2.5.0] - 2019-04-17 ### Added: - **[cli]** Adds renew hook - **[dnsprovider]** Adds 'Since' to DNS providers documentation ### Changed: - **[dnsprovider]** gcloud: use public DNS zones - **[dnsprovider]** route53: enhance documentation. ### Fixed: - **[dnsprovider]** cloudns: fix TTL and status validation - **[dnsprovider]** sakuracloud: supports concurrent update - **[dnsprovider]** Disable authz when solve fail. - Add tzdata to the Docker image. ## [v2.4.0] - 2019-03-25 - Migrate from xenolf/lego to go-acme/lego. ### Added: - **[dnsprovider]** Add DNS Provider for Domain Offensive (do.de) - **[dnsprovider]** Adds information about '_FILE' suffix. ### Fixed: - **[cli,dnsprovider]** Add 'manual' provider to the output of dnshelp - **[dnsprovider]** hostingde: Use provided ZoneName instead of domain - **[dnsprovider]** pdns: fix wildcard with SANs ## [v2.3.0] - 2019-03-11 ### Added: - **[dnsprovider]** Add DNS Provider for ClouDNS.net - **[dnsprovider]** Add DNS Provider for Oracle Cloud ### Changed: - **[cli]** Adds log when no renewal. - **[dnsprovider,lib]** Add a mechanism to wrap a PreCheckFunc - **[dnsprovider]** oraclecloud: better way to get private key. - **[dnsprovider]** exoscale: update library ### Fixed: - **[dnsprovider]** OVH: Refresh zone after deleting challenge record - **[dnsprovider]** oraclecloud: ttl config and timeout - **[dnsprovider]** hostingde: fix client fails if customer has no access to dns-groups - **[dnsprovider]** vscale: getting sub-domain - **[dnsprovider]** selectel: getting sub-domain - **[dnsprovider]** vscale: fix TXT records clean up - **[dnsprovider]** selectel: fix TXT records clean up ## [v2.2.0] - 2019-02-08 ### Added: - **[dnsprovider]** Add support for Openstack Designate as a DNS provider - **[dnsprovider]** gcloud: Option to specify gcloud service account json by env as string - **[experimental feature]** Resolve CNAME when creating dns-01 challenge. To enable: set `LEGO_EXPERIMENTAL_CNAME_SUPPORT` to `true`. ### Changed: - **[cli]** Applies Let’s Encrypt’s recommendation about renew. The option `--days` of the command `renew` has a new default value (`30`) - **[lib]** Uses a jittered exponential backoff ### Fixed: - **[cli]** CLI and key type. - **[dnsprovider]** httpreq: Endpoint with path. - **[dnsprovider]** fastdns: Do not overwrite existing TXT records - Log wildcard domain correctly in validation ## [v2.1.0] - 2019-01-24 ### Added: - **[dnsprovider]** Add support for zone.ee as a DNS provider. ### Changed: - **[dnsprovider]** nifcloud: Change DNS base url. - **[dnsprovider]** gcloud: More detailed information about Google Cloud DNS. ### Fixed: - **[lib]** fix: OCSP, set HTTP client. - **[dnsprovider]** alicloud: fix pagination. - **[dnsprovider]** namecheap: fix panic. ## [v2.0.0] - 2019-01-09 ### Added: - **[cli,lib]** Option to disable the complete propagation Requirement - **[lib,cli]** Support non-ascii domain name (punnycode) - **[cli,lib]** Add configurable timeout when obtaining certificates - **[cli]** Archive revoked certificates - **[cli]** Add command to list certificates. - **[cli]** support for renew with CSR - **[cli]** add SAN on renew - **[lib]** Adds `Remove` for challenges - **[lib]** Add version to xenolf-acme in User-Agent. - **[dnsprovider]** The ability for a DNS provider to solve the challenge sequentially - **[dnsprovider]** Add DNS provider for "HTTP request". - **[dnsprovider]** Add DNS Provider for Vscale - **[dnsprovider]** Add DNS Provider for TransIP - **[dnsprovider]** Add DNS Provider for inwx - **[dnsprovider]** alidns: add support to handle more than 20 domains ### Changed: - **[lib]** Check all challenges in a predictable order - **[lib]** Poll authz URL instead of challenge URL - **[lib]** Check all nameservers in a predictable order - **[lib]** Logs every iteration of waiting for the propagation - **[cli]** `--http`: enable HTTP challenge **important** - **[cli]** `--http.port`: previously named `--http` - **[cli]** `--http.webroot`: previously named `--webroot` - **[cli]** `--http.memcached-host`: previously named `--memcached-host` - **[cli]** `--tls`: enable TLS challenge **important** - **[cli]** `--tls.port`: previously named `--tls` - **[cli]** `--dns.resolvers`: previously named `--dns-resolvers` - **[cli]** the option `--days` of the command `renew` has default value (`15`) - **[dnsprovider]** gcloud: Use GCE_PROJECT for project always, if specified ### Removed: - **[lib]** Remove `SetHTTP01Address` - **[lib]** Remove `SetTLSALPN01Address` - **[lib]** Remove `Exclude` - **[cli]** Remove `--exclude`, `-x` ### Fixed: - **[lib]** Fixes revocation for subdomains and non-ascii domains - **[lib]** Disable pending authorizations - **[dnsprovider]** transip: concurrent access to the API. - **[dnsprovider]** gcloud: fix for wildcard - **[dnsprovider]** Azure: Do not overwrite existing TXT records - **[dnsprovider]** fix: Cloudflare error. ## [v1.2.0] - 2018-11-04 ### Added: - **[dnsprovider]** Add DNS Provider for ConoHa DNS - **[dnsprovider]** Add DNS Provider for MyDNS.jp - **[dnsprovider]** Add DNS Provider for Selectel ### Fixed: - **[dnsprovider]** netcup: make unmarshalling of api-responses more lenient. ### Changed: - **[dnsprovider]** aurora: change DNS client - **[dnsprovider]** azure: update auth to support instance metadata service - **[dnsprovider]** dnsmadeeasy: log response body on error - **[lib]** TLS-ALPN-01: Update idPeAcmeIdentifierV1, draft refs. - **[lib]** Do not send a JWS body when POSTing challenges. - **[lib]** Support POST-as-GET. ## [v1.1.0] - 2018-10-16 ### Added: - **[lib]** TLS-ALPN-01 Challenge - **[cli]** Add filename parameter - **[dnsprovider]** Allow to configure TTL, interval and timeout - **[dnsprovider]** Add support for reading DNS provider setup from files - **[dnsprovider]** Add DNS Provider for ACME-DNS - **[dnsprovider]** Add DNS Provider for ALIYUN DNS - **[dnsprovider]** Add DNS Provider for DreamHost - **[dnsprovider]** Add DNS provider for hosting.de - **[dnsprovider]** Add DNS Provider for IIJ - **[dnsprovider]** Add DNS Provider for netcup - **[dnsprovider]** Add DNS Provider for NIFCLOUD DNS - **[dnsprovider]** Add DNS Provider for SAKURA Cloud - **[dnsprovider]** Add DNS Provider for Stackpath - **[dnsprovider]** Add DNS Provider for VegaDNS - **[dnsprovider]** exec: add EXEC_MODE=RAW support. - **[dnsprovider]** cloudflare: support for CF_API_KEY and CF_API_EMAIL ### Fixed: - **[lib]** Don't trust identifiers order. - **[lib]** Fix missing issuer certificates from Let's Encrypt - **[dnsprovider]** duckdns: fix TXT record update url - **[dnsprovider]** duckdns: fix subsubdomain - **[dnsprovider]** gcloud: update findTxtRecords to use Name=fqdn and Type=TXT - **[dnsprovider]** lightsail: Fix Domain does not exist error - **[dnsprovider]** ns1: use the authoritative zone and not the domain name - **[dnsprovider]** ovh: check error to avoid panic due to nil client ### Changed: - **[lib]** Submit all dns records up front, then validate serially ## [v1.0.0] - 2018-05-30 ### Changed: - **[lib]** ACME v2 Support. - **[dnsprovider]** Renamed `/providers/dns/googlecloud` to `/providers/dns/gcloud`. - **[dnsprovider]** Modified Google Cloud provider `gcloud.NewDNSProviderServiceAccount` function to extract the project id directly from the service account file. - **[dnsprovider]** Made errors more verbose for the Cloudflare provider. ## [v0.5.0] - 2018-05-29 ### Added: - **[dnsprovider]** Add DNS challenge provider `exec` - **[dnsprovider]** Add DNS Provider for Akamai FastDNS - **[dnsprovider]** Add DNS Provider for Bluecat DNS - **[dnsprovider]** Add DNS Provider for CloudXNS - **[dnsprovider]** Add DNS Provider for Duck DNS - **[dnsprovider]** Add DNS Provider for Gandi Beta Platform (LiveDNS) - **[dnsprovider]** Add DNS Provider for GleSYS API - **[dnsprovider]** Add DNS Provider for GoDaddy - **[dnsprovider]** Add DNS Provider for Lightsail - **[dnsprovider]** Add DNS Provider for Name.com ### Fixed: - **[dnsprovider]** Azure: Added missing environment variable in the comments - **[dnsprovider]** PowerDNS: Fix zone URL, add leading slash. - **[dnsprovider]** DNSimple: Fix api - **[cli]** Correct help text for `--dns-resolvers` default. - **[cli]** renew/revoke - don't panic on wrong account. - **[lib]** Fix zone detection for cross-zone cnames. - **[lib]** Use proxies from environment when making outbound http connections. ### Changed: - **[lib]** Users of an effective top-level domain can use the DNS challenge. - **[dnsprovider]** Azure: Refactor to work with new Azure SDK version. - **[dnsprovider]** Cloudflare and Azure: Adding output of which envvars are missing. - **[dnsprovider]** Dyn DNS: Slightly improve provider error reporting. - **[dnsprovider]** Exoscale: update to latest egoscale version. - **[dnsprovider]** Route53: Use NewSessionWithOptions instead of deprecated New. ## [0.4.1] - 2017-09-26 ### Added: - lib: A new DNS provider for OTC. - lib: The `AWS_HOSTED_ZONE_ID` environment variable for the Route53 DNS provider to directly specify the zone. - lib: The `RFC2136_TIMEOUT` enviroment variable to make the timeout for the RFC2136 provider configurable. - lib: The `GCE_SERVICE_ACCOUNT_FILE` environment variable to specify a service account file for the Google Cloud DNS provider. ### Fixed: - lib: Fixed an authentication issue with the latest Azure SDK. ## [0.4.0] - 2017-07-13 ### Added: - CLI: The `--http-timeout` switch. This allows for an override of the default client HTTP timeout. - lib: The `HTTPClient` field. This allows for an override of the default HTTP timeout for library HTTP requests. - CLI: The `--dns-timeout` switch. This allows for an override of the default DNS timeout for library DNS requests. - lib: The `DNSTimeout` switch. This allows for an override of the default client DNS timeout. - lib: The `QueryRegistration` function on `acme.Client`. This performs a POST on the client registration's URI and gets the updated registration info. - lib: The `DeleteRegistration` function on `acme.Client`. This deletes the registration as currently configured in the client. - lib: The `ObtainCertificateForCSR` function on `acme.Client`. The function allows to request a certificate for an already existing CSR. - CLI: The `--csr` switch. Allows to use already existing CSRs for certificate requests on the command line. - CLI: The `--pem` flag. This will change the certificate output so it outputs a .pem file concatanating the .key and .crt files together. - CLI: The `--dns-resolvers` flag. Allows for users to override the default DNS servers used for recursive lookup. - lib: Added a memcached provider for the HTTP challenge. - CLI: The `--memcached-host` flag. This allows to use memcached for challenge storage. - CLI: The `--must-staple` flag. This enables OCSP must staple in the generated CSR. - lib: The library will now honor entries in your resolv.conf. - lib: Added a field `IssuerCertificate` to the `CertificateResource` struct. - lib: A new DNS provider for OVH. - lib: A new DNS provider for DNSMadeEasy. - lib: A new DNS provider for Linode. - lib: A new DNS provider for AuroraDNS. - lib: A new DNS provider for NS1. - lib: A new DNS provider for Azure DNS. - lib: A new DNS provider for Rackspace DNS. - lib: A new DNS provider for Exoscale DNS. - lib: A new DNS provider for DNSPod. ### Changed: - lib: Exported the `PreCheckDNS` field so library users can manage the DNS check in tests. - lib: The library will now skip challenge solving if a valid Authz already exists. ### Removed: - lib: The library will no longer check for auto renewed certificates. This has been removed from the spec and is not supported in Boulder. ### Fixed: - lib: Fix a problem with the Route53 provider where it was possible the verification was published to a private zone. - lib: Loading an account from file should fail if a integral part is nil - lib: Fix a potential issue where the Dyn provider could resolve to an incorrect zone. - lib: If a registration encounteres a conflict, the old registration is now recovered. - CLI: The account.json file no longer has the executable flag set. - lib: Made the client registration more robust in case of a 403 HTTP response. - lib: Fixed an issue with zone lookups when they have a CNAME in another zone. - lib: Fixed the lookup for the authoritative zone for Google Cloud. - lib: Fixed a race condition in the nonce store. - lib: The Google Cloud provider now removes old entries before trying to add new ones. - lib: Fixed a condition where we could stall due to an early error condition. - lib: Fixed an issue where Authz object could end up in an active state after an error condition. ## [0.3.1] - 2016-04-19 ### Added: - lib: A new DNS provider for Vultr. ### Fixed: - lib: DNS Provider for DigitalOcean could not handle subdomains properly. - lib: handleHTTPError should only try to JSON decode error messages with the right content type. - lib: The propagation checker for the DNS challenge would not retry on send errors. ## [0.3.0] - 2016-03-19 ### Added: - CLI: The `--dns` switch. To include the DNS challenge for consideration. When using this switch, all other solvers are disabled. Supported are the following solvers: cloudflare, digitalocean, dnsimple, dyn, gandi, googlecloud, namecheap, route53, rfc2136 and manual. - CLI: The `--accept-tos` switch. Indicates your acceptance of the Let's Encrypt terms of service without prompting you. - CLI: The `--webroot` switch. The HTTP-01 challenge may now be completed by dropping a file into a webroot. When using this switch, all other solvers are disabled. - CLI: The `--key-type` switch. This replaces the `--rsa-key-size` switch and supports the following key types: EC256, EC384, RSA2048, RSA4096 and RSA8192. - CLI: The `--dnshelp` switch. This displays a more in-depth help topic for DNS solvers. - 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. - lib: A new type for challenge identifiers `Challenge` - lib: A new interface for custom challenge providers `acme.ChallengeProvider` - lib: A new interface for DNS-01 providers to allow for custom timeouts for the validation function `acme.ChallengeProviderTimeout` - lib: SetChallengeProvider function. Pass a challenge identifier and a Provider to replace the default behaviour of a challenge. - 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. - lib: The `acme.KeyType` type was added and is used for the configuration of crypto parameters for RSA and EC keys. Valid KeyTypes are: EC256, EC384, RSA2048, RSA4096 and RSA8192. ### Changed - lib: ExcludeChallenges now expects to be passed an array of `Challenge` types. - lib: HTTP-01 now supports custom solvers using the `ChallengeProvider` interface. - lib: TLS-SNI-01 now supports custom solvers using the `ChallengeProvider` interface. - 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. - lib: The `acme.NewClient` function now expects an `acme.KeyType` instead of the keyBits parameter. ### Removed - CLI: The `rsa-key-size` switch was removed in favor of `key-type` to support EC keys. ### Fixed - lib: Fixed a race condition in HTTP-01 - lib: Fixed an issue where status codes on ACME challenge responses could lead to no action being taken. - lib: Fixed a regression when calling the Renew function with a SAN certificate. ## [0.2.0] - 2016-01-09 ### Added: - CLI: The `--exclude` or `-x` switch. To exclude a challenge from being solved. - CLI: The `--http` switch. To set the listen address and port of HTTP based challenges. Supports `host:port` and `:port` for any interface. - CLI: The `--tls` switch. To set the listen address and port of TLS based challenges. Supports `host:port` and `:port` for any interface. - CLI: The `--reuse-key` switch for the `renew` operation. This lets you reuse an existing private key for renewals. - lib: ExcludeChallenges function. Pass an array of challenge identifiers to exclude them from solving. - lib: SetHTTPAddress function. Pass a port to set the listen port for HTTP based challenges. - lib: SetTLSAddress function. Pass a port to set the listen port of TLS based challenges. - lib: acme.UserAgent variable. Use this to customize the user agent on all requests sent by lego. ### Changed: - lib: NewClient does no longer accept the optPort parameter - lib: ObtainCertificate now returns a SAN certificate if you pass more then one domain. - lib: GetOCSPForCert now returns the parsed OCSP response instead of just the status. - lib: ObtainCertificate has a new parameter `privKey crypto.PrivateKey` which lets you reuse an existing private key for new certificates. - lib: RenewCertificate now expects the PrivateKey property of the CertificateResource to be set only if you want to reuse the key. ### Removed: - CLI: The `--port` switch was removed. - lib: RenewCertificate does no longer offer to also revoke your old certificate. ### Fixed: - CLI: Fix logic using the `--days` parameter for renew ## [0.1.1] - 2015-12-18 ### Added: - CLI: Added a way to automate renewal through a cronjob using the --days parameter to renew ### Changed: - lib: Improved log output on challenge failures. ### Fixed: - CLI: The short parameter for domains would not get accepted - CLI: The cli did not return proper exit codes on error library errors. - lib: RenewCertificate did not properly renew SAN certificates. ### Security - lib: Fix possible DOS on GetOCSPForCert ## [0.1.0] - 2015-12-03 - Initial release [0.3.1]: https://github.com/go-acme/lego/compare/v0.3.0...v0.3.1 [0.3.0]: https://github.com/go-acme/lego/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/go-acme/lego/compare/v0.1.1...v0.2.0 [0.1.1]: https://github.com/go-acme/lego/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/go-acme/lego/tree/v0.1.0 lego-4.9.1/CONTRIBUTING.md000066400000000000000000000050471434020463500147130ustar00rootroot00000000000000# How to contribute to lego Contributions in the form of patches and proposals are essential to keep lego great and to make it even better. To ensure a great and easy experience for everyone, please review the few guidelines in this document. ## Bug reports - Use the issue search to see if the issue has already been reported. - Also look for closed issues to see if your issue has already been fixed. - If both of the above do not apply create a new issue and include as much information as possible. Bug reports should include all information a person could need to reproduce your problem without the need to follow up for more information. If possible, provide detailed steps for us to reproduce it, the expected behaviour and the actual behaviour. ## Feature proposals and requests Feature requests are welcome and should be discussed in an issue. Please keep proposals focused on one thing at a time and be as detailed as possible. It 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. ## Pull requests Patches, new features and improvements are a great way to help the project. Please keep them focused on one thing and do not include unrelated commits. All pull requests which alter the behaviour of the program, add new behaviour or somehow alter code in a non-trivial way should **always** include tests. If you want to contribute a significant pull request (with a non-trivial workload for you) please **ask first**. We do not want you to spend a lot of time on something the project's developers might not want to merge into the project. **IMPORTANT**: By submitting a patch, you agree to allow the project owners to license your work under the terms of the [MIT License](LICENSE). ### How to create a pull request Requirements: - `go` v1.15+ - environment variable: `GO111MODULE=on` First, you have to install [GoLang](https://golang.org/doc/install) and [golangci-lint](https://github.com/golangci/golangci-lint#install). ```bash # Create the root folder mkdir -p $GOPATH/src/github.com/go-acme cd $GOPATH/src/github.com/go-acme # clone your fork git clone git@github.com:YOUR_USERNAME/lego.git cd lego # Add the go-acme/lego remote git remote add upstream git@github.com:go-acme/lego.git git fetch upstream ``` ```bash # Create your branch git checkout -b my-feature ## Create your code ## ``` ```bash # Format make fmt # Linters make checks # Tests make test # Compile make build ``` ```bash # push your branch git push -u origin my-feature ## create a pull request on GitHub ## ``` lego-4.9.1/Dockerfile000066400000000000000000000006371434020463500144540ustar00rootroot00000000000000FROM golang:1-alpine as builder RUN apk --no-cache --no-progress add make git WORKDIR /go/lego ENV GO111MODULE on # Download go modules COPY go.mod . COPY go.sum . RUN go mod download COPY . . RUN make build FROM alpine:3.12 RUN apk update \ && apk add --no-cache ca-certificates tzdata \ && update-ca-certificates COPY --from=builder /go/lego/dist/lego /usr/bin/lego ENTRYPOINT [ "/usr/bin/lego" ] lego-4.9.1/LICENSE000066400000000000000000000021001434020463500134520ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015-2017 Sebastian Erhart Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lego-4.9.1/Makefile000066400000000000000000000035021434020463500141140ustar00rootroot00000000000000.PHONY: clean checks test build image e2e fmt export GO111MODULE=on export CGO_ENABLED=0 LEGO_IMAGE := goacme/lego MAIN_DIRECTORY := ./cmd/lego/ BIN_OUTPUT := $(if $(filter $(shell go env GOOS), windows), dist/lego.exe, dist/lego) TAG_NAME := $(shell git tag -l --contains HEAD) SHA := $(shell git rev-parse HEAD) VERSION := $(if $(TAG_NAME),$(TAG_NAME),$(SHA)) default: clean generate-dns checks test build clean: @echo BIN_OUTPUT: ${BIN_OUTPUT} rm -rf dist/ builds/ cover.out build: clean @echo Version: $(VERSION) go build -trimpath -ldflags '-X "main.version=${VERSION}"' -o ${BIN_OUTPUT} ${MAIN_DIRECTORY} image: @echo Version: $(VERSION) docker build -t $(LEGO_IMAGE) . publish-images: seihon publish -v "$(TAG_NAME)" -v "latest" --image-name="$(LEGO_IMAGE)" --dry-run=false test: clean go test -v -cover ./... e2e: clean LEGO_E2E_TESTS=local go test -count=1 -v ./e2e/... checks: golangci-lint run # Release helper .PHONY: patch minor major detach patch: go run internal/release.go release -m patch minor: go run internal/release.go release -m minor major: go run internal/release.go release -m major detach: go run internal/release.go detach # Docs .PHONY: docs-build docs-serve docs-themes docs-build: generate-dns @make -C ./docs hugo-build docs-serve: generate-dns @make -C ./docs hugo docs-themes: @make -C ./docs hugo-themes # DNS Documentation .PHONY: generate-dns validate-doc generate-dns: go generate ./... validate-doc: generate-dns validate-doc: DOC_DIRECTORIES := ./docs/ ./cmd/ validate-doc: if git diff --exit-code --quiet $(DOC_DIRECTORIES) 2>/dev/null; then \ echo 'All documentation changes are done the right way.'; \ else \ echo 'The documentation must be regenerated, please use `make generate-dns`.'; \ git status --porcelain -- $(DOC_DIRECTORIES) 2>/dev/null; \ exit 2; \ fi lego-4.9.1/README.md000066400000000000000000000261411434020463500137370ustar00rootroot00000000000000
lego logo

Automatic Certificates and HTTPS for everyone.

# Lego Let's Encrypt client and ACME library written in Go. [![GoDoc](https://godoc.org/github.com/go-acme/lego?status.svg)](https://pkg.go.dev/mod/github.com/go-acme/lego/v4) [![Build Status](https://github.com//go-acme/lego/workflows/Main/badge.svg?branch=master)](https://github.com//go-acme/lego/actions) [![Docker Pulls](https://img.shields.io/docker/pulls/goacme/lego.svg)](https://hub.docker.com/r/goacme/lego/) ## Features - ACME v2 [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html) - Register with CA - Obtain certificates, both from scratch or with an existing CSR - Renew certificates - Revoke certificates - Robust implementation of all ACME challenges - HTTP (http-01) - DNS (dns-01) - TLS (tls-alpn-01) - SAN certificate support - Comes with multiple optional [DNS providers](https://go-acme.github.io/lego/dns) - [Custom challenge solvers](https://go-acme.github.io/lego/usage/library/writing-a-challenge-solver/) - Certificate bundling - OCSP helper function ## Installation How to [install](https://go-acme.github.io/lego/installation/). ## Usage - as a [CLI](https://go-acme.github.io/lego/usage/cli) - as a [library](https://go-acme.github.io/lego/usage/library) ## Documentation Documentation is hosted live at https://go-acme.github.io/lego/. ## DNS providers Detailed documentation is available [here](https://go-acme.github.io/lego/dns). | | | | | |---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------| | [Akamai EdgeDNS](https://go-acme.github.io/lego/dns/edgedns/) | [Alibaba Cloud DNS](https://go-acme.github.io/lego/dns/alidns/) | [all-inkl](https://go-acme.github.io/lego/dns/allinkl/) | [Amazon Lightsail](https://go-acme.github.io/lego/dns/lightsail/) | | [Amazon Route 53](https://go-acme.github.io/lego/dns/route53/) | [ArvanCloud](https://go-acme.github.io/lego/dns/arvancloud/) | [Aurora DNS](https://go-acme.github.io/lego/dns/auroradns/) | [Autodns](https://go-acme.github.io/lego/dns/autodns/) | | [Azure](https://go-acme.github.io/lego/dns/azure/) | [Bindman](https://go-acme.github.io/lego/dns/bindman/) | [Bluecat](https://go-acme.github.io/lego/dns/bluecat/) | [Checkdomain](https://go-acme.github.io/lego/dns/checkdomain/) | | [Civo](https://go-acme.github.io/lego/dns/civo/) | [CloudDNS](https://go-acme.github.io/lego/dns/clouddns/) | [Cloudflare](https://go-acme.github.io/lego/dns/cloudflare/) | [ClouDNS](https://go-acme.github.io/lego/dns/cloudns/) | | [CloudXNS](https://go-acme.github.io/lego/dns/cloudxns/) | [ConoHa](https://go-acme.github.io/lego/dns/conoha/) | [Constellix](https://go-acme.github.io/lego/dns/constellix/) | [deSEC.io](https://go-acme.github.io/lego/dns/desec/) | | [Designate DNSaaS for Openstack](https://go-acme.github.io/lego/dns/designate/) | [Digital Ocean](https://go-acme.github.io/lego/dns/digitalocean/) | [DNS Made Easy](https://go-acme.github.io/lego/dns/dnsmadeeasy/) | [DNSimple](https://go-acme.github.io/lego/dns/dnsimple/) | | [DNSPod (deprecated)](https://go-acme.github.io/lego/dns/dnspod/) | [Domain Offensive (do.de)](https://go-acme.github.io/lego/dns/dode/) | [Domeneshop](https://go-acme.github.io/lego/dns/domeneshop/) | [DreamHost](https://go-acme.github.io/lego/dns/dreamhost/) | | [Duck DNS](https://go-acme.github.io/lego/dns/duckdns/) | [Dyn](https://go-acme.github.io/lego/dns/dyn/) | [Dynu](https://go-acme.github.io/lego/dns/dynu/) | [EasyDNS](https://go-acme.github.io/lego/dns/easydns/) | | [Epik](https://go-acme.github.io/lego/dns/epik/) | [Exoscale](https://go-acme.github.io/lego/dns/exoscale/) | [External program](https://go-acme.github.io/lego/dns/exec/) | [freemyip.com](https://go-acme.github.io/lego/dns/freemyip/) | | [G-Core Labs](https://go-acme.github.io/lego/dns/gcore/) | [Gandi Live DNS (v5)](https://go-acme.github.io/lego/dns/gandiv5/) | [Gandi](https://go-acme.github.io/lego/dns/gandi/) | [Glesys](https://go-acme.github.io/lego/dns/glesys/) | | [Go Daddy](https://go-acme.github.io/lego/dns/godaddy/) | [Google Cloud](https://go-acme.github.io/lego/dns/gcloud/) | [Hetzner](https://go-acme.github.io/lego/dns/hetzner/) | [Hosting.de](https://go-acme.github.io/lego/dns/hostingde/) | | [Hosttech](https://go-acme.github.io/lego/dns/hosttech/) | [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) | [Hurricane Electric DNS](https://go-acme.github.io/lego/dns/hurricane/) | [HyperOne](https://go-acme.github.io/lego/dns/hyperone/) | | [IBM Cloud (SoftLayer)](https://go-acme.github.io/lego/dns/ibmcloud/) | [IIJ DNS Platform Service](https://go-acme.github.io/lego/dns/iijdpf/) | [Infoblox](https://go-acme.github.io/lego/dns/infoblox/) | [Infomaniak](https://go-acme.github.io/lego/dns/infomaniak/) | | [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) | [Internet.bs](https://go-acme.github.io/lego/dns/internetbs/) | [INWX](https://go-acme.github.io/lego/dns/inwx/) | [Ionos](https://go-acme.github.io/lego/dns/ionos/) | | [iwantmyname](https://go-acme.github.io/lego/dns/iwantmyname/) | [Joker](https://go-acme.github.io/lego/dns/joker/) | [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linode/) | | [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) | [Loopia](https://go-acme.github.io/lego/dns/loopia/) | [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) | [Manual](https://go-acme.github.io/lego/dns/manual/) | | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | | [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [NearlyFreeSpeech.NET](https://go-acme.github.io/lego/dns/nearlyfreespeech/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) | [Netlify](https://go-acme.github.io/lego/dns/netlify/) | | [Nicmanager](https://go-acme.github.io/lego/dns/nicmanager/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | [Njalla](https://go-acme.github.io/lego/dns/njalla/) | [NS1](https://go-acme.github.io/lego/dns/ns1/) | | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | [OVH](https://go-acme.github.io/lego/dns/ovh/) | [Porkbun](https://go-acme.github.io/lego/dns/porkbun/) | | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | [reg.ru](https://go-acme.github.io/lego/dns/regru/) | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) | | [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [Simply.com](https://go-acme.github.io/lego/dns/simply/) | [Sonic](https://go-acme.github.io/lego/dns/sonic/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | | [Tencent Cloud DNS](https://go-acme.github.io/lego/dns/tencentcloud/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) | [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/) | [Variomedia](https://go-acme.github.io/lego/dns/variomedia/) | | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Vercel](https://go-acme.github.io/lego/dns/vercel/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/) | | [VK Cloud](https://go-acme.github.io/lego/dns/vkcloud/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/) | [Yandex PDD](https://go-acme.github.io/lego/dns/yandex/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | If your DNS provider is not supported, please open an [issue](https://github.com/go-acme/lego/issues/new?assignees=&labels=enhancement%2C+new-provider&template=new_dns_provider.md). lego-4.9.1/acme/000077500000000000000000000000001434020463500133615ustar00rootroot00000000000000lego-4.9.1/acme/api/000077500000000000000000000000001434020463500141325ustar00rootroot00000000000000lego-4.9.1/acme/api/account.go000066400000000000000000000042141434020463500161160ustar00rootroot00000000000000package api import ( "encoding/base64" "errors" "fmt" "github.com/go-acme/lego/v4/acme" ) type AccountService service // New Creates a new account. func (a *AccountService) New(req acme.Account) (acme.ExtendedAccount, error) { var account acme.Account resp, err := a.core.post(a.core.GetDirectory().NewAccountURL, req, &account) location := getLocation(resp) if len(location) > 0 { a.core.jws.SetKid(location) } if err != nil { return acme.ExtendedAccount{Location: location}, err } return acme.ExtendedAccount{Account: account, Location: location}, nil } // NewEAB Creates a new account with an External Account Binding. func (a *AccountService) NewEAB(accMsg acme.Account, kid, hmacEncoded string) (acme.ExtendedAccount, error) { hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded) if err != nil { return acme.ExtendedAccount{}, fmt.Errorf("acme: could not decode hmac key: %w", err) } eabJWS, err := a.core.signEABContent(a.core.GetDirectory().NewAccountURL, kid, hmac) if err != nil { return acme.ExtendedAccount{}, fmt.Errorf("acme: error signing eab content: %w", err) } accMsg.ExternalAccountBinding = eabJWS return a.New(accMsg) } // Get Retrieves an account. func (a *AccountService) Get(accountURL string) (acme.Account, error) { if accountURL == "" { return acme.Account{}, errors.New("account[get]: empty URL") } var account acme.Account _, err := a.core.postAsGet(accountURL, &account) if err != nil { return acme.Account{}, err } return account, nil } // Update Updates an account. func (a *AccountService) Update(accountURL string, req acme.Account) (acme.Account, error) { if accountURL == "" { return acme.Account{}, errors.New("account[update]: empty URL") } var account acme.Account _, err := a.core.post(accountURL, req, &account) if err != nil { return acme.Account{}, err } return account, nil } // Deactivate Deactivates an account. func (a *AccountService) Deactivate(accountURL string) error { if accountURL == "" { return errors.New("account[deactivate]: empty URL") } req := acme.Account{Status: acme.StatusDeactivated} _, err := a.core.post(accountURL, req, nil) return err } lego-4.9.1/acme/api/api.go000066400000000000000000000107341434020463500152370ustar00rootroot00000000000000package api import ( "bytes" "crypto" "encoding/json" "errors" "fmt" "net/http" "time" "github.com/cenkalti/backoff/v4" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api/internal/nonces" "github.com/go-acme/lego/v4/acme/api/internal/secure" "github.com/go-acme/lego/v4/acme/api/internal/sender" "github.com/go-acme/lego/v4/log" ) // Core ACME/LE core API. type Core struct { doer *sender.Doer nonceManager *nonces.Manager jws *secure.JWS directory acme.Directory HTTPClient *http.Client common service // Reuse a single struct instead of allocating one for each service on the heap. Accounts *AccountService Authorizations *AuthorizationService Certificates *CertificateService Challenges *ChallengeService Orders *OrderService } // New Creates a new Core. func New(httpClient *http.Client, userAgent, caDirURL, kid string, privateKey crypto.PrivateKey) (*Core, error) { doer := sender.NewDoer(httpClient, userAgent) dir, err := getDirectory(doer, caDirURL) if err != nil { return nil, err } nonceManager := nonces.NewManager(doer, dir.NewNonceURL) jws := secure.NewJWS(privateKey, kid, nonceManager) c := &Core{doer: doer, nonceManager: nonceManager, jws: jws, directory: dir, HTTPClient: httpClient} c.common.core = c c.Accounts = (*AccountService)(&c.common) c.Authorizations = (*AuthorizationService)(&c.common) c.Certificates = (*CertificateService)(&c.common) c.Challenges = (*ChallengeService)(&c.common) c.Orders = (*OrderService)(&c.common) return c, nil } // post performs an HTTP POST request and parses the response body as JSON, // into the provided respBody object. func (a *Core) post(uri string, reqBody, response interface{}) (*http.Response, error) { content, err := json.Marshal(reqBody) if err != nil { return nil, errors.New("failed to marshal message") } return a.retrievablePost(uri, content, response) } // postAsGet performs an HTTP POST ("POST-as-GET") request. // https://www.rfc-editor.org/rfc/rfc8555.html#section-6.3 func (a *Core) postAsGet(uri string, response interface{}) (*http.Response, error) { return a.retrievablePost(uri, []byte{}, response) } func (a *Core) retrievablePost(uri string, content []byte, response interface{}) (*http.Response, error) { // during tests, allow to support ~90% of bad nonce with a minimum of attempts. bo := backoff.NewExponentialBackOff() bo.InitialInterval = 200 * time.Millisecond bo.MaxInterval = 5 * time.Second bo.MaxElapsedTime = 20 * time.Second var resp *http.Response operation := func() error { var err error resp, err = a.signedPost(uri, content, response) if err != nil { // Retry if the nonce was invalidated var e *acme.NonceError if errors.As(err, &e) { return err } return backoff.Permanent(err) } return nil } notify := func(err error, duration time.Duration) { log.Infof("retry due to: %v", err) } err := backoff.RetryNotify(operation, bo, notify) if err != nil { return resp, err } return resp, nil } func (a *Core) signedPost(uri string, content []byte, response interface{}) (*http.Response, error) { signedContent, err := a.jws.SignContent(uri, content) if err != nil { return nil, fmt.Errorf("failed to post JWS message: failed to sign content: %w", err) } signedBody := bytes.NewBuffer([]byte(signedContent.FullSerialize())) resp, err := a.doer.Post(uri, signedBody, "application/jose+json", response) // nonceErr is ignored to keep the root error. nonce, nonceErr := nonces.GetFromResponse(resp) if nonceErr == nil { a.nonceManager.Push(nonce) } return resp, err } func (a *Core) signEABContent(newAccountURL, kid string, hmac []byte) ([]byte, error) { eabJWS, err := a.jws.SignEABContent(newAccountURL, kid, hmac) if err != nil { return nil, err } return []byte(eabJWS.FullSerialize()), nil } // GetKeyAuthorization Gets the key authorization. func (a *Core) GetKeyAuthorization(token string) (string, error) { return a.jws.GetKeyAuthorization(token) } func (a *Core) GetDirectory() acme.Directory { return a.directory } func getDirectory(do *sender.Doer, caDirURL string) (acme.Directory, error) { var dir acme.Directory if _, err := do.Get(caDirURL, &dir); err != nil { return dir, fmt.Errorf("get directory at '%s': %w", caDirURL, err) } if dir.NewAccountURL == "" { return dir, errors.New("directory missing new registration URL") } if dir.NewOrderURL == "" { return dir, errors.New("directory missing new order URL") } return dir, nil } lego-4.9.1/acme/api/authorization.go000066400000000000000000000014621434020463500173640ustar00rootroot00000000000000package api import ( "errors" "github.com/go-acme/lego/v4/acme" ) type AuthorizationService service // Get Gets an authorization. func (c *AuthorizationService) Get(authzURL string) (acme.Authorization, error) { if authzURL == "" { return acme.Authorization{}, errors.New("authorization[get]: empty URL") } var authz acme.Authorization _, err := c.core.postAsGet(authzURL, &authz) if err != nil { return acme.Authorization{}, err } return authz, nil } // Deactivate Deactivates an authorization. func (c *AuthorizationService) Deactivate(authzURL string) error { if authzURL == "" { return errors.New("authorization[deactivate]: empty URL") } var disabledAuth acme.Authorization _, err := c.core.post(authzURL, acme.Authorization{Status: acme.StatusDeactivated}, &disabledAuth) return err } lego-4.9.1/acme/api/certificate.go000066400000000000000000000075331434020463500167530ustar00rootroot00000000000000package api import ( "bytes" "crypto/x509" "encoding/pem" "errors" "io" "net/http" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/log" ) // maxBodySize is the maximum size of body that we will read. const maxBodySize = 1024 * 1024 type CertificateService service // Get Returns the certificate and the issuer certificate. // 'bundle' is only applied if the issuer is provided by the 'up' link. func (c *CertificateService) Get(certURL string, bundle bool) ([]byte, []byte, error) { cert, _, err := c.get(certURL, bundle) if err != nil { return nil, nil, err } return cert.Cert, cert.Issuer, nil } // GetAll the certificates and the alternate certificates. // bundle' is only applied if the issuer is provided by the 'up' link. func (c *CertificateService) GetAll(certURL string, bundle bool) (map[string]*acme.RawCertificate, error) { cert, headers, err := c.get(certURL, bundle) if err != nil { return nil, err } certs := map[string]*acme.RawCertificate{certURL: cert} // URLs of "alternate" link relation // - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2 alts := getLinks(headers, "alternate") for _, alt := range alts { altCert, _, err := c.get(alt, bundle) if err != nil { return nil, err } certs[alt] = altCert } return certs, nil } // Revoke Revokes a certificate. func (c *CertificateService) Revoke(req acme.RevokeCertMessage) error { _, err := c.core.post(c.core.GetDirectory().RevokeCertURL, req, nil) return err } // get Returns the certificate and the "up" link. func (c *CertificateService) get(certURL string, bundle bool) (*acme.RawCertificate, http.Header, error) { if certURL == "" { return nil, nil, errors.New("certificate[get]: empty URL") } resp, err := c.core.postAsGet(certURL, nil) if err != nil { return nil, nil, err } data, err := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize)) if err != nil { return nil, resp.Header, err } cert := c.getCertificateChain(data, resp.Header, bundle, certURL) return cert, resp.Header, err } // getCertificateChain Returns the certificate and the issuer certificate. func (c *CertificateService) getCertificateChain(cert []byte, headers http.Header, bundle bool, certURL string) *acme.RawCertificate { // Get issuerCert from bundled response from Let's Encrypt // See https://community.letsencrypt.org/t/acme-v2-no-up-link-in-response/64962 _, issuer := pem.Decode(cert) if issuer != nil { // If bundle is false, we want to return a single certificate. // To do this, we remove the issuer cert(s) from the issued cert. if !bundle { cert = bytes.TrimSuffix(cert, issuer) } return &acme.RawCertificate{Cert: cert, Issuer: issuer} } // The issuer certificate link may be supplied via an "up" link // in the response headers of a new certificate. // See https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4.2 up := getLink(headers, "up") issuer, err := c.getIssuerFromLink(up) if err != nil { // If we fail to acquire the issuer cert, return the issued certificate - do not fail. log.Warnf("acme: Could not bundle issuer certificate [%s]: %v", certURL, err) } else if len(issuer) > 0 { // If bundle is true, we want to return a certificate bundle. // To do this, we append the issuer cert to the issued cert. if bundle { cert = append(cert, issuer...) } } return &acme.RawCertificate{Cert: cert, Issuer: issuer} } // getIssuerFromLink requests the issuer certificate. func (c *CertificateService) getIssuerFromLink(up string) ([]byte, error) { if up == "" { return nil, nil } log.Infof("acme: Requesting issuer cert from %s", up) cert, _, err := c.get(up, false) if err != nil { return nil, err } _, err = x509.ParseCertificate(cert.Cert) if err != nil { return nil, err } return certcrypto.PEMEncode(certcrypto.DERCertificateBytes(cert.Cert)), nil } lego-4.9.1/acme/api/certificate_test.go000066400000000000000000000125171434020463500200100ustar00rootroot00000000000000package api import ( "crypto/rand" "crypto/rsa" "encoding/pem" "net/http" "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const certResponseMock = `-----BEGIN CERTIFICATE----- MIIDEDCCAfigAwIBAgIHPhckqW5fPDANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQD Ex1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDM5NWU2MTAeFw0xODExMDcxNzQ2NTZa Fw0yMzExMDcxNzQ2NTZaMBMxETAPBgNVBAMTCGFjbWUud3RmMIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtLNKvZXD20XPUQCWYSK9rUSKxD9Eb0c9fag bxOxOkLRTgL8LH6yln+bxc3MrHDou4PpDUdeo2CyOQu3CKsTS5mrH3NXYHu0H7p5 y3riOJTHnfkGKLT9LciGz7GkXd62nvNP57bOf5Sk4P2M+Qbxd0hPTSfu52740LSy 144cnxe2P1aDYehrEp6nYCESuyD/CtUHTo0qwJmzIy163Sp3rSs15BuCPyhySnE3 BJ8Ggv+qC6D5I1932DfSqyQJ79iq/HRm0Fn84am3KwvRlUfWxabmsUGARXoqCgnE zcbJVOZKewv0zlQJpfac+b+Imj6Lvt1TGjIz2mVyefYgLx8gwwIDAQABo1QwUjAO BgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwG A1UdEwEB/wQCMAAwEwYDVR0RBAwwCoIIYWNtZS53dGYwDQYJKoZIhvcNAQELBQAD ggEBABB/0iYhmfPSQot5RaeeovQnsqYjI5ryQK2cwzW6qcTJfv8N6+p6XkqF1+W4 jXZjrQP8MvgO9KNWlvx12vhINE6wubk88L+2piAi5uS2QejmZbXpyYB9s+oPqlk9 IDvfdlVYOqvYAhSx7ggGi+j73mjZVtjAavP6dKuu475ZCeq+NIC15RpbbikWKtYE HBJ7BW8XQKx67iHGx8ygHTDLbREL80Bck3oUm7wIYGMoNijD6RBl25p4gYl9dzOd TqGl5hW/1P5hMbgEzHbr4O3BfWqU2g7tV36TASy3jbC3ONFRNNYrpEZ1AL3+cUri OPPkKtAKAbQkKbUIfsHpBZjKZMU= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE AxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw NzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl NjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT SZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh 0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen SRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx HAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt D1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu mB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD AQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA upU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm iUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd QqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ wlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2 7R4IbHGnj0BJA2vMYC4hSw== -----END CERTIFICATE----- ` const issuerMock = `-----BEGIN CERTIFICATE----- MIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE AxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw NzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl NjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT SZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh 0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen SRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx HAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt D1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu mB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD AQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA upU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm iUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd QqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ wlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2 7R4IbHGnj0BJA2vMYC4hSw== -----END CERTIFICATE----- ` func TestCertificateService_Get_issuerRelUp(t *testing.T) { mux, apiURL := tester.SetupFakeAPI(t) mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Link", "<"+apiURL+`/issuer>; rel="up"`) _, err := w.Write([]byte(certResponseMock)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) mux.HandleFunc("/issuer", func(w http.ResponseWriter, _ *http.Request) { p, _ := pem.Decode([]byte(issuerMock)) _, err := w.Write(p.Bytes) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) cert, issuer, err := core.Certificates.Get(apiURL+"/certificate", true) require.NoError(t, err) assert.Equal(t, certResponseMock, string(cert), "Certificate") assert.Equal(t, issuerMock, string(issuer), "IssuerCertificate") } func TestCertificateService_Get_embeddedIssuer(t *testing.T) { mux, apiURL := tester.SetupFakeAPI(t) mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) { _, err := w.Write([]byte(certResponseMock)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) cert, issuer, err := core.Certificates.Get(apiURL+"/certificate", true) require.NoError(t, err) assert.Equal(t, certResponseMock, string(cert), "Certificate") assert.Equal(t, issuerMock, string(issuer), "IssuerCertificate") } lego-4.9.1/acme/api/challenge.go000066400000000000000000000023071434020463500164050ustar00rootroot00000000000000package api import ( "errors" "github.com/go-acme/lego/v4/acme" ) type ChallengeService service // New Creates a challenge. func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) { if chlgURL == "" { return acme.ExtendedChallenge{}, errors.New("challenge[new]: empty URL") } // Challenge initiation is done by sending a JWS payload containing the trivial JSON object `{}`. // We use an empty struct instance as the postJSON payload here to achieve this result. var chlng acme.ExtendedChallenge resp, err := c.core.post(chlgURL, struct{}{}, &chlng) if err != nil { return acme.ExtendedChallenge{}, err } chlng.AuthorizationURL = getLink(resp.Header, "up") chlng.RetryAfter = getRetryAfter(resp) return chlng, nil } // Get Gets a challenge. func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) { if chlgURL == "" { return acme.ExtendedChallenge{}, errors.New("challenge[get]: empty URL") } var chlng acme.ExtendedChallenge resp, err := c.core.postAsGet(chlgURL, &chlng) if err != nil { return acme.ExtendedChallenge{}, err } chlng.AuthorizationURL = getLink(resp.Header, "up") chlng.RetryAfter = getRetryAfter(resp) return chlng, nil } lego-4.9.1/acme/api/internal/000077500000000000000000000000001434020463500157465ustar00rootroot00000000000000lego-4.9.1/acme/api/internal/nonces/000077500000000000000000000000001434020463500172335ustar00rootroot00000000000000lego-4.9.1/acme/api/internal/nonces/nonce_manager.go000066400000000000000000000027101434020463500223560ustar00rootroot00000000000000package nonces import ( "errors" "fmt" "net/http" "sync" "github.com/go-acme/lego/v4/acme/api/internal/sender" ) // Manager Manages nonces. type Manager struct { do *sender.Doer nonceURL string nonces []string sync.Mutex } // NewManager Creates a new Manager. func NewManager(do *sender.Doer, nonceURL string) *Manager { return &Manager{ do: do, nonceURL: nonceURL, } } // Pop Pops a nonce. func (n *Manager) Pop() (string, bool) { n.Lock() defer n.Unlock() if len(n.nonces) == 0 { return "", false } nonce := n.nonces[len(n.nonces)-1] n.nonces = n.nonces[:len(n.nonces)-1] return nonce, true } // Push Pushes a nonce. func (n *Manager) Push(nonce string) { n.Lock() defer n.Unlock() n.nonces = append(n.nonces, nonce) } // Nonce implement jose.NonceSource. func (n *Manager) Nonce() (string, error) { if nonce, ok := n.Pop(); ok { return nonce, nil } return n.getNonce() } func (n *Manager) getNonce() (string, error) { resp, err := n.do.Head(n.nonceURL) if err != nil { return "", fmt.Errorf("failed to get nonce from HTTP HEAD: %w", err) } return GetFromResponse(resp) } // GetFromResponse Extracts a nonce from a HTTP response. func GetFromResponse(resp *http.Response) (string, error) { if resp == nil { return "", errors.New("nil response") } nonce := resp.Header.Get("Replay-Nonce") if nonce == "" { return "", errors.New("server did not respond with a proper nonce header") } return nonce, nil } lego-4.9.1/acme/api/internal/nonces/nonce_manager_test.go000066400000000000000000000023711434020463500234200ustar00rootroot00000000000000package nonces import ( "net/http" "net/http/httptest" "testing" "time" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api/internal/sender" "github.com/go-acme/lego/v4/platform/tester" ) func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { time.Sleep(250 * time.Millisecond) w.Header().Set("Replay-Nonce", "12345") w.Header().Set("Retry-After", "0") err := tester.WriteJSONResponse(w, &acme.Challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"}) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } })) t.Cleanup(server.Close) doer := sender.NewDoer(http.DefaultClient, "lego-test") j := NewManager(doer, server.URL) ch := make(chan bool) resultCh := make(chan bool) go func() { _, errN := j.Nonce() if errN != nil { t.Log(errN) } ch <- true }() go func() { _, errN := j.Nonce() if errN != nil { t.Log(errN) } ch <- true }() go func() { <-ch <-ch resultCh <- true }() select { case <-resultCh: case <-time.After(500 * time.Millisecond): t.Fatal("JWS is probably holding a lock while making HTTP request") } } lego-4.9.1/acme/api/internal/secure/000077500000000000000000000000001434020463500172345ustar00rootroot00000000000000lego-4.9.1/acme/api/internal/secure/jws.go000066400000000000000000000057161434020463500203770ustar00rootroot00000000000000package secure import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rsa" "encoding/base64" "fmt" "github.com/go-acme/lego/v4/acme/api/internal/nonces" jose "gopkg.in/square/go-jose.v2" ) // JWS Represents a JWS. type JWS struct { privKey crypto.PrivateKey kid string // Key identifier nonces *nonces.Manager } // NewJWS Create a new JWS. func NewJWS(privateKey crypto.PrivateKey, kid string, nonceManager *nonces.Manager) *JWS { return &JWS{ privKey: privateKey, nonces: nonceManager, kid: kid, } } // SetKid Sets a key identifier. func (j *JWS) SetKid(kid string) { j.kid = kid } // SignContent Signs a content with the JWS. func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, error) { var alg jose.SignatureAlgorithm switch k := j.privKey.(type) { case *rsa.PrivateKey: alg = jose.RS256 case *ecdsa.PrivateKey: if k.Curve == elliptic.P256() { alg = jose.ES256 } else if k.Curve == elliptic.P384() { alg = jose.ES384 } } signKey := jose.SigningKey{ Algorithm: alg, Key: jose.JSONWebKey{Key: j.privKey, KeyID: j.kid}, } options := jose.SignerOptions{ NonceSource: j.nonces, ExtraHeaders: map[jose.HeaderKey]interface{}{ "url": url, }, } if j.kid == "" { options.EmbedJWK = true } signer, err := jose.NewSigner(signKey, &options) if err != nil { return nil, fmt.Errorf("failed to create jose signer: %w", err) } signed, err := signer.Sign(content) if err != nil { return nil, fmt.Errorf("failed to sign content: %w", err) } return signed, nil } // SignEABContent Signs an external account binding content with the JWS. func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) { jwk := jose.JSONWebKey{Key: j.privKey} jwkJSON, err := jwk.Public().MarshalJSON() if err != nil { return nil, fmt.Errorf("acme: error encoding eab jwk key: %w", err) } signer, err := jose.NewSigner( jose.SigningKey{Algorithm: jose.HS256, Key: hmac}, &jose.SignerOptions{ EmbedJWK: false, ExtraHeaders: map[jose.HeaderKey]interface{}{ "kid": kid, "url": url, }, }, ) if err != nil { return nil, fmt.Errorf("failed to create External Account Binding jose signer: %w", err) } signed, err := signer.Sign(jwkJSON) if err != nil { return nil, fmt.Errorf("failed to External Account Binding sign content: %w", err) } return signed, nil } // GetKeyAuthorization Gets the key authorization for a token. func (j *JWS) GetKeyAuthorization(token string) (string, error) { var publicKey crypto.PublicKey switch k := j.privKey.(type) { case *ecdsa.PrivateKey: publicKey = k.Public() case *rsa.PrivateKey: publicKey = k.Public() } // Generate the Key Authorization for the challenge jwk := &jose.JSONWebKey{Key: publicKey} thumbBytes, err := jwk.Thumbprint(crypto.SHA256) if err != nil { return "", err } // unpad the base64URL keyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes) return token + "." + keyThumb, nil } lego-4.9.1/acme/api/internal/secure/jws_test.go000066400000000000000000000024671434020463500214360ustar00rootroot00000000000000package secure import ( "net/http" "net/http/httptest" "testing" "time" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api/internal/nonces" "github.com/go-acme/lego/v4/acme/api/internal/sender" "github.com/go-acme/lego/v4/platform/tester" ) func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { time.Sleep(250 * time.Millisecond) w.Header().Set("Replay-Nonce", "12345") w.Header().Set("Retry-After", "0") err := tester.WriteJSONResponse(w, &acme.Challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"}) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } })) t.Cleanup(server.Close) doer := sender.NewDoer(http.DefaultClient, "lego-test") j := nonces.NewManager(doer, server.URL) ch := make(chan bool) resultCh := make(chan bool) go func() { _, errN := j.Nonce() if errN != nil { t.Log(errN) } ch <- true }() go func() { _, errN := j.Nonce() if errN != nil { t.Log(errN) } ch <- true }() go func() { <-ch <-ch resultCh <- true }() select { case <-resultCh: case <-time.After(500 * time.Millisecond): t.Fatal("JWS is probably holding a lock while making HTTP request") } } lego-4.9.1/acme/api/internal/sender/000077500000000000000000000000001434020463500172265ustar00rootroot00000000000000lego-4.9.1/acme/api/internal/sender/sender.go000066400000000000000000000071301434020463500210360ustar00rootroot00000000000000package sender import ( "encoding/json" "fmt" "io" "net/http" "runtime" "strings" "github.com/go-acme/lego/v4/acme" ) type RequestOption func(*http.Request) error func contentType(ct string) RequestOption { return func(req *http.Request) error { req.Header.Set("Content-Type", ct) return nil } } type Doer struct { httpClient *http.Client userAgent string } // NewDoer Creates a new Doer. func NewDoer(client *http.Client, userAgent string) *Doer { return &Doer{ httpClient: client, userAgent: userAgent, } } // Get performs a GET request with a proper User-Agent string. // If "response" is not provided, callers should close resp.Body when done reading from it. func (d *Doer) Get(url string, response interface{}) (*http.Response, error) { req, err := d.newRequest(http.MethodGet, url, nil) if err != nil { return nil, err } return d.do(req, response) } // Head performs a HEAD request with a proper User-Agent string. // The response body (resp.Body) is already closed when this function returns. func (d *Doer) Head(url string) (*http.Response, error) { req, err := d.newRequest(http.MethodHead, url, nil) if err != nil { return nil, err } return d.do(req, nil) } // Post performs a POST request with a proper User-Agent string. // If "response" is not provided, callers should close resp.Body when done reading from it. func (d *Doer) Post(url string, body io.Reader, bodyType string, response interface{}) (*http.Response, error) { req, err := d.newRequest(http.MethodPost, url, body, contentType(bodyType)) if err != nil { return nil, err } return d.do(req, response) } func (d *Doer) newRequest(method, uri string, body io.Reader, opts ...RequestOption) (*http.Request, error) { req, err := http.NewRequest(method, uri, body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("User-Agent", d.formatUserAgent()) for _, opt := range opts { err = opt(req) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } } return req, nil } func (d *Doer) do(req *http.Request, response interface{}) (*http.Response, error) { resp, err := d.httpClient.Do(req) if err != nil { return nil, err } if err = checkError(req, resp); err != nil { return resp, err } if response != nil { raw, err := io.ReadAll(resp.Body) if err != nil { return resp, err } defer resp.Body.Close() err = json.Unmarshal(raw, response) if err != nil { return resp, fmt.Errorf("failed to unmarshal %q to type %T: %w", raw, response, err) } } return resp, nil } // formatUserAgent builds and returns the User-Agent string to use in requests. func (d *Doer) formatUserAgent() string { ua := fmt.Sprintf("%s %s (%s; %s; %s)", d.userAgent, ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH) return strings.TrimSpace(ua) } func checkError(req *http.Request, resp *http.Response) error { if resp.StatusCode >= http.StatusBadRequest { body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("%d :: %s :: %s :: %w", resp.StatusCode, req.Method, req.URL, err) } var errorDetails *acme.ProblemDetails err = json.Unmarshal(body, &errorDetails) if err != nil { return fmt.Errorf("%d ::%s :: %s :: %w :: %s", resp.StatusCode, req.Method, req.URL, err, string(body)) } errorDetails.Method = req.Method errorDetails.URL = req.URL.String() // Check for errors we handle specifically if errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr { return &acme.NonceError{ProblemDetails: errorDetails} } return errorDetails } return nil } lego-4.9.1/acme/api/internal/sender/sender_test.go000066400000000000000000000027471434020463500221060ustar00rootroot00000000000000package sender import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDo_UserAgentOnAllHTTPMethod(t *testing.T) { var ua, method string server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { ua = r.Header.Get("User-Agent") method = r.Method })) t.Cleanup(server.Close) doer := NewDoer(http.DefaultClient, "") testCases := []struct { method string call func(u string) (*http.Response, error) }{ { method: http.MethodGet, call: func(u string) (*http.Response, error) { return doer.Get(u, nil) }, }, { method: http.MethodHead, call: doer.Head, }, { method: http.MethodPost, call: func(u string) (*http.Response, error) { return doer.Post(u, strings.NewReader("falalalala"), "text/plain", nil) }, }, } for _, test := range testCases { t.Run(test.method, func(t *testing.T) { _, err := test.call(server.URL) require.NoError(t, err) assert.Equal(t, test.method, method) assert.Contains(t, ua, ourUserAgent, "User-Agent") }) } } func TestDo_CustomUserAgent(t *testing.T) { customUA := "MyApp/1.2.3" doer := NewDoer(http.DefaultClient, customUA) ua := doer.formatUserAgent() assert.Contains(t, ua, ourUserAgent) assert.Contains(t, ua, customUA) if strings.HasSuffix(ua, " ") { t.Errorf("UA should not have trailing spaces; got '%s'", ua) } assert.Len(t, strings.Split(ua, " "), 5) } lego-4.9.1/acme/api/internal/sender/useragent.go000066400000000000000000000006561434020463500215610ustar00rootroot00000000000000package sender // CODE GENERATED AUTOMATICALLY // THIS FILE MUST NOT BE EDITED BY HAND const ( // ourUserAgent is the User-Agent of this underlying library package. ourUserAgent = "xenolf-acme/4.9.1" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. ourUserAgentComment = "release" ) lego-4.9.1/acme/api/order.go000066400000000000000000000027661434020463500156070ustar00rootroot00000000000000package api import ( "encoding/base64" "errors" "github.com/go-acme/lego/v4/acme" ) type OrderService service // New Creates a new order. func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) { var identifiers []acme.Identifier for _, domain := range domains { identifiers = append(identifiers, acme.Identifier{Type: "dns", Value: domain}) } orderReq := acme.Order{Identifiers: identifiers} var order acme.Order resp, err := o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order) if err != nil { return acme.ExtendedOrder{}, err } return acme.ExtendedOrder{ Order: order, Location: resp.Header.Get("Location"), }, nil } // Get Gets an order. func (o *OrderService) Get(orderURL string) (acme.ExtendedOrder, error) { if orderURL == "" { return acme.ExtendedOrder{}, errors.New("order[get]: empty URL") } var order acme.Order _, err := o.core.postAsGet(orderURL, &order) if err != nil { return acme.ExtendedOrder{}, err } return acme.ExtendedOrder{Order: order}, nil } // UpdateForCSR Updates an order for a CSR. func (o *OrderService) UpdateForCSR(orderURL string, csr []byte) (acme.ExtendedOrder, error) { csrMsg := acme.CSRMessage{ Csr: base64.RawURLEncoding.EncodeToString(csr), } var order acme.Order _, err := o.core.post(orderURL, csrMsg, &order) if err != nil { return acme.ExtendedOrder{}, err } if order.Status == acme.StatusInvalid { return acme.ExtendedOrder{}, order.Error } return acme.ExtendedOrder{Order: order}, nil } lego-4.9.1/acme/api/order_test.go000066400000000000000000000037471434020463500166460ustar00rootroot00000000000000package api import ( "crypto/rand" "crypto/rsa" "encoding/json" "io" "net/http" "testing" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/square/go-jose.v2" ) func TestOrderService_New(t *testing.T) { mux, apiURL := tester.SetupFakeAPI(t) // small value keeps test fast privateKey, errK := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, errK, "Could not generate test key") mux.HandleFunc("/newOrder", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } body, err := readSignedBody(r, privateKey) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } order := acme.Order{} err = json.Unmarshal(body, &order) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } err = tester.WriteJSONResponse(w, acme.Order{ Status: acme.StatusValid, Identifiers: order.Identifiers, }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) order, err := core.Orders.New([]string{"example.com"}) require.NoError(t, err) expected := acme.ExtendedOrder{ Order: acme.Order{ Status: "valid", Identifiers: []acme.Identifier{{Type: "dns", Value: "example.com"}}, }, } assert.Equal(t, expected, order) } func readSignedBody(r *http.Request, privateKey *rsa.PrivateKey) ([]byte, error) { reqBody, err := io.ReadAll(r.Body) if err != nil { return nil, err } jws, err := jose.ParseSigned(string(reqBody)) if err != nil { return nil, err } body, err := jws.Verify(&jose.JSONWebKey{ Key: privateKey.Public(), Algorithm: "RSA", }) if err != nil { return nil, err } return body, nil } lego-4.9.1/acme/api/service.go000066400000000000000000000017431434020463500161260ustar00rootroot00000000000000package api import ( "net/http" "regexp" ) type service struct { core *Core } // getLink get a rel into the Link header. func getLink(header http.Header, rel string) string { links := getLinks(header, rel) if len(links) < 1 { return "" } return links[0] } func getLinks(header http.Header, rel string) []string { linkExpr := regexp.MustCompile(`<(.+?)>(?:;[^;]+)*?;\s*rel="(.+?)"`) var links []string for _, link := range header["Link"] { for _, m := range linkExpr.FindAllStringSubmatch(link, -1) { if len(m) != 3 { continue } if m[2] == rel { links = append(links, m[1]) } } } return links } // getLocation get the value of the header Location. func getLocation(resp *http.Response) string { if resp == nil { return "" } return resp.Header.Get("Location") } // getRetryAfter get the value of the header Retry-After. func getRetryAfter(resp *http.Response) string { if resp == nil { return "" } return resp.Header.Get("Retry-After") } lego-4.9.1/acme/api/service_test.go000066400000000000000000000023121434020463500171560ustar00rootroot00000000000000package api import ( "net/http" "testing" "github.com/stretchr/testify/assert" ) func Test_getLink(t *testing.T) { testCases := []struct { desc string header http.Header relName string expected string }{ { desc: "success", header: http.Header{ "Link": []string{`; rel="next", ; rel="up"`}, }, relName: "up", expected: "https://acme-staging-v02.api.letsencrypt.org/up?query", }, { desc: "success several lines", header: http.Header{ "Link": []string{`; rel="next"`, `; rel="up"`}, }, relName: "up", expected: "https://acme-staging-v02.api.letsencrypt.org/up?query", }, { desc: "no link", header: http.Header{}, relName: "up", expected: "", }, { desc: "no header", relName: "up", expected: "", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() link := getLink(test.header, test.relName) assert.Equal(t, test.expected, link) }) } } lego-4.9.1/acme/commons.go000066400000000000000000000317361434020463500153750ustar00rootroot00000000000000// Package acme contains all objects related the ACME endpoints. // https://www.rfc-editor.org/rfc/rfc8555.html package acme import ( "encoding/json" "time" ) // ACME status values of Account, Order, Authorization and Challenge objects. // See https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.6 for details. const ( StatusDeactivated = "deactivated" StatusExpired = "expired" StatusInvalid = "invalid" StatusPending = "pending" StatusProcessing = "processing" StatusReady = "ready" StatusRevoked = "revoked" StatusUnknown = "unknown" StatusValid = "valid" ) // CRL reason codes as defined in RFC 5280. // https://datatracker.ietf.org/doc/html/rfc5280#section-5.3.1 const ( CRLReasonUnspecified uint = 0 CRLReasonKeyCompromise uint = 1 CRLReasonCACompromise uint = 2 CRLReasonAffiliationChanged uint = 3 CRLReasonSuperseded uint = 4 CRLReasonCessationOfOperation uint = 5 CRLReasonCertificateHold uint = 6 CRLReasonRemoveFromCRL uint = 8 CRLReasonPrivilegeWithdrawn uint = 9 CRLReasonAACompromise uint = 10 ) // Directory the ACME directory object. // - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1 type Directory struct { NewNonceURL string `json:"newNonce"` NewAccountURL string `json:"newAccount"` NewOrderURL string `json:"newOrder"` NewAuthzURL string `json:"newAuthz"` RevokeCertURL string `json:"revokeCert"` KeyChangeURL string `json:"keyChange"` Meta Meta `json:"meta"` } // Meta the ACME meta object (related to Directory). // - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.1 type Meta struct { // termsOfService (optional, string): // A URL identifying the current terms of service. TermsOfService string `json:"termsOfService"` // website (optional, string): // An HTTP or HTTPS URL locating a website providing more information about the ACME server. Website string `json:"website"` // caaIdentities (optional, array of string): // The hostnames that the ACME server recognizes as referring to itself // for the purposes of CAA record validation as defined in [RFC6844]. // Each string MUST represent the same sequence of ASCII code points // that the server will expect to see as the "Issuer Domain Name" in a CAA issue or issuewild property tag. // This allows clients to determine the correct issuer domain name to use when configuring CAA records. CaaIdentities []string `json:"caaIdentities"` // externalAccountRequired (optional, boolean): // If this field is present and set to "true", // then the CA requires that all new- account requests include an "externalAccountBinding" field // associating the new account with an external account. ExternalAccountRequired bool `json:"externalAccountRequired"` } // ExtendedAccount a extended Account. type ExtendedAccount struct { Account // Contains the value of the response header `Location` Location string `json:"-"` } // Account the ACME account Object. // - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.2 // - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3 type Account struct { // status (required, string): // The status of this account. // Possible values are: "valid", "deactivated", and "revoked". // The value "deactivated" should be used to indicate client-initiated deactivation // whereas "revoked" should be used to indicate server- initiated deactivation. (See Section 7.1.6) Status string `json:"status,omitempty"` // contact (optional, array of string): // An array of URLs that the server can use to contact the client for issues related to this account. // For example, the server may wish to notify the client about server-initiated revocation or certificate expiration. // For information on supported URL schemes, see Section 7.3 Contact []string `json:"contact,omitempty"` // termsOfServiceAgreed (optional, boolean): // Including this field in a new-account request, // with a value of true, indicates the client's agreement with the terms of service. // This field is not updateable by the client. TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` // orders (required, string): // A URL from which a list of orders submitted by this account can be fetched via a POST-as-GET request, // as described in Section 7.1.2.1. Orders string `json:"orders,omitempty"` // onlyReturnExisting (optional, boolean): // If this field is present with the value "true", // then the server MUST NOT create a new account if one does not already exist. // This allows a client to look up an account URL based on an account key (see Section 7.3.1). OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` // externalAccountBinding (optional, object): // An optional field for binding the new account with an existing non-ACME account (see Section 7.3.4). ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"` } // ExtendedOrder a extended Order. type ExtendedOrder struct { Order // The order URL, contains the value of the response header `Location` Location string `json:"-"` } // Order the ACME order Object. // - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.3 type Order struct { // status (required, string): // The status of this order. // Possible values are: "pending", "ready", "processing", "valid", and "invalid". Status string `json:"status,omitempty"` // expires (optional, string): // The timestamp after which the server will consider this order invalid, // encoded in the format specified in RFC 3339 [RFC3339]. // This field is REQUIRED for objects with "pending" or "valid" in the status field. Expires string `json:"expires,omitempty"` // identifiers (required, array of object): // An array of identifier objects that the order pertains to. Identifiers []Identifier `json:"identifiers"` // notBefore (optional, string): // The requested value of the notBefore field in the certificate, // in the date format defined in [RFC3339]. NotBefore string `json:"notBefore,omitempty"` // notAfter (optional, string): // The requested value of the notAfter field in the certificate, // in the date format defined in [RFC3339]. NotAfter string `json:"notAfter,omitempty"` // error (optional, object): // The error that occurred while processing the order, if any. // This field is structured as a problem document [RFC7807]. Error *ProblemDetails `json:"error,omitempty"` // authorizations (required, array of string): // For pending orders, // the authorizations that the client needs to complete before the requested certificate can be issued (see Section 7.5), // including unexpired authorizations that the client has completed in the past for identifiers specified in the order. // The authorizations required are dictated by server policy // and there may not be a 1:1 relationship between the order identifiers and the authorizations required. // For final orders (in the "valid" or "invalid" state), the authorizations that were completed. // Each entry is a URL from which an authorization can be fetched with a POST-as-GET request. Authorizations []string `json:"authorizations,omitempty"` // finalize (required, string): // A URL that a CSR must be POSTed to once all of the order's authorizations are satisfied to finalize the order. // The result of a successful finalization will be the population of the certificate URL for the order. Finalize string `json:"finalize,omitempty"` // certificate (optional, string): // A URL for the certificate that has been issued in response to this order Certificate string `json:"certificate,omitempty"` } // Authorization the ACME authorization object. // - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.4 type Authorization struct { // status (required, string): // The status of this authorization. // Possible values are: "pending", "valid", "invalid", "deactivated", "expired", and "revoked". Status string `json:"status"` // expires (optional, string): // The timestamp after which the server will consider this authorization invalid, // encoded in the format specified in RFC 3339 [RFC3339]. // This field is REQUIRED for objects with "valid" in the "status" field. Expires time.Time `json:"expires,omitempty"` // identifier (required, object): // The identifier that the account is authorized to represent Identifier Identifier `json:"identifier,omitempty"` // challenges (required, array of objects): // For pending authorizations, the challenges that the client can fulfill in order to prove possession of the identifier. // For valid authorizations, the challenge that was validated. // For invalid authorizations, the challenge that was attempted and failed. // Each array entry is an object with parameters required to validate the challenge. // A client should attempt to fulfill one of these challenges, // and a server should consider any one of the challenges sufficient to make the authorization valid. Challenges []Challenge `json:"challenges,omitempty"` // wildcard (optional, boolean): // For authorizations created as a result of a newOrder request containing a DNS identifier // with a value that contained a wildcard prefix this field MUST be present, and true. Wildcard bool `json:"wildcard,omitempty"` } // ExtendedChallenge a extended Challenge. type ExtendedChallenge struct { Challenge // Contains the value of the response header `Retry-After` RetryAfter string `json:"-"` // Contains the value of the response header `Link` rel="up" AuthorizationURL string `json:"-"` } // Challenge the ACME challenge object. // - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.5 // - https://www.rfc-editor.org/rfc/rfc8555.html#section-8 type Challenge struct { // type (required, string): // The type of challenge encoded in the object. Type string `json:"type"` // url (required, string): // The URL to which a response can be posted. URL string `json:"url"` // status (required, string): // The status of this challenge. Possible values are: "pending", "processing", "valid", and "invalid". Status string `json:"status"` // validated (optional, string): // The time at which the server validated this challenge, // encoded in the format specified in RFC 3339 [RFC3339]. // This field is REQUIRED if the "status" field is "valid". Validated time.Time `json:"validated,omitempty"` // error (optional, object): // Error that occurred while the server was validating the challenge, if any, // structured as a problem document [RFC7807]. // Multiple errors can be indicated by using subproblems Section 6.7.1. // A challenge object with an error MUST have status equal to "invalid". Error *ProblemDetails `json:"error,omitempty"` // token (required, string): // A random value that uniquely identifies the challenge. // This value MUST have at least 128 bits of entropy. // It MUST NOT contain any characters outside the base64url alphabet, // and MUST NOT include base64 padding characters ("="). // See [RFC4086] for additional information on randomness requirements. // https://www.rfc-editor.org/rfc/rfc8555.html#section-8.3 // https://www.rfc-editor.org/rfc/rfc8555.html#section-8.4 Token string `json:"token"` // https://www.rfc-editor.org/rfc/rfc8555.html#section-8.1 KeyAuthorization string `json:"keyAuthorization"` } // Identifier the ACME identifier object. // - https://www.rfc-editor.org/rfc/rfc8555.html#section-9.7.7 type Identifier struct { Type string `json:"type"` Value string `json:"value"` } // CSRMessage Certificate Signing Request. // - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4 type CSRMessage struct { // csr (required, string): // A CSR encoding the parameters for the certificate being requested [RFC2986]. // The CSR is sent in the base64url-encoded version of the DER format. // (Note: Because this field uses base64url, and does not include headers, it is different from PEM.). Csr string `json:"csr"` } // RevokeCertMessage a certificate revocation message. // - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.6 // - https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1 type RevokeCertMessage struct { // certificate (required, string): // The certificate to be revoked, in the base64url-encoded version of the DER format. // (Note: Because this field uses base64url, and does not include headers, it is different from PEM.) Certificate string `json:"certificate"` // reason (optional, int): // One of the revocation reasonCodes defined in Section 5.3.1 of [RFC5280] to be used when generating OCSP responses and CRLs. // If this field is not set the server SHOULD omit the reasonCode CRL entry extension when generating OCSP responses and CRLs. // The server MAY disallow a subset of reasonCodes from being used by the user. // If a request contains a disallowed reasonCode the server MUST reject it with the error type "urn:ietf:params:acme:error:badRevocationReason". // The problem document detail SHOULD indicate which reasonCodes are allowed. Reason *uint `json:"reason,omitempty"` } // RawCertificate raw data of a certificate. type RawCertificate struct { Cert []byte Issuer []byte } lego-4.9.1/acme/errors.go000066400000000000000000000031271434020463500152270ustar00rootroot00000000000000package acme import ( "fmt" ) // Errors types. const ( errNS = "urn:ietf:params:acme:error:" BadNonceErr = errNS + "badNonce" ) // ProblemDetails the problem details object. // - https://www.rfc-editor.org/rfc/rfc7807.html#section-3.1 // - https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3.3 type ProblemDetails struct { Type string `json:"type,omitempty"` Detail string `json:"detail,omitempty"` HTTPStatus int `json:"status,omitempty"` Instance string `json:"instance,omitempty"` SubProblems []SubProblem `json:"subproblems,omitempty"` // additional values to have a better error message (Not defined by the RFC) Method string `json:"method,omitempty"` URL string `json:"url,omitempty"` } // SubProblem a "subproblems". // - https://www.rfc-editor.org/rfc/rfc8555.html#section-6.7.1 type SubProblem struct { Type string `json:"type,omitempty"` Detail string `json:"detail,omitempty"` Identifier Identifier `json:"identifier,omitempty"` } func (p ProblemDetails) Error() string { msg := fmt.Sprintf("acme: error: %d", p.HTTPStatus) if p.Method != "" || p.URL != "" { msg += fmt.Sprintf(" :: %s :: %s", p.Method, p.URL) } msg += fmt.Sprintf(" :: %s :: %s", p.Type, p.Detail) for _, sub := range p.SubProblems { msg += fmt.Sprintf(", problem: %q :: %s", sub.Type, sub.Detail) } if p.Instance != "" { msg += ", url: " + p.Instance } return msg } // NonceError represents the error which is returned // if the nonce sent by the client was not accepted by the server. type NonceError struct { *ProblemDetails } lego-4.9.1/certcrypto/000077500000000000000000000000001434020463500146525ustar00rootroot00000000000000lego-4.9.1/certcrypto/crypto.go000066400000000000000000000173021434020463500165240ustar00rootroot00000000000000package certcrypto import ( "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/pem" "errors" "fmt" "math/big" "strings" "time" "golang.org/x/crypto/ocsp" ) // Constants for all key types we support. const ( EC256 = KeyType("P256") EC384 = KeyType("P384") RSA2048 = KeyType("2048") RSA4096 = KeyType("4096") RSA8192 = KeyType("8192") ) const ( // OCSPGood means that the certificate is valid. OCSPGood = ocsp.Good // OCSPRevoked means that the certificate has been deliberately revoked. OCSPRevoked = ocsp.Revoked // OCSPUnknown means that the OCSP responder doesn't know about the certificate. OCSPUnknown = ocsp.Unknown // OCSPServerFailed means that the OCSP responder failed to process the request. OCSPServerFailed = ocsp.ServerFailed ) // Constants for OCSP must staple. var ( tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24} ocspMustStapleFeature = []byte{0x30, 0x03, 0x02, 0x01, 0x05} ) // KeyType represents the key algo as well as the key size or curve to use. type KeyType string type DERCertificateBytes []byte // ParsePEMBundle parses a certificate bundle from top to bottom and returns // a slice of x509 certificates. This function will error if no certificates are found. func ParsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { var certificates []*x509.Certificate var certDERBlock *pem.Block for { certDERBlock, bundle = pem.Decode(bundle) if certDERBlock == nil { break } if certDERBlock.Type == "CERTIFICATE" { cert, err := x509.ParseCertificate(certDERBlock.Bytes) if err != nil { return nil, err } certificates = append(certificates, cert) } } if len(certificates) == 0 { return nil, errors.New("no certificates were found while parsing the bundle") } return certificates, nil } // ParsePEMPrivateKey parses a private key from key, which is a PEM block. // Borrowed from Go standard library, to handle various private key and PEM block types. // https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308 // https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238) func ParsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) { keyBlockDER, _ := pem.Decode(key) if keyBlockDER == nil { return nil, fmt.Errorf("invalid PEM block") } if keyBlockDER.Type != "PRIVATE KEY" && !strings.HasSuffix(keyBlockDER.Type, " PRIVATE KEY") { return nil, fmt.Errorf("unknown PEM header %q", keyBlockDER.Type) } if key, err := x509.ParsePKCS1PrivateKey(keyBlockDER.Bytes); err == nil { return key, nil } if key, err := x509.ParsePKCS8PrivateKey(keyBlockDER.Bytes); err == nil { switch key := key.(type) { case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey: return key, nil default: return nil, fmt.Errorf("found unknown private key type in PKCS#8 wrapping: %T", key) } } if key, err := x509.ParseECPrivateKey(keyBlockDER.Bytes); err == nil { return key, nil } return nil, errors.New("failed to parse private key") } func GeneratePrivateKey(keyType KeyType) (crypto.PrivateKey, error) { switch keyType { case EC256: return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) case EC384: return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) case RSA2048: return rsa.GenerateKey(rand.Reader, 2048) case RSA4096: return rsa.GenerateKey(rand.Reader, 4096) case RSA8192: return rsa.GenerateKey(rand.Reader, 8192) } return nil, fmt.Errorf("invalid KeyType: %s", keyType) } func GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) { template := x509.CertificateRequest{ Subject: pkix.Name{CommonName: domain}, DNSNames: san, } if mustStaple { template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{ Id: tlsFeatureExtensionOID, Value: ocspMustStapleFeature, }) } return x509.CreateCertificateRequest(rand.Reader, &template, privateKey) } func PEMEncode(data interface{}) []byte { return pem.EncodeToMemory(PEMBlock(data)) } func PEMBlock(data interface{}) *pem.Block { var pemBlock *pem.Block switch key := data.(type) { case *ecdsa.PrivateKey: keyBytes, _ := x509.MarshalECPrivateKey(key) pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} case *rsa.PrivateKey: pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} case *x509.CertificateRequest: pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw} case DERCertificateBytes: pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(DERCertificateBytes))} } return pemBlock } func pemDecode(data []byte) (*pem.Block, error) { pemBlock, _ := pem.Decode(data) if pemBlock == nil { return nil, errors.New("PEM decode did not yield a valid block. Is the certificate in the right format?") } return pemBlock, nil } func PemDecodeTox509CSR(data []byte) (*x509.CertificateRequest, error) { pemBlock, err := pemDecode(data) if pemBlock == nil { return nil, err } if pemBlock.Type != "CERTIFICATE REQUEST" && pemBlock.Type != "NEW CERTIFICATE REQUEST" { return nil, errors.New("PEM block is not a certificate request") } return x509.ParseCertificateRequest(pemBlock.Bytes) } // ParsePEMCertificate returns Certificate from a PEM encoded certificate. // The certificate has to be PEM encoded. Any other encodings like DER will fail. func ParsePEMCertificate(cert []byte) (*x509.Certificate, error) { pemBlock, err := pemDecode(cert) if pemBlock == nil { return nil, err } // from a DER encoded certificate return x509.ParseCertificate(pemBlock.Bytes) } func ExtractDomains(cert *x509.Certificate) []string { var domains []string if cert.Subject.CommonName != "" { domains = append(domains, cert.Subject.CommonName) } // Check for SAN certificate for _, sanDomain := range cert.DNSNames { if sanDomain == cert.Subject.CommonName { continue } domains = append(domains, sanDomain) } return domains } func ExtractDomainsCSR(csr *x509.CertificateRequest) []string { var domains []string if csr.Subject.CommonName != "" { domains = append(domains, csr.Subject.CommonName) } // loop over the SubjectAltName DNS names for _, sanName := range csr.DNSNames { if containsSAN(domains, sanName) { // Duplicate; skip this name continue } // Name is unique domains = append(domains, sanName) } return domains } func containsSAN(domains []string, sanName string) bool { for _, existingName := range domains { if existingName == sanName { return true } } return false } func GeneratePemCert(privateKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) { derBytes, err := generateDerCert(privateKey, time.Time{}, domain, extensions) if err != nil { return nil, err } return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil } func generateDerCert(privateKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]byte, error) { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { return nil, err } if expiration.IsZero() { expiration = time.Now().AddDate(1, 0, 0) } template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ CommonName: "ACME Challenge TEMP", }, NotBefore: time.Now(), NotAfter: expiration, KeyUsage: x509.KeyUsageKeyEncipherment, BasicConstraintsValid: true, DNSNames: []string{domain}, ExtraExtensions: extensions, } return x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) } lego-4.9.1/certcrypto/crypto_test.go000066400000000000000000000111341434020463500175600ustar00rootroot00000000000000package certcrypto import ( "bytes" "crypto" "crypto/rand" "crypto/rsa" "encoding/pem" "regexp" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGeneratePrivateKey(t *testing.T) { key, err := GeneratePrivateKey(RSA2048) require.NoError(t, err, "Error generating private key") assert.NotNil(t, key) } func TestGenerateCSR(t *testing.T) { privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err, "Error generating private key") type expected struct { len int error bool } testCases := []struct { desc string privateKey crypto.PrivateKey domain string san []string mustStaple bool expected expected }{ { desc: "without SAN", privateKey: privateKey, domain: "lego.acme", mustStaple: true, expected: expected{len: 245}, }, { desc: "without SAN", privateKey: privateKey, domain: "lego.acme", san: []string{}, mustStaple: true, expected: expected{len: 245}, }, { desc: "with SAN", privateKey: privateKey, domain: "lego.acme", san: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"}, mustStaple: true, expected: expected{len: 296}, }, { desc: "no domain", privateKey: privateKey, domain: "", mustStaple: true, expected: expected{len: 225}, }, { desc: "no domain with SAN", privateKey: privateKey, domain: "", san: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"}, mustStaple: true, expected: expected{len: 276}, }, { desc: "private key nil", privateKey: nil, domain: "fizz.buzz", mustStaple: true, expected: expected{error: true}, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() csr, err := GenerateCSR(test.privateKey, test.domain, test.san, test.mustStaple) if test.expected.error { require.Error(t, err) } else { require.NoError(t, err, "Error generating CSR") assert.NotEmpty(t, csr) assert.Len(t, csr, test.expected.len) } }) } } func TestPEMEncode(t *testing.T) { buf := bytes.NewBufferString("TestingRSAIsSoMuchFun") reader := MockRandReader{b: buf} key, err := rsa.GenerateKey(reader, 32) require.NoError(t, err, "Error generating private key") data := PEMEncode(key) require.NotNil(t, data) exp := regexp.MustCompile(`^-----BEGIN RSA PRIVATE KEY-----\s+\S{60,}\s+-----END RSA PRIVATE KEY-----\s+`) assert.Regexp(t, exp, string(data)) } func TestParsePEMCertificate(t *testing.T) { privateKey, err := GeneratePrivateKey(RSA2048) require.NoError(t, err, "Error generating private key") expiration := time.Now().Add(365).Round(time.Second) certBytes, err := generateDerCert(privateKey.(*rsa.PrivateKey), expiration, "test.com", nil) require.NoError(t, err, "Error generating cert") buf := bytes.NewBufferString("TestingRSAIsSoMuchFun") // Some random string should return an error. cert, err := ParsePEMCertificate(buf.Bytes()) require.Errorf(t, err, "returned %v", cert) // A DER encoded certificate should return an error. _, err = ParsePEMCertificate(certBytes) require.Error(t, err, "Expected to return an error for DER certificates") // A PEM encoded certificate should work ok. pemCert := PEMEncode(DERCertificateBytes(certBytes)) cert, err = ParsePEMCertificate(pemCert) require.NoError(t, err) assert.Equal(t, expiration.UTC(), cert.NotAfter) } func TestParsePEMPrivateKey(t *testing.T) { privateKey, err := GeneratePrivateKey(RSA2048) require.NoError(t, err, "Error generating private key") pemPrivateKey := PEMEncode(privateKey) // Decoding a key should work and create an identical key to the original decoded, err := ParsePEMPrivateKey(pemPrivateKey) require.NoError(t, err) assert.Equal(t, decoded, privateKey) // Decoding a PEM block that doesn't contain a private key should error _, err = ParsePEMPrivateKey(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE"})) require.Errorf(t, err, "Expected to return an error for non-private key input") // Decoding a PEM block that doesn't actually contain a key should error _, err = ParsePEMPrivateKey(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY"})) require.Errorf(t, err, "Expected to return an error for empty input") // Decoding non-PEM input should return an error _, err = ParsePEMPrivateKey([]byte("This is not PEM")) require.Errorf(t, err, "Expected to return an error for non-PEM input") } type MockRandReader struct { b *bytes.Buffer } func (r MockRandReader) Read(p []byte) (int, error) { return r.b.Read(p) } lego-4.9.1/certificate/000077500000000000000000000000001434020463500147365ustar00rootroot00000000000000lego-4.9.1/certificate/authorization.go000066400000000000000000000040471434020463500201720ustar00rootroot00000000000000package certificate import ( "time" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/log" ) const ( // overallRequestLimit is the overall number of request per second // limited on the "new-reg", "new-authz" and "new-cert" endpoints. // From the documentation the limitation is 20 requests per second, // but using 20 as value doesn't work but 18 do. overallRequestLimit = 18 ) func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authorization, error) { resc, errc := make(chan acme.Authorization), make(chan domainError) delay := time.Second / overallRequestLimit for _, authzURL := range order.Authorizations { time.Sleep(delay) go func(authzURL string) { authz, err := c.core.Authorizations.Get(authzURL) if err != nil { errc <- domainError{Domain: authz.Identifier.Value, Error: err} return } resc <- authz }(authzURL) } var responses []acme.Authorization failures := make(obtainError) for i := 0; i < len(order.Authorizations); i++ { select { case res := <-resc: responses = append(responses, res) case err := <-errc: failures[err.Domain] = err.Error } } for i, auth := range order.Authorizations { log.Infof("[%s] AuthURL: %s", order.Identifiers[i].Value, auth) } close(resc) close(errc) // be careful to not return an empty failures map; // even if empty, they become non-nil error values if len(failures) > 0 { return responses, failures } return responses, nil } func (c *Certifier) deactivateAuthorizations(order acme.ExtendedOrder, force bool) { for _, authzURL := range order.Authorizations { auth, err := c.core.Authorizations.Get(authzURL) if err != nil { log.Infof("Unable to get the authorization for: %s", authzURL) continue } if auth.Status == acme.StatusValid && !force { log.Infof("Skipping deactivating of valid auth: %s", authzURL) continue } log.Infof("Deactivating auth: %s", authzURL) if c.core.Authorizations.Deactivate(authzURL) != nil { log.Infof("Unable to deactivate the authorization: %s", authzURL) } } } lego-4.9.1/certificate/certificates.go000066400000000000000000000447521434020463500177460ustar00rootroot00000000000000package certificate import ( "bytes" "crypto" "crypto/x509" "encoding/base64" "errors" "fmt" "io" "net/http" "strings" "time" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/wait" "golang.org/x/crypto/ocsp" "golang.org/x/net/idna" ) // maxBodySize is the maximum size of body that we will read. const maxBodySize = 1024 * 1024 // Resource represents a CA issued certificate. // PrivateKey, Certificate and IssuerCertificate are all // already PEM encoded and can be directly written to disk. // Certificate may be a certificate bundle, // depending on the options supplied to create it. type Resource struct { Domain string `json:"domain"` CertURL string `json:"certUrl"` CertStableURL string `json:"certStableUrl"` PrivateKey []byte `json:"-"` Certificate []byte `json:"-"` IssuerCertificate []byte `json:"-"` CSR []byte `json:"-"` } // ObtainRequest The request to obtain certificate. // // The first domain in domains is used for the CommonName field of the certificate, // all other domains are added using the Subject Alternate Names extension. // // A new private key is generated for every invocation of the function Obtain. // If you do not want that you can supply your own private key in the privateKey parameter. // If this parameter is non-nil it will be used instead of generating a new one. // // If `Bundle` is true, the `[]byte` contains both the issuer certificate and your issued certificate as a bundle. // // If `AlwaysDeactivateAuthorizations` is true, the authorizations are also relinquished if the obtain request was successful. // See https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2. type ObtainRequest struct { Domains []string Bundle bool PrivateKey crypto.PrivateKey MustStaple bool PreferredChain string AlwaysDeactivateAuthorizations bool } // ObtainForCSRRequest The request to obtain a certificate matching the CSR passed into it. // // If `Bundle` is true, the `[]byte` contains both the issuer certificate and your issued certificate as a bundle. // // If `AlwaysDeactivateAuthorizations` is true, the authorizations are also relinquished if the obtain request was successful. // See https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2. type ObtainForCSRRequest struct { CSR *x509.CertificateRequest Bundle bool PreferredChain string AlwaysDeactivateAuthorizations bool } type resolver interface { Solve(authorizations []acme.Authorization) error } type CertifierOptions struct { KeyType certcrypto.KeyType Timeout time.Duration } // Certifier A service to obtain/renew/revoke certificates. type Certifier struct { core *api.Core resolver resolver options CertifierOptions } // NewCertifier creates a Certifier. func NewCertifier(core *api.Core, resolver resolver, options CertifierOptions) *Certifier { return &Certifier{ core: core, resolver: resolver, options: options, } } // Obtain tries to obtain a single certificate using all domains passed into it. // // This function will never return a partial certificate. // If one domain in the list fails, the whole certificate will fail. func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) { if len(request.Domains) == 0 { return nil, errors.New("no domains to obtain a certificate for") } domains := sanitizeDomain(request.Domains) if request.Bundle { log.Infof("[%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", ")) } else { log.Infof("[%s] acme: Obtaining SAN certificate", strings.Join(domains, ", ")) } order, err := c.core.Orders.New(domains) if err != nil { return nil, err } authz, err := c.getAuthorizations(order) if err != nil { // If any challenge fails, return. Do not generate partial SAN certificates. c.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations) return nil, err } err = c.resolver.Solve(authz) if err != nil { // If any challenge fails, return. Do not generate partial SAN certificates. c.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations) return nil, err } log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) failures := make(obtainError) cert, err := c.getForOrder(domains, order, request.Bundle, request.PrivateKey, request.MustStaple, request.PreferredChain) if err != nil { for _, auth := range authz { failures[challenge.GetTargetedDomain(auth)] = err } } if request.AlwaysDeactivateAuthorizations { c.deactivateAuthorizations(order, true) } // Do not return an empty failures map, because // it would still be a non-nil error value if len(failures) > 0 { return cert, failures } return cert, nil } // ObtainForCSR tries to obtain a certificate matching the CSR passed into it. // // The domains are inferred from the CommonName and SubjectAltNames, if any. // The private key for this CSR is not required. // // If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle. // // This function will never return a partial certificate. // If one domain in the list fails, the whole certificate will fail. func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error) { if request.CSR == nil { return nil, errors.New("cannot obtain resource for CSR: CSR is missing") } // figure out what domains it concerns // start with the common name domains := certcrypto.ExtractDomainsCSR(request.CSR) if request.Bundle { log.Infof("[%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", ")) } else { log.Infof("[%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", ")) } order, err := c.core.Orders.New(domains) if err != nil { return nil, err } authz, err := c.getAuthorizations(order) if err != nil { // If any challenge fails, return. Do not generate partial SAN certificates. c.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations) return nil, err } err = c.resolver.Solve(authz) if err != nil { // If any challenge fails, return. Do not generate partial SAN certificates. c.deactivateAuthorizations(order, request.AlwaysDeactivateAuthorizations) return nil, err } log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) failures := make(obtainError) cert, err := c.getForCSR(domains, order, request.Bundle, request.CSR.Raw, nil, request.PreferredChain) if err != nil { for _, auth := range authz { failures[challenge.GetTargetedDomain(auth)] = err } } if request.AlwaysDeactivateAuthorizations { c.deactivateAuthorizations(order, true) } if cert != nil { // Add the CSR to the certificate so that it can be used for renewals. cert.CSR = certcrypto.PEMEncode(request.CSR) } // Do not return an empty failures map, // because it would still be a non-nil error value if len(failures) > 0 { return cert, failures } return cert, nil } func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool, preferredChain string) (*Resource, error) { if privateKey == nil { var err error privateKey, err = certcrypto.GeneratePrivateKey(c.options.KeyType) if err != nil { return nil, err } } // Determine certificate name(s) based on the authorization resources commonName := domains[0] // RFC8555 Section 7.4 "Applying for Certificate Issuance" // https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4 // says: // Clients SHOULD NOT make any assumptions about the sort order of // "identifiers" or "authorizations" elements in the returned order // object. san := []string{commonName} for _, auth := range order.Identifiers { if auth.Value != commonName { san = append(san, auth.Value) } } // TODO: should the CSR be customizable? csr, err := certcrypto.GenerateCSR(privateKey, commonName, san, mustStaple) if err != nil { return nil, err } return c.getForCSR(domains, order, bundle, csr, certcrypto.PEMEncode(privateKey), preferredChain) } func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle bool, csr, privateKeyPem []byte, preferredChain string) (*Resource, error) { respOrder, err := c.core.Orders.UpdateForCSR(order.Finalize, csr) if err != nil { return nil, err } commonName := domains[0] certRes := &Resource{ Domain: commonName, CertURL: respOrder.Certificate, PrivateKey: privateKeyPem, } if respOrder.Status == acme.StatusValid { // if the certificate is available right away, short cut! ok, errR := c.checkResponse(respOrder, certRes, bundle, preferredChain) if errR != nil { return nil, errR } if ok { return certRes, nil } } timeout := c.options.Timeout if c.options.Timeout <= 0 { timeout = 30 * time.Second } err = wait.For("certificate", timeout, timeout/60, func() (bool, error) { ord, errW := c.core.Orders.Get(order.Location) if errW != nil { return false, errW } done, errW := c.checkResponse(ord, certRes, bundle, preferredChain) if errW != nil { return false, errW } return done, nil }) return certRes, err } // checkResponse checks to see if the certificate is ready and a link is contained in the response. // // If so, loads it into certRes and returns true. // If the cert is not yet ready, it returns false. // // The certRes input should already have the Domain (common name) field populated. // // If bundle is true, the certificate will be bundled with the issuer's cert. func (c *Certifier) checkResponse(order acme.ExtendedOrder, certRes *Resource, bundle bool, preferredChain string) (bool, error) { valid, err := checkOrderStatus(order) if err != nil || !valid { return valid, err } certs, err := c.core.Certificates.GetAll(order.Certificate, bundle) if err != nil { return false, err } // Set the default certificate certRes.IssuerCertificate = certs[order.Certificate].Issuer certRes.Certificate = certs[order.Certificate].Cert certRes.CertURL = order.Certificate certRes.CertStableURL = order.Certificate if preferredChain == "" { log.Infof("[%s] Server responded with a certificate.", certRes.Domain) return true, nil } for link, cert := range certs { ok, err := hasPreferredChain(cert.Issuer, preferredChain) if err != nil { return false, err } if ok { log.Infof("[%s] Server responded with a certificate for the preferred certificate chains %q.", certRes.Domain, preferredChain) certRes.IssuerCertificate = cert.Issuer certRes.Certificate = cert.Cert certRes.CertURL = link certRes.CertStableURL = link return true, nil } } log.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) return true, nil } // Revoke takes a PEM encoded certificate or bundle and tries to revoke it at the CA. func (c *Certifier) Revoke(cert []byte) error { return c.RevokeWithReason(cert, nil) } // RevokeWithReason takes a PEM encoded certificate or bundle and tries to revoke it at the CA. func (c *Certifier) RevokeWithReason(cert []byte, reason *uint) error { certificates, err := certcrypto.ParsePEMBundle(cert) if err != nil { return err } x509Cert := certificates[0] if x509Cert.IsCA { return errors.New("certificate bundle starts with a CA certificate") } revokeMsg := acme.RevokeCertMessage{ Certificate: base64.RawURLEncoding.EncodeToString(x509Cert.Raw), Reason: reason, } return c.core.Certificates.Revoke(revokeMsg) } // Renew takes a Resource and tries to renew the certificate. // // If the renewal process succeeds, the new certificate will be returned in a new CertResource. // Please be aware that this function will return a new certificate in ANY case that is not an error. // If the server does not provide us with a new cert on a GET request to the CertURL // this function will start a new-cert flow where a new certificate gets generated. // // If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle. // // For private key reuse the PrivateKey property of the passed in Resource should be non-nil. func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool, preferredChain string) (*Resource, error) { // Input certificate is PEM encoded. // Decode it here as we may need the decoded cert later on in the renewal process. // The input may be a bundle or a single certificate. certificates, err := certcrypto.ParsePEMBundle(certRes.Certificate) if err != nil { return nil, err } x509Cert := certificates[0] if x509Cert.IsCA { return nil, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", certRes.Domain) } // This is just meant to be informal for the user. timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC()) log.Infof("[%s] acme: Trying renewal with %d hours remaining", certRes.Domain, int(timeLeft.Hours())) // We always need to request a new certificate to renew. // Start by checking to see if the certificate was based off a CSR, // and use that if it's defined. if len(certRes.CSR) > 0 { csr, errP := certcrypto.PemDecodeTox509CSR(certRes.CSR) if errP != nil { return nil, errP } return c.ObtainForCSR(ObtainForCSRRequest{ CSR: csr, Bundle: bundle, PreferredChain: preferredChain, }) } var privateKey crypto.PrivateKey if certRes.PrivateKey != nil { privateKey, err = certcrypto.ParsePEMPrivateKey(certRes.PrivateKey) if err != nil { return nil, err } } query := ObtainRequest{ Domains: certcrypto.ExtractDomains(x509Cert), Bundle: bundle, PrivateKey: privateKey, MustStaple: mustStaple, PreferredChain: preferredChain, } return c.Obtain(query) } // GetOCSP takes a PEM encoded cert or cert bundle returning the raw OCSP response, // the parsed response, and an error, if any. // // The returned []byte can be passed directly into the OCSPStaple property of a tls.Certificate. // If the bundle only contains the issued certificate, // this function will try to get the issuer certificate from the IssuingCertificateURL in the certificate. // // If the []byte and/or ocsp.Response return values are nil, the OCSP status may be assumed OCSPUnknown. func (c *Certifier) GetOCSP(bundle []byte) ([]byte, *ocsp.Response, error) { certificates, err := certcrypto.ParsePEMBundle(bundle) if err != nil { return nil, nil, err } // We expect the certificate slice to be ordered downwards the chain. // SRV CRT -> CA. We need to pull the leaf and issuer certs out of it, // which should always be the first two certificates. // If there's no OCSP server listed in the leaf cert, there's nothing to do. // And if we have only one certificate so far, we need to get the issuer cert. issuedCert := certificates[0] if len(issuedCert.OCSPServer) == 0 { return nil, nil, errors.New("no OCSP server specified in cert") } if len(certificates) == 1 { // TODO: build fallback. If this fails, check the remaining array entries. if len(issuedCert.IssuingCertificateURL) == 0 { return nil, nil, errors.New("no issuing certificate URL") } resp, errC := c.core.HTTPClient.Get(issuedCert.IssuingCertificateURL[0]) if errC != nil { return nil, nil, errC } defer resp.Body.Close() issuerBytes, errC := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize)) if errC != nil { return nil, nil, errC } issuerCert, errC := x509.ParseCertificate(issuerBytes) if errC != nil { return nil, nil, errC } // Insert it into the slice on position 0 // We want it ordered right SRV CRT -> CA certificates = append(certificates, issuerCert) } issuerCert := certificates[1] // Finally kick off the OCSP request. ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil) if err != nil { return nil, nil, err } resp, err := c.core.HTTPClient.Post(issuedCert.OCSPServer[0], "application/ocsp-request", bytes.NewReader(ocspReq)) if err != nil { return nil, nil, err } defer resp.Body.Close() ocspResBytes, err := io.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize)) if err != nil { return nil, nil, err } ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert) if err != nil { return nil, nil, err } return ocspResBytes, ocspRes, nil } // Get attempts to fetch the certificate at the supplied URL. // The URL is the same as what would normally be supplied at the Resource's CertURL. // // The returned Resource will not have the PrivateKey and CSR fields populated as these will not be available. // // If bundle is true, the Certificate field in the returned Resource includes the issuer certificate. func (c *Certifier) Get(url string, bundle bool) (*Resource, error) { cert, issuer, err := c.core.Certificates.Get(url, bundle) if err != nil { return nil, err } // Parse the returned cert bundle so that we can grab the domain from the common name. x509Certs, err := certcrypto.ParsePEMBundle(cert) if err != nil { return nil, err } return &Resource{ Domain: x509Certs[0].Subject.CommonName, Certificate: cert, IssuerCertificate: issuer, CertURL: url, CertStableURL: url, }, nil } func hasPreferredChain(issuer []byte, preferredChain string) (bool, error) { certs, err := certcrypto.ParsePEMBundle(issuer) if err != nil { return false, err } topCert := certs[len(certs)-1] if topCert.Issuer.CommonName == preferredChain { return true, nil } return false, nil } func checkOrderStatus(order acme.ExtendedOrder) (bool, error) { switch order.Status { case acme.StatusValid: return true, nil case acme.StatusInvalid: return false, order.Error default: return false, nil } } // https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.4 // The domain name MUST be encoded in the form in which it would appear in a certificate. // That is, it MUST be encoded according to the rules in Section 7 of [RFC5280]. // // https://www.rfc-editor.org/rfc/rfc5280.html#section-7 func sanitizeDomain(domains []string) []string { var sanitizedDomains []string for _, domain := range domains { sanitizedDomain, err := idna.ToASCII(domain) if err != nil { log.Infof("skip domain %q: unable to sanitize (punnycode): %v", domain, err) } else { sanitizedDomains = append(sanitizedDomains, sanitizedDomain) } } return sanitizedDomains } lego-4.9.1/certificate/certificates_test.go000066400000000000000000000400571434020463500207770ustar00rootroot00000000000000package certificate import ( "crypto/rand" "crypto/rsa" "encoding/pem" "fmt" "net/http" "testing" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const certResponseNoBundleMock = `-----BEGIN CERTIFICATE----- MIIDEDCCAfigAwIBAgIHPhckqW5fPDANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQD Ex1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDM5NWU2MTAeFw0xODExMDcxNzQ2NTZa Fw0yMzExMDcxNzQ2NTZaMBMxETAPBgNVBAMTCGFjbWUud3RmMIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtLNKvZXD20XPUQCWYSK9rUSKxD9Eb0c9fag bxOxOkLRTgL8LH6yln+bxc3MrHDou4PpDUdeo2CyOQu3CKsTS5mrH3NXYHu0H7p5 y3riOJTHnfkGKLT9LciGz7GkXd62nvNP57bOf5Sk4P2M+Qbxd0hPTSfu52740LSy 144cnxe2P1aDYehrEp6nYCESuyD/CtUHTo0qwJmzIy163Sp3rSs15BuCPyhySnE3 BJ8Ggv+qC6D5I1932DfSqyQJ79iq/HRm0Fn84am3KwvRlUfWxabmsUGARXoqCgnE zcbJVOZKewv0zlQJpfac+b+Imj6Lvt1TGjIz2mVyefYgLx8gwwIDAQABo1QwUjAO BgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwG A1UdEwEB/wQCMAAwEwYDVR0RBAwwCoIIYWNtZS53dGYwDQYJKoZIhvcNAQELBQAD ggEBABB/0iYhmfPSQot5RaeeovQnsqYjI5ryQK2cwzW6qcTJfv8N6+p6XkqF1+W4 jXZjrQP8MvgO9KNWlvx12vhINE6wubk88L+2piAi5uS2QejmZbXpyYB9s+oPqlk9 IDvfdlVYOqvYAhSx7ggGi+j73mjZVtjAavP6dKuu475ZCeq+NIC15RpbbikWKtYE HBJ7BW8XQKx67iHGx8ygHTDLbREL80Bck3oUm7wIYGMoNijD6RBl25p4gYl9dzOd TqGl5hW/1P5hMbgEzHbr4O3BfWqU2g7tV36TASy3jbC3ONFRNNYrpEZ1AL3+cUri OPPkKtAKAbQkKbUIfsHpBZjKZMU= -----END CERTIFICATE----- ` const certResponseMock = `-----BEGIN CERTIFICATE----- MIIDEDCCAfigAwIBAgIHPhckqW5fPDANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQD Ex1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDM5NWU2MTAeFw0xODExMDcxNzQ2NTZa Fw0yMzExMDcxNzQ2NTZaMBMxETAPBgNVBAMTCGFjbWUud3RmMIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtLNKvZXD20XPUQCWYSK9rUSKxD9Eb0c9fag bxOxOkLRTgL8LH6yln+bxc3MrHDou4PpDUdeo2CyOQu3CKsTS5mrH3NXYHu0H7p5 y3riOJTHnfkGKLT9LciGz7GkXd62nvNP57bOf5Sk4P2M+Qbxd0hPTSfu52740LSy 144cnxe2P1aDYehrEp6nYCESuyD/CtUHTo0qwJmzIy163Sp3rSs15BuCPyhySnE3 BJ8Ggv+qC6D5I1932DfSqyQJ79iq/HRm0Fn84am3KwvRlUfWxabmsUGARXoqCgnE zcbJVOZKewv0zlQJpfac+b+Imj6Lvt1TGjIz2mVyefYgLx8gwwIDAQABo1QwUjAO BgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwG A1UdEwEB/wQCMAAwEwYDVR0RBAwwCoIIYWNtZS53dGYwDQYJKoZIhvcNAQELBQAD ggEBABB/0iYhmfPSQot5RaeeovQnsqYjI5ryQK2cwzW6qcTJfv8N6+p6XkqF1+W4 jXZjrQP8MvgO9KNWlvx12vhINE6wubk88L+2piAi5uS2QejmZbXpyYB9s+oPqlk9 IDvfdlVYOqvYAhSx7ggGi+j73mjZVtjAavP6dKuu475ZCeq+NIC15RpbbikWKtYE HBJ7BW8XQKx67iHGx8ygHTDLbREL80Bck3oUm7wIYGMoNijD6RBl25p4gYl9dzOd TqGl5hW/1P5hMbgEzHbr4O3BfWqU2g7tV36TASy3jbC3ONFRNNYrpEZ1AL3+cUri OPPkKtAKAbQkKbUIfsHpBZjKZMU= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE AxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw NzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl NjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT SZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh 0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen SRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx HAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt D1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu mB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD AQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA upU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm iUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd QqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ wlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2 7R4IbHGnj0BJA2vMYC4hSw== -----END CERTIFICATE----- ` const issuerMock = `-----BEGIN CERTIFICATE----- MIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE AxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw NzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl NjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT SZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh 0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen SRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx HAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt D1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu mB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD AQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA upU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm iUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd QqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ wlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2 7R4IbHGnj0BJA2vMYC4hSw== -----END CERTIFICATE----- ` const certResponseMock2 = ` -----BEGIN CERTIFICATE----- MIIFUzCCBDugAwIBAgISA/z9btaZCSo/qlVwmJrHpoyPMA0GCSqGSIb3DQEBCwUA MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDA3MjUwNjUxNDRaFw0y MDEwMjMwNjUxNDRaMBgxFjAUBgNVBAMTDW5hdHVyZS5nbG9iYWwwggEiMA0GCSqG SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDN/PF8lWub3i+lO3CLl/HJAM86pQH9hWej Whci1PPNzKyEByJq2psNLCO1W1mXK3ClWSyifptCf7+AAFAOoBojPMwjaKMziw1M BxAQiX8MzZLv4Hr4Uk08cQX31QHiEpOv4pMHqB0UpodTYY10dZnDdyJHaGKzxfJh nQPYIVto+UegcVu9iZIDow7ugoT2Gh8nB8jOAc4wtBgmylgeAFmYR6QZ4PYSYFh0 DLZGGB1WuU/4YC5OciwTDv5EiqP3KM3NdkmGhPY0A3jcTrjN+HhcE4pYBtG1wHi8 PEuqqKyCLa3AjHq4WrZyCCkCMXPbIDS1Qt7botDmUZr/26xJZnl5AgMBAAGjggJj MIICXzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF BwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFFm72Cv7LnjVhcLqUujrykUr70lF MB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEBBGMw YTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0Lm9y ZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0Lm9y Zy8wGAYDVR0RBBEwD4INbmF0dXJlLmdsb2JhbDBMBgNVHSAERTBDMAgGBmeBDAEC ATA3BgsrBgEEAYLfEwEBATAoMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNl bmNyeXB0Lm9yZzCCAQUGCisGAQQB1nkCBAIEgfYEgfMA8QB3ALIeBcyLos2KIE6H ZvkruYolIGdr2vpw57JJUy3vi5BeAAABc4T006IAAAQDAEgwRgIhAPEEvCEMkekD 8XLDaxHPnJ85UZL72JqGgNK+7I/NdFNuAiEA5D78b4V1YsD8wvWz/sk6Ks8VgjED eKGl/TyXwKEpzEIAdgBvU3asMfAxGdiZAKRRFf93FRwR2QLBACkGjbIImjfZEwAA AXOE9NPrAAAEAwBHMEUCIAu4YFfGZIN/P+0eRG0krSddHKCSf6rqr6aVqUWkJY3F AiEAz0HkTe0alED1gW9nEAJ1qqK1MLMjRM8SsUv9Is86+CwwDQYJKoZIhvcNAQEL BQADggEBAGriSVi9YuBnm50w84gjlinmeGdvxgugblIoEqKoXd3d5/zx0DvW9Tm6 YGfXsvAJUSCag7dZ/s/PEu23jKNdFoaBmDaUHHKnUwbWWF7/ptYZ+YuDVGOJo8PL CULNfUMon20rPU9smzW4BFDBZ6KmX/r4Q8cQ7FLOqKdcng0yMcqIfq4cBxEvd0uQ pHR3AwCjAIGpV6Q9WHHiHx+SEd/Xc18Z5pXa9m3Rz4i6Mfv+AYLtnsZDxcH81cVM 7rYp80vhXM9tFd4wyrqLuaVZgYD1ylxTYpTI7sijIq4Sl984f3IPA/olN+zK6E8d EbiufIcKeju/aSellDzzBabEo80YT4o= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw 7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ -----END CERTIFICATE----- ` const issuerMock2 = `-----BEGIN CERTIFICATE----- MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw 7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ -----END CERTIFICATE----- ` func Test_checkResponse(t *testing.T) { mux, apiURL := tester.SetupFakeAPI(t) mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) { _, err := w.Write([]byte(certResponseMock)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) order := acme.ExtendedOrder{ Order: acme.Order{ Status: acme.StatusValid, Certificate: apiURL + "/certificate", }, } certRes := &Resource{} valid, err := certifier.checkResponse(order, certRes, true, "") require.NoError(t, err) assert.True(t, valid) assert.NotNil(t, certRes) assert.Equal(t, "", certRes.Domain) assert.Contains(t, certRes.CertStableURL, "/certificate") assert.Contains(t, certRes.CertURL, "/certificate") assert.Nil(t, certRes.CSR) assert.Nil(t, certRes.PrivateKey) assert.Equal(t, certResponseMock, string(certRes.Certificate), "Certificate") assert.Equal(t, issuerMock, string(certRes.IssuerCertificate), "IssuerCertificate") } func Test_checkResponse_issuerRelUp(t *testing.T) { mux, apiURL := tester.SetupFakeAPI(t) mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Link", "<"+apiURL+`/issuer>; rel="up"`) _, err := w.Write([]byte(certResponseMock)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) mux.HandleFunc("/issuer", func(w http.ResponseWriter, _ *http.Request) { p, _ := pem.Decode([]byte(issuerMock)) _, err := w.Write(p.Bytes) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) order := acme.ExtendedOrder{ Order: acme.Order{ Status: acme.StatusValid, Certificate: apiURL + "/certificate", }, } certRes := &Resource{} valid, err := certifier.checkResponse(order, certRes, true, "") require.NoError(t, err) assert.True(t, valid) assert.NotNil(t, certRes) assert.Equal(t, "", certRes.Domain) assert.Contains(t, certRes.CertStableURL, "/certificate") assert.Contains(t, certRes.CertURL, "/certificate") assert.Nil(t, certRes.CSR) assert.Nil(t, certRes.PrivateKey) assert.Equal(t, certResponseMock, string(certRes.Certificate), "Certificate") assert.Equal(t, issuerMock, string(certRes.IssuerCertificate), "IssuerCertificate") } func Test_checkResponse_no_bundle(t *testing.T) { mux, apiURL := tester.SetupFakeAPI(t) mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) { _, err := w.Write([]byte(certResponseMock)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) order := acme.ExtendedOrder{ Order: acme.Order{ Status: acme.StatusValid, Certificate: apiURL + "/certificate", }, } certRes := &Resource{} valid, err := certifier.checkResponse(order, certRes, false, "") require.NoError(t, err) assert.True(t, valid) assert.NotNil(t, certRes) assert.Equal(t, "", certRes.Domain) assert.Contains(t, certRes.CertStableURL, "/certificate") assert.Contains(t, certRes.CertURL, "/certificate") assert.Nil(t, certRes.CSR) assert.Nil(t, certRes.PrivateKey) assert.Equal(t, certResponseNoBundleMock, string(certRes.Certificate), "Certificate") assert.Equal(t, issuerMock, string(certRes.IssuerCertificate), "IssuerCertificate") } func Test_checkResponse_alternate(t *testing.T) { mux, apiURL := tester.SetupFakeAPI(t) mux.HandleFunc("/certificate", func(w http.ResponseWriter, _ *http.Request) { w.Header().Add("Link", fmt.Sprintf(`<%s/certificate/1>;title="foo";rel="alternate"`, apiURL)) _, err := w.Write([]byte(certResponseMock)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) mux.HandleFunc("/certificate/1", func(w http.ResponseWriter, _ *http.Request) { _, err := w.Write([]byte(certResponseMock2)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) order := acme.ExtendedOrder{ Order: acme.Order{ Status: acme.StatusValid, Certificate: apiURL + "/certificate", }, } certRes := &Resource{ Domain: "example.com", } valid, err := certifier.checkResponse(order, certRes, true, "DST Root CA X3") require.NoError(t, err) assert.True(t, valid) assert.NotNil(t, certRes) assert.Equal(t, "example.com", certRes.Domain) assert.Contains(t, certRes.CertStableURL, "/certificate/1") assert.Contains(t, certRes.CertURL, "/certificate/1") assert.Nil(t, certRes.CSR) assert.Nil(t, certRes.PrivateKey) assert.Equal(t, certResponseMock2, string(certRes.Certificate), "Certificate") assert.Equal(t, issuerMock2, string(certRes.IssuerCertificate), "IssuerCertificate") } func Test_Get(t *testing.T) { mux, apiURL := tester.SetupFakeAPI(t) mux.HandleFunc("/acme/cert/test-cert", func(w http.ResponseWriter, _ *http.Request) { _, err := w.Write([]byte(certResponseMock)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) certifier := NewCertifier(core, &resolverMock{}, CertifierOptions{KeyType: certcrypto.RSA2048}) certRes, err := certifier.Get(apiURL+"/acme/cert/test-cert", true) require.NoError(t, err) assert.NotNil(t, certRes) assert.Equal(t, "acme.wtf", certRes.Domain) assert.Equal(t, apiURL+"/acme/cert/test-cert", certRes.CertStableURL) assert.Equal(t, apiURL+"/acme/cert/test-cert", certRes.CertURL) assert.Nil(t, certRes.CSR) assert.Nil(t, certRes.PrivateKey) assert.Equal(t, certResponseMock, string(certRes.Certificate), "Certificate") assert.Equal(t, issuerMock, string(certRes.IssuerCertificate), "IssuerCertificate") } type resolverMock struct { error error } func (r *resolverMock) Solve(_ []acme.Authorization) error { return r.error } lego-4.9.1/certificate/errors.go000066400000000000000000000011141434020463500165760ustar00rootroot00000000000000package certificate import ( "bytes" "fmt" "sort" ) // obtainError is returned when there are specific errors available per domain. type obtainError map[string]error func (e obtainError) Error() string { buffer := bytes.NewBufferString("error: one or more domains had a problem:\n") var domains []string for domain := range e { domains = append(domains, domain) } sort.Strings(domains) for _, domain := range domains { buffer.WriteString(fmt.Sprintf("[%s] %s\n", domain, e[domain])) } return buffer.String() } type domainError struct { Domain string Error error } lego-4.9.1/challenge/000077500000000000000000000000001434020463500143765ustar00rootroot00000000000000lego-4.9.1/challenge/challenges.go000066400000000000000000000023261434020463500170350ustar00rootroot00000000000000package challenge import ( "fmt" "github.com/go-acme/lego/v4/acme" ) // Type is a string that identifies a particular challenge type and version of ACME challenge. type Type string const ( // HTTP01 is the "http-01" ACME challenge https://www.rfc-editor.org/rfc/rfc8555.html#section-8.3 // Note: ChallengePath returns the URL path to fulfill this challenge. HTTP01 = Type("http-01") // DNS01 is the "dns-01" ACME challenge https://www.rfc-editor.org/rfc/rfc8555.html#section-8.4 // Note: GetRecord returns a DNS record which will fulfill this challenge. DNS01 = Type("dns-01") // TLSALPN01 is the "tls-alpn-01" ACME challenge https://www.rfc-editor.org/rfc/rfc8737.html TLSALPN01 = Type("tls-alpn-01") ) func (t Type) String() string { return string(t) } func FindChallenge(chlgType Type, authz acme.Authorization) (acme.Challenge, error) { for _, chlg := range authz.Challenges { if chlg.Type == string(chlgType) { return chlg, nil } } return acme.Challenge{}, fmt.Errorf("[%s] acme: unable to find challenge %s", GetTargetedDomain(authz), chlgType) } func GetTargetedDomain(authz acme.Authorization) string { if authz.Wildcard { return "*." + authz.Identifier.Value } return authz.Identifier.Value } lego-4.9.1/challenge/dns01/000077500000000000000000000000001434020463500153235ustar00rootroot00000000000000lego-4.9.1/challenge/dns01/cname.go000066400000000000000000000004361434020463500167400ustar00rootroot00000000000000package dns01 import "github.com/miekg/dns" // Update FQDN with CNAME if any. func updateDomainWithCName(r *dns.Msg, fqdn string) string { for _, rr := range r.Answer { if cn, ok := rr.(*dns.CNAME); ok { if cn.Hdr.Name == fqdn { return cn.Target } } } return fqdn } lego-4.9.1/challenge/dns01/dns_challenge.go000066400000000000000000000122401434020463500204370ustar00rootroot00000000000000package dns01 import ( "crypto/sha256" "encoding/base64" "fmt" "os" "strconv" "time" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/wait" "github.com/miekg/dns" ) const ( // DefaultPropagationTimeout default propagation timeout. DefaultPropagationTimeout = 60 * time.Second // DefaultPollingInterval default polling interval. DefaultPollingInterval = 2 * time.Second // DefaultTTL default TTL. DefaultTTL = 120 ) type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error type ChallengeOption func(*Challenge) error // CondOption Conditional challenge option. func CondOption(condition bool, opt ChallengeOption) ChallengeOption { if !condition { // NoOp options return func(*Challenge) error { return nil } } return opt } // Challenge implements the dns-01 challenge. type Challenge struct { core *api.Core validate ValidateFunc provider challenge.Provider preCheck preCheck dnsTimeout time.Duration } func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge { chlg := &Challenge{ core: core, validate: validate, provider: provider, preCheck: newPreCheck(), dnsTimeout: 10 * time.Second, } for _, opt := range opts { err := opt(chlg) if err != nil { log.Infof("challenge option error: %v", err) } } return chlg } // PreSolve just submits the txt record to the dns provider. // It does not validate record propagation, or do anything at all with the acme server. func (c *Challenge) PreSolve(authz acme.Authorization) error { domain := challenge.GetTargetedDomain(authz) log.Infof("[%s] acme: Preparing to solve DNS-01", domain) chlng, err := challenge.FindChallenge(challenge.DNS01, authz) if err != nil { return err } if c.provider == nil { return fmt.Errorf("[%s] acme: no DNS Provider configured", domain) } // Generate the Key Authorization for the challenge keyAuth, err := c.core.GetKeyAuthorization(chlng.Token) if err != nil { return err } err = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth) if err != nil { return fmt.Errorf("[%s] acme: error presenting token: %w", domain, err) } return nil } func (c *Challenge) Solve(authz acme.Authorization) error { domain := challenge.GetTargetedDomain(authz) log.Infof("[%s] acme: Trying to solve DNS-01", domain) chlng, err := challenge.FindChallenge(challenge.DNS01, authz) if err != nil { return err } // Generate the Key Authorization for the challenge keyAuth, err := c.core.GetKeyAuthorization(chlng.Token) if err != nil { return err } fqdn, value := GetRecord(authz.Identifier.Value, keyAuth) var timeout, interval time.Duration switch provider := c.provider.(type) { case challenge.ProviderTimeout: timeout, interval = provider.Timeout() default: timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval } log.Infof("[%s] acme: Checking DNS record propagation using %+v", domain, recursiveNameservers) time.Sleep(interval) err = wait.For("propagation", timeout, interval, func() (bool, error) { stop, errP := c.preCheck.call(domain, fqdn, value) if !stop || errP != nil { log.Infof("[%s] acme: Waiting for DNS record propagation.", domain) } return stop, errP }) if err != nil { return err } chlng.KeyAuthorization = keyAuth return c.validate(c.core, domain, chlng) } // CleanUp cleans the challenge. func (c *Challenge) CleanUp(authz acme.Authorization) error { log.Infof("[%s] acme: Cleaning DNS-01 challenge", challenge.GetTargetedDomain(authz)) chlng, err := challenge.FindChallenge(challenge.DNS01, authz) if err != nil { return err } keyAuth, err := c.core.GetKeyAuthorization(chlng.Token) if err != nil { return err } return c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth) } func (c *Challenge) Sequential() (bool, time.Duration) { if p, ok := c.provider.(sequential); ok { return ok, p.Sequential() } return false, 0 } type sequential interface { Sequential() time.Duration } // GetRecord returns a DNS record which will fulfill the `dns-01` challenge. func GetRecord(domain, keyAuth string) (fqdn, value string) { keyAuthShaBytes := sha256.Sum256([]byte(keyAuth)) // base64URL encoding without padding value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size]) fqdn = getChallengeFqdn(domain) return } func getChallengeFqdn(domain string) string { fqdn := fmt.Sprintf("_acme-challenge.%s.", domain) if ok, _ := strconv.ParseBool(os.Getenv("LEGO_DISABLE_CNAME_SUPPORT")); ok { return fqdn } // recursion counter so it doesn't spin out of control for limit := 0; limit < 50; limit++ { // Keep following CNAMEs r, err := dnsQuery(fqdn, dns.TypeCNAME, recursiveNameservers, true) if err != nil || r.Rcode != dns.RcodeSuccess { // No more CNAME records to follow, exit break } // Check if the domain has CNAME then use that cname := updateDomainWithCName(r, fqdn) if cname == fqdn { break } log.Infof("Found CNAME entry for %q: %q", fqdn, cname) fqdn = cname } return fqdn } lego-4.9.1/challenge/dns01/dns_challenge_manual.go000066400000000000000000000027741434020463500220070ustar00rootroot00000000000000package dns01 import ( "bufio" "fmt" "os" "time" ) const ( dnsTemplate = `%s %d IN TXT %q` ) // DNSProviderManual is an implementation of the ChallengeProvider interface. type DNSProviderManual struct{} // NewDNSProviderManual returns a DNSProviderManual instance. func NewDNSProviderManual() (*DNSProviderManual, error) { return &DNSProviderManual{}, nil } // Present prints instructions for manually creating the TXT record. func (*DNSProviderManual) Present(domain, token, keyAuth string) error { fqdn, value := GetRecord(domain, keyAuth) authZone, err := FindZoneByFqdn(fqdn) if err != nil { return err } fmt.Printf("lego: Please create the following TXT record in your %s zone:\n", authZone) fmt.Printf(dnsTemplate+"\n", fqdn, DefaultTTL, value) fmt.Printf("lego: Press 'Enter' when you are done\n") _, err = bufio.NewReader(os.Stdin).ReadBytes('\n') return err } // CleanUp prints instructions for manually removing the TXT record. func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error { fqdn, _ := GetRecord(domain, keyAuth) authZone, err := FindZoneByFqdn(fqdn) if err != nil { return err } fmt.Printf("lego: You can now remove this TXT record from your %s zone:\n", authZone) fmt.Printf(dnsTemplate+"\n", fqdn, DefaultTTL, "...") return nil } // Sequential All DNS challenges for this provider will be resolved sequentially. // Returns the interval between each iteration. func (d *DNSProviderManual) Sequential() time.Duration { return DefaultPropagationTimeout } lego-4.9.1/challenge/dns01/dns_challenge_manual_test.go000066400000000000000000000021721434020463500230360ustar00rootroot00000000000000package dns01 import ( "io" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDNSProviderManual(t *testing.T) { backupStdin := os.Stdin defer func() { os.Stdin = backupStdin }() testCases := []struct { desc string input string expectError bool }{ { desc: "Press enter", input: "ok\n", }, { desc: "Missing enter", input: "ok", expectError: true, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { file, err := os.CreateTemp("", "lego_test") assert.NoError(t, err) defer func() { _ = os.Remove(file.Name()) }() _, err = file.WriteString(test.input) assert.NoError(t, err) _, err = file.Seek(0, io.SeekStart) assert.NoError(t, err) os.Stdin = file manualProvider, err := NewDNSProviderManual() require.NoError(t, err) err = manualProvider.Present("example.com", "", "") if test.expectError { require.Error(t, err) } else { require.NoError(t, err) err = manualProvider.CleanUp("example.com", "", "") require.NoError(t, err) } }) } } lego-4.9.1/challenge/dns01/dns_challenge_test.go000066400000000000000000000176151434020463500215110ustar00rootroot00000000000000package dns01 import ( "crypto/rand" "crypto/rsa" "errors" "net/http" "testing" "time" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) type providerMock struct { present, cleanUp error } func (p *providerMock) Present(domain, token, keyAuth string) error { return p.present } func (p *providerMock) CleanUp(domain, token, keyAuth string) error { return p.cleanUp } type providerTimeoutMock struct { present, cleanUp error timeout, interval time.Duration } func (p *providerTimeoutMock) Present(domain, token, keyAuth string) error { return p.present } func (p *providerTimeoutMock) CleanUp(domain, token, keyAuth string) error { return p.cleanUp } func (p *providerTimeoutMock) Timeout() (time.Duration, time.Duration) { return p.timeout, p.interval } func TestChallenge_PreSolve(t *testing.T) { _, apiURL := tester.SetupFakeAPI(t) privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err) core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) testCases := []struct { desc string validate ValidateFunc preCheck WrapPreCheckFunc provider challenge.Provider expectError bool }{ { desc: "success", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil }, provider: &providerMock{}, }, { desc: "validate fail", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return errors.New("OOPS") }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil }, provider: &providerMock{ present: nil, cleanUp: nil, }, }, { desc: "preCheck fail", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return false, errors.New("OOPS") }, provider: &providerTimeoutMock{ timeout: 2 * time.Second, interval: 500 * time.Millisecond, }, }, { desc: "present fail", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil }, provider: &providerMock{ present: errors.New("OOPS"), }, expectError: true, }, { desc: "cleanUp fail", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil }, provider: &providerMock{ cleanUp: errors.New("OOPS"), }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { chlg := NewChallenge(core, test.validate, test.provider, WrapPreCheck(test.preCheck)) authz := acme.Authorization{ Identifier: acme.Identifier{ Value: "example.com", }, Challenges: []acme.Challenge{ {Type: challenge.DNS01.String()}, }, } err = chlg.PreSolve(authz) if test.expectError { require.Error(t, err) } else { require.NoError(t, err) } }) } } func TestChallenge_Solve(t *testing.T) { _, apiURL := tester.SetupFakeAPI(t) privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err) core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) testCases := []struct { desc string validate ValidateFunc preCheck WrapPreCheckFunc provider challenge.Provider expectError bool }{ { desc: "success", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil }, provider: &providerMock{}, }, { desc: "validate fail", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return errors.New("OOPS") }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil }, provider: &providerMock{ present: nil, cleanUp: nil, }, expectError: true, }, { desc: "preCheck fail", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return false, errors.New("OOPS") }, provider: &providerTimeoutMock{ timeout: 2 * time.Second, interval: 500 * time.Millisecond, }, expectError: true, }, { desc: "present fail", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil }, provider: &providerMock{ present: errors.New("OOPS"), }, }, { desc: "cleanUp fail", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil }, provider: &providerMock{ cleanUp: errors.New("OOPS"), }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { var options []ChallengeOption if test.preCheck != nil { options = append(options, WrapPreCheck(test.preCheck)) } chlg := NewChallenge(core, test.validate, test.provider, options...) authz := acme.Authorization{ Identifier: acme.Identifier{ Value: "example.com", }, Challenges: []acme.Challenge{ {Type: challenge.DNS01.String()}, }, } err = chlg.Solve(authz) if test.expectError { require.Error(t, err) } else { require.NoError(t, err) } }) } } func TestChallenge_CleanUp(t *testing.T) { _, apiURL := tester.SetupFakeAPI(t) privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err) core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) testCases := []struct { desc string validate ValidateFunc preCheck WrapPreCheckFunc provider challenge.Provider expectError bool }{ { desc: "success", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil }, provider: &providerMock{}, }, { desc: "validate fail", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return errors.New("OOPS") }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil }, provider: &providerMock{ present: nil, cleanUp: nil, }, }, { desc: "preCheck fail", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return false, errors.New("OOPS") }, provider: &providerTimeoutMock{ timeout: 2 * time.Second, interval: 500 * time.Millisecond, }, }, { desc: "present fail", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil }, provider: &providerMock{ present: errors.New("OOPS"), }, }, { desc: "cleanUp fail", validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, preCheck: func(_, _, _ string, _ PreCheckFunc) (bool, error) { return true, nil }, provider: &providerMock{ cleanUp: errors.New("OOPS"), }, expectError: true, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { chlg := NewChallenge(core, test.validate, test.provider, WrapPreCheck(test.preCheck)) authz := acme.Authorization{ Identifier: acme.Identifier{ Value: "example.com", }, Challenges: []acme.Challenge{ {Type: challenge.DNS01.String()}, }, } err = chlg.CleanUp(authz) if test.expectError { require.Error(t, err) } else { require.NoError(t, err) } }) } } lego-4.9.1/challenge/dns01/fixtures/000077500000000000000000000000001434020463500171745ustar00rootroot00000000000000lego-4.9.1/challenge/dns01/fixtures/resolv.conf.1000066400000000000000000000002031434020463500215070ustar00rootroot00000000000000domain company.com nameserver 10.200.3.249 nameserver 10.200.3.250:5353 nameserver 2001:4860:4860::8844 nameserver [10.0.0.1]:5353 lego-4.9.1/challenge/dns01/fqdn.go000066400000000000000000000006071434020463500166050ustar00rootroot00000000000000package dns01 // ToFqdn converts the name into a fqdn appending a trailing dot. func ToFqdn(name string) string { n := len(name) if n == 0 || name[n-1] == '.' { return name } return name + "." } // UnFqdn converts the fqdn into a name removing the trailing dot. func UnFqdn(name string) string { n := len(name) if n != 0 && name[n-1] == '.' { return name[:n-1] } return name } lego-4.9.1/challenge/dns01/fqdn_test.go000066400000000000000000000020471434020463500176440ustar00rootroot00000000000000package dns01 import ( "testing" "github.com/stretchr/testify/assert" ) func TestToFqdn(t *testing.T) { testCases := []struct { desc string domain string expected string }{ { desc: "simple", domain: "foo.bar.com", expected: "foo.bar.com.", }, { desc: "already FQDN", domain: "foo.bar.com.", expected: "foo.bar.com.", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() fqdn := ToFqdn(test.domain) assert.Equal(t, test.expected, fqdn) }) } } func TestUnFqdn(t *testing.T) { testCases := []struct { desc string fqdn string expected string }{ { desc: "simple", fqdn: "foo.bar.com.", expected: "foo.bar.com", }, { desc: "already domain", fqdn: "foo.bar.com", expected: "foo.bar.com", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() domain := UnFqdn(test.fqdn) assert.Equal(t, test.expected, domain) }) } } lego-4.9.1/challenge/dns01/nameserver.go000066400000000000000000000161421434020463500200250ustar00rootroot00000000000000package dns01 import ( "errors" "fmt" "net" "strings" "sync" "time" "github.com/miekg/dns" ) const defaultResolvConf = "/etc/resolv.conf" var ( fqdnSoaCache = map[string]*soaCacheEntry{} muFqdnSoaCache sync.Mutex ) var defaultNameservers = []string{ "google-public-dns-a.google.com:53", "google-public-dns-b.google.com:53", } // recursiveNameservers are used to pre-check DNS propagation. var recursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers) // soaCacheEntry holds a cached SOA record (only selected fields). type soaCacheEntry struct { zone string // zone apex (a domain name) primaryNs string // primary nameserver for the zone apex expires time.Time // time when this cache entry should be evicted } func newSoaCacheEntry(soa *dns.SOA) *soaCacheEntry { return &soaCacheEntry{ zone: soa.Hdr.Name, primaryNs: soa.Ns, expires: time.Now().Add(time.Duration(soa.Refresh) * time.Second), } } // isExpired checks whether a cache entry should be considered expired. func (cache *soaCacheEntry) isExpired() bool { return time.Now().After(cache.expires) } // ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing. func ClearFqdnCache() { muFqdnSoaCache.Lock() fqdnSoaCache = map[string]*soaCacheEntry{} muFqdnSoaCache.Unlock() } func AddDNSTimeout(timeout time.Duration) ChallengeOption { return func(_ *Challenge) error { dnsTimeout = timeout return nil } } func AddRecursiveNameservers(nameservers []string) ChallengeOption { return func(_ *Challenge) error { recursiveNameservers = ParseNameservers(nameservers) return nil } } // getNameservers attempts to get systems nameservers before falling back to the defaults. func getNameservers(path string, defaults []string) []string { config, err := dns.ClientConfigFromFile(path) if err != nil || len(config.Servers) == 0 { return defaults } return ParseNameservers(config.Servers) } func ParseNameservers(servers []string) []string { var resolvers []string for _, resolver := range servers { // ensure all servers have a port number if _, _, err := net.SplitHostPort(resolver); err != nil { resolvers = append(resolvers, net.JoinHostPort(resolver, "53")) } else { resolvers = append(resolvers, resolver) } } return resolvers } // lookupNameservers returns the authoritative nameservers for the given fqdn. func lookupNameservers(fqdn string) ([]string, error) { var authoritativeNss []string zone, err := FindZoneByFqdn(fqdn) if err != nil { return nil, fmt.Errorf("could not determine the zone: %w", err) } r, err := dnsQuery(zone, dns.TypeNS, recursiveNameservers, true) if err != nil { return nil, err } for _, rr := range r.Answer { if ns, ok := rr.(*dns.NS); ok { authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns)) } } if len(authoritativeNss) > 0 { return authoritativeNss, nil } return nil, errors.New("could not determine authoritative nameservers") } // FindPrimaryNsByFqdn determines the primary nameserver of the zone apex for the given fqdn // by recursing up the domain labels until the nameserver returns a SOA record in the answer section. func FindPrimaryNsByFqdn(fqdn string) (string, error) { return FindPrimaryNsByFqdnCustom(fqdn, recursiveNameservers) } // FindPrimaryNsByFqdnCustom determines the primary nameserver of the zone apex for the given fqdn // by recursing up the domain labels until the nameserver returns a SOA record in the answer section. func FindPrimaryNsByFqdnCustom(fqdn string, nameservers []string) (string, error) { soa, err := lookupSoaByFqdn(fqdn, nameservers) if err != nil { return "", err } return soa.primaryNs, nil } // FindZoneByFqdn determines the zone apex for the given fqdn // by recursing up the domain labels until the nameserver returns a SOA record in the answer section. func FindZoneByFqdn(fqdn string) (string, error) { return FindZoneByFqdnCustom(fqdn, recursiveNameservers) } // FindZoneByFqdnCustom determines the zone apex for the given fqdn // by recursing up the domain labels until the nameserver returns a SOA record in the answer section. func FindZoneByFqdnCustom(fqdn string, nameservers []string) (string, error) { soa, err := lookupSoaByFqdn(fqdn, nameservers) if err != nil { return "", err } return soa.zone, nil } func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) { muFqdnSoaCache.Lock() defer muFqdnSoaCache.Unlock() // Do we have it cached and is it still fresh? if ent := fqdnSoaCache[fqdn]; ent != nil && !ent.isExpired() { return ent, nil } ent, err := fetchSoaByFqdn(fqdn, nameservers) if err != nil { return nil, err } fqdnSoaCache[fqdn] = ent return ent, nil } func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) { var err error var in *dns.Msg labelIndexes := dns.Split(fqdn) for _, index := range labelIndexes { domain := fqdn[index:] in, err = dnsQuery(domain, dns.TypeSOA, nameservers, true) if err != nil { continue } if in == nil { continue } switch in.Rcode { case dns.RcodeSuccess: // Check if we got a SOA RR in the answer section if len(in.Answer) == 0 { continue } // CNAME records cannot/should not exist at the root of a zone. // So we skip a domain when a CNAME is found. if dnsMsgContainsCNAME(in) { continue } for _, ans := range in.Answer { if soa, ok := ans.(*dns.SOA); ok { return newSoaCacheEntry(soa), nil } } case dns.RcodeNameError: // NXDOMAIN default: // Any response code other than NOERROR and NXDOMAIN is treated as error return nil, fmt.Errorf("unexpected response code '%s' for %s", dns.RcodeToString[in.Rcode], domain) } } return nil, fmt.Errorf("could not find the start of authority for %s%s", fqdn, formatDNSError(in, err)) } // dnsMsgContainsCNAME checks for a CNAME answer in msg. func dnsMsgContainsCNAME(msg *dns.Msg) bool { for _, ans := range msg.Answer { if _, ok := ans.(*dns.CNAME); ok { return true } } return false } func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (*dns.Msg, error) { m := createDNSMsg(fqdn, rtype, recursive) var in *dns.Msg var err error for _, ns := range nameservers { in, err = sendDNSQuery(m, ns) if err == nil && len(in.Answer) > 0 { break } } return in, err } func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg { m := new(dns.Msg) m.SetQuestion(fqdn, rtype) m.SetEdns0(4096, false) if !recursive { m.RecursionDesired = false } return m } func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) { udp := &dns.Client{Net: "udp", Timeout: dnsTimeout} in, _, err := udp.Exchange(m, ns) if in != nil && in.Truncated { tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout} // If the TCP request succeeds, the err will reset to nil in, _, err = tcp.Exchange(m, ns) } return in, err } func formatDNSError(msg *dns.Msg, err error) string { var parts []string if msg != nil { parts = append(parts, dns.RcodeToString[msg.Rcode]) } if err != nil { parts = append(parts, err.Error()) } if len(parts) > 0 { return ": " + strings.Join(parts, " ") } return "" } lego-4.9.1/challenge/dns01/nameserver_test.go000066400000000000000000000112661434020463500210660ustar00rootroot00000000000000package dns01 import ( "sort" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestLookupNameserversOK(t *testing.T) { testCases := []struct { fqdn string nss []string }{ { fqdn: "books.google.com.ng.", nss: []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, }, { fqdn: "www.google.com.", nss: []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, }, { fqdn: "physics.georgetown.edu.", nss: []string{"ns4.georgetown.edu.", "ns5.georgetown.edu.", "ns6.georgetown.edu."}, }, } for _, test := range testCases { test := test t.Run(test.fqdn, func(t *testing.T) { t.Parallel() nss, err := lookupNameservers(test.fqdn) require.NoError(t, err) sort.Strings(nss) sort.Strings(test.nss) assert.EqualValues(t, test.nss, nss) }) } } func TestLookupNameserversErr(t *testing.T) { testCases := []struct { desc string fqdn string error string }{ { desc: "invalid tld", fqdn: "_null.n0n0.", error: "could not determine the zone", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() _, err := lookupNameservers(test.fqdn) require.Error(t, err) assert.Contains(t, err.Error(), test.error) }) } } var findXByFqdnTestCases = []struct { desc string fqdn string zone string primaryNs string nameservers []string expectedError string }{ { desc: "domain is a CNAME", fqdn: "mail.google.com.", zone: "google.com.", primaryNs: "ns1.google.com.", nameservers: recursiveNameservers, }, { desc: "domain is a non-existent subdomain", fqdn: "foo.google.com.", zone: "google.com.", primaryNs: "ns1.google.com.", nameservers: recursiveNameservers, }, { desc: "domain is a eTLD", fqdn: "example.com.ac.", zone: "ac.", primaryNs: "a0.nic.ac.", nameservers: recursiveNameservers, }, { desc: "domain is a cross-zone CNAME", fqdn: "cross-zone-example.assets.sh.", zone: "assets.sh.", primaryNs: "gina.ns.cloudflare.com.", nameservers: recursiveNameservers, }, { desc: "NXDOMAIN", fqdn: "test.lego.zz.", zone: "lego.zz.", nameservers: []string{"8.8.8.8:53"}, expectedError: "could not find the start of authority for test.lego.zz.: NXDOMAIN", }, { desc: "several non existent nameservers", fqdn: "mail.google.com.", zone: "google.com.", primaryNs: "ns1.google.com.", nameservers: []string{":7053", ":8053", "8.8.8.8:53"}, }, { desc: "only non-existent nameservers", fqdn: "mail.google.com.", zone: "google.com.", nameservers: []string{":7053", ":8053", ":9053"}, expectedError: "could not find the start of authority for mail.google.com.: read udp", }, { desc: "no nameservers", fqdn: "test.ldez.com.", zone: "ldez.com.", nameservers: []string{}, expectedError: "could not find the start of authority for test.ldez.com.", }, } func TestFindZoneByFqdnCustom(t *testing.T) { for _, test := range findXByFqdnTestCases { t.Run(test.desc, func(t *testing.T) { ClearFqdnCache() zone, err := FindZoneByFqdnCustom(test.fqdn, test.nameservers) if test.expectedError != "" { require.Error(t, err) assert.Contains(t, err.Error(), test.expectedError) } else { require.NoError(t, err) assert.Equal(t, test.zone, zone) } }) } } func TestFindPrimaryNsByFqdnCustom(t *testing.T) { for _, test := range findXByFqdnTestCases { t.Run(test.desc, func(t *testing.T) { ClearFqdnCache() ns, err := FindPrimaryNsByFqdnCustom(test.fqdn, test.nameservers) if test.expectedError != "" { require.Error(t, err) assert.Contains(t, err.Error(), test.expectedError) } else { require.NoError(t, err) assert.Equal(t, test.primaryNs, ns) } }) } } func TestResolveConfServers(t *testing.T) { testCases := []struct { fixture string expected []string defaults []string }{ { fixture: "fixtures/resolv.conf.1", defaults: []string{"127.0.0.1:53"}, expected: []string{"10.200.3.249:53", "10.200.3.250:5353", "[2001:4860:4860::8844]:53", "[10.0.0.1]:5353"}, }, { fixture: "fixtures/resolv.conf.nonexistant", defaults: []string{"127.0.0.1:53"}, expected: []string{"127.0.0.1:53"}, }, } for _, test := range testCases { t.Run(test.fixture, func(t *testing.T) { result := getNameservers(test.fixture, test.defaults) sort.Strings(result) sort.Strings(test.expected) assert.Equal(t, test.expected, result) }) } } lego-4.9.1/challenge/dns01/nameserver_unix.go000066400000000000000000000002361434020463500210650ustar00rootroot00000000000000//go:build !windows package dns01 import "time" // dnsTimeout is used to override the default DNS timeout of 10 seconds. var dnsTimeout = 10 * time.Second lego-4.9.1/challenge/dns01/nameserver_windows.go000066400000000000000000000002351434020463500215730ustar00rootroot00000000000000//go:build windows package dns01 import "time" // dnsTimeout is used to override the default DNS timeout of 20 seconds. var dnsTimeout = 20 * time.Second lego-4.9.1/challenge/dns01/precheck.go000066400000000000000000000056261434020463500174470ustar00rootroot00000000000000package dns01 import ( "fmt" "net" "strings" "github.com/miekg/dns" ) // PreCheckFunc checks DNS propagation before notifying ACME that the DNS challenge is ready. type PreCheckFunc func(fqdn, value string) (bool, error) // WrapPreCheckFunc wraps a PreCheckFunc in order to do extra operations before or after // the main check, put it in a loop, etc. type WrapPreCheckFunc func(domain, fqdn, value string, check PreCheckFunc) (bool, error) // WrapPreCheck Allow to define checks before notifying ACME that the DNS challenge is ready. func WrapPreCheck(wrap WrapPreCheckFunc) ChallengeOption { return func(chlg *Challenge) error { chlg.preCheck.checkFunc = wrap return nil } } func DisableCompletePropagationRequirement() ChallengeOption { return func(chlg *Challenge) error { chlg.preCheck.requireCompletePropagation = false return nil } } type preCheck struct { // checks DNS propagation before notifying ACME that the DNS challenge is ready. checkFunc WrapPreCheckFunc // require the TXT record to be propagated to all authoritative name servers requireCompletePropagation bool } func newPreCheck() preCheck { return preCheck{ requireCompletePropagation: true, } } func (p preCheck) call(domain, fqdn, value string) (bool, error) { if p.checkFunc == nil { return p.checkDNSPropagation(fqdn, value) } return p.checkFunc(domain, fqdn, value, p.checkDNSPropagation) } // checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers. func (p preCheck) checkDNSPropagation(fqdn, value string) (bool, error) { // Initial attempt to resolve at the recursive NS r, err := dnsQuery(fqdn, dns.TypeTXT, recursiveNameservers, true) if err != nil { return false, err } if !p.requireCompletePropagation { return true, nil } if r.Rcode == dns.RcodeSuccess { fqdn = updateDomainWithCName(r, fqdn) } authoritativeNss, err := lookupNameservers(fqdn) if err != nil { return false, err } return checkAuthoritativeNss(fqdn, value, authoritativeNss) } // checkAuthoritativeNss queries each of the given nameservers for the expected TXT record. func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) { for _, ns := range nameservers { r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false) if err != nil { return false, err } if r.Rcode != dns.RcodeSuccess { return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn) } var records []string var found bool for _, rr := range r.Answer { if txt, ok := rr.(*dns.TXT); ok { record := strings.Join(txt.Txt, "") records = append(records, record) if record == value { found = true break } } } if !found { return false, fmt.Errorf("NS %s did not return the expected TXT record [fqdn: %s, value: %s]: %s", ns, fqdn, value, strings.Join(records, " ,")) } } return true, nil } lego-4.9.1/challenge/dns01/precheck_test.go000066400000000000000000000051351434020463500205010ustar00rootroot00000000000000package dns01 import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCheckDNSPropagation(t *testing.T) { testCases := []struct { desc string fqdn string value string expectError bool }{ { desc: "success", fqdn: "postman-echo.com.", value: "postman-domain-verification=c85de626cb79d941310696e06558e2e790223802f3697dfbdcaf65510152d52c", }, { desc: "no TXT record", fqdn: "acme-staging.api.letsencrypt.org.", value: "fe01=", expectError: true, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() ClearFqdnCache() check := newPreCheck() ok, err := check.checkDNSPropagation(test.fqdn, test.value) if test.expectError { assert.Errorf(t, err, "PreCheckDNS must failed for %s", test.fqdn) assert.False(t, ok, "PreCheckDNS must failed for %s", test.fqdn) } else { assert.NoErrorf(t, err, "PreCheckDNS failed for %s", test.fqdn) assert.True(t, ok, "PreCheckDNS failed for %s", test.fqdn) } }) } } func TestCheckAuthoritativeNss(t *testing.T) { testCases := []struct { desc string fqdn, value string ns []string expected bool }{ { desc: "TXT RR w/ expected value", fqdn: "8.8.8.8.asn.routeviews.org.", value: "151698.8.8.024", ns: []string{"asnums.routeviews.org."}, expected: true, }, { desc: "No TXT RR", fqdn: "ns1.google.com.", ns: []string{"ns2.google.com."}, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() ClearFqdnCache() ok, _ := checkAuthoritativeNss(test.fqdn, test.value, test.ns) assert.Equal(t, test.expected, ok, test.fqdn) }) } } func TestCheckAuthoritativeNssErr(t *testing.T) { testCases := []struct { desc string fqdn, value string ns []string error string }{ { desc: "TXT RR /w unexpected value", fqdn: "8.8.8.8.asn.routeviews.org.", value: "fe01=", ns: []string{"asnums.routeviews.org."}, error: "did not return the expected TXT record", }, { desc: "No TXT RR", fqdn: "ns1.google.com.", value: "fe01=", ns: []string{"ns2.google.com."}, error: "did not return the expected TXT record", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() ClearFqdnCache() _, err := checkAuthoritativeNss(test.fqdn, test.value, test.ns) require.Error(t, err) assert.Contains(t, err.Error(), test.error) }) } } lego-4.9.1/challenge/http01/000077500000000000000000000000001434020463500155165ustar00rootroot00000000000000lego-4.9.1/challenge/http01/domain_matcher.go000066400000000000000000000112651434020463500210240ustar00rootroot00000000000000package http01 import ( "fmt" "net/http" "strings" ) // A domainMatcher tries to match a domain (the one we're requesting a certificate for) // in the HTTP request coming from the ACME validation servers. // This step is part of DNS rebind attack prevention, // where the webserver matches incoming requests to a list of domain the server acts authoritative for. // // The most simple check involves finding the domain in the HTTP Host header; // this is what hostMatcher does. // Use it, when the http01.ProviderServer is directly reachable from the internet, // or when it operates behind a transparent proxy. // // In many (reverse) proxy setups, Apache and NGINX traditionally move the Host header to a new header named X-Forwarded-Host. // Use arbitraryMatcher("X-Forwarded-Host") in this case, // or the appropriate header name for other proxy servers. // // RFC7239 has standardized the different forwarding headers into a single header named Forwarded. // The header value has a different format, so you should use forwardedMatcher // when the http01.ProviderServer operates behind a RFC7239 compatible proxy. // https://www.rfc-editor.org/rfc/rfc7239.html // // Note: RFC7239 also reminds us, "that an HTTP list [...] may be split over multiple header fields" (section 7.1), // meaning that // // X-Header: a // X-Header: b // // is equal to // // X-Header: a, b // // All matcher implementations (explicitly not excluding arbitraryMatcher!) // have in common that they only match against the first value in such lists. type domainMatcher interface { // matches checks whether the request is valid for the given domain. matches(request *http.Request, domain string) bool // name returns the header name used in the check. // This is primarily used to create meaningful error messages. name() string } // hostMatcher checks whether (*net/http).Request.Host starts with a domain name. type hostMatcher struct{} func (m *hostMatcher) name() string { return "Host" } func (m *hostMatcher) matches(r *http.Request, domain string) bool { return strings.HasPrefix(r.Host, domain) } // hostMatcher checks whether the specified (*net/http.Request).Header value starts with a domain name. type arbitraryMatcher string func (m arbitraryMatcher) name() string { return string(m) } func (m arbitraryMatcher) matches(r *http.Request, domain string) bool { return strings.HasPrefix(r.Header.Get(m.name()), domain) } // forwardedMatcher checks whether the Forwarded header contains a "host" element starting with a domain name. // See https://www.rfc-editor.org/rfc/rfc7239.html for details. type forwardedMatcher struct{} func (m *forwardedMatcher) name() string { return "Forwarded" } func (m *forwardedMatcher) matches(r *http.Request, domain string) bool { fwds, err := parseForwardedHeader(r.Header.Get(m.name())) if err != nil { return false } if len(fwds) == 0 { return false } host := fwds[0]["host"] return strings.HasPrefix(host, domain) } // parsing requires some form of state machine. func parseForwardedHeader(s string) (elements []map[string]string, err error) { cur := make(map[string]string) key := "" val := "" inquote := false pos := 0 l := len(s) for i := 0; i < l; i++ { r := rune(s[i]) if inquote { if r == '"' { cur[key] = s[pos:i] key = "" pos = i inquote = false } continue } switch { case r == '"': // start of quoted-string if key == "" { return nil, fmt.Errorf("unexpected quoted string as pos %d", i) } inquote = true pos = i + 1 case r == ';': // end of forwarded-pair cur[key] = s[pos:i] key = "" i = skipWS(s, i) pos = i + 1 case r == '=': // end of token key = strings.ToLower(strings.TrimFunc(s[pos:i], isWS)) i = skipWS(s, i) pos = i + 1 case r == ',': // end of forwarded-element if key != "" { if val == "" { val = s[pos:i] } cur[key] = val } elements = append(elements, cur) cur = make(map[string]string) key = "" val = "" i = skipWS(s, i) pos = i + 1 case tchar(r) || isWS(r): // valid token character or whitespace continue default: return nil, fmt.Errorf("invalid token character at pos %d: %c", i, r) } } if inquote { return nil, fmt.Errorf("unterminated quoted-string at pos %d", len(s)) } if key != "" { if pos < len(s) { val = s[pos:] } cur[key] = val } if len(cur) > 0 { elements = append(elements, cur) } return elements, nil } func tchar(r rune) bool { return strings.ContainsRune("!#$%&'*+-.^_`|~", r) || '0' <= r && r <= '9' || 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' } func skipWS(s string, i int) int { for isWS(rune(s[i+1])) { i++ } return i } func isWS(r rune) bool { return strings.ContainsRune(" \t\v\r\n", r) } lego-4.9.1/challenge/http01/domain_matcher_test.go000066400000000000000000000031651434020463500220630ustar00rootroot00000000000000package http01 import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestParseForwardedHeader(t *testing.T) { testCases := []struct { name string input string want []map[string]string err string }{ { name: "empty input", input: "", want: nil, }, { name: "simple case", input: `for=1.2.3.4;host=example.com; by=127.0.0.1`, want: []map[string]string{ {"for": "1.2.3.4", "host": "example.com", "by": "127.0.0.1"}, }, }, { name: "quoted-string", input: `foo="bar"`, want: []map[string]string{ {"foo": "bar"}, }, }, { name: "multiple entries", input: `a=1, b=2; c=3, d=4`, want: []map[string]string{ {"a": "1"}, {"b": "2", "c": "3"}, {"d": "4"}, }, }, { name: "whitespace", input: " a = 1,\tb\n=\r\n2,c=\" untrimmed \"", want: []map[string]string{ {"a": "1"}, {"b": "2"}, {"c": " untrimmed "}, }, }, { name: "unterminated quote", input: `x="y`, err: "unterminated quoted-string", }, { name: "unexpected quote", input: `"x=y"`, err: "unexpected quote", }, { name: "invalid token", input: `a=b, ipv6=[fe80::1], x=y`, err: "invalid token character at pos 10: [", }, } for _, test := range testCases { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() actual, err := parseForwardedHeader(test.input) if test.err == "" { require.NoError(t, err) assert.EqualValues(t, test.want, actual) } else { require.Error(t, err) assert.Contains(t, err.Error(), test.err) } }) } } lego-4.9.1/challenge/http01/http_challenge.go000066400000000000000000000031401434020463500210240ustar00rootroot00000000000000package http01 import ( "fmt" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/log" ) type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error // ChallengePath returns the URL path for the `http-01` challenge. func ChallengePath(token string) string { return "/.well-known/acme-challenge/" + token } type Challenge struct { core *api.Core validate ValidateFunc provider challenge.Provider } func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge { return &Challenge{ core: core, validate: validate, provider: provider, } } func (c *Challenge) SetProvider(provider challenge.Provider) { c.provider = provider } func (c *Challenge) Solve(authz acme.Authorization) error { domain := challenge.GetTargetedDomain(authz) log.Infof("[%s] acme: Trying to solve HTTP-01", domain) chlng, err := challenge.FindChallenge(challenge.HTTP01, authz) if err != nil { return err } // Generate the Key Authorization for the challenge keyAuth, err := c.core.GetKeyAuthorization(chlng.Token) if err != nil { return err } err = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth) if err != nil { return fmt.Errorf("[%s] acme: error presenting token: %w", domain, err) } defer func() { err := c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth) if err != nil { log.Warnf("[%s] acme: cleaning up failed: %v", domain, err) } }() chlng.KeyAuthorization = keyAuth return c.validate(c.core, domain, chlng) } lego-4.9.1/challenge/http01/http_challenge_server.go000066400000000000000000000107161434020463500224210ustar00rootroot00000000000000package http01 import ( "fmt" "io/fs" "net" "net/http" "net/textproto" "os" "strings" "github.com/go-acme/lego/v4/log" ) // ProviderServer implements ChallengeProvider for `http-01` challenge. // It may be instantiated without using the NewProviderServer function if // you want only to use the default values. type ProviderServer struct { address string network string // must be valid argument to net.Listen socketMode fs.FileMode matcher domainMatcher done chan bool listener net.Listener } // NewProviderServer creates a new ProviderServer on the selected interface and port. // Setting iface and / or port to an empty string will make the server fall back to // the "any" interface and port 80 respectively. func NewProviderServer(iface, port string) *ProviderServer { if port == "" { port = "80" } return &ProviderServer{network: "tcp", address: net.JoinHostPort(iface, port), matcher: &hostMatcher{}} } func NewUnixProviderServer(socketPath string, mode fs.FileMode) *ProviderServer { return &ProviderServer{network: "unix", address: socketPath, socketMode: mode, matcher: &hostMatcher{}} } // Present starts a web server and makes the token available at `ChallengePath(token)` for web requests. func (s *ProviderServer) Present(domain, token, keyAuth string) error { var err error s.listener, err = net.Listen(s.network, s.GetAddress()) if err != nil { return fmt.Errorf("could not start HTTP server for challenge: %w", err) } if s.network == "unix" { if err = os.Chmod(s.address, s.socketMode); err != nil { return fmt.Errorf("chmod %s: %w", s.address, err) } } s.done = make(chan bool) go s.serve(domain, token, keyAuth) return nil } func (s *ProviderServer) GetAddress() string { return s.address } // CleanUp closes the HTTP server and removes the token from `ChallengePath(token)`. func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error { if s.listener == nil { return nil } s.listener.Close() <-s.done return nil } // SetProxyHeader changes the validation of incoming requests. // By default, s matches the "Host" header value to the domain name. // // When the server runs behind a proxy server, this is not the correct place to look at; // Apache and NGINX have traditionally moved the original Host header into a new header named "X-Forwarded-Host". // Other webservers might use different names; // and RFC7239 has standardized a new header named "Forwarded" (with slightly different semantics). // // The exact behavior depends on the value of headerName: // - "" (the empty string) and "Host" will restore the default and only check the Host header // - "Forwarded" will look for a Forwarded header, and inspect it according to https://www.rfc-editor.org/rfc/rfc7239.html // - any other value will check the header value with the same name. func (s *ProviderServer) SetProxyHeader(headerName string) { switch h := textproto.CanonicalMIMEHeaderKey(headerName); h { case "", "Host": s.matcher = &hostMatcher{} case "Forwarded": s.matcher = &forwardedMatcher{} default: s.matcher = arbitraryMatcher(h) } } func (s *ProviderServer) serve(domain, token, keyAuth string) { path := ChallengePath(token) // The incoming request will be validated to prevent DNS rebind attacks. // We only respond with the keyAuth, when we're receiving a GET requests with // the "Host" header matching the domain (the latter is configurable though SetProxyHeader). mux := http.NewServeMux() mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet && s.matcher.matches(r, domain) { w.Header().Set("Content-Type", "text/plain") _, err := w.Write([]byte(keyAuth)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } log.Infof("[%s] Served key authentication", domain) } else { log.Warnf("Received request for domain %s with method %s but the domain did not match any challenge. Please ensure you are passing the %s header properly.", r.Host, r.Method, s.matcher.name()) _, err := w.Write([]byte("TEST")) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } }) httpServer := &http.Server{Handler: mux} // Once httpServer is shut down // we don't want any lingering connections, so disable KeepAlives. httpServer.SetKeepAlivesEnabled(false) err := httpServer.Serve(s.listener) if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { log.Println(err) } s.done <- true } lego-4.9.1/challenge/http01/http_challenge_test.go000066400000000000000000000241021434020463500220640ustar00rootroot00000000000000package http01 import ( "context" "crypto/rand" "crypto/rsa" "fmt" "io" "io/fs" "net" "net/http" "net/textproto" "os" "path/filepath" "runtime" "testing" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestProviderServer_GetAddress(t *testing.T) { dir := t.TempDir() t.Cleanup(func() { _ = os.RemoveAll(dir) }) sock := filepath.Join(dir, "var", "run", "test") testCases := []struct { desc string server *ProviderServer expected string }{ { desc: "TCP default address", server: NewProviderServer("", ""), expected: ":80", }, { desc: "TCP with explicit port", server: NewProviderServer("", "8080"), expected: ":8080", }, { desc: "TCP with host and port", server: NewProviderServer("localhost", "8080"), expected: "localhost:8080", }, { desc: "UDS socket", server: NewUnixProviderServer(sock, fs.ModeSocket|0o666), expected: sock, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() address := test.server.GetAddress() assert.Equal(t, test.expected, address) }) } } func TestChallenge(t *testing.T) { _, apiURL := tester.SetupFakeAPI(t) providerServer := NewProviderServer("", "23457") validate := func(_ *api.Core, _ string, chlng acme.Challenge) error { uri := "http://localhost" + providerServer.GetAddress() + ChallengePath(chlng.Token) resp, err := http.DefaultClient.Get(uri) if err != nil { return err } defer resp.Body.Close() if want := "text/plain"; resp.Header.Get("Content-Type") != want { t.Errorf("Get(%q) Content-Type: got %q, want %q", uri, resp.Header.Get("Content-Type"), want) } body, err := io.ReadAll(resp.Body) if err != nil { return err } bodyStr := string(body) if bodyStr != chlng.KeyAuthorization { t.Errorf("Get(%q) Body: got %q, want %q", uri, bodyStr, chlng.KeyAuthorization) } return nil } privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err, "Could not generate test key") core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) solver := NewChallenge(core, validate, providerServer) authz := acme.Authorization{ Identifier: acme.Identifier{ Value: "localhost:23457", }, Challenges: []acme.Challenge{ {Type: challenge.HTTP01.String(), Token: "http1"}, }, } err = solver.Solve(authz) require.NoError(t, err) } func TestChallengeUnix(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("only for UNIX systems") } _, apiURL := tester.SetupFakeAPI(t) dir := t.TempDir() t.Cleanup(func() { _ = os.RemoveAll(dir) }) socket := filepath.Join(dir, "lego-challenge-test.sock") providerServer := NewUnixProviderServer(socket, fs.ModeSocket|0o666) validate := func(_ *api.Core, _ string, chlng acme.Challenge) error { // any uri will do, as we hijack the dial uri := "http://localhost" + ChallengePath(chlng.Token) client := &http.Client{Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return net.Dial("unix", socket) }, }} resp, err := client.Get(uri) if err != nil { return err } defer resp.Body.Close() if want := "text/plain"; resp.Header.Get("Content-Type") != want { t.Errorf("Get(%q) Content-Type: got %q, want %q", uri, resp.Header.Get("Content-Type"), want) } body, err := io.ReadAll(resp.Body) if err != nil { return err } bodyStr := string(body) if bodyStr != chlng.KeyAuthorization { t.Errorf("Get(%q) Body: got %q, want %q", uri, bodyStr, chlng.KeyAuthorization) } return nil } privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err, "Could not generate test key") core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) solver := NewChallenge(core, validate, providerServer) authz := acme.Authorization{ Identifier: acme.Identifier{ Value: "localhost", }, Challenges: []acme.Challenge{ {Type: challenge.HTTP01.String(), Token: "http1"}, }, } err = solver.Solve(authz) require.NoError(t, err) } func TestChallengeInvalidPort(t *testing.T) { _, apiURL := tester.SetupFakeAPI(t) privateKey, err := rsa.GenerateKey(rand.Reader, 128) require.NoError(t, err, "Could not generate test key") core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) validate := func(_ *api.Core, _ string, _ acme.Challenge) error { return nil } solver := NewChallenge(core, validate, NewProviderServer("", "123456")) authz := acme.Authorization{ Identifier: acme.Identifier{ Value: "localhost:123456", }, Challenges: []acme.Challenge{ {Type: challenge.HTTP01.String(), Token: "http2"}, }, } err = solver.Solve(authz) require.Error(t, err) assert.Contains(t, err.Error(), "invalid port") assert.Contains(t, err.Error(), "123456") } type testProxyHeader struct { name string values []string } func (h *testProxyHeader) update(r *http.Request) { if h == nil || len(h.values) == 0 { return } if h.name == "Host" { r.Host = h.values[0] } else if h.name != "" { r.Header[h.name] = h.values } } func TestChallengeWithProxy(t *testing.T) { h := func(name string, values ...string) *testProxyHeader { name = textproto.CanonicalMIMEHeaderKey(name) return &testProxyHeader{name, values} } const ( ok = "localhost:23457" nook = "example.com" ) testCases := []struct { name string header *testProxyHeader extra *testProxyHeader isErr bool }{ // tests for hostMatcher { name: "no proxy", }, { name: "empty string", header: h(""), }, { name: "empty Host", header: h("host"), }, { name: "matching Host", header: h("host", ok), }, { name: "Host mismatch", header: h("host", nook), isErr: true, }, { name: "Host mismatch (ignoring forwarding header)", header: h("host", nook), extra: h("X-Forwarded-Host", ok), isErr: true, }, // test for arbitraryMatcher { name: "matching X-Forwarded-Host", header: h("X-Forwarded-Host", ok), }, { name: "matching X-Forwarded-Host (multiple fields)", header: h("X-Forwarded-Host", ok, nook), }, { name: "matching X-Forwarded-Host (chain value)", header: h("X-Forwarded-Host", ok+", "+nook), }, { name: "X-Forwarded-Host mismatch", header: h("X-Forwarded-Host", nook), extra: h("host", ok), isErr: true, }, { name: "X-Forwarded-Host mismatch (multiple fields)", header: h("X-Forwarded-Host", nook, ok), isErr: true, }, { name: "matching X-Something-Else", header: h("X-Something-Else", ok), }, { name: "matching X-Something-Else (multiple fields)", header: h("X-Something-Else", ok, nook), }, { name: "matching X-Something-Else (chain value)", header: h("X-Something-Else", ok+", "+nook), }, { name: "X-Something-Else mismatch", header: h("X-Something-Else", nook), isErr: true, }, { name: "X-Something-Else mismatch (multiple fields)", header: h("X-Something-Else", nook, ok), isErr: true, }, { name: "X-Something-Else mismatch (chain value)", header: h("X-Something-Else", nook+", "+ok), isErr: true, }, // tests for forwardedHeader { name: "matching Forwarded", header: h("Forwarded", fmt.Sprintf("host=%q;foo=bar", ok)), }, { name: "matching Forwarded (multiple fields)", header: h("Forwarded", fmt.Sprintf("host=%q", ok), "host="+nook), }, { name: "matching Forwarded (chain value)", header: h("Forwarded", fmt.Sprintf("host=%q, host=%s", ok, nook)), }, { name: "Forwarded mismatch", header: h("Forwarded", "host="+nook), isErr: true, }, { name: "Forwarded mismatch (missing information)", header: h("Forwarded", "for=127.0.0.1"), isErr: true, }, { name: "Forwarded mismatch (multiple fields)", header: h("Forwarded", "host="+nook, fmt.Sprintf("host=%q", ok)), isErr: true, }, { name: "Forwarded mismatch (chain value)", header: h("Forwarded", fmt.Sprintf("host=%s, host=%q", nook, ok)), isErr: true, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { testServeWithProxy(t, test.header, test.extra, test.isErr) }) } } func testServeWithProxy(t *testing.T, header, extra *testProxyHeader, expectError bool) { t.Helper() _, apiURL := tester.SetupFakeAPI(t) providerServer := NewProviderServer("localhost", "23457") if header != nil { providerServer.SetProxyHeader(header.name) } validate := func(_ *api.Core, _ string, chlng acme.Challenge) error { uri := "http://" + providerServer.GetAddress() + ChallengePath(chlng.Token) req, err := http.NewRequest(http.MethodGet, uri, nil) if err != nil { return err } header.update(req) extra.update(req) resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if want := "text/plain"; resp.Header.Get("Content-Type") != want { return fmt.Errorf("Get(%q) Content-Type: got %q, want %q", uri, resp.Header.Get("Content-Type"), want) } body, err := io.ReadAll(resp.Body) if err != nil { return err } bodyStr := string(body) if bodyStr != chlng.KeyAuthorization { return fmt.Errorf("Get(%q) Body: got %q, want %q", uri, bodyStr, chlng.KeyAuthorization) } return nil } privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err, "Could not generate test key") core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) solver := NewChallenge(core, validate, providerServer) authz := acme.Authorization{ Identifier: acme.Identifier{ Value: "localhost:23457", }, Challenges: []acme.Challenge{ {Type: challenge.HTTP01.String(), Token: "http1"}, }, } err = solver.Solve(authz) if expectError { require.Error(t, err) } else { require.NoError(t, err) } } lego-4.9.1/challenge/provider.go000066400000000000000000000020361434020463500165600ustar00rootroot00000000000000package challenge import "time" // Provider enables implementing a custom challenge // provider. Present presents the solution to a challenge available to // be solved. CleanUp will be called by the challenge if Present ends // in a non-error state. type Provider interface { Present(domain, token, keyAuth string) error CleanUp(domain, token, keyAuth string) error } // ProviderTimeout allows for implementing a // Provider where an unusually long timeout is required when // waiting for an ACME challenge to be satisfied, such as when // checking for DNS record propagation. If an implementor of a // Provider provides a Timeout method, then the return values // of the Timeout method will be used when appropriate by the acme // package. The interval value is the time between checks. // // The default values used for timeout and interval are 60 seconds and // 2 seconds respectively. These are used when no Timeout method is // defined for the Provider. type ProviderTimeout interface { Provider Timeout() (timeout, interval time.Duration) } lego-4.9.1/challenge/resolver/000077500000000000000000000000001434020463500162375ustar00rootroot00000000000000lego-4.9.1/challenge/resolver/errors.go000066400000000000000000000010171434020463500201010ustar00rootroot00000000000000package resolver import ( "bytes" "fmt" "sort" ) // obtainError is returned when there are specific errors available per domain. type obtainError map[string]error func (e obtainError) Error() string { buffer := bytes.NewBufferString("error: one or more domains had a problem:\n") var domains []string for domain := range e { domains = append(domains, domain) } sort.Strings(domains) for _, domain := range domains { buffer.WriteString(fmt.Sprintf("[%s] %s\n", domain, e[domain])) } return buffer.String() } lego-4.9.1/challenge/resolver/prober.go000066400000000000000000000111001434020463500200500ustar00rootroot00000000000000package resolver import ( "fmt" "time" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/log" ) // Interface for all challenge solvers to implement. type solver interface { Solve(authorization acme.Authorization) error } // Interface for challenges like dns, where we can set a record in advance for ALL challenges. // This saves quite a bit of time vs creating the records and solving them serially. type preSolver interface { PreSolve(authorization acme.Authorization) error } // Interface for challenges like dns, where we can solve all the challenges before to delete them. type cleanup interface { CleanUp(authorization acme.Authorization) error } type sequential interface { Sequential() (bool, time.Duration) } // an authz with the solver we have chosen and the index of the challenge associated with it. type selectedAuthSolver struct { authz acme.Authorization solver solver } type Prober struct { solverManager *SolverManager } func NewProber(solverManager *SolverManager) *Prober { return &Prober{ solverManager: solverManager, } } // Solve Looks through the challenge combinations to find a solvable match. // Then solves the challenges in series and returns. func (p *Prober) Solve(authorizations []acme.Authorization) error { failures := make(obtainError) var authSolvers []*selectedAuthSolver var authSolversSequential []*selectedAuthSolver // Loop through the resources, basically through the domains. // First pass just selects a solver for each authz. for _, authz := range authorizations { domain := challenge.GetTargetedDomain(authz) if authz.Status == acme.StatusValid { // Boulder might recycle recent validated authz (see issue #267) log.Infof("[%s] acme: authorization already valid; skipping challenge", domain) continue } if solvr := p.solverManager.chooseSolver(authz); solvr != nil { authSolver := &selectedAuthSolver{authz: authz, solver: solvr} switch s := solvr.(type) { case sequential: if ok, _ := s.Sequential(); ok { authSolversSequential = append(authSolversSequential, authSolver) } else { authSolvers = append(authSolvers, authSolver) } default: authSolvers = append(authSolvers, authSolver) } } else { failures[domain] = fmt.Errorf("[%s] acme: could not determine solvers", domain) } } parallelSolve(authSolvers, failures) sequentialSolve(authSolversSequential, failures) // Be careful not to return an empty failures map, // for even an empty obtainError is a non-nil error value if len(failures) > 0 { return failures } return nil } func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) { for i, authSolver := range authSolvers { // Submit the challenge domain := challenge.GetTargetedDomain(authSolver.authz) if solvr, ok := authSolver.solver.(preSolver); ok { err := solvr.PreSolve(authSolver.authz) if err != nil { failures[domain] = err cleanUp(authSolver.solver, authSolver.authz) continue } } // Solve challenge err := authSolver.solver.Solve(authSolver.authz) if err != nil { failures[domain] = err cleanUp(authSolver.solver, authSolver.authz) continue } // Clean challenge cleanUp(authSolver.solver, authSolver.authz) if len(authSolvers)-1 > i { solvr := authSolver.solver.(sequential) _, interval := solvr.Sequential() log.Infof("sequence: wait for %s", interval) time.Sleep(interval) } } } func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) { // For all valid preSolvers, first submit the challenges so they have max time to propagate for _, authSolver := range authSolvers { authz := authSolver.authz if solvr, ok := authSolver.solver.(preSolver); ok { err := solvr.PreSolve(authz) if err != nil { failures[challenge.GetTargetedDomain(authz)] = err } } } defer func() { // Clean all created TXT records for _, authSolver := range authSolvers { cleanUp(authSolver.solver, authSolver.authz) } }() // Finally solve all challenges for real for _, authSolver := range authSolvers { authz := authSolver.authz domain := challenge.GetTargetedDomain(authz) if failures[domain] != nil { // already failed in previous loop continue } err := authSolver.solver.Solve(authz) if err != nil { failures[domain] = err } } } func cleanUp(solvr solver, authz acme.Authorization) { if solvr, ok := solvr.(cleanup); ok { domain := challenge.GetTargetedDomain(authz) err := solvr.CleanUp(authz) if err != nil { log.Warnf("[%s] acme: cleaning up failed: %v ", domain, err) } } } lego-4.9.1/challenge/resolver/prober_mock_test.go000066400000000000000000000017331434020463500221330ustar00rootroot00000000000000package resolver import ( "time" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/challenge" ) type preSolverMock struct { preSolve map[string]error solve map[string]error cleanUp map[string]error } func (s *preSolverMock) PreSolve(authorization acme.Authorization) error { return s.preSolve[authorization.Identifier.Value] } func (s *preSolverMock) Solve(authorization acme.Authorization) error { return s.solve[authorization.Identifier.Value] } func (s *preSolverMock) CleanUp(authorization acme.Authorization) error { return s.cleanUp[authorization.Identifier.Value] } func createStubAuthorizationHTTP01(domain, status string) acme.Authorization { return acme.Authorization{ Status: status, Expires: time.Now(), Identifier: acme.Identifier{ Type: challenge.HTTP01.String(), Value: domain, }, Challenges: []acme.Challenge{ { Type: challenge.HTTP01.String(), Validated: time.Now(), Error: nil, }, }, } } lego-4.9.1/challenge/resolver/prober_test.go000066400000000000000000000064151434020463500211240ustar00rootroot00000000000000package resolver import ( "errors" "testing" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/challenge" "github.com/stretchr/testify/require" ) func TestProber_Solve(t *testing.T) { testCases := []struct { desc string solvers map[challenge.Type]solver authz []acme.Authorization expectedError string }{ { desc: "success", solvers: map[challenge.Type]solver{ challenge.HTTP01: &preSolverMock{ preSolve: map[string]error{}, solve: map[string]error{}, cleanUp: map[string]error{}, }, }, authz: []acme.Authorization{ createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing), createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing), createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing), }, }, { desc: "already valid", solvers: map[challenge.Type]solver{ challenge.HTTP01: &preSolverMock{ preSolve: map[string]error{}, solve: map[string]error{}, cleanUp: map[string]error{}, }, }, authz: []acme.Authorization{ createStubAuthorizationHTTP01("acme.wtf", acme.StatusValid), createStubAuthorizationHTTP01("lego.wtf", acme.StatusValid), createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusValid), }, }, { desc: "when preSolve fail, auth is flagged as error and skipped", solvers: map[challenge.Type]solver{ challenge.HTTP01: &preSolverMock{ preSolve: map[string]error{ "acme.wtf": errors.New("preSolve error acme.wtf"), }, solve: map[string]error{ "acme.wtf": errors.New("solve error acme.wtf"), }, cleanUp: map[string]error{ "acme.wtf": errors.New("clean error acme.wtf"), }, }, }, authz: []acme.Authorization{ createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing), createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing), createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing), }, expectedError: `error: one or more domains had a problem: [acme.wtf] preSolve error acme.wtf `, }, { desc: "errors at different stages", solvers: map[challenge.Type]solver{ challenge.HTTP01: &preSolverMock{ preSolve: map[string]error{ "acme.wtf": errors.New("preSolve error acme.wtf"), }, solve: map[string]error{ "acme.wtf": errors.New("solve error acme.wtf"), "lego.wtf": errors.New("solve error lego.wtf"), }, cleanUp: map[string]error{ "mydomain.wtf": errors.New("clean error mydomain.wtf"), }, }, }, authz: []acme.Authorization{ createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing), createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing), createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing), }, expectedError: `error: one or more domains had a problem: [acme.wtf] preSolve error acme.wtf [lego.wtf] solve error lego.wtf `, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() prober := &Prober{ solverManager: &SolverManager{solvers: test.solvers}, } err := prober.Solve(test.authz) if test.expectedError != "" { require.EqualError(t, err, test.expectedError) } else { require.NoError(t, err) } }) } } lego-4.9.1/challenge/resolver/solver_manager.go000066400000000000000000000114711434020463500215760ustar00rootroot00000000000000package resolver import ( "errors" "fmt" "sort" "strconv" "time" "github.com/cenkalti/backoff/v4" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/go-acme/lego/v4/log" ) type byType []acme.Challenge func (a byType) Len() int { return len(a) } func (a byType) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byType) Less(i, j int) bool { return a[i].Type > a[j].Type } type SolverManager struct { core *api.Core solvers map[challenge.Type]solver } func NewSolversManager(core *api.Core) *SolverManager { return &SolverManager{ solvers: map[challenge.Type]solver{}, core: core, } } // SetHTTP01Provider specifies a custom provider p that can solve the given HTTP-01 challenge. func (c *SolverManager) SetHTTP01Provider(p challenge.Provider) error { c.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p) return nil } // SetTLSALPN01Provider specifies a custom provider p that can solve the given TLS-ALPN-01 challenge. func (c *SolverManager) SetTLSALPN01Provider(p challenge.Provider) error { c.solvers[challenge.TLSALPN01] = tlsalpn01.NewChallenge(c.core, validate, p) return nil } // SetDNS01Provider specifies a custom provider p that can solve the given DNS-01 challenge. func (c *SolverManager) SetDNS01Provider(p challenge.Provider, opts ...dns01.ChallengeOption) error { c.solvers[challenge.DNS01] = dns01.NewChallenge(c.core, validate, p, opts...) return nil } // Remove Remove a challenge type from the available solvers. func (c *SolverManager) Remove(chlgType challenge.Type) { delete(c.solvers, chlgType) } // Checks all challenges from the server in order and returns the first matching solver. func (c *SolverManager) chooseSolver(authz acme.Authorization) solver { // Allow to have a deterministic challenge order sort.Sort(byType(authz.Challenges)) domain := challenge.GetTargetedDomain(authz) for _, chlg := range authz.Challenges { if solvr, ok := c.solvers[challenge.Type(chlg.Type)]; ok { log.Infof("[%s] acme: use %s solver", domain, chlg.Type) return solvr } log.Infof("[%s] acme: Could not find solver for: %s", domain, chlg.Type) } return nil } func validate(core *api.Core, domain string, chlg acme.Challenge) error { chlng, err := core.Challenges.New(chlg.URL) if err != nil { return fmt.Errorf("failed to initiate challenge: %w", err) } valid, err := checkChallengeStatus(chlng) if err != nil { return err } if valid { log.Infof("[%s] The server validated our request", domain) return nil } ra, err := strconv.Atoi(chlng.RetryAfter) if err != nil { // The ACME server MUST return a Retry-After. // If it doesn't, we'll just poll hard. // Boulder does not implement the ability to retry challenges or the Retry-After header. // https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md#section-82 ra = 5 } initialInterval := time.Duration(ra) * time.Second bo := backoff.NewExponentialBackOff() bo.InitialInterval = initialInterval bo.MaxInterval = 10 * initialInterval bo.MaxElapsedTime = 100 * initialInterval // After the path is sent, the ACME server will access our server. // Repeatedly check the server for an updated status on our request. operation := func() error { authz, err := core.Authorizations.Get(chlng.AuthorizationURL) if err != nil { return backoff.Permanent(err) } valid, err := checkAuthorizationStatus(authz) if err != nil { return backoff.Permanent(err) } if valid { log.Infof("[%s] The server validated our request", domain) return nil } return errors.New("the server didn't respond to our request") } return backoff.Retry(operation, bo) } func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) { switch chlng.Status { case acme.StatusValid: return true, nil case acme.StatusPending, acme.StatusProcessing: return false, nil case acme.StatusInvalid: return false, chlng.Error default: return false, errors.New("the server returned an unexpected state") } } func checkAuthorizationStatus(authz acme.Authorization) (bool, error) { switch authz.Status { case acme.StatusValid: return true, nil case acme.StatusPending, acme.StatusProcessing: return false, nil case acme.StatusDeactivated, acme.StatusExpired, acme.StatusRevoked: return false, fmt.Errorf("the authorization state %s", authz.Status) case acme.StatusInvalid: for _, chlg := range authz.Challenges { if chlg.Status == acme.StatusInvalid && chlg.Error != nil { return false, chlg.Error } } return false, fmt.Errorf("the authorization state %s", authz.Status) default: return false, errors.New("the server returned an unexpected state") } } lego-4.9.1/challenge/resolver/solver_manager_test.go000066400000000000000000000103361434020463500226340ustar00rootroot00000000000000package resolver import ( "crypto/rand" "crypto/rsa" "fmt" "io" "net/http" "sort" "testing" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/square/go-jose.v2" ) func TestByType(t *testing.T) { challenges := []acme.Challenge{ {Type: "dns-01"}, {Type: "tlsalpn-01"}, {Type: "http-01"}, } sort.Sort(byType(challenges)) expected := []acme.Challenge{ {Type: "tlsalpn-01"}, {Type: "http-01"}, {Type: "dns-01"}, } assert.Equal(t, expected, challenges) } func TestValidate(t *testing.T) { mux, apiURL := tester.SetupFakeAPI(t) var statuses []string privateKey, _ := rsa.GenerateKey(rand.Reader, 512) mux.HandleFunc("/chlg", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } if err := validateNoBody(privateKey, r); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.Header().Set("Link", "<"+apiURL+`/my-authz>; rel="up"`) st := statuses[0] statuses = statuses[1:] chlg := &acme.Challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"} if st == acme.StatusInvalid { chlg.Error = &acme.ProblemDetails{} } err := tester.WriteJSONResponse(w, chlg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) mux.HandleFunc("/my-authz", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } st := statuses[0] statuses = statuses[1:] authorization := acme.Authorization{ Status: st, Challenges: []acme.Challenge{}, } if st == acme.StatusInvalid { chlg := acme.Challenge{ Status: acme.StatusInvalid, Error: &acme.ProblemDetails{}, } authorization.Challenges = append(authorization.Challenges, chlg) } err := tester.WriteJSONResponse(w, authorization) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) testCases := []struct { name string statuses []string want string }{ { name: "POST-unexpected", statuses: []string{"weird"}, want: "unexpected", }, { name: "POST-valid", statuses: []string{acme.StatusValid}, }, { name: "POST-invalid", statuses: []string{acme.StatusInvalid}, want: "error", }, { name: "POST-pending-unexpected", statuses: []string{acme.StatusPending, "weird"}, want: "unexpected", }, { name: "POST-pending-valid", statuses: []string{acme.StatusPending, acme.StatusValid}, }, { name: "POST-pending-invalid", statuses: []string{acme.StatusPending, acme.StatusInvalid}, want: "error", }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { statuses = test.statuses err := validate(core, "example.com", acme.Challenge{Type: "http-01", Token: "token", URL: apiURL + "/chlg"}) if test.want == "" { require.NoError(t, err) } else { require.Error(t, err) assert.Contains(t, err.Error(), test.want) } }) } } // validateNoBody reads the http.Request POST body, parses the JWS and validates it to read the body. // If there is an error doing this, // or if the JWS body is not the empty JSON payload "{}" or a POST-as-GET payload "" an error is returned. // We use this to verify challenge POSTs to the ts below do not send a JWS body. func validateNoBody(privateKey *rsa.PrivateKey, r *http.Request) error { reqBody, err := io.ReadAll(r.Body) if err != nil { return err } jws, err := jose.ParseSigned(string(reqBody)) if err != nil { return err } body, err := jws.Verify(&jose.JSONWebKey{ Key: privateKey.Public(), Algorithm: "RSA", }) if err != nil { return err } if bodyStr := string(body); bodyStr != "{}" && bodyStr != "" { return fmt.Errorf(`expected JWS POST body "{}" or "", got %q`, bodyStr) } return nil } lego-4.9.1/challenge/tlsalpn01/000077500000000000000000000000001434020463500162145ustar00rootroot00000000000000lego-4.9.1/challenge/tlsalpn01/tls_alpn_challenge.go000066400000000000000000000073051434020463500223660ustar00rootroot00000000000000package tlsalpn01 import ( "crypto/rsa" "crypto/sha256" "crypto/tls" "crypto/x509/pkix" "encoding/asn1" "fmt" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/log" ) // idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension. // Reference: https://www.rfc-editor.org/rfc/rfc8737.html#section-6.1 var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31} type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error type Challenge struct { core *api.Core validate ValidateFunc provider challenge.Provider } func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge { return &Challenge{ core: core, validate: validate, provider: provider, } } func (c *Challenge) SetProvider(provider challenge.Provider) { c.provider = provider } // Solve manages the provider to validate and solve the challenge. func (c *Challenge) Solve(authz acme.Authorization) error { domain := authz.Identifier.Value log.Infof("[%s] acme: Trying to solve TLS-ALPN-01", challenge.GetTargetedDomain(authz)) chlng, err := challenge.FindChallenge(challenge.TLSALPN01, authz) if err != nil { return err } // Generate the Key Authorization for the challenge keyAuth, err := c.core.GetKeyAuthorization(chlng.Token) if err != nil { return err } err = c.provider.Present(domain, chlng.Token, keyAuth) if err != nil { return fmt.Errorf("[%s] acme: error presenting token: %w", challenge.GetTargetedDomain(authz), err) } defer func() { err := c.provider.CleanUp(domain, chlng.Token, keyAuth) if err != nil { log.Warnf("[%s] acme: cleaning up failed: %v", challenge.GetTargetedDomain(authz), err) } }() chlng.KeyAuthorization = keyAuth return c.validate(c.core, domain, chlng) } // ChallengeBlocks returns PEM blocks (certPEMBlock, keyPEMBlock) with the acmeValidation-v1 extension // and domain name for the `tls-alpn-01` challenge. func ChallengeBlocks(domain, keyAuth string) ([]byte, []byte, error) { // Compute the SHA-256 digest of the key authorization. zBytes := sha256.Sum256([]byte(keyAuth)) value, err := asn1.Marshal(zBytes[:sha256.Size]) if err != nil { return nil, nil, err } // Add the keyAuth digest as the acmeValidation-v1 extension // (marked as critical such that it won't be used by non-ACME software). // Reference: https://www.rfc-editor.org/rfc/rfc8737.html#section-3 extensions := []pkix.Extension{ { Id: idPeAcmeIdentifierV1, Critical: true, Value: value, }, } // Generate a new RSA key for the certificates. tempPrivateKey, err := certcrypto.GeneratePrivateKey(certcrypto.RSA2048) if err != nil { return nil, nil, err } rsaPrivateKey := tempPrivateKey.(*rsa.PrivateKey) // Generate the PEM certificate using the provided private key, domain, and extra extensions. tempCertPEM, err := certcrypto.GeneratePemCert(rsaPrivateKey, domain, extensions) if err != nil { return nil, nil, err } // Encode the private key into a PEM format. We'll need to use it to generate the x509 keypair. rsaPrivatePEM := certcrypto.PEMEncode(rsaPrivateKey) return tempCertPEM, rsaPrivatePEM, nil } // ChallengeCert returns a certificate with the acmeValidation-v1 extension // and domain name for the `tls-alpn-01` challenge. func ChallengeCert(domain, keyAuth string) (*tls.Certificate, error) { tempCertPEM, rsaPrivatePEM, err := ChallengeBlocks(domain, keyAuth) if err != nil { return nil, err } cert, err := tls.X509KeyPair(tempCertPEM, rsaPrivatePEM) if err != nil { return nil, err } return &cert, nil } lego-4.9.1/challenge/tlsalpn01/tls_alpn_challenge_server.go000066400000000000000000000053101434020463500237460ustar00rootroot00000000000000package tlsalpn01 import ( "crypto/tls" "errors" "fmt" "net" "net/http" "strings" "github.com/go-acme/lego/v4/log" ) const ( // ACMETLS1Protocol is the ALPN Protocol ID for the ACME-TLS/1 Protocol. ACMETLS1Protocol = "acme-tls/1" // defaultTLSPort is the port that the ProviderServer will default to // when no other port is provided. defaultTLSPort = "443" ) // ProviderServer implements ChallengeProvider for `TLS-ALPN-01` challenge. // It may be instantiated without using the NewProviderServer // if you want only to use the default values. type ProviderServer struct { iface string port string listener net.Listener } // NewProviderServer creates a new ProviderServer on the selected interface and port. // Setting iface and / or port to an empty string will make the server fall back to // the "any" interface and port 443 respectively. func NewProviderServer(iface, port string) *ProviderServer { return &ProviderServer{iface: iface, port: port} } func (s *ProviderServer) GetAddress() string { return net.JoinHostPort(s.iface, s.port) } // Present generates a certificate with a SHA-256 digest of the keyAuth provided // as the acmeValidation-v1 extension value to conform to the ACME-TLS-ALPN spec. func (s *ProviderServer) Present(domain, token, keyAuth string) error { if s.port == "" { // Fallback to port 443 if the port was not provided. s.port = defaultTLSPort } // Generate the challenge certificate using the provided keyAuth and domain. cert, err := ChallengeCert(domain, keyAuth) if err != nil { return err } // Place the generated certificate with the extension into the TLS config // so that it can serve the correct details. tlsConf := new(tls.Config) tlsConf.Certificates = []tls.Certificate{*cert} // We must set that the `acme-tls/1` application level protocol is supported // so that the protocol negotiation can succeed. Reference: // https://www.rfc-editor.org/rfc/rfc8737.html#section-6.2 tlsConf.NextProtos = []string{ACMETLS1Protocol} // Create the listener with the created tls.Config. s.listener, err = tls.Listen("tcp", s.GetAddress(), tlsConf) if err != nil { return fmt.Errorf("could not start HTTPS server for challenge: %w", err) } // Shut the server down when we're finished. go func() { err := http.Serve(s.listener, nil) if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { log.Println(err) } }() return nil } // CleanUp closes the HTTPS server. func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error { if s.listener == nil { return nil } // Server was created, close it. if err := s.listener.Close(); err != nil && errors.Is(err, http.ErrServerClosed) { return err } return nil } lego-4.9.1/challenge/tlsalpn01/tls_alpn_challenge_test.go000066400000000000000000000066431434020463500234310ustar00rootroot00000000000000package tlsalpn01 import ( "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/subtle" "crypto/tls" "encoding/asn1" "net/http" "testing" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestChallenge(t *testing.T) { _, apiURL := tester.SetupFakeAPI(t) domain := "localhost:23457" mockValidate := func(_ *api.Core, _ string, chlng acme.Challenge) error { conn, err := tls.Dial("tcp", domain, &tls.Config{ InsecureSkipVerify: true, }) require.NoError(t, err, "Expected to connect to challenge server without an error") // Expect the server to only return one certificate connState := conn.ConnectionState() assert.Len(t, connState.PeerCertificates, 1, "Expected the challenge server to return exactly one certificate") remoteCert := connState.PeerCertificates[0] assert.Len(t, remoteCert.DNSNames, 1, "Expected the challenge certificate to have exactly one DNSNames entry") assert.Equal(t, domain, remoteCert.DNSNames[0], "challenge certificate DNSName ") assert.NotEmpty(t, remoteCert.Extensions, "Expected the challenge certificate to contain extensions") idx := -1 for i, ext := range remoteCert.Extensions { if idPeAcmeIdentifierV1.Equal(ext.Id) { idx = i break } } require.NotEqual(t, -1, idx, "Expected the challenge certificate to contain an extension with the id-pe-acmeIdentifier id,") ext := remoteCert.Extensions[idx] assert.True(t, ext.Critical, "Expected the challenge certificate id-pe-acmeIdentifier extension to be marked as critical") zBytes := sha256.Sum256([]byte(chlng.KeyAuthorization)) value, err := asn1.Marshal(zBytes[:sha256.Size]) require.NoError(t, err, "Expected marshaling of the keyAuth to return no error") if subtle.ConstantTimeCompare(value, ext.Value) != 1 { t.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) } return nil } privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err, "Could not generate test key") core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) solver := NewChallenge( core, mockValidate, &ProviderServer{port: "23457"}, ) authz := acme.Authorization{ Identifier: acme.Identifier{ Value: domain, }, Challenges: []acme.Challenge{ {Type: challenge.TLSALPN01.String(), Token: "tlsalpn1"}, }, } err = solver.Solve(authz) require.NoError(t, err) } func TestChallengeInvalidPort(t *testing.T) { _, apiURL := tester.SetupFakeAPI(t) privateKey, err := rsa.GenerateKey(rand.Reader, 128) require.NoError(t, err, "Could not generate test key") core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) require.NoError(t, err) solver := NewChallenge( core, func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, &ProviderServer{port: "123456"}, ) authz := acme.Authorization{ Identifier: acme.Identifier{ Value: "localhost:123456", }, Challenges: []acme.Challenge{ {Type: challenge.TLSALPN01.String(), Token: "tlsalpn1"}, }, } err = solver.Solve(authz) require.Error(t, err) assert.Contains(t, err.Error(), "invalid port") assert.Contains(t, err.Error(), "123456") } lego-4.9.1/cmd/000077500000000000000000000000001434020463500132175ustar00rootroot00000000000000lego-4.9.1/cmd/account.go000066400000000000000000000013401434020463500152000ustar00rootroot00000000000000package cmd import ( "crypto" "github.com/go-acme/lego/v4/registration" ) // Account represents a users local saved credentials. type Account struct { Email string `json:"email"` Registration *registration.Resource `json:"registration"` key crypto.PrivateKey } /** Implementation of the registration.User interface **/ // GetEmail returns the email address for the account. func (a *Account) GetEmail() string { return a.Email } // GetPrivateKey returns the private RSA account key. func (a *Account) GetPrivateKey() crypto.PrivateKey { return a.key } // GetRegistration returns the server registration. func (a *Account) GetRegistration() *registration.Resource { return a.Registration } lego-4.9.1/cmd/accounts_storage.go000066400000000000000000000146571434020463500171260ustar00rootroot00000000000000package cmd import ( "crypto" "crypto/x509" "encoding/json" "encoding/pem" "errors" "net/url" "os" "path/filepath" "strings" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/registration" "github.com/urfave/cli/v2" ) const ( baseAccountsRootFolderName = "accounts" baseKeysFolderName = "keys" accountFileName = "account.json" ) // AccountsStorage A storage for account data. // // rootPath: // // ./.lego/accounts/ // │ └── root accounts directory // └── "path" option // // rootUserPath: // // ./.lego/accounts/localhost_14000/hubert@hubert.com/ // │ │ │ └── userID ("email" option) // │ │ └── CA server ("server" option) // │ └── root accounts directory // └── "path" option // // keysPath: // // ./.lego/accounts/localhost_14000/hubert@hubert.com/keys/ // │ │ │ │ └── root keys directory // │ │ │ └── userID ("email" option) // │ │ └── CA server ("server" option) // │ └── root accounts directory // └── "path" option // // accountFilePath: // // ./.lego/accounts/localhost_14000/hubert@hubert.com/account.json // │ │ │ │ └── account file // │ │ │ └── userID ("email" option) // │ │ └── CA server ("server" option) // │ └── root accounts directory // └── "path" option type AccountsStorage struct { userID string rootPath string rootUserPath string keysPath string accountFilePath string ctx *cli.Context } // NewAccountsStorage Creates a new AccountsStorage. func NewAccountsStorage(ctx *cli.Context) *AccountsStorage { // TODO: move to account struct? Currently MUST pass email. email := getEmail(ctx) serverURL, err := url.Parse(ctx.String("server")) if err != nil { log.Fatal(err) } rootPath := filepath.Join(ctx.String("path"), baseAccountsRootFolderName) serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host) accountsPath := filepath.Join(rootPath, serverPath) rootUserPath := filepath.Join(accountsPath, email) return &AccountsStorage{ userID: email, rootPath: rootPath, rootUserPath: rootUserPath, keysPath: filepath.Join(rootUserPath, baseKeysFolderName), accountFilePath: filepath.Join(rootUserPath, accountFileName), ctx: ctx, } } func (s *AccountsStorage) ExistsAccountFilePath() bool { accountFile := filepath.Join(s.rootUserPath, accountFileName) if _, err := os.Stat(accountFile); os.IsNotExist(err) { return false } else if err != nil { log.Fatal(err) } return true } func (s *AccountsStorage) GetRootPath() string { return s.rootPath } func (s *AccountsStorage) GetRootUserPath() string { return s.rootUserPath } func (s *AccountsStorage) GetUserID() string { return s.userID } func (s *AccountsStorage) Save(account *Account) error { jsonBytes, err := json.MarshalIndent(account, "", "\t") if err != nil { return err } return os.WriteFile(s.accountFilePath, jsonBytes, filePerm) } func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account { fileBytes, err := os.ReadFile(s.accountFilePath) if err != nil { log.Fatalf("Could not load file for account %s: %v", s.userID, err) } var account Account err = json.Unmarshal(fileBytes, &account) if err != nil { log.Fatalf("Could not parse file for account %s: %v", s.userID, err) } account.key = privateKey if account.Registration == nil || account.Registration.Body.Status == "" { reg, err := tryRecoverRegistration(s.ctx, privateKey) if err != nil { log.Fatalf("Could not load account for %s. Registration is nil: %#v", s.userID, err) } account.Registration = reg err = s.Save(&account) if err != nil { log.Fatalf("Could not save account for %s. Registration is nil: %#v", s.userID, err) } } return &account } func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.PrivateKey { accKeyPath := filepath.Join(s.keysPath, s.userID+".key") if _, err := os.Stat(accKeyPath); os.IsNotExist(err) { log.Printf("No key found for account %s. Generating a %s key.", s.userID, keyType) s.createKeysFolder() privateKey, err := generatePrivateKey(accKeyPath, keyType) if err != nil { log.Fatalf("Could not generate RSA private account key for account %s: %v", s.userID, err) } log.Printf("Saved key to %s", accKeyPath) return privateKey } privateKey, err := loadPrivateKey(accKeyPath) if err != nil { log.Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err) } return privateKey } func (s *AccountsStorage) createKeysFolder() { if err := createNonExistingFolder(s.keysPath); err != nil { log.Fatalf("Could not check/create directory for account %s: %v", s.userID, err) } } func generatePrivateKey(file string, keyType certcrypto.KeyType) (crypto.PrivateKey, error) { privateKey, err := certcrypto.GeneratePrivateKey(keyType) if err != nil { return nil, err } certOut, err := os.Create(file) if err != nil { return nil, err } defer certOut.Close() pemKey := certcrypto.PEMBlock(privateKey) err = pem.Encode(certOut, pemKey) if err != nil { return nil, err } return privateKey, nil } func loadPrivateKey(file string) (crypto.PrivateKey, error) { keyBytes, err := os.ReadFile(file) if err != nil { return nil, err } keyBlock, _ := pem.Decode(keyBytes) switch keyBlock.Type { case "RSA PRIVATE KEY": return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) case "EC PRIVATE KEY": return x509.ParseECPrivateKey(keyBlock.Bytes) } return nil, errors.New("unknown private key type") } func tryRecoverRegistration(ctx *cli.Context, privateKey crypto.PrivateKey) (*registration.Resource, error) { // couldn't load account but got a key. Try to look the account up. config := lego.NewConfig(&Account{key: privateKey}) config.CADirURL = ctx.String("server") config.UserAgent = getUserAgent(ctx) client, err := lego.NewClient(config) if err != nil { return nil, err } reg, err := client.Registration.ResolveAccountByKey() if err != nil { return nil, err } return reg, nil } lego-4.9.1/cmd/certs_storage.go000066400000000000000000000173231434020463500164200ustar00rootroot00000000000000package cmd import ( "bytes" "crypto" "crypto/rand" "crypto/x509" "encoding/json" "encoding/pem" "fmt" "os" "path/filepath" "strconv" "strings" "time" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/log" "github.com/urfave/cli/v2" "golang.org/x/net/idna" "software.sslmate.com/src/go-pkcs12" ) const ( baseCertificatesFolderName = "certificates" baseArchivesFolderName = "archives" ) // CertificatesStorage a certificates' storage. // // rootPath: // // ./.lego/certificates/ // │ └── root certificates directory // └── "path" option // // archivePath: // // ./.lego/archives/ // │ └── archived certificates directory // └── "path" option type CertificatesStorage struct { rootPath string archivePath string pem bool pfx bool pfxPassword string filename string // Deprecated } // NewCertificatesStorage create a new certificates storage. func NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage { return &CertificatesStorage{ rootPath: filepath.Join(ctx.String("path"), baseCertificatesFolderName), archivePath: filepath.Join(ctx.String("path"), baseArchivesFolderName), pem: ctx.Bool("pem"), pfx: ctx.Bool("pfx"), pfxPassword: ctx.String("pfx.pass"), filename: ctx.String("filename"), } } func (s *CertificatesStorage) CreateRootFolder() { err := createNonExistingFolder(s.rootPath) if err != nil { log.Fatalf("Could not check/create path: %v", err) } } func (s *CertificatesStorage) CreateArchiveFolder() { err := createNonExistingFolder(s.archivePath) if err != nil { log.Fatalf("Could not check/create path: %v", err) } } func (s *CertificatesStorage) GetRootPath() string { return s.rootPath } func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) { domain := certRes.Domain // We store the certificate, private key and metadata in different files // as web servers would not be able to work with a combined file. err := s.WriteFile(domain, ".crt", certRes.Certificate) if err != nil { log.Fatalf("Unable to save Certificate for domain %s\n\t%v", domain, err) } if certRes.IssuerCertificate != nil { err = s.WriteFile(domain, ".issuer.crt", certRes.IssuerCertificate) if err != nil { log.Fatalf("Unable to save IssuerCertificate for domain %s\n\t%v", domain, err) } } // if we were given a CSR, we don't know the private key if certRes.PrivateKey != nil { err = s.WriteCertificateFiles(domain, certRes) if err != nil { log.Fatalf("Unable to save PrivateKey for domain %s\n\t%v", domain, err) } } else if s.pem || s.pfx { // we don't have the private key; can't write the .pem or .pfx file log.Fatalf("Unable to save PEM or PFX without private key for domain %s. Are you using a CSR?", domain) } jsonBytes, err := json.MarshalIndent(certRes, "", "\t") if err != nil { log.Fatalf("Unable to marshal CertResource for domain %s\n\t%v", domain, err) } err = s.WriteFile(domain, ".json", jsonBytes) if err != nil { log.Fatalf("Unable to save CertResource for domain %s\n\t%v", domain, err) } } func (s *CertificatesStorage) ReadResource(domain string) certificate.Resource { raw, err := s.ReadFile(domain, ".json") if err != nil { log.Fatalf("Error while loading the meta data for domain %s\n\t%v", domain, err) } var resource certificate.Resource if err = json.Unmarshal(raw, &resource); err != nil { log.Fatalf("Error while marshaling the meta data for domain %s\n\t%v", domain, err) } return resource } func (s *CertificatesStorage) ExistsFile(domain, extension string) bool { filePath := s.GetFileName(domain, extension) if _, err := os.Stat(filePath); os.IsNotExist(err) { return false } else if err != nil { log.Fatal(err) } return true } func (s *CertificatesStorage) ReadFile(domain, extension string) ([]byte, error) { return os.ReadFile(s.GetFileName(domain, extension)) } func (s *CertificatesStorage) GetFileName(domain, extension string) string { filename := sanitizedDomain(domain) + extension return filepath.Join(s.rootPath, filename) } func (s *CertificatesStorage) ReadCertificate(domain, extension string) ([]*x509.Certificate, error) { content, err := s.ReadFile(domain, extension) if err != nil { return nil, err } // The input may be a bundle or a single certificate. return certcrypto.ParsePEMBundle(content) } func (s *CertificatesStorage) WriteFile(domain, extension string, data []byte) error { var baseFileName string if s.filename != "" { baseFileName = s.filename } else { baseFileName = sanitizedDomain(domain) } filePath := filepath.Join(s.rootPath, baseFileName+extension) return os.WriteFile(filePath, data, filePerm) } func (s *CertificatesStorage) WriteCertificateFiles(domain string, certRes *certificate.Resource) error { err := s.WriteFile(domain, ".key", certRes.PrivateKey) if err != nil { return fmt.Errorf("unable to save key file: %w", err) } if s.pem { err = s.WriteFile(domain, ".pem", bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil)) if err != nil { return fmt.Errorf("unable to save PEM file: %w", err) } } if s.pfx { err = s.WritePFXFile(domain, certRes) if err != nil { return fmt.Errorf("unable to save PFX file: %w", err) } } return nil } func (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.Resource) error { certPemBlock, _ := pem.Decode(certRes.Certificate) if certPemBlock == nil { return fmt.Errorf("unable to parse Certificate for domain %s", domain) } cert, err := x509.ParseCertificate(certPemBlock.Bytes) if err != nil { return fmt.Errorf("unable to load Certificate for domain %s: %w", domain, err) } issuerCertPemBlock, _ := pem.Decode(certRes.IssuerCertificate) if issuerCertPemBlock == nil { return fmt.Errorf("unable to parse Issuer Certificate for domain %s", domain) } issuerCert, err := x509.ParseCertificate(issuerCertPemBlock.Bytes) if err != nil { return fmt.Errorf("unable to load Issuer Certificate for domain %s: %w", domain, err) } keyPemBlock, _ := pem.Decode(certRes.PrivateKey) if keyPemBlock == nil { return fmt.Errorf("unable to parse PrivateKey for domain %s", domain) } var privateKey crypto.Signer var keyErr error switch keyPemBlock.Type { case "RSA PRIVATE KEY": privateKey, keyErr = x509.ParsePKCS1PrivateKey(keyPemBlock.Bytes) if keyErr != nil { return fmt.Errorf("unable to load RSA PrivateKey for domain %s: %w", domain, keyErr) } case "EC PRIVATE KEY": privateKey, keyErr = x509.ParseECPrivateKey(keyPemBlock.Bytes) if keyErr != nil { return fmt.Errorf("unable to load EC PrivateKey for domain %s: %w", domain, keyErr) } default: return fmt.Errorf("unsupported PrivateKey type '%s' for domain %s", keyPemBlock.Type, domain) } pfxBytes, err := pkcs12.Encode(rand.Reader, privateKey, cert, []*x509.Certificate{issuerCert}, s.pfxPassword) if err != nil { return fmt.Errorf("unable to encode PFX data for domain %s: %w", domain, err) } return s.WriteFile(domain, ".pfx", pfxBytes) } func (s *CertificatesStorage) MoveToArchive(domain string) error { matches, err := filepath.Glob(filepath.Join(s.rootPath, sanitizedDomain(domain)+".*")) if err != nil { return err } for _, oldFile := range matches { date := strconv.FormatInt(time.Now().Unix(), 10) filename := date + "." + filepath.Base(oldFile) newFile := filepath.Join(s.archivePath, filename) err = os.Rename(oldFile, newFile) if err != nil { return err } } return nil } // sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)). func sanitizedDomain(domain string) string { safe, err := idna.ToASCII(strings.ReplaceAll(domain, "*", "_")) if err != nil { log.Fatal(err) } return safe } lego-4.9.1/cmd/cmd.go000066400000000000000000000003651434020463500143150ustar00rootroot00000000000000package cmd import "github.com/urfave/cli/v2" // CreateCommands Creates all CLI commands. func CreateCommands() []*cli.Command { return []*cli.Command{ createRun(), createRevoke(), createRenew(), createDNSHelp(), createList(), } } lego-4.9.1/cmd/cmd_before.go000066400000000000000000000007641434020463500156420ustar00rootroot00000000000000package cmd import ( "github.com/go-acme/lego/v4/log" "github.com/urfave/cli/v2" ) func Before(ctx *cli.Context) error { if ctx.String("path") == "" { log.Fatal("Could not determine current working directory. Please pass --path.") } err := createNonExistingFolder(ctx.String("path")) if err != nil { log.Fatalf("Could not check/create path: %v", err) } if ctx.String("server") == "" { log.Fatal("Could not determine current working server. Please pass --server.") } return nil } lego-4.9.1/cmd/cmd_dnshelp.go000066400000000000000000000026211434020463500160270ustar00rootroot00000000000000package cmd import ( "fmt" "io" "os" "strings" "text/tabwriter" "github.com/urfave/cli/v2" ) func createDNSHelp() *cli.Command { return &cli.Command{ Name: "dnshelp", Usage: "Shows additional help for the '--dns' global option", Action: dnsHelp, Flags: []cli.Flag{ &cli.StringFlag{ Name: "code", Aliases: []string{"c"}, Usage: fmt.Sprintf("DNS code: %s", allDNSCodes()), }, }, } } func dnsHelp(ctx *cli.Context) error { code := ctx.String("code") if code == "" { w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) ew := &errWriter{w: w} ew.writeln(`Credentials for DNS providers must be passed through environment variables.`) ew.writeln() ew.writeln(`To display the documentation for a DNS providers:`) ew.writeln() ew.writeln("\t$ lego dnshelp -c code") ew.writeln() ew.writeln("All DNS codes:") ew.writef("\t%s\n", allDNSCodes()) ew.writeln() ew.writeln("More information: https://go-acme.github.io/lego/dns") if ew.err != nil { return ew.err } return w.Flush() } return displayDNSHelp(strings.ToLower(code)) } type errWriter struct { w io.Writer err error } func (ew *errWriter) writeln(a ...interface{}) { if ew.err != nil { return } _, ew.err = fmt.Fprintln(ew.w, a...) } func (ew *errWriter) writef(format string, a ...interface{}) { if ew.err != nil { return } _, ew.err = fmt.Fprintf(ew.w, format, a...) } lego-4.9.1/cmd/cmd_list.go000066400000000000000000000051761434020463500153550ustar00rootroot00000000000000package cmd import ( "encoding/json" "fmt" "net/url" "os" "path/filepath" "strings" "github.com/go-acme/lego/v4/certcrypto" "github.com/urfave/cli/v2" ) func createList() *cli.Command { return &cli.Command{ Name: "list", Usage: "Display certificates and accounts information.", Action: list, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "accounts", Aliases: []string{"a"}, Usage: "Display accounts.", }, &cli.BoolFlag{ Name: "names", Aliases: []string{"n"}, Usage: "Display certificate common names only.", }, }, } } func list(ctx *cli.Context) error { if ctx.Bool("accounts") && !ctx.Bool("names") { if err := listAccount(ctx); err != nil { return err } } return listCertificates(ctx) } func listCertificates(ctx *cli.Context) error { certsStorage := NewCertificatesStorage(ctx) matches, err := filepath.Glob(filepath.Join(certsStorage.GetRootPath(), "*.crt")) if err != nil { return err } names := ctx.Bool("names") if len(matches) == 0 { if !names { fmt.Println("No certificates found.") } return nil } if !names { fmt.Println("Found the following certs:") } for _, filename := range matches { if strings.HasSuffix(filename, ".issuer.crt") { continue } data, err := os.ReadFile(filename) if err != nil { return err } pCert, err := certcrypto.ParsePEMCertificate(data) if err != nil { return err } if names { fmt.Println(pCert.Subject.CommonName) } else { fmt.Println(" Certificate Name:", pCert.Subject.CommonName) fmt.Println(" Domains:", strings.Join(pCert.DNSNames, ", ")) fmt.Println(" Expiry Date:", pCert.NotAfter) fmt.Println(" Certificate Path:", filename) fmt.Println() } } return nil } func listAccount(ctx *cli.Context) error { // fake email, needed by NewAccountsStorage if err := ctx.Set("email", "unknown"); err != nil { return err } accountsStorage := NewAccountsStorage(ctx) matches, err := filepath.Glob(filepath.Join(accountsStorage.GetRootPath(), "*", "*", "*.json")) if err != nil { return err } if len(matches) == 0 { fmt.Println("No accounts found.") return nil } fmt.Println("Found the following accounts:") for _, filename := range matches { data, err := os.ReadFile(filename) if err != nil { return err } var account Account err = json.Unmarshal(data, &account) if err != nil { return err } uri, err := url.Parse(account.Registration.URI) if err != nil { return err } fmt.Println(" Email:", account.Email) fmt.Println(" Server:", uri.Host) fmt.Println(" Path:", filepath.Dir(filename)) fmt.Println() } return nil } lego-4.9.1/cmd/cmd_renew.go000066400000000000000000000177071434020463500155250ustar00rootroot00000000000000package cmd import ( "crypto" "crypto/x509" "math/rand" "os" "time" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/log" "github.com/mattn/go-isatty" "github.com/urfave/cli/v2" ) const ( renewEnvAccountEmail = "LEGO_ACCOUNT_EMAIL" renewEnvCertDomain = "LEGO_CERT_DOMAIN" renewEnvCertPath = "LEGO_CERT_PATH" renewEnvCertKeyPath = "LEGO_CERT_KEY_PATH" renewEnvCertPEMPath = "LEGO_CERT_PEM_PATH" renewEnvCertPFXPath = "LEGO_CERT_PFX_PATH" ) func createRenew() *cli.Command { return &cli.Command{ Name: "renew", Usage: "Renew a certificate", Action: renew, Before: func(ctx *cli.Context) error { // we require either domains or csr, but not both hasDomains := len(ctx.StringSlice("domains")) > 0 hasCsr := len(ctx.String("csr")) > 0 if hasDomains && hasCsr { log.Fatal("Please specify either --domains/-d or --csr/-c, but not both") } if !hasDomains && !hasCsr { log.Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)") } return nil }, Flags: []cli.Flag{ &cli.IntFlag{ Name: "days", Value: 30, Usage: "The number of days left on a certificate to renew it.", }, &cli.BoolFlag{ Name: "reuse-key", Usage: "Used to indicate you want to reuse your current private key for the new certificate.", }, &cli.BoolFlag{ Name: "no-bundle", Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.", }, &cli.BoolFlag{ Name: "must-staple", Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.", }, &cli.StringFlag{ Name: "renew-hook", Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.", }, &cli.StringFlag{ Name: "preferred-chain", Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.", }, &cli.StringFlag{ Name: "always-deactivate-authorizations", Usage: "Force the authorizations to be relinquished even if the certificate request was successful.", }, &cli.BoolFlag{ Name: "no-random-sleep", Usage: "Do not add a random sleep before the renewal. We do not recommend using this flag if you are doing your renewals in an automated way.", }, }, } } func renew(ctx *cli.Context) error { account, client := setup(ctx, NewAccountsStorage(ctx)) setupChallenges(ctx, client) if account.Registration == nil { log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", account.Email) } certsStorage := NewCertificatesStorage(ctx) bundle := !ctx.Bool("no-bundle") meta := map[string]string{renewEnvAccountEmail: account.Email} // CSR if ctx.IsSet("csr") { return renewForCSR(ctx, client, certsStorage, bundle, meta) } // Domains return renewForDomains(ctx, client, certsStorage, bundle, meta) } func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error { domains := ctx.StringSlice("domains") domain := domains[0] // load the cert resource from files. // We store the certificate, private key and metadata in different files // as web servers would not be able to work with a combined file. certificates, err := certsStorage.ReadCertificate(domain, ".crt") if err != nil { log.Fatalf("Error while loading the certificate for domain %s\n\t%v", domain, err) } cert := certificates[0] if !needRenewal(cert, domain, ctx.Int("days")) { return nil } // This is just meant to be informal for the user. timeLeft := cert.NotAfter.Sub(time.Now().UTC()) log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours())) certDomains := certcrypto.ExtractDomains(cert) var privateKey crypto.PrivateKey if ctx.Bool("reuse-key") { keyBytes, errR := certsStorage.ReadFile(domain, ".key") if errR != nil { log.Fatalf("Error while loading the private key for domain %s\n\t%v", domain, errR) } privateKey, errR = certcrypto.ParsePEMPrivateKey(keyBytes) if errR != nil { return errR } } // https://github.com/go-acme/lego/issues/1656 // https://github.com/certbot/certbot/blob/284023a1b7672be2bd4018dd7623b3b92197d4b0/certbot/certbot/_internal/renewal.py#L435-L440 if !isatty.IsTerminal(os.Stdout.Fd()) && !ctx.Bool("no-random-sleep") { // https://github.com/certbot/certbot/blob/284023a1b7672be2bd4018dd7623b3b92197d4b0/certbot/certbot/_internal/renewal.py#L472 const jitter = 8 * time.Minute rnd := rand.New(rand.NewSource(time.Now().UnixNano())) sleepTime := time.Duration(rnd.Int63n(int64(jitter))) log.Infof("renewal: random delay of %s", sleepTime) time.Sleep(sleepTime) } request := certificate.ObtainRequest{ Domains: merge(certDomains, domains), Bundle: bundle, PrivateKey: privateKey, MustStaple: ctx.Bool("must-staple"), PreferredChain: ctx.String("preferred-chain"), AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"), } certRes, err := client.Certificate.Obtain(request) if err != nil { log.Fatal(err) } certsStorage.SaveResource(certRes) meta[renewEnvCertDomain] = domain meta[renewEnvCertPath] = certsStorage.GetFileName(domain, ".crt") meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, ".key") meta[renewEnvCertPEMPath] = certsStorage.GetFileName(domain, ".pem") meta[renewEnvCertPFXPath] = certsStorage.GetFileName(domain, ".pfx") return launchHook(ctx.String("renew-hook"), meta) } func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error { csr, err := readCSRFile(ctx.String("csr")) if err != nil { log.Fatal(err) } domain := csr.Subject.CommonName // load the cert resource from files. // We store the certificate, private key and metadata in different files // as web servers would not be able to work with a combined file. certificates, err := certsStorage.ReadCertificate(domain, ".crt") if err != nil { log.Fatalf("Error while loading the certificate for domain %s\n\t%v", domain, err) } cert := certificates[0] if !needRenewal(cert, domain, ctx.Int("days")) { return nil } // This is just meant to be informal for the user. timeLeft := cert.NotAfter.Sub(time.Now().UTC()) log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours())) certRes, err := client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{ CSR: csr, Bundle: bundle, PreferredChain: ctx.String("preferred-chain"), AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"), }) if err != nil { log.Fatal(err) } certsStorage.SaveResource(certRes) meta[renewEnvCertDomain] = domain meta[renewEnvCertPath] = certsStorage.GetFileName(domain, ".crt") meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, ".key") return launchHook(ctx.String("renew-hook"), meta) } func needRenewal(x509Cert *x509.Certificate, domain string, days int) bool { if x509Cert.IsCA { log.Fatalf("[%s] Certificate bundle starts with a CA certificate", domain) } if days >= 0 { notAfter := int(time.Until(x509Cert.NotAfter).Hours() / 24.0) if notAfter > days { log.Printf("[%s] The certificate expires in %d days, the number of days defined to perform the renewal is %d: no renewal.", domain, notAfter, days) return false } } return true } func merge(prevDomains, nextDomains []string) []string { for _, next := range nextDomains { var found bool for _, prev := range prevDomains { if prev == next { found = true break } } if !found { prevDomains = append(prevDomains, next) } } return prevDomains } lego-4.9.1/cmd/cmd_renew_test.go000066400000000000000000000046341434020463500165570ustar00rootroot00000000000000package cmd import ( "crypto/x509" "testing" "time" "github.com/stretchr/testify/assert" ) func Test_merge(t *testing.T) { testCases := []struct { desc string prevDomains []string nextDomains []string expected []string }{ { desc: "all empty", prevDomains: []string{}, nextDomains: []string{}, expected: []string{}, }, { desc: "next empty", prevDomains: []string{"a", "b", "c"}, nextDomains: []string{}, expected: []string{"a", "b", "c"}, }, { desc: "prev empty", prevDomains: []string{}, nextDomains: []string{"a", "b", "c"}, expected: []string{"a", "b", "c"}, }, { desc: "merge append", prevDomains: []string{"a", "b", "c"}, nextDomains: []string{"a", "c", "d"}, expected: []string{"a", "b", "c", "d"}, }, { desc: "merge same", prevDomains: []string{"a", "b", "c"}, nextDomains: []string{"a", "b", "c"}, expected: []string{"a", "b", "c"}, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := merge(test.prevDomains, test.nextDomains) assert.Equal(t, test.expected, actual) }) } } func Test_needRenewal(t *testing.T) { testCases := []struct { desc string x509Cert *x509.Certificate days int expected bool }{ { desc: "30 days, NotAfter now", x509Cert: &x509.Certificate{ NotAfter: time.Now(), }, days: 30, expected: true, }, { desc: "30 days, NotAfter 31 days", x509Cert: &x509.Certificate{ NotAfter: time.Now().Add(31*24*time.Hour + 1*time.Second), }, days: 30, expected: false, }, { desc: "30 days, NotAfter 30 days", x509Cert: &x509.Certificate{ NotAfter: time.Now().Add(30 * 24 * time.Hour), }, days: 30, expected: true, }, { desc: "0 days, NotAfter 30 days: only the day of the expiration", x509Cert: &x509.Certificate{ NotAfter: time.Now().Add(30 * 24 * time.Hour), }, days: 0, expected: false, }, { desc: "-1 days, NotAfter 30 days: always renew", x509Cert: &x509.Certificate{ NotAfter: time.Now().Add(30 * 24 * time.Hour), }, days: -1, expected: true, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { actual := needRenewal(test.x509Cert, "foo.com", test.days) assert.Equal(t, test.expected, actual) }) } } lego-4.9.1/cmd/cmd_revoke.go000066400000000000000000000035761434020463500156770ustar00rootroot00000000000000package cmd import ( "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/log" "github.com/urfave/cli/v2" ) func createRevoke() *cli.Command { return &cli.Command{ Name: "revoke", Usage: "Revoke a certificate", Action: revoke, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "keep", Aliases: []string{"k"}, Usage: "Keep the certificates after the revocation instead of archiving them.", }, &cli.UintFlag{ Name: "reason", Usage: "Identifies the reason for the certificate revocation. See https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1. 0(unspecified),1(keyCompromise),2(cACompromise),3(affiliationChanged),4(superseded),5(cessationOfOperation),6(certificateHold),8(removeFromCRL),9(privilegeWithdrawn),10(aACompromise)", Value: acme.CRLReasonUnspecified, }, }, } } func revoke(ctx *cli.Context) error { acc, client := setup(ctx, NewAccountsStorage(ctx)) if acc.Registration == nil { log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email) } certsStorage := NewCertificatesStorage(ctx) certsStorage.CreateRootFolder() for _, domain := range ctx.StringSlice("domains") { log.Printf("Trying to revoke certificate for domain %s", domain) certBytes, err := certsStorage.ReadFile(domain, ".crt") if err != nil { log.Fatalf("Error while revoking the certificate for domain %s\n\t%v", domain, err) } reason := ctx.Uint("reason") err = client.Certificate.RevokeWithReason(certBytes, &reason) if err != nil { log.Fatalf("Error while revoking the certificate for domain %s\n\t%v", domain, err) } log.Println("Certificate was revoked.") if ctx.Bool("keep") { return nil } certsStorage.CreateArchiveFolder() err = certsStorage.MoveToArchive(domain) if err != nil { return err } log.Println("Certificate was archived for domain:", domain) } return nil } lego-4.9.1/cmd/cmd_run.go000066400000000000000000000132301434020463500151740ustar00rootroot00000000000000package cmd import ( "bufio" "fmt" "os" "strings" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/registration" "github.com/urfave/cli/v2" ) func createRun() *cli.Command { return &cli.Command{ Name: "run", Usage: "Register an account, then create and install a certificate", Before: func(ctx *cli.Context) error { // we require either domains or csr, but not both hasDomains := len(ctx.StringSlice("domains")) > 0 hasCsr := len(ctx.String("csr")) > 0 if hasDomains && hasCsr { log.Fatal("Please specify either --domains/-d or --csr/-c, but not both") } if !hasDomains && !hasCsr { log.Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)") } return nil }, Action: run, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "no-bundle", Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.", }, &cli.BoolFlag{ Name: "must-staple", Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.", }, &cli.StringFlag{ Name: "run-hook", Usage: "Define a hook. The hook is executed when the certificates are effectively created.", }, &cli.StringFlag{ Name: "preferred-chain", Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.", }, &cli.StringFlag{ Name: "always-deactivate-authorizations", Usage: "Force the authorizations to be relinquished even if the certificate request was successful.", }, }, } } const rootPathWarningMessage = `!!!! HEADS UP !!!! Your account credentials have been saved in your Let's Encrypt configuration directory at "%s". You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained from Let's Encrypt so making regular backups of this folder is ideal. ` func run(ctx *cli.Context) error { accountsStorage := NewAccountsStorage(ctx) account, client := setup(ctx, accountsStorage) setupChallenges(ctx, client) if account.Registration == nil { reg, err := register(ctx, client) if err != nil { log.Fatalf("Could not complete registration\n\t%v", err) } account.Registration = reg if err = accountsStorage.Save(account); err != nil { log.Fatal(err) } fmt.Printf(rootPathWarningMessage, accountsStorage.GetRootPath()) } certsStorage := NewCertificatesStorage(ctx) certsStorage.CreateRootFolder() cert, err := obtainCertificate(ctx, client) if err != nil { // Make sure to return a non-zero exit code if ObtainSANCertificate returned at least one error. // Due to us not returning partial certificate we can just exit here instead of at the end. log.Fatalf("Could not obtain certificates:\n\t%v", err) } certsStorage.SaveResource(cert) meta := map[string]string{ renewEnvAccountEmail: account.Email, renewEnvCertDomain: cert.Domain, renewEnvCertPath: certsStorage.GetFileName(cert.Domain, ".crt"), renewEnvCertKeyPath: certsStorage.GetFileName(cert.Domain, ".key"), } return launchHook(ctx.String("run-hook"), meta) } func handleTOS(ctx *cli.Context, client *lego.Client) bool { // Check for a global accept override if ctx.Bool("accept-tos") { return true } reader := bufio.NewReader(os.Stdin) log.Printf("Please review the TOS at %s", client.GetToSURL()) for { fmt.Println("Do you accept the TOS? Y/n") text, err := reader.ReadString('\n') if err != nil { log.Fatalf("Could not read from console: %v", err) } text = strings.Trim(text, "\r\n") switch text { case "", "y", "Y": return true case "n", "N": return false default: fmt.Println("Your input was invalid. Please answer with one of Y/y, n/N or by pressing enter.") } } } func register(ctx *cli.Context, client *lego.Client) (*registration.Resource, error) { accepted := handleTOS(ctx, client) if !accepted { log.Fatal("You did not accept the TOS. Unable to proceed.") } if ctx.Bool("eab") { kid := ctx.String("kid") hmacEncoded := ctx.String("hmac") if kid == "" || hmacEncoded == "" { log.Fatalf("Requires arguments --kid and --hmac.") } return client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ TermsOfServiceAgreed: accepted, Kid: kid, HmacEncoded: hmacEncoded, }) } return client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) } func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Resource, error) { bundle := !ctx.Bool("no-bundle") domains := ctx.StringSlice("domains") if len(domains) > 0 { // obtain a certificate, generating a new private key request := certificate.ObtainRequest{ Domains: domains, Bundle: bundle, MustStaple: ctx.Bool("must-staple"), PreferredChain: ctx.String("preferred-chain"), AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"), } return client.Certificate.Obtain(request) } // read the CSR csr, err := readCSRFile(ctx.String("csr")) if err != nil { return nil, err } // obtain a certificate for this CSR return client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{ CSR: csr, Bundle: bundle, PreferredChain: ctx.String("preferred-chain"), AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"), }) } lego-4.9.1/cmd/flags.go000066400000000000000000000116211434020463500146430ustar00rootroot00000000000000package cmd import ( "github.com/go-acme/lego/v4/lego" "github.com/urfave/cli/v2" "software.sslmate.com/src/go-pkcs12" ) func CreateFlags(defaultPath string) []cli.Flag { return []cli.Flag{ &cli.StringSliceFlag{ Name: "domains", Aliases: []string{"d"}, Usage: "Add a domain to the process. Can be specified multiple times.", }, &cli.StringFlag{ Name: "server", Aliases: []string{"s"}, Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.", Value: lego.LEDirectoryProduction, }, &cli.BoolFlag{ Name: "accept-tos", Aliases: []string{"a"}, Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.", }, &cli.StringFlag{ Name: "email", Aliases: []string{"m"}, Usage: "Email used for registration and recovery contact.", }, &cli.StringFlag{ Name: "csr", Aliases: []string{"c"}, Usage: "Certificate signing request filename, if an external CSR is to be used.", }, &cli.BoolFlag{ Name: "eab", Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.", }, &cli.StringFlag{ Name: "kid", Usage: "Key identifier from External CA. Used for External Account Binding.", }, &cli.StringFlag{ Name: "hmac", Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.", }, &cli.StringFlag{ Name: "key-type", Aliases: []string{"k"}, Value: "ec256", Usage: "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384.", }, &cli.StringFlag{ Name: "filename", Usage: "(deprecated) Filename of the generated certificate.", }, &cli.StringFlag{ Name: "path", EnvVars: []string{"LEGO_PATH"}, Usage: "Directory to use for storing the data.", Value: defaultPath, }, &cli.BoolFlag{ Name: "http", Usage: "Use the HTTP challenge to solve challenges. Can be mixed with other types of challenges.", }, &cli.StringFlag{ Name: "http.port", Usage: "Set the port and interface to use for HTTP based challenges to listen on.Supported: interface:port or :port.", Value: ":80", }, &cli.StringFlag{ Name: "http.proxy-header", Usage: "Validate against this HTTP header when solving HTTP based challenges behind a reverse proxy.", Value: "Host", }, &cli.StringFlag{ Name: "http.webroot", Usage: "Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge", }, &cli.StringSliceFlag{ Name: "http.memcached-host", Usage: "Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.", }, &cli.BoolFlag{ Name: "tls", Usage: "Use the TLS challenge to solve challenges. Can be mixed with other types of challenges.", }, &cli.StringFlag{ Name: "tls.port", Usage: "Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port.", Value: ":443", }, &cli.StringFlag{ Name: "dns", Usage: "Solve a DNS challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.", }, &cli.BoolFlag{ Name: "dns.disable-cp", Usage: "By setting this flag to true, disables the need to wait the propagation of the TXT record to all authoritative name servers.", }, &cli.StringSliceFlag{ Name: "dns.resolvers", Usage: "Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.", }, &cli.IntFlag{ Name: "http-timeout", Usage: "Set the HTTP timeout value to a specific value in seconds.", }, &cli.IntFlag{ Name: "dns-timeout", Usage: "Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries.", Value: 10, }, &cli.BoolFlag{ Name: "pem", Usage: "Generate a .pem file by concatenating the .key and .crt files together.", }, &cli.BoolFlag{ Name: "pfx", Usage: "Generate a .pfx (PKCS#12) file by with the .key and .crt and issuer .crt files together.", }, &cli.StringFlag{ Name: "pfx.pass", Usage: "The password used to encrypt the .pfx (PCKS#12) file.", Value: pkcs12.DefaultPassword, }, &cli.IntFlag{ Name: "cert.timeout", Usage: "Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates.", Value: 30, }, &cli.StringFlag{ Name: "user-agent", Usage: "Add to the user-agent sent to the CA to identify an application embedding lego-cli", }, } } lego-4.9.1/cmd/hook.go000066400000000000000000000014221434020463500145050ustar00rootroot00000000000000package cmd import ( "context" "errors" "fmt" "os" "os/exec" "strings" "time" ) func launchHook(hook string, meta map[string]string) error { if hook == "" { return nil } ctxCmd, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() parts := strings.Fields(hook) cmdCtx := exec.CommandContext(ctxCmd, parts[0], parts[1:]...) cmdCtx.Env = append(os.Environ(), metaToEnv(meta)...) output, err := cmdCtx.CombinedOutput() if len(output) > 0 { fmt.Println(string(output)) } if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) { return errors.New("hook timed out") } return err } func metaToEnv(meta map[string]string) []string { var envs []string for k, v := range meta { envs = append(envs, k+"="+v) } return envs } lego-4.9.1/cmd/lego/000077500000000000000000000000001434020463500141455ustar00rootroot00000000000000lego-4.9.1/cmd/lego/main.go000066400000000000000000000016061434020463500154230ustar00rootroot00000000000000// Let's Encrypt client to go! // CLI application for generating Let's Encrypt certificates using the ACME package. package main import ( "fmt" "os" "path/filepath" "runtime" "github.com/go-acme/lego/v4/cmd" "github.com/go-acme/lego/v4/log" "github.com/urfave/cli/v2" ) var version = "dev" func main() { app := cli.NewApp() app.Name = "lego" app.HelpName = "lego" app.Usage = "Let's Encrypt client written in Go" app.EnableBashCompletion = true app.Version = version cli.VersionPrinter = func(c *cli.Context) { fmt.Printf("lego version %s %s/%s\n", c.App.Version, runtime.GOOS, runtime.GOARCH) } var defaultPath string cwd, err := os.Getwd() if err == nil { defaultPath = filepath.Join(cwd, ".lego") } app.Flags = cmd.CreateFlags(defaultPath) app.Before = cmd.Before app.Commands = cmd.CreateCommands() err = app.Run(os.Args) if err != nil { log.Fatal(err) } } lego-4.9.1/cmd/setup.go000066400000000000000000000061441434020463500147130ustar00rootroot00000000000000package cmd import ( "crypto/x509" "encoding/pem" "fmt" "os" "strings" "time" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/registration" "github.com/urfave/cli/v2" ) const filePerm os.FileMode = 0o600 func setup(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, *lego.Client) { keyType := getKeyType(ctx) privateKey := accountsStorage.GetPrivateKey(keyType) var account *Account if accountsStorage.ExistsAccountFilePath() { account = accountsStorage.LoadAccount(privateKey) } else { account = &Account{Email: accountsStorage.GetUserID(), key: privateKey} } client := newClient(ctx, account, keyType) return account, client } func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyType) *lego.Client { config := lego.NewConfig(acc) config.CADirURL = ctx.String("server") config.Certificate = lego.CertificateConfig{ KeyType: keyType, Timeout: time.Duration(ctx.Int("cert.timeout")) * time.Second, } config.UserAgent = getUserAgent(ctx) if ctx.IsSet("http-timeout") { config.HTTPClient.Timeout = time.Duration(ctx.Int("http-timeout")) * time.Second } client, err := lego.NewClient(config) if err != nil { log.Fatalf("Could not create client: %v", err) } if client.GetExternalAccountRequired() && !ctx.IsSet("eab") { log.Fatal("Server requires External Account Binding. Use --eab with --kid and --hmac.") } return client } // getKeyType the type from which private keys should be generated. func getKeyType(ctx *cli.Context) certcrypto.KeyType { keyType := ctx.String("key-type") switch strings.ToUpper(keyType) { case "RSA2048": return certcrypto.RSA2048 case "RSA4096": return certcrypto.RSA4096 case "RSA8192": return certcrypto.RSA8192 case "EC256": return certcrypto.EC256 case "EC384": return certcrypto.EC384 } log.Fatalf("Unsupported KeyType: %s", keyType) return "" } func getEmail(ctx *cli.Context) string { email := ctx.String("email") if email == "" { log.Fatal("You have to pass an account (email address) to the program using --email or -m") } return email } func getUserAgent(ctx *cli.Context) string { return strings.TrimSpace(fmt.Sprintf("%s lego-cli/%s", ctx.String("user-agent"), ctx.App.Version)) } func createNonExistingFolder(path string) error { if _, err := os.Stat(path); os.IsNotExist(err) { return os.MkdirAll(path, 0o700) } else if err != nil { return err } return nil } func readCSRFile(filename string) (*x509.CertificateRequest, error) { bytes, err := os.ReadFile(filename) if err != nil { return nil, err } raw := bytes // see if we can find a PEM-encoded CSR var p *pem.Block rest := bytes for { // decode a PEM block p, rest = pem.Decode(rest) // did we fail? if p == nil { break } // did we get a CSR? if p.Type == "CERTIFICATE REQUEST" || p.Type == "NEW CERTIFICATE REQUEST" { raw = p.Bytes } } // no PEM-encoded CSR // assume we were given a DER-encoded ASN.1 CSR // (if this assumption is wrong, parsing these bytes will fail) return x509.ParseCertificateRequest(raw) } lego-4.9.1/cmd/setup_challenges.go000066400000000000000000000064041434020463500170770ustar00rootroot00000000000000package cmd import ( "net" "strings" "time" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/providers/dns" "github.com/go-acme/lego/v4/providers/http/memcached" "github.com/go-acme/lego/v4/providers/http/webroot" "github.com/urfave/cli/v2" ) func setupChallenges(ctx *cli.Context, client *lego.Client) { if !ctx.Bool("http") && !ctx.Bool("tls") && !ctx.IsSet("dns") { log.Fatal("No challenge selected. You must specify at least one challenge: `--http`, `--tls`, `--dns`.") } if ctx.Bool("http") { err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx)) if err != nil { log.Fatal(err) } } if ctx.Bool("tls") { err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx)) if err != nil { log.Fatal(err) } } if ctx.IsSet("dns") { setupDNS(ctx, client) } } func setupHTTPProvider(ctx *cli.Context) challenge.Provider { switch { case ctx.IsSet("http.webroot"): ps, err := webroot.NewHTTPProvider(ctx.String("http.webroot")) if err != nil { log.Fatal(err) } return ps case ctx.IsSet("http.memcached-host"): ps, err := memcached.NewMemcachedProvider(ctx.StringSlice("http.memcached-host")) if err != nil { log.Fatal(err) } return ps case ctx.IsSet("http.port"): iface := ctx.String("http.port") if !strings.Contains(iface, ":") { log.Fatalf("The --http switch only accepts interface:port or :port for its argument.") } host, port, err := net.SplitHostPort(iface) if err != nil { log.Fatal(err) } srv := http01.NewProviderServer(host, port) if header := ctx.String("http.proxy-header"); header != "" { srv.SetProxyHeader(header) } return srv case ctx.Bool("http"): srv := http01.NewProviderServer("", "") if header := ctx.String("http.proxy-header"); header != "" { srv.SetProxyHeader(header) } return srv default: log.Fatal("Invalid HTTP challenge options.") return nil } } func setupTLSProvider(ctx *cli.Context) challenge.Provider { switch { case ctx.IsSet("tls.port"): iface := ctx.String("tls.port") if !strings.Contains(iface, ":") { log.Fatalf("The --tls switch only accepts interface:port or :port for its argument.") } host, port, err := net.SplitHostPort(iface) if err != nil { log.Fatal(err) } return tlsalpn01.NewProviderServer(host, port) case ctx.Bool("tls"): return tlsalpn01.NewProviderServer("", "") default: log.Fatal("Invalid HTTP challenge options.") return nil } } func setupDNS(ctx *cli.Context, client *lego.Client) { provider, err := dns.NewDNSChallengeProviderByName(ctx.String("dns")) if err != nil { log.Fatal(err) } servers := ctx.StringSlice("dns.resolvers") err = client.Challenge.SetDNS01Provider(provider, dns01.CondOption(len(servers) > 0, dns01.AddRecursiveNameservers(dns01.ParseNameservers(ctx.StringSlice("dns.resolvers")))), dns01.CondOption(ctx.Bool("dns.disable-cp"), dns01.DisableCompletePropagationRequirement()), dns01.CondOption(ctx.IsSet("dns-timeout"), dns01.AddDNSTimeout(time.Duration(ctx.Int("dns-timeout"))*time.Second)), ) if err != nil { log.Fatal(err) } } lego-4.9.1/cmd/zz_gen_cmd_dnshelp.go000066400000000000000000002735231434020463500174160ustar00rootroot00000000000000package cmd // CODE GENERATED AUTOMATICALLY // THIS FILE MUST NOT BE EDITED BY HAND import ( "fmt" "os" "sort" "strings" "text/tabwriter" ) func allDNSCodes() string { providers := []string{ "manual", "acme-dns", "alidns", "allinkl", "arvancloud", "auroradns", "autodns", "azure", "bindman", "bluecat", "checkdomain", "civo", "clouddns", "cloudflare", "cloudns", "cloudxns", "conoha", "constellix", "desec", "designate", "digitalocean", "dnsimple", "dnsmadeeasy", "dnspod", "dode", "domeneshop", "dreamhost", "duckdns", "dyn", "dynu", "easydns", "edgedns", "epik", "exec", "exoscale", "freemyip", "gandi", "gandiv5", "gcloud", "gcore", "glesys", "godaddy", "hetzner", "hostingde", "hosttech", "httpreq", "hurricane", "hyperone", "ibmcloud", "iij", "iijdpf", "infoblox", "infomaniak", "internetbs", "inwx", "ionos", "iwantmyname", "joker", "lightsail", "linode", "liquidweb", "loopia", "luadns", "mydnsjp", "mythicbeasts", "namecheap", "namedotcom", "namesilo", "nearlyfreespeech", "netcup", "netlify", "nicmanager", "nifcloud", "njalla", "ns1", "oraclecloud", "otc", "ovh", "pdns", "porkbun", "rackspace", "regru", "rfc2136", "rimuhosting", "route53", "safedns", "sakuracloud", "scaleway", "selectel", "servercow", "simply", "sonic", "stackpath", "tencentcloud", "transip", "variomedia", "vegadns", "vercel", "versio", "vinyldns", "vkcloud", "vscale", "vultr", "wedos", "yandex", "yandexcloud", "zoneee", "zonomi", } sort.Strings(providers) return strings.Join(providers, ", ") } func displayDNSHelp(name string) error { w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) ew := &errWriter{w: w} switch name { case "acme-dns": // generated from: providers/dns/acmedns/acmedns.toml ew.writeln(`Configuration for Joohoi's ACME-DNS.`) ew.writeln(`Code: 'acme-dns'`) ew.writeln(`Since: 'v1.1.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "ACME_DNS_API_BASE": The ACME-DNS API address`) ew.writeln(` - "ACME_DNS_STORAGE_PATH": The ACME-DNS JSON account data file. A per-domain account will be registered/persisted to this file and used for TXT updates.`) ew.writeln() ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/acme-dns`) case "alidns": // generated from: providers/dns/alidns/alidns.toml ew.writeln(`Configuration for Alibaba Cloud DNS.`) ew.writeln(`Code: 'alidns'`) ew.writeln(`Since: 'v1.1.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "ALICLOUD_ACCESS_KEY": Access key ID`) ew.writeln(` - "ALICLOUD_RAM_ROLE": Your instance RAM role (https://www.alibabacloud.com/help/doc-detail/54579.htm)`) ew.writeln(` - "ALICLOUD_SECRET_KEY": Access Key secret`) ew.writeln(` - "ALICLOUD_SECURITY_TOKEN": STS Security Token (optional)`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "ALICLOUD_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "ALICLOUD_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "ALICLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "ALICLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/alidns`) case "allinkl": // generated from: providers/dns/allinkl/allinkl.toml ew.writeln(`Configuration for all-inkl.`) ew.writeln(`Code: 'allinkl'`) ew.writeln(`Since: 'v4.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "ALL_INKL_LOGIN": KAS login`) ew.writeln(` - "ALL_INKL_PASSWORD": KAS password`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "ALL_INKL_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "ALL_INKL_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "ALL_INKL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/allinkl`) case "arvancloud": // generated from: providers/dns/arvancloud/arvancloud.toml ew.writeln(`Configuration for ArvanCloud.`) ew.writeln(`Code: 'arvancloud'`) ew.writeln(`Since: 'v3.8.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "ARVANCLOUD_API_KEY": API key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "ARVANCLOUD_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "ARVANCLOUD_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "ARVANCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "ARVANCLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/arvancloud`) case "auroradns": // generated from: providers/dns/auroradns/auroradns.toml ew.writeln(`Configuration for Aurora DNS.`) ew.writeln(`Code: 'auroradns'`) ew.writeln(`Since: 'v0.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "AURORA_API_KEY": API key or username to used`) ew.writeln(` - "AURORA_SECRET": Secret password to be used`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "AURORA_ENDPOINT": API endpoint URL`) ew.writeln(` - "AURORA_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "AURORA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "AURORA_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/auroradns`) case "autodns": // generated from: providers/dns/autodns/autodns.toml ew.writeln(`Configuration for Autodns.`) ew.writeln(`Code: 'autodns'`) ew.writeln(`Since: 'v3.2.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "AUTODNS_API_PASSWORD": User Password`) ew.writeln(` - "AUTODNS_API_USER": Username`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "AUTODNS_CONTEXT": API context (4 for production, 1 for testing. Defaults to 4)`) ew.writeln(` - "AUTODNS_ENDPOINT": API endpoint URL, defaults to https://api.autodns.com/v1/`) ew.writeln(` - "AUTODNS_HTTP_TIMEOUT": API request timeout, defaults to 30 seconds`) ew.writeln(` - "AUTODNS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "AUTODNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "AUTODNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/autodns`) case "azure": // generated from: providers/dns/azure/azure.toml ew.writeln(`Configuration for Azure.`) ew.writeln(`Code: 'azure'`) ew.writeln(`Since: 'v0.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "AZURE_CLIENT_ID": Client ID`) ew.writeln(` - "AZURE_CLIENT_SECRET": Client secret`) ew.writeln(` - "AZURE_ENVIRONMENT": Azure environment, one of: public, usgovernment, german, and china`) ew.writeln(` - "AZURE_RESOURCE_GROUP": Resource group`) ew.writeln(` - "AZURE_SUBSCRIPTION_ID": Subscription ID`) ew.writeln(` - "AZURE_TENANT_ID": Tenant ID`) ew.writeln(` - "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).`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "AZURE_METADATA_ENDPOINT": Metadata Service endpoint URL`) ew.writeln(` - "AZURE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "AZURE_PRIVATE_ZONE": Set to true to use Azure Private DNS Zones and not public`) ew.writeln(` - "AZURE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "AZURE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "AZURE_ZONE_NAME": Zone name to use inside Azure DNS service to add the TXT record in`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/azure`) case "bindman": // generated from: providers/dns/bindman/bindman.toml ew.writeln(`Configuration for Bindman.`) ew.writeln(`Code: 'bindman'`) ew.writeln(`Since: 'v2.6.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "BINDMAN_MANAGER_ADDRESS": The server URL, should have scheme, hostname, and port (if required) of the Bindman-DNS Manager server`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "BINDMAN_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "BINDMAN_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "BINDMAN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/bindman`) case "bluecat": // generated from: providers/dns/bluecat/bluecat.toml ew.writeln(`Configuration for Bluecat.`) ew.writeln(`Code: 'bluecat'`) ew.writeln(`Since: 'v0.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "BLUECAT_CONFIG_NAME": Configuration name`) ew.writeln(` - "BLUECAT_DNS_VIEW": External DNS View Name`) ew.writeln(` - "BLUECAT_PASSWORD": API password`) ew.writeln(` - "BLUECAT_SERVER_URL": The server URL, should have scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve`) ew.writeln(` - "BLUECAT_USER_NAME": API username`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "BLUECAT_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "BLUECAT_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "BLUECAT_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "BLUECAT_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/bluecat`) case "checkdomain": // generated from: providers/dns/checkdomain/checkdomain.toml ew.writeln(`Configuration for Checkdomain.`) ew.writeln(`Code: 'checkdomain'`) ew.writeln(`Since: 'v3.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "CHECKDOMAIN_TOKEN": API token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "CHECKDOMAIN_ENDPOINT": API endpoint URL, defaults to https://api.checkdomain.de`) ew.writeln(` - "CHECKDOMAIN_HTTP_TIMEOUT": API request timeout, defaults to 30 seconds`) ew.writeln(` - "CHECKDOMAIN_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "CHECKDOMAIN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "CHECKDOMAIN_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/checkdomain`) case "civo": // generated from: providers/dns/civo/civo.toml ew.writeln(`Configuration for Civo.`) ew.writeln(`Code: 'civo'`) ew.writeln(`Since: 'v4.9.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "CIVO_TOKEN": Authentication token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "CIVO_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "CIVO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "CIVO_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/civo`) case "clouddns": // generated from: providers/dns/clouddns/clouddns.toml ew.writeln(`Configuration for CloudDNS.`) ew.writeln(`Code: 'clouddns'`) ew.writeln(`Since: 'v3.6.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "CLOUDDNS_CLIENT_ID": Client ID`) ew.writeln(` - "CLOUDDNS_EMAIL": Account email`) ew.writeln(` - "CLOUDDNS_PASSWORD": Account password`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "CLOUDDNS_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "CLOUDDNS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "CLOUDDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "CLOUDDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/clouddns`) case "cloudflare": // generated from: providers/dns/cloudflare/cloudflare.toml ew.writeln(`Configuration for Cloudflare.`) ew.writeln(`Code: 'cloudflare'`) ew.writeln(`Since: 'v0.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "CF_API_EMAIL": Account email`) ew.writeln(` - "CF_API_KEY": API key`) ew.writeln(` - "CF_DNS_API_TOKEN": API token with DNS:Edit permission (since v3.1.0)`) ew.writeln(` - "CF_ZONE_API_TOKEN": API token with Zone:Read permission (since v3.1.0)`) ew.writeln(` - "CLOUDFLARE_API_KEY": Alias to CF_API_KEY`) ew.writeln(` - "CLOUDFLARE_DNS_API_TOKEN": Alias to CF_DNS_API_TOKEN`) ew.writeln(` - "CLOUDFLARE_EMAIL": Alias to CF_API_EMAIL`) ew.writeln(` - "CLOUDFLARE_ZONE_API_TOKEN": Alias to CF_ZONE_API_TOKEN`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "CLOUDFLARE_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "CLOUDFLARE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "CLOUDFLARE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "CLOUDFLARE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudflare`) case "cloudns": // generated from: providers/dns/cloudns/cloudns.toml ew.writeln(`Configuration for ClouDNS.`) ew.writeln(`Code: 'cloudns'`) ew.writeln(`Since: 'v2.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "CLOUDNS_AUTH_ID": The API user ID`) ew.writeln(` - "CLOUDNS_AUTH_PASSWORD": The password for API user ID`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "CLOUDNS_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "CLOUDNS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "CLOUDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "CLOUDNS_SUB_AUTH_ID": The API sub user ID`) ew.writeln(` - "CLOUDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudns`) case "cloudxns": // generated from: providers/dns/cloudxns/cloudxns.toml ew.writeln(`Configuration for CloudXNS.`) ew.writeln(`Code: 'cloudxns'`) ew.writeln(`Since: 'v0.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "CLOUDXNS_API_KEY": The API key`) ew.writeln(` - "CLOUDXNS_SECRET_KEY": The API secret key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "CLOUDXNS_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "CLOUDXNS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "CLOUDXNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "CLOUDXNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/cloudxns`) case "conoha": // generated from: providers/dns/conoha/conoha.toml ew.writeln(`Configuration for ConoHa.`) ew.writeln(`Code: 'conoha'`) ew.writeln(`Since: 'v1.2.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "CONOHA_API_PASSWORD": The API password`) ew.writeln(` - "CONOHA_API_USERNAME": The API username`) ew.writeln(` - "CONOHA_TENANT_ID": Tenant ID`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "CONOHA_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "CONOHA_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "CONOHA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "CONOHA_REGION": The region`) ew.writeln(` - "CONOHA_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/conoha`) case "constellix": // generated from: providers/dns/constellix/constellix.toml ew.writeln(`Configuration for Constellix.`) ew.writeln(`Code: 'constellix'`) ew.writeln(`Since: 'v3.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "CONSTELLIX_API_KEY": User API key`) ew.writeln(` - "CONSTELLIX_SECRET_KEY": User secret key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "CONSTELLIX_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "CONSTELLIX_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "CONSTELLIX_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "CONSTELLIX_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/constellix`) case "desec": // generated from: providers/dns/desec/desec.toml ew.writeln(`Configuration for deSEC.io.`) ew.writeln(`Code: 'desec'`) ew.writeln(`Since: 'v3.7.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "DESEC_TOKEN": Domain token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "DESEC_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "DESEC_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "DESEC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "DESEC_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/desec`) case "designate": // generated from: providers/dns/designate/designate.toml ew.writeln(`Configuration for Designate DNSaaS for Openstack.`) ew.writeln(`Code: 'designate'`) ew.writeln(`Since: 'v2.2.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "OS_APPLICATION_CREDENTIAL_ID": Application credential ID`) ew.writeln(` - "OS_APPLICATION_CREDENTIAL_NAME": Application credential name`) ew.writeln(` - "OS_APPLICATION_CREDENTIAL_SECRET": Application credential secret`) ew.writeln(` - "OS_AUTH_URL": Identity endpoint URL`) ew.writeln(` - "OS_PASSWORD": Password`) ew.writeln(` - "OS_PROJECT_NAME": Project name`) ew.writeln(` - "OS_REGION_NAME": Region name`) ew.writeln(` - "OS_USERNAME": Username`) ew.writeln(` - "OS_USER_ID": User ID`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "DESIGNATE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "DESIGNATE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "DESIGNATE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "OS_PROJECT_ID": Project ID`) ew.writeln(` - "OS_TENANT_NAME": Tenant name (deprecated see OS_PROJECT_NAME and OS_PROJECT_ID)`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/designate`) case "digitalocean": // generated from: providers/dns/digitalocean/digitalocean.toml ew.writeln(`Configuration for Digital Ocean.`) ew.writeln(`Code: 'digitalocean'`) ew.writeln(`Since: 'v0.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "DO_AUTH_TOKEN": Authentication token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "DO_API_URL": The URL of the API`) ew.writeln(` - "DO_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "DO_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "DO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "DO_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/digitalocean`) case "dnsimple": // generated from: providers/dns/dnsimple/dnsimple.toml ew.writeln(`Configuration for DNSimple.`) ew.writeln(`Code: 'dnsimple'`) ew.writeln(`Since: 'v0.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "DNSIMPLE_OAUTH_TOKEN": OAuth token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "DNSIMPLE_BASE_URL": API endpoint URL`) ew.writeln(` - "DNSIMPLE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "DNSIMPLE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "DNSIMPLE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dnsimple`) case "dnsmadeeasy": // generated from: providers/dns/dnsmadeeasy/dnsmadeeasy.toml ew.writeln(`Configuration for DNS Made Easy.`) ew.writeln(`Code: 'dnsmadeeasy'`) ew.writeln(`Since: 'v0.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "DNSMADEEASY_API_KEY": The API key`) ew.writeln(` - "DNSMADEEASY_API_SECRET": The API Secret key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "DNSMADEEASY_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "DNSMADEEASY_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "DNSMADEEASY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "DNSMADEEASY_SANDBOX": Activate the sandbox (boolean)`) ew.writeln(` - "DNSMADEEASY_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dnsmadeeasy`) case "dnspod": // generated from: providers/dns/dnspod/dnspod.toml ew.writeln(`Configuration for DNSPod (deprecated).`) ew.writeln(`Code: 'dnspod'`) ew.writeln(`Since: 'v0.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "DNSPOD_API_KEY": The user token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "DNSPOD_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "DNSPOD_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "DNSPOD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "DNSPOD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dnspod`) case "dode": // generated from: providers/dns/dode/dode.toml ew.writeln(`Configuration for Domain Offensive (do.de).`) ew.writeln(`Code: 'dode'`) ew.writeln(`Since: 'v2.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "DODE_TOKEN": API token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "DODE_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "DODE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "DODE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "DODE_SEQUENCE_INTERVAL": Time between sequential requests`) ew.writeln(` - "DODE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dode`) case "domeneshop": // generated from: providers/dns/domeneshop/domeneshop.toml ew.writeln(`Configuration for Domeneshop.`) ew.writeln(`Code: 'domeneshop'`) ew.writeln(`Since: 'v4.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "DOMENESHOP_API_SECRET": API secret`) ew.writeln(` - "DOMENESHOP_API_TOKEN": API token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "DOMENESHOP_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "DOMENESHOP_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "DOMENESHOP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/domeneshop`) case "dreamhost": // generated from: providers/dns/dreamhost/dreamhost.toml ew.writeln(`Configuration for DreamHost.`) ew.writeln(`Code: 'dreamhost'`) ew.writeln(`Since: 'v1.1.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "DREAMHOST_API_KEY": The API key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "DREAMHOST_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "DREAMHOST_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "DREAMHOST_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "DREAMHOST_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dreamhost`) case "duckdns": // generated from: providers/dns/duckdns/duckdns.toml ew.writeln(`Configuration for Duck DNS.`) ew.writeln(`Code: 'duckdns'`) ew.writeln(`Since: 'v0.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "DUCKDNS_TOKEN": Account token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "DUCKDNS_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "DUCKDNS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "DUCKDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "DUCKDNS_SEQUENCE_INTERVAL": Time between sequential requests`) ew.writeln(` - "DUCKDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/duckdns`) case "dyn": // generated from: providers/dns/dyn/dyn.toml ew.writeln(`Configuration for Dyn.`) ew.writeln(`Code: 'dyn'`) ew.writeln(`Since: 'v0.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "DYN_CUSTOMER_NAME": Customer name`) ew.writeln(` - "DYN_PASSWORD": Password`) ew.writeln(` - "DYN_USER_NAME": User name`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "DYN_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "DYN_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "DYN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "DYN_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dyn`) case "dynu": // generated from: providers/dns/dynu/dynu.toml ew.writeln(`Configuration for Dynu.`) ew.writeln(`Code: 'dynu'`) ew.writeln(`Since: 'v3.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "DYNU_API_KEY": API key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "DYNU_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "DYNU_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "DYNU_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "DYNU_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/dynu`) case "easydns": // generated from: providers/dns/easydns/easydns.toml ew.writeln(`Configuration for EasyDNS.`) ew.writeln(`Code: 'easydns'`) ew.writeln(`Since: 'v2.6.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "EASYDNS_KEY": API Key`) ew.writeln(` - "EASYDNS_TOKEN": API Token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "EASYDNS_ENDPOINT": The endpoint URL of the API Server`) ew.writeln(` - "EASYDNS_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "EASYDNS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "EASYDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "EASYDNS_SEQUENCE_INTERVAL": Time between sequential requests`) ew.writeln(` - "EASYDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/easydns`) case "edgedns": // generated from: providers/dns/edgedns/edgedns.toml ew.writeln(`Configuration for Akamai EdgeDNS.`) ew.writeln(`Code: 'edgedns'`) ew.writeln(`Since: 'v3.9.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "AKAMAI_ACCESS_TOKEN": Access token, managed by the Akamai EdgeGrid client`) ew.writeln(` - "AKAMAI_CLIENT_SECRET": Client secret, managed by the Akamai EdgeGrid client`) ew.writeln(` - "AKAMAI_CLIENT_TOKEN": Client token, managed by the Akamai EdgeGrid client`) ew.writeln(` - "AKAMAI_EDGERC": Path to the .edgerc file, managed by the Akamai EdgeGrid client`) ew.writeln(` - "AKAMAI_EDGERC_SECTION": Configuration section, managed by the Akamai EdgeGrid client`) ew.writeln(` - "AKAMAI_HOST": API host, managed by the Akamai EdgeGrid client`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "AKAMAI_POLLING_INTERVAL": Time between DNS propagation check. Default: 15 seconds`) ew.writeln(` - "AKAMAI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation. Default: 3 minutes`) ew.writeln(` - "AKAMAI_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/edgedns`) case "epik": // generated from: providers/dns/epik/epik.toml ew.writeln(`Configuration for Epik.`) ew.writeln(`Code: 'epik'`) ew.writeln(`Since: 'v4.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "EPIK_SIGNATURE": Epik API signature (https://registrar.epik.com/account/api-settings/)`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "EPIK_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "EPIK_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "EPIK_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "EPIK_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/epik`) case "exec": // generated from: providers/dns/exec/exec.toml ew.writeln(`Configuration for External program.`) ew.writeln(`Code: 'exec'`) ew.writeln(`Since: 'v0.5.0'`) ew.writeln() ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/exec`) case "exoscale": // generated from: providers/dns/exoscale/exoscale.toml ew.writeln(`Configuration for Exoscale.`) ew.writeln(`Code: 'exoscale'`) ew.writeln(`Since: 'v0.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "EXOSCALE_API_KEY": API key`) ew.writeln(` - "EXOSCALE_API_SECRET": API secret`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "EXOSCALE_API_ZONE": API zone`) ew.writeln(` - "EXOSCALE_ENDPOINT": API endpoint URL`) ew.writeln(` - "EXOSCALE_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "EXOSCALE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "EXOSCALE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "EXOSCALE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/exoscale`) case "freemyip": // generated from: providers/dns/freemyip/freemyip.toml ew.writeln(`Configuration for freemyip.com.`) ew.writeln(`Code: 'freemyip'`) ew.writeln(`Since: 'v4.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "FREEMYIP_TOKEN": Account token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "FREEMYIP_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "FREEMYIP_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "FREEMYIP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "FREEMYIP_SEQUENCE_INTERVAL": Time between sequential requests`) ew.writeln(` - "FREEMYIP_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/freemyip`) case "gandi": // generated from: providers/dns/gandi/gandi.toml ew.writeln(`Configuration for Gandi.`) ew.writeln(`Code: 'gandi'`) ew.writeln(`Since: 'v0.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "GANDI_API_KEY": API key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "GANDI_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "GANDI_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "GANDI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "GANDI_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/gandi`) case "gandiv5": // generated from: providers/dns/gandiv5/gandiv5.toml ew.writeln(`Configuration for Gandi Live DNS (v5).`) ew.writeln(`Code: 'gandiv5'`) ew.writeln(`Since: 'v0.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "GANDIV5_API_KEY": API key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "GANDIV5_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "GANDIV5_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "GANDIV5_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "GANDIV5_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/gandiv5`) case "gcloud": // generated from: providers/dns/gcloud/gcloud.toml ew.writeln(`Configuration for Google Cloud.`) ew.writeln(`Code: 'gcloud'`) ew.writeln(`Since: 'v0.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "Application Default Credentials": [Documentation](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application)`) ew.writeln(` - "GCE_PROJECT": Project name (by default, the project name is auto-detected by using the metadata service)`) ew.writeln(` - "GCE_SERVICE_ACCOUNT": Account`) ew.writeln(` - "GCE_SERVICE_ACCOUNT_FILE": Account file path`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "GCE_ALLOW_PRIVATE_ZONE": Allows requested domain to be in private DNS zone, works only with a private ACME server (by default: false)`) ew.writeln(` - "GCE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "GCE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "GCE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/gcloud`) case "gcore": // generated from: providers/dns/gcore/gcore.toml ew.writeln(`Configuration for G-Core Labs.`) ew.writeln(`Code: 'gcore'`) ew.writeln(`Since: 'v4.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "GCORE_PERMANENT_API_TOKEN": Permanent API tokene (https://gcorelabs.com/blog/permanent-api-token-explained/)`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "GCORE_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "GCORE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "GCORE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "GCORE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/gcore`) case "glesys": // generated from: providers/dns/glesys/glesys.toml ew.writeln(`Configuration for Glesys.`) ew.writeln(`Code: 'glesys'`) ew.writeln(`Since: 'v0.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "GLESYS_API_KEY": API key`) ew.writeln(` - "GLESYS_API_USER": API user`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "GLESYS_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "GLESYS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "GLESYS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "GLESYS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/glesys`) case "godaddy": // generated from: providers/dns/godaddy/godaddy.toml ew.writeln(`Configuration for Go Daddy.`) ew.writeln(`Code: 'godaddy'`) ew.writeln(`Since: 'v0.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "GODADDY_API_KEY": API key`) ew.writeln(` - "GODADDY_API_SECRET": API secret`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "GODADDY_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "GODADDY_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "GODADDY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "GODADDY_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/godaddy`) case "hetzner": // generated from: providers/dns/hetzner/hetzner.toml ew.writeln(`Configuration for Hetzner.`) ew.writeln(`Code: 'hetzner'`) ew.writeln(`Since: 'v3.7.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "HETZNER_API_KEY": API key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "HETZNER_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "HETZNER_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "HETZNER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "HETZNER_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/hetzner`) case "hostingde": // generated from: providers/dns/hostingde/hostingde.toml ew.writeln(`Configuration for Hosting.de.`) ew.writeln(`Code: 'hostingde'`) ew.writeln(`Since: 'v1.1.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "HOSTINGDE_API_KEY": API key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "HOSTINGDE_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "HOSTINGDE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "HOSTINGDE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "HOSTINGDE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "HOSTINGDE_ZONE_NAME": Zone name in ACE format`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/hostingde`) case "hosttech": // generated from: providers/dns/hosttech/hosttech.toml ew.writeln(`Configuration for Hosttech.`) ew.writeln(`Code: 'hosttech'`) ew.writeln(`Since: 'v4.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "HOSTTECH_API_KEY": API login`) ew.writeln(` - "HOSTTECH_PASSWORD": API password`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "HOSTTECH_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "HOSTTECH_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "HOSTTECH_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "HOSTTECH_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/hosttech`) case "httpreq": // generated from: providers/dns/httpreq/httpreq.toml ew.writeln(`Configuration for HTTP request.`) ew.writeln(`Code: 'httpreq'`) ew.writeln(`Since: 'v2.0.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "HTTPREQ_ENDPOINT": The URL of the server`) ew.writeln(` - "HTTPREQ_MODE": 'RAW', none`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "HTTPREQ_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "HTTPREQ_PASSWORD": Basic authentication password`) ew.writeln(` - "HTTPREQ_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "HTTPREQ_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "HTTPREQ_USERNAME": Basic authentication username`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/httpreq`) case "hurricane": // generated from: providers/dns/hurricane/hurricane.toml ew.writeln(`Configuration for Hurricane Electric DNS.`) ew.writeln(`Code: 'hurricane'`) ew.writeln(`Since: 'v4.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "HURRICANE_TOKENS": TXT record names and tokens`) ew.writeln() ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/hurricane`) case "hyperone": // generated from: providers/dns/hyperone/hyperone.toml ew.writeln(`Configuration for HyperOne.`) ew.writeln(`Code: 'hyperone'`) ew.writeln(`Since: 'v3.9.0'`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "HYPERONE_API_URL": Allows to pass custom API Endpoint to be used in the challenge (default https://api.hyperone.com/v2)`) ew.writeln(` - "HYPERONE_LOCATION_ID": Specifies location (region) to be used in API calls. (default pl-waw-1)`) ew.writeln(` - "HYPERONE_PASSPORT_LOCATION": Allows to pass custom passport file location (default ~/.h1/passport.json)`) ew.writeln(` - "HYPERONE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "HYPERONE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "HYPERONE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/hyperone`) case "ibmcloud": // generated from: providers/dns/ibmcloud/ibmcloud.toml ew.writeln(`Configuration for IBM Cloud (SoftLayer).`) ew.writeln(`Code: 'ibmcloud'`) ew.writeln(`Since: 'v4.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "SOFTLAYER_API_KEY": Classic Infrastructure API key`) ew.writeln(` - "SOFTLAYER_USERNAME": User name (IBM Cloud is _)`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "SOFTLAYER_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "SOFTLAYER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "SOFTLAYER_TIMEOUT": API request timeout`) ew.writeln(` - "SOFTLAYER_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ibmcloud`) case "iij": // generated from: providers/dns/iij/iij.toml ew.writeln(`Configuration for Internet Initiative Japan.`) ew.writeln(`Code: 'iij'`) ew.writeln(`Since: 'v1.1.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "IIJ_API_ACCESS_KEY": API access key`) ew.writeln(` - "IIJ_API_SECRET_KEY": API secret key`) ew.writeln(` - "IIJ_DO_SERVICE_CODE": DO service code`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "IIJ_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "IIJ_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "IIJ_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/iij`) case "iijdpf": // generated from: providers/dns/iijdpf/iijdpf.toml ew.writeln(`Configuration for IIJ DNS Platform Service.`) ew.writeln(`Code: 'iijdpf'`) ew.writeln(`Since: 'v4.7.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "IIJ_DPF_API_TOKEN": API token`) ew.writeln(` - "IIJ_DPF_DPM_SERVICE_CODE": IIJ Managed DNS Service's service code`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "IIJ_DPF_API_ENDPOINT": API endpoint URL, defaults to https://api.dns-platform.jp/dpf/v1`) ew.writeln(` - "IIJ_DPF_POLLING_INTERVAL": Time between DNS propagation check, defaults to 5 second`) ew.writeln(` - "IIJ_DPF_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation, defaults to 660 second`) ew.writeln(` - "IIJ_DPF_TTL": The TTL of the TXT record used for the DNS challenge, default to 300`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/iijdpf`) case "infoblox": // generated from: providers/dns/infoblox/infoblox.toml ew.writeln(`Configuration for Infoblox.`) ew.writeln(`Code: 'infoblox'`) ew.writeln(`Since: 'v4.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "INFOBLOX_HOST": Host URI`) ew.writeln(` - "INFOBLOX_PASSWORD": Account Password`) ew.writeln(` - "INFOBLOX_USERNAME": Account Username`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "INFOBLOX_DNS_VIEW": The view for the TXT records, default: External`) ew.writeln(` - "INFOBLOX_HTTP_TIMEOUT": HTTP request timeout`) ew.writeln(` - "INFOBLOX_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "INFOBLOX_PORT": The port for the infoblox grid manager, default: 443`) ew.writeln(` - "INFOBLOX_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "INFOBLOX_SSL_VERIFY": Whether or not to verify the TLS certificate, default: true`) ew.writeln(` - "INFOBLOX_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "INFOBLOX_WAPI_VERSION": The version of WAPI being used, default: 2.11`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/infoblox`) case "infomaniak": // generated from: providers/dns/infomaniak/infomaniak.toml ew.writeln(`Configuration for Infomaniak.`) ew.writeln(`Code: 'infomaniak'`) ew.writeln(`Since: 'v4.1.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "INFOMANIAK_ACCESS_TOKEN": Access token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "INFOMANIAK_ENDPOINT": https://api.infomaniak.com`) ew.writeln(` - "INFOMANIAK_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "INFOMANIAK_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "INFOMANIAK_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "INFOMANIAK_TTL": The TTL of the TXT record used for the DNS challenge in seconds`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/infomaniak`) case "internetbs": // generated from: providers/dns/internetbs/internetbs.toml ew.writeln(`Configuration for Internet.bs.`) ew.writeln(`Code: 'internetbs'`) ew.writeln(`Since: 'v4.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "INTERNET_BS_API_KEY": API key`) ew.writeln(` - "INTERNET_BS_PASSWORD": API password`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "INTERNET_BS_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "INTERNET_BS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "INTERNET_BS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "INTERNET_BS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/internetbs`) case "inwx": // generated from: providers/dns/inwx/inwx.toml ew.writeln(`Configuration for INWX.`) ew.writeln(`Code: 'inwx'`) ew.writeln(`Since: 'v2.0.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "INWX_PASSWORD": Password`) ew.writeln(` - "INWX_USERNAME": Username`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "INWX_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "INWX_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation (default 360s)`) ew.writeln(` - "INWX_SANDBOX": Activate the sandbox (boolean)`) ew.writeln(` - "INWX_SHARED_SECRET": shared secret related to 2FA`) ew.writeln(` - "INWX_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/inwx`) case "ionos": // generated from: providers/dns/ionos/ionos.toml ew.writeln(`Configuration for Ionos.`) ew.writeln(`Code: 'ionos'`) ew.writeln(`Since: 'v4.2.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "IONOS_API_KEY": API key '.' https://developer.hosting.ionos.com/docs/getstarted`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "IONOS_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "IONOS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "IONOS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "IONOS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ionos`) case "iwantmyname": // generated from: providers/dns/iwantmyname/iwantmyname.toml ew.writeln(`Configuration for iwantmyname.`) ew.writeln(`Code: 'iwantmyname'`) ew.writeln(`Since: 'v4.7.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "IWANTMYNAME_PASSWORD": API password`) ew.writeln(` - "IWANTMYNAME_USERNAME": API username`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "IWANTMYNAME_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "IWANTMYNAME_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "IWANTMYNAME_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "IWANTMYNAME_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/iwantmyname`) case "joker": // generated from: providers/dns/joker/joker.toml ew.writeln(`Configuration for Joker.`) ew.writeln(`Code: 'joker'`) ew.writeln(`Since: 'v2.6.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "JOKER_API_KEY": API key (only with DMAPI mode)`) ew.writeln(` - "JOKER_API_MODE": 'DMAPI' or 'SVC'. DMAPI is for resellers accounts. (Default: DMAPI)`) ew.writeln(` - "JOKER_PASSWORD": Joker.com password`) ew.writeln(` - "JOKER_USERNAME": Joker.com username`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "JOKER_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "JOKER_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "JOKER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "JOKER_SEQUENCE_INTERVAL": Time between sequential requests (only with 'SVC' mode)`) ew.writeln(` - "JOKER_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/joker`) case "lightsail": // generated from: providers/dns/lightsail/lightsail.toml ew.writeln(`Configuration for Amazon Lightsail.`) ew.writeln(`Code: 'lightsail'`) ew.writeln(`Since: 'v0.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "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)`) ew.writeln(` - "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)`) ew.writeln(` - "DNS_ZONE": Domain name of the DNS zone`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "AWS_SHARED_CREDENTIALS_FILE": Managed by the AWS client. Shared credentials file.`) ew.writeln(` - "LIGHTSAIL_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "LIGHTSAIL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/lightsail`) case "linode": // generated from: providers/dns/linode/linode.toml ew.writeln(`Configuration for Linode (v4).`) ew.writeln(`Code: 'linode'`) ew.writeln(`Since: 'v1.1.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "LINODE_TOKEN": API token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "LINODE_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "LINODE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "LINODE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "LINODE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/linode`) case "liquidweb": // generated from: providers/dns/liquidweb/liquidweb.toml ew.writeln(`Configuration for Liquid Web.`) ew.writeln(`Code: 'liquidweb'`) ew.writeln(`Since: 'v3.1.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "LIQUID_WEB_PASSWORD": Storm API Password`) ew.writeln(` - "LIQUID_WEB_USERNAME": Storm API Username`) ew.writeln(` - "LIQUID_WEB_ZONE": DNS Zone`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "LIQUID_WEB_HTTP_TIMEOUT": Maximum waiting time for the DNS records to be created (not verified)`) ew.writeln(` - "LIQUID_WEB_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "LIQUID_WEB_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "LIQUID_WEB_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln(` - "LIQUID_WEB_URL": Storm API endpoint`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/liquidweb`) case "loopia": // generated from: providers/dns/loopia/loopia.toml ew.writeln(`Configuration for Loopia.`) ew.writeln(`Code: 'loopia'`) ew.writeln(`Since: 'v4.2.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "LOOPIA_API_PASSWORD": API password`) ew.writeln(` - "LOOPIA_API_USER": API username`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "LOOPIA_API_URL": API endpoint. Ex: https://api.loopia.se/RPCSERV or https://api.loopia.rs/RPCSERV`) ew.writeln(` - "LOOPIA_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "LOOPIA_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "LOOPIA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "LOOPIA_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/loopia`) case "luadns": // generated from: providers/dns/luadns/luadns.toml ew.writeln(`Configuration for LuaDNS.`) ew.writeln(`Code: 'luadns'`) ew.writeln(`Since: 'v3.7.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "LUADNS_API_TOKEN": API token`) ew.writeln(` - "LUADNS_API_USERNAME": Username (your email)`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "LUADNS_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "LUADNS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "LUADNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "LUADNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/luadns`) case "mydnsjp": // generated from: providers/dns/mydnsjp/mydnsjp.toml ew.writeln(`Configuration for MyDNS.jp.`) ew.writeln(`Code: 'mydnsjp'`) ew.writeln(`Since: 'v1.2.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "MYDNSJP_MASTER_ID": Master ID`) ew.writeln(` - "MYDNSJP_PASSWORD": Password`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "MYDNSJP_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "MYDNSJP_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "MYDNSJP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "MYDNSJP_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/mydnsjp`) case "mythicbeasts": // generated from: providers/dns/mythicbeasts/mythicbeasts.toml ew.writeln(`Configuration for MythicBeasts.`) ew.writeln(`Code: 'mythicbeasts'`) ew.writeln(`Since: 'v0.3.7'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "MYTHICBEASTS_PASSWORD": Password`) ew.writeln(` - "MYTHICBEASTS_USERNAME": User name`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "MYTHICBEASTS_API_ENDPOINT": The endpoint for the API (must implement v2)`) ew.writeln(` - "MYTHICBEASTS_AUTH_API_ENDPOINT": The endpoint for Mythic Beasts' Authentication`) ew.writeln(` - "MYTHICBEASTS_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "MYTHICBEASTS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "MYTHICBEASTS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "MYTHICBEASTS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/mythicbeasts`) case "namecheap": // generated from: providers/dns/namecheap/namecheap.toml ew.writeln(`Configuration for Namecheap.`) ew.writeln(`Code: 'namecheap'`) ew.writeln(`Since: 'v0.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "NAMECHEAP_API_KEY": API key`) ew.writeln(` - "NAMECHEAP_API_USER": API user`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "NAMECHEAP_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "NAMECHEAP_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "NAMECHEAP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "NAMECHEAP_SANDBOX": Activate the sandbox (boolean)`) ew.writeln(` - "NAMECHEAP_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/namecheap`) case "namedotcom": // generated from: providers/dns/namedotcom/namedotcom.toml ew.writeln(`Configuration for Name.com.`) ew.writeln(`Code: 'namedotcom'`) ew.writeln(`Since: 'v0.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "NAMECOM_API_TOKEN": API token`) ew.writeln(` - "NAMECOM_USERNAME": Username`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "NAMECOM_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "NAMECOM_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "NAMECOM_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "NAMECOM_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/namedotcom`) case "namesilo": // generated from: providers/dns/namesilo/namesilo.toml ew.writeln(`Configuration for Namesilo.`) ew.writeln(`Code: 'namesilo'`) ew.writeln(`Since: 'v2.7.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "NAMESILO_API_KEY": Client ID`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "NAMESILO_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "NAMESILO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation, it is better to set larger than 15m`) ew.writeln(` - "NAMESILO_TTL": The TTL of the TXT record used for the DNS challenge, should be in [3600, 2592000]`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/namesilo`) case "nearlyfreespeech": // generated from: providers/dns/nearlyfreespeech/nearlyfreespeech.toml ew.writeln(`Configuration for NearlyFreeSpeech.NET.`) ew.writeln(`Code: 'nearlyfreespeech'`) ew.writeln(`Since: 'v4.8.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "NEARLYFREESPEECH_API_KEY": API Key for API requests`) ew.writeln(` - "NEARLYFREESPEECH_LOGIN": Username for API requests`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "NEARLYFREESPEECH_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "NEARLYFREESPEECH_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "NEARLYFREESPEECH_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "NEARLYFREESPEECH_SEQUENCE_INTERVAL": Time between sequential requests`) ew.writeln(` - "NEARLYFREESPEECH_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/nearlyfreespeech`) case "netcup": // generated from: providers/dns/netcup/netcup.toml ew.writeln(`Configuration for Netcup.`) ew.writeln(`Code: 'netcup'`) ew.writeln(`Since: 'v1.1.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "NETCUP_API_KEY": API key`) ew.writeln(` - "NETCUP_API_PASSWORD": API password`) ew.writeln(` - "NETCUP_CUSTOMER_NUMBER": Customer number`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "NETCUP_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "NETCUP_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "NETCUP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "NETCUP_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/netcup`) case "netlify": // generated from: providers/dns/netlify/netlify.toml ew.writeln(`Configuration for Netlify.`) ew.writeln(`Code: 'netlify'`) ew.writeln(`Since: 'v3.7.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "NETLIFY_TOKEN": Token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "NETLIFY_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "NETLIFY_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "NETLIFY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "NETLIFY_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/netlify`) case "nicmanager": // generated from: providers/dns/nicmanager/nicmanager.toml ew.writeln(`Configuration for Nicmanager.`) ew.writeln(`Code: 'nicmanager'`) ew.writeln(`Since: 'v4.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "NICMANAGER_API_EMAIL": Email-based login`) ew.writeln(` - "NICMANAGER_API_LOGIN": Login, used for Username-based login`) ew.writeln(` - "NICMANAGER_API_PASSWORD": Password, always required`) ew.writeln(` - "NICMANAGER_API_USERNAME": Username, used for Username-based login`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "NICMANAGER_API_MODE": mode: 'anycast' or 'zone' (default: 'anycast')`) ew.writeln(` - "NICMANAGER_API_OTP": TOTP Secret (optional)`) ew.writeln(` - "NICMANAGER_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "NICMANAGER_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "NICMANAGER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "NICMANAGER_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/nicmanager`) case "nifcloud": // generated from: providers/dns/nifcloud/nifcloud.toml ew.writeln(`Configuration for NIFCloud.`) ew.writeln(`Code: 'nifcloud'`) ew.writeln(`Since: 'v1.1.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "NIFCLOUD_ACCESS_KEY_ID": Access key`) ew.writeln(` - "NIFCLOUD_SECRET_ACCESS_KEY": Secret access key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "NIFCLOUD_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "NIFCLOUD_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "NIFCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "NIFCLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/nifcloud`) case "njalla": // generated from: providers/dns/njalla/njalla.toml ew.writeln(`Configuration for Njalla.`) ew.writeln(`Code: 'njalla'`) ew.writeln(`Since: 'v4.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "NJALLA_TOKEN": API token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "NJALLA_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "NJALLA_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "NJALLA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "NJALLA_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/njalla`) case "ns1": // generated from: providers/dns/ns1/ns1.toml ew.writeln(`Configuration for NS1.`) ew.writeln(`Code: 'ns1'`) ew.writeln(`Since: 'v0.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "NS1_API_KEY": API key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "NS1_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "NS1_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "NS1_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "NS1_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ns1`) case "oraclecloud": // generated from: providers/dns/oraclecloud/oraclecloud.toml ew.writeln(`Configuration for Oracle Cloud.`) ew.writeln(`Code: 'oraclecloud'`) ew.writeln(`Since: 'v2.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "OCI_COMPARTMENT_OCID": Compartment OCID`) ew.writeln(` - "OCI_PRIVKEY_FILE": Private key file`) ew.writeln(` - "OCI_PRIVKEY_PASS": Private key password`) ew.writeln(` - "OCI_PUBKEY_FINGERPRINT": Public key fingerprint`) ew.writeln(` - "OCI_REGION": Region`) ew.writeln(` - "OCI_TENANCY_OCID": Tenancy OCID`) ew.writeln(` - "OCI_USER_OCID": User OCID`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "OCI_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "OCI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "OCI_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/oraclecloud`) case "otc": // generated from: providers/dns/otc/otc.toml ew.writeln(`Configuration for Open Telekom Cloud.`) ew.writeln(`Code: 'otc'`) ew.writeln(`Since: 'v0.4.1'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "OTC_DOMAIN_NAME": Domain name`) ew.writeln(` - "OTC_IDENTITY_ENDPOINT": Identity endpoint URL`) ew.writeln(` - "OTC_PASSWORD": Password`) ew.writeln(` - "OTC_PROJECT_NAME": Project name`) ew.writeln(` - "OTC_USER_NAME": User name`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "OTC_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "OTC_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "OTC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "OTC_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/otc`) case "ovh": // generated from: providers/dns/ovh/ovh.toml ew.writeln(`Configuration for OVH.`) ew.writeln(`Code: 'ovh'`) ew.writeln(`Since: 'v0.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "OVH_APPLICATION_KEY": Application key`) ew.writeln(` - "OVH_APPLICATION_SECRET": Application secret`) ew.writeln(` - "OVH_CONSUMER_KEY": Consumer key`) ew.writeln(` - "OVH_ENDPOINT": Endpoint URL (ovh-eu or ovh-ca)`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "OVH_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "OVH_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "OVH_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "OVH_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/ovh`) case "pdns": // generated from: providers/dns/pdns/pdns.toml ew.writeln(`Configuration for PowerDNS.`) ew.writeln(`Code: 'pdns'`) ew.writeln(`Since: 'v0.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "PDNS_API_KEY": API key`) ew.writeln(` - "PDNS_API_URL": API URL`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "PDNS_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "PDNS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "PDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "PDNS_SERVER_NAME": Name of the server in the URL, 'localhost' by default`) ew.writeln(` - "PDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/pdns`) case "porkbun": // generated from: providers/dns/porkbun/porkbun.toml ew.writeln(`Configuration for Porkbun.`) ew.writeln(`Code: 'porkbun'`) ew.writeln(`Since: 'v4.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "PORKBUN_API_KEY": API key`) ew.writeln(` - "PORKBUN_SECRET_API_KEY": secret API key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "PORKBUN_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "PORKBUN_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "PORKBUN_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "PORKBUN_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/porkbun`) case "rackspace": // generated from: providers/dns/rackspace/rackspace.toml ew.writeln(`Configuration for Rackspace.`) ew.writeln(`Code: 'rackspace'`) ew.writeln(`Since: 'v0.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "RACKSPACE_API_KEY": API key`) ew.writeln(` - "RACKSPACE_USER": API user`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "RACKSPACE_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "RACKSPACE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "RACKSPACE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "RACKSPACE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/rackspace`) case "regru": // generated from: providers/dns/regru/regru.toml ew.writeln(`Configuration for reg.ru.`) ew.writeln(`Code: 'regru'`) ew.writeln(`Since: 'v3.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "REGRU_PASSWORD": API password`) ew.writeln(` - "REGRU_USERNAME": API username`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "REGRU_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "REGRU_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "REGRU_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "REGRU_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/regru`) case "rfc2136": // generated from: providers/dns/rfc2136/rfc2136.toml ew.writeln(`Configuration for RFC2136.`) ew.writeln(`Code: 'rfc2136'`) ew.writeln(`Since: 'v0.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "RFC2136_NAMESERVER": Network address in the form "host" or "host:port"`) ew.writeln(` - "RFC2136_TSIG_ALGORITHM": TSIG algorithm. See [miekg/dns#tsig.go](https://github.com/miekg/dns/blob/master/tsig.go) for supported values. To disable TSIG authentication, leave the 'RFC2136_TSIG*' variables unset.`) ew.writeln(` - "RFC2136_TSIG_KEY": Name of the secret key as defined in DNS server configuration. To disable TSIG authentication, leave the 'RFC2136_TSIG*' variables unset.`) ew.writeln(` - "RFC2136_TSIG_SECRET": Secret key payload. To disable TSIG authentication, leave the' RFC2136_TSIG*' variables unset.`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "RFC2136_DNS_TIMEOUT": API request timeout`) ew.writeln(` - "RFC2136_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "RFC2136_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "RFC2136_SEQUENCE_INTERVAL": Time between sequential requests`) ew.writeln(` - "RFC2136_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/rfc2136`) case "rimuhosting": // generated from: providers/dns/rimuhosting/rimuhosting.toml ew.writeln(`Configuration for RimuHosting.`) ew.writeln(`Code: 'rimuhosting'`) ew.writeln(`Since: 'v0.3.5'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "RIMUHOSTING_API_KEY": User API key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "RIMUHOSTING_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "RIMUHOSTING_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "RIMUHOSTING_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "RIMUHOSTING_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/rimuhosting`) case "route53": // generated from: providers/dns/route53/route53.toml ew.writeln(`Configuration for Amazon Route 53.`) ew.writeln(`Code: 'route53'`) ew.writeln(`Since: 'v0.3.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "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)`) ew.writeln(` - "AWS_ASSUME_ROLE_ARN": Managed by the AWS Role ARN ('AWS_ASSUME_ROLE_ARN' is not supported)`) ew.writeln(` - "AWS_HOSTED_ZONE_ID": Override the hosted zone ID.`) ew.writeln(` - "AWS_PROFILE": Managed by the AWS client ('AWS_PROFILE_FILE' is not supported)`) ew.writeln(` - "AWS_REGION": Managed by the AWS client ('AWS_REGION_FILE' is not supported)`) ew.writeln(` - "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)`) ew.writeln(` - "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)`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "AWS_MAX_RETRIES": The number of maximum returns the service will use to make an individual API request`) ew.writeln(` - "AWS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "AWS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "AWS_SHARED_CREDENTIALS_FILE": Managed by the AWS client. Shared credentials file.`) ew.writeln(` - "AWS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/route53`) case "safedns": // generated from: providers/dns/safedns/safedns.toml ew.writeln(`Configuration for UKFast SafeDNS.`) ew.writeln(`Code: 'safedns'`) ew.writeln(`Since: 'v4.6.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "SAFEDNS_AUTH_TOKEN": Authentication token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "SAFEDNS_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "SAFEDNS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "SAFEDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "SAFEDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/safedns`) case "sakuracloud": // generated from: providers/dns/sakuracloud/sakuracloud.toml ew.writeln(`Configuration for Sakura Cloud.`) ew.writeln(`Code: 'sakuracloud'`) ew.writeln(`Since: 'v1.1.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "SAKURACLOUD_ACCESS_TOKEN": Access token`) ew.writeln(` - "SAKURACLOUD_ACCESS_TOKEN_SECRET": Access token secret`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "SAKURACLOUD_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "SAKURACLOUD_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "SAKURACLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "SAKURACLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/sakuracloud`) case "scaleway": // generated from: providers/dns/scaleway/scaleway.toml ew.writeln(`Configuration for Scaleway.`) ew.writeln(`Code: 'scaleway'`) ew.writeln(`Since: 'v3.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "SCALEWAY_API_TOKEN": API token`) ew.writeln(` - "SCALEWAY_PROJECT_ID": Project to use (optional)`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "SCALEWAY_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "SCALEWAY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "SCALEWAY_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/scaleway`) case "selectel": // generated from: providers/dns/selectel/selectel.toml ew.writeln(`Configuration for Selectel.`) ew.writeln(`Code: 'selectel'`) ew.writeln(`Since: 'v1.2.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "SELECTEL_API_TOKEN": API token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "SELECTEL_BASE_URL": API endpoint URL`) ew.writeln(` - "SELECTEL_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "SELECTEL_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "SELECTEL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "SELECTEL_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/selectel`) case "servercow": // generated from: providers/dns/servercow/servercow.toml ew.writeln(`Configuration for Servercow.`) ew.writeln(`Code: 'servercow'`) ew.writeln(`Since: 'v3.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "SERVERCOW_PASSWORD": API password`) ew.writeln(` - "SERVERCOW_USERNAME": API username`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "SERVERCOW_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "SERVERCOW_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "SERVERCOW_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "SERVERCOW_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/servercow`) case "simply": // generated from: providers/dns/simply/simply.toml ew.writeln(`Configuration for Simply.com.`) ew.writeln(`Code: 'simply'`) ew.writeln(`Since: 'v4.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "SIMPLY_ACCOUNT_NAME": Account name`) ew.writeln(` - "SIMPLY_API_KEY": API key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "SIMPLY_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "SIMPLY_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "SIMPLY_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "SIMPLY_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/simply`) case "sonic": // generated from: providers/dns/sonic/sonic.toml ew.writeln(`Configuration for Sonic.`) ew.writeln(`Code: 'sonic'`) ew.writeln(`Since: 'v4.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "SONIC_API_KEY": API Key`) ew.writeln(` - "SONIC_USER_ID": User ID`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "SONIC_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "SONIC_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "SONIC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "SONIC_SEQUENCE_INTERVAL": Time between sequential requests`) ew.writeln(` - "SONIC_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/sonic`) case "stackpath": // generated from: providers/dns/stackpath/stackpath.toml ew.writeln(`Configuration for Stackpath.`) ew.writeln(`Code: 'stackpath'`) ew.writeln(`Since: 'v1.1.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "STACKPATH_CLIENT_ID": Client ID`) ew.writeln(` - "STACKPATH_CLIENT_SECRET": Client secret`) ew.writeln(` - "STACKPATH_STACK_ID": Stack ID`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "STACKPATH_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "STACKPATH_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "STACKPATH_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/stackpath`) case "tencentcloud": // generated from: providers/dns/tencentcloud/tencentcloud.toml ew.writeln(`Configuration for Tencent Cloud DNS.`) ew.writeln(`Code: 'tencentcloud'`) ew.writeln(`Since: 'v4.6.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "TENCENTCLOUD_SECRET_ID": Access key ID`) ew.writeln(` - "TENCENTCLOUD_SECRET_KEY": Access Key secret`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "TENCENTCLOUD_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "TENCENTCLOUD_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "TENCENTCLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "TENCENTCLOUD_REGION": Region`) ew.writeln(` - "TENCENTCLOUD_SESSION_TOKEN": Access Key token`) ew.writeln(` - "TENCENTCLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/tencentcloud`) case "transip": // generated from: providers/dns/transip/transip.toml ew.writeln(`Configuration for TransIP.`) ew.writeln(`Code: 'transip'`) ew.writeln(`Since: 'v2.0.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "TRANSIP_ACCOUNT_NAME": Account name`) ew.writeln(` - "TRANSIP_PRIVATE_KEY_PATH": Private key path`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "TRANSIP_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "TRANSIP_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "TRANSIP_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/transip`) case "variomedia": // generated from: providers/dns/variomedia/variomedia.toml ew.writeln(`Configuration for Variomedia.`) ew.writeln(`Code: 'variomedia'`) ew.writeln(`Since: 'v4.8.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "VARIOMEDIA_API_TOKEN": API token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "DODE_SEQUENCE_INTERVAL": Time between sequential requests`) ew.writeln(` - "VARIOMEDIA_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "VARIOMEDIA_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "VARIOMEDIA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "VARIOMEDIA_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/variomedia`) case "vegadns": // generated from: providers/dns/vegadns/vegadns.toml ew.writeln(`Configuration for VegaDNS.`) ew.writeln(`Code: 'vegadns'`) ew.writeln(`Since: 'v1.1.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "SECRET_VEGADNS_KEY": API key`) ew.writeln(` - "SECRET_VEGADNS_SECRET": API secret`) ew.writeln(` - "VEGADNS_URL": API endpoint URL`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "VEGADNS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "VEGADNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "VEGADNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/vegadns`) case "vercel": // generated from: providers/dns/vercel/vercel.toml ew.writeln(`Configuration for Vercel.`) ew.writeln(`Code: 'vercel'`) ew.writeln(`Since: 'v4.7.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "VERCEL_API_TOKEN": Authentication token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "VERCEL_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "VERCEL_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "VERCEL_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "VERCEL_TEAM_ID": Team ID (ex: team_xxxxxxxxxxxxxxxxxxxxxxxx)`) ew.writeln(` - "VERCEL_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/vercel`) case "versio": // generated from: providers/dns/versio/versio.toml ew.writeln(`Configuration for Versio.[nl|eu|uk].`) ew.writeln(`Code: 'versio'`) ew.writeln(`Since: 'v2.7.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "VERSIO_PASSWORD": Basic authentication password`) ew.writeln(` - "VERSIO_USERNAME": Basic authentication username`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "VERSIO_ENDPOINT": The endpoint URL of the API Server`) ew.writeln(` - "VERSIO_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "VERSIO_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "VERSIO_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "VERSIO_SEQUENCE_INTERVAL": Time between sequential requests, default 60s`) ew.writeln(` - "VERSIO_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/versio`) case "vinyldns": // generated from: providers/dns/vinyldns/vinyldns.toml ew.writeln(`Configuration for VinylDNS.`) ew.writeln(`Code: 'vinyldns'`) ew.writeln(`Since: 'v4.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "VINYLDNS_ACCESS_KEY": The VinylDNS API key`) ew.writeln(` - "VINYLDNS_HOST": The VinylDNS API URL`) ew.writeln(` - "VINYLDNS_SECRET_KEY": The VinylDNS API Secret key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "VINYLDNS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "VINYLDNS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "VINYLDNS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/vinyldns`) case "vkcloud": // generated from: providers/dns/vkcloud/vkcloud.toml ew.writeln(`Configuration for VK Cloud.`) ew.writeln(`Code: 'vkcloud'`) ew.writeln(`Since: 'v4.9.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "VK_CLOUD_PASSWORD": Password for VK Cloud account`) ew.writeln(` - "VK_CLOUD_PROJECT_ID": String ID of project in VK Cloud`) ew.writeln(` - "VK_CLOUD_USERNAME": Email of VK Cloud account`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "VK_CLOUD_DNS_ENDPOINT": URL of DNS API. Defaults to https://mcs.mail.ru/public-dns but can be changed for usage with private clouds`) ew.writeln(` - "VK_CLOUD_DOMAIN_NAME": Openstack users domain name. Defaults to 'users' but can be changed for usage with private clouds`) ew.writeln(` - "VK_CLOUD_IDENTITY_ENDPOINT": URL of OpenStack Auth API, Defaults to https://infra.mail.ru:35357/v3/ but can be changed for usage with private clouds`) ew.writeln(` - "VK_CLOUD_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "VK_CLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "VK_CLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/vkcloud`) case "vscale": // generated from: providers/dns/vscale/vscale.toml ew.writeln(`Configuration for Vscale.`) ew.writeln(`Code: 'vscale'`) ew.writeln(`Since: 'v2.0.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "VSCALE_API_TOKEN": API token`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "VSCALE_BASE_URL": API endpoint URL`) ew.writeln(` - "VSCALE_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "VSCALE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "VSCALE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "VSCALE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/vscale`) case "vultr": // generated from: providers/dns/vultr/vultr.toml ew.writeln(`Configuration for Vultr.`) ew.writeln(`Code: 'vultr'`) ew.writeln(`Since: 'v0.3.1'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "VULTR_API_KEY": API key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "VULTR_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "VULTR_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "VULTR_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "VULTR_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/vultr`) case "wedos": // generated from: providers/dns/wedos/wedos.toml ew.writeln(`Configuration for WEDOS.`) ew.writeln(`Code: 'wedos'`) ew.writeln(`Since: 'v4.4.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "WEDOS_USERNAME": Username is the same as for the admin account`) ew.writeln(` - "WEDOS_WAPI_PASSWORD": Password needs to be generated and IP allowed in the admin interface`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "WEDOS_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "WEDOS_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "WEDOS_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "WEDOS_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/wedos`) case "yandex": // generated from: providers/dns/yandex/yandex.toml ew.writeln(`Configuration for Yandex PDD.`) ew.writeln(`Code: 'yandex'`) ew.writeln(`Since: 'v3.7.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "YANDEX_PDD_TOKEN": Basic authentication username`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "YANDEX_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "YANDEX_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "YANDEX_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "YANDEX_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/yandex`) case "yandexcloud": // generated from: providers/dns/yandexcloud/yandexcloud.toml ew.writeln(`Configuration for Yandex Cloud.`) ew.writeln(`Code: 'yandexcloud'`) ew.writeln(`Since: 'v4.9.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "YANDEX_CLOUD_FOLDER_ID": The string id of folder (aka project) in Yandex Cloud`) ew.writeln(` - "YANDEX_CLOUD_IAM_TOKEN": The base64 encoded json which contains inforamtion about iam token of serivce account with 'dns.admin' permissions`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "YANDEX_CLOUD_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "YANDEX_CLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "YANDEX_CLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/yandexcloud`) case "zoneee": // generated from: providers/dns/zoneee/zoneee.toml ew.writeln(`Configuration for Zone.ee.`) ew.writeln(`Code: 'zoneee'`) ew.writeln(`Since: 'v2.1.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "ZONEEE_API_KEY": API key`) ew.writeln(` - "ZONEEE_API_USER": API user`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "ZONEEE_ENDPOINT": API endpoint URL`) ew.writeln(` - "ZONEEE_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "ZONEEE_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "ZONEEE_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "ZONEEE_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/zoneee`) case "zonomi": // generated from: providers/dns/zonomi/zonomi.toml ew.writeln(`Configuration for Zonomi.`) ew.writeln(`Code: 'zonomi'`) ew.writeln(`Since: 'v3.5.0'`) ew.writeln() ew.writeln(`Credentials:`) ew.writeln(` - "ZONOMI_API_KEY": User API key`) ew.writeln() ew.writeln(`Additional Configuration:`) ew.writeln(` - "ZONOMI_HTTP_TIMEOUT": API request timeout`) ew.writeln(` - "ZONOMI_POLLING_INTERVAL": Time between DNS propagation check`) ew.writeln(` - "ZONOMI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) ew.writeln(` - "ZONOMI_TTL": The TTL of the TXT record used for the DNS challenge`) ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/zonomi`) case "manual": ew.writeln(`Solving the DNS-01 challenge using CLI prompt.`) default: return fmt.Errorf("%q is not yet supported", name) } if ew.err != nil { return fmt.Errorf("error: %w", ew.err) } return w.Flush() } lego-4.9.1/docs/000077500000000000000000000000001434020463500134045ustar00rootroot00000000000000lego-4.9.1/docs/.gitignore000066400000000000000000000000411434020463500153670ustar00rootroot00000000000000themes/ public/ .hugo_build.lock lego-4.9.1/docs/Makefile000066400000000000000000000006321434020463500150450ustar00rootroot00000000000000.PHONY: default clean hugo hugo-build default: hugo clean: rm -rf public/ hugo-build: clean hugo-themes hugo --enableGitInfo --source . hugo: hugo server --disableFastRender --enableGitInfo --watch --source . # hugo server -D hugo-themes: rm -rf themes mkdir themes git clone --depth=1 https://github.com/matcornic/hugo-theme-learn.git themes/hugo-theme-learn rm -rf themes/hugo-theme-learn/.git lego-4.9.1/docs/archetypes/000077500000000000000000000000001434020463500155535ustar00rootroot00000000000000lego-4.9.1/docs/archetypes/default.md000066400000000000000000000001241434020463500175160ustar00rootroot00000000000000--- title: "{{ replace .Name "-" " " | title }}" date: {{ .Date }} draft: true --- lego-4.9.1/docs/config.toml000066400000000000000000000052241434020463500155510ustar00rootroot00000000000000baseURL = "https://go-acme.github.io/lego/" languageCode = "en-us" title = "Lego" theme = "hugo-theme-learn" # Code higlighting settings pygmentsCodefences = true pygmentsCodeFencesGuesSsyntax = false pygmentsOptions = "" pygmentsStyle = "monokai" # The monokai stylesheet is included in the base template. pygmentsUseClasses = true [permalinks] dns = "/dns/:slug/" [params] # Prefix URL to edit current page. Will display an "Edit this page" button on top right hand corner of every page. # Useful to give opportunity to people to create merge request for your doc. # See the config.toml file from this documentation site to have an example. # editURL = "" # Author of the site, will be used in meta information author = "Lego Team" # Description of the site, will be used in meta information # description = "" # Shows a checkmark for visited pages on the menu showVisitedLinks = true # Disable search function. It will hide search bar # disableSearch = false # Javascript and CSS cache are automatically busted when new version of site is generated. # Set this to true to disable this behavior (some proxies don't handle well this optimization) # disableAssetsBusting = false # Set this to true to disable copy-to-clipboard button for inline code. # disableInlineCopyToClipBoard = true # A title for shortcuts in menu is set by default. Set this to true to disable it. # disableShortcutsTitle = false # When using mulitlingual website, disable the switch language button. # disableLanguageSwitchingButton = false # Hide breadcrumbs in the header and only show the current page title # disableBreadcrumb = true # Hide Next and Previous page buttons normally displayed full height beside content # disableNextPrev = true # Order sections in menu by "weight" or "title". Default to "weight" # ordersectionsby = "weight" # Change default color scheme with a variant one. Can be "red", "blue", "green". themeVariant = "blue" custom_css = ["css/theme-custom.css"] disableLandingPageButton = true [Languages] [Languages.en] title = "Let’s Encrypt client and ACME library written in Go." weight = 1 languageName = "English" [[Languages.en.menu.shortcuts]] name = " GitHub repo" identifier = "ds" url = "https://github.com/go-acme/lego" weight = 10 [[Languages.en.menu.shortcuts]] name = " Issues" url = "https://github.com/go-acme/lego/issues" weight = 11 [[Languages.en.menu.shortcuts]] name = " Discussions" url = "https://github.com/go-acme/lego/discussions" weight = 12 [outputs] home = [ "HTML", "RSS", "JSON"] lego-4.9.1/docs/content/000077500000000000000000000000001434020463500150565ustar00rootroot00000000000000lego-4.9.1/docs/content/_index.md000066400000000000000000000012511434020463500166450ustar00rootroot00000000000000--- title: "Welcome" date: 2019-03-03T16:39:46+01:00 draft: false chapter: true --- # Lego Let's Encrypt client and ACME library written in Go. ## Features - ACME v2 [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html) - Register with CA - Obtain certificates, both from scratch or with an existing CSR - Renew certificates - Revoke certificates - Robust implementation of all ACME challenges - HTTP (http-01) - DNS (dns-01) - TLS (tls-alpn-01) - SAN certificate support - Comes with multiple optional [DNS providers]({{< ref "dns" >}}) - [Custom challenge solvers]({{< ref "usage/library/Writing-a-Challenge-Solver" >}}) - Certificate bundling - OCSP helper function lego-4.9.1/docs/content/dns/000077500000000000000000000000001434020463500156425ustar00rootroot00000000000000lego-4.9.1/docs/content/dns/_index.md000066400000000000000000000023231434020463500174320ustar00rootroot00000000000000--- title: "DNS Providers" date: 2019-03-03T16:39:46+01:00 draft: false weight: 3 --- ## Configuration and Credentials Credentials and DNS configuration for DNS providers must be passed through environment variables. ### Environment Variables: Value The environment variables can reference a value. Here is an example bash command using the Cloudflare DNS provider: ```console $ CLOUDFLARE_EMAIL=you@example.com \ CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ lego --dns cloudflare --domains www.example.com --email you@example.com run ``` ### Environment Variables: File The environment variables can reference a path to file. In this case the name of environment variable must be suffixed by `_FILE`. {{% notice note %}} The file must contain only the value. {{% /notice %}} Here is an example bash command using the CloudFlare DNS provider: ```console $ cat /the/path/to/my/key b9841238feb177a84330febba8a83208921177bffe733 $ cat /the/path/to/my/email you@example.com $ CLOUDFLARE_EMAIL_FILE=/the/path/to/my/email \ CLOUDFLARE_API_KEY_FILE=/the/path/to/my/key \ lego --dns cloudflare --domains www.example.com --email you@example.com run ``` ## DNS Providers {{% tableofdnsproviders %}} lego-4.9.1/docs/content/dns/manual.md000066400000000000000000000050771434020463500174520ustar00rootroot00000000000000--- title: "Manual" date: 2019-03-03T16:39:46+01:00 draft: false slug: manual dnsprovider: since: v0.3.0 code: manual url: --- Solving the DNS-01 challenge using CLI prompt. ## Example To start using the CLI prompt "provider", start lego with `--dns manual`: ```console $ lego --email "you@example.com" --domains="example.com" --dns "manual" run ``` What follows are a few log print outs, interspersed with some prompts, asking for you to do perform some actions: ```txt No key found for account you@example.com. Generating a P256 key. Saved key to ./.lego/accounts/acme-v02.api.letsencrypt.org/you@example.com/keys/you@example.com.key Please review the TOS at https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf Do you accept the TOS? Y/n ``` If you accept the linked Terms of Service, hit `Enter`. ```txt [INFO] acme: Registering account for you@example.com !!!! HEADS UP !!!! Your account credentials have been saved in your Let's Encrypt configuration directory at "./.lego/accounts". You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained from Let's Encrypt so making regular backups of this folder is ideal. [INFO] [example.com] acme: Obtaining bundled SAN certificate [INFO] [example.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/2345678901 [INFO] [example.com] acme: Could not find solver for: tls-alpn-01 [INFO] [example.com] acme: Could not find solver for: http-01 [INFO] [example.com] acme: use dns-01 solver [INFO] [example.com] acme: Preparing to solve DNS-01 lego: Please create the following TXT record in your example.com. zone: _acme-challenge.example.com. 120 IN TXT "hX0dPkG6Gfs9hUvBAchQclkyyoEKbShbpvJ9mY5q2JQ" lego: Press 'Enter' when you are done ``` Do as instructed, and create the TXT records, and hit `Enter`. ```txt [INFO] [example.com] acme: Trying to solve DNS-01 [INFO] [example.com] acme: Checking DNS record propagation using [192.168.8.1:53] [INFO] Wait for propagation [timeout: 1m0s, interval: 2s] [INFO] [example.com] acme: Waiting for DNS record propagation. [INFO] [example.com] The server validated our request [INFO] [example.com] acme: Cleaning DNS-01 challenge lego: You can now remove this TXT record from your example.com. zone: _acme-challenge.example.com. 120 IN TXT "hX0dPkG6Gfs9hUvBAchQclkyyoEKbShbpvJ9mY5q2JQ" [INFO] [example.com] acme: Validations succeeded; requesting certificates [INFO] [example.com] Server responded with a certificate. ``` As mentioned, you can now remove the TXT record again. lego-4.9.1/docs/content/dns/zz_gen_acme-dns.md000066400000000000000000000030351434020463500212300ustar00rootroot00000000000000--- title: "Joohoi's ACME-DNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: acme-dns dnsprovider: since: "v1.1.0" code: "acme-dns" url: "https://github.com/joohoi/acme-dns" --- Configuration for [Joohoi's ACME-DNS](https://github.com/joohoi/acme-dns). - Code: `acme-dns` - Since: v1.1.0 Here is an example bash command using the Joohoi's ACME-DNS provider: ```bash ACME_DNS_API_BASE=http://10.0.0.8:4443 \ ACME_DNS_STORAGE_PATH=/root/.lego-acme-dns-accounts.json \ lego --email you@example.com --dns acme-dns --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `ACME_DNS_API_BASE` | The ACME-DNS API address | | `ACME_DNS_STORAGE_PATH` | The ACME-DNS JSON account data file. A per-domain account will be registered/persisted to this file and used for TXT updates. | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://github.com/joohoi/acme-dns#api) - [Go client](https://github.com/cpu/goacmedns) lego-4.9.1/docs/content/dns/zz_gen_alidns.md000066400000000000000000000045251434020463500210200ustar00rootroot00000000000000--- title: "Alibaba Cloud DNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: alidns dnsprovider: since: "v1.1.0" code: "alidns" url: "https://www.alibabacloud.com/product/dns" --- Configuration for [Alibaba Cloud DNS](https://www.alibabacloud.com/product/dns). - Code: `alidns` - Since: v1.1.0 Here is an example bash command using the Alibaba Cloud DNS provider: ```bash # Setup using instance RAM role ALICLOUD_RAM_ROLE=lego \ lego --email you@example.com --dns alidns --domains my.example.org run # Or, using credentials ALICLOUD_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ ALICLOUD_SECRET_KEY=your-secret-key \ ALICLOUD_SECURITY_TOKEN=your-sts-token \ lego --email you@example.com --dns alidns --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `ALICLOUD_ACCESS_KEY` | Access key ID | | `ALICLOUD_RAM_ROLE` | Your instance RAM role (https://www.alibabacloud.com/help/doc-detail/54579.htm) | | `ALICLOUD_SECRET_KEY` | Access Key secret | | `ALICLOUD_SECURITY_TOKEN` | STS Security Token (optional) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `ALICLOUD_HTTP_TIMEOUT` | API request timeout | | `ALICLOUD_POLLING_INTERVAL` | Time between DNS propagation check | | `ALICLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `ALICLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.alibabacloud.com/help/doc-detail/42875.htm) - [Go client](https://github.com/aliyun/alibaba-cloud-sdk-go) lego-4.9.1/docs/content/dns/zz_gen_allinkl.md000066400000000000000000000034441434020463500211730ustar00rootroot00000000000000--- title: "all-inkl" date: 2019-03-03T16:39:46+01:00 draft: false slug: allinkl dnsprovider: since: "v4.5.0" code: "allinkl" url: "https://all-inkl.com" --- Configuration for [all-inkl](https://all-inkl.com). - Code: `allinkl` - Since: v4.5.0 Here is an example bash command using the all-inkl provider: ```bash ALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ ALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ lego --email you@example.com --dns allinkl --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `ALL_INKL_LOGIN` | KAS login | | `ALL_INKL_PASSWORD` | KAS password | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `ALL_INKL_HTTP_TIMEOUT` | API request timeout | | `ALL_INKL_POLLING_INTERVAL` | Time between DNS propagation check | | `ALL_INKL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://kasapi.kasserver.com/dokumentation/phpdoc/index.html) lego-4.9.1/docs/content/dns/zz_gen_arvancloud.md000066400000000000000000000035121434020463500216770ustar00rootroot00000000000000--- title: "ArvanCloud" date: 2019-03-03T16:39:46+01:00 draft: false slug: arvancloud dnsprovider: since: "v3.8.0" code: "arvancloud" url: "https://arvancloud.com" --- Configuration for [ArvanCloud](https://arvancloud.com). - Code: `arvancloud` - Since: v3.8.0 Here is an example bash command using the ArvanCloud provider: ```bash ARVANCLOUD_API_KEY="Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \ lego --email you@example.com --dns arvancloud --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `ARVANCLOUD_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `ARVANCLOUD_HTTP_TIMEOUT` | API request timeout | | `ARVANCLOUD_POLLING_INTERVAL` | Time between DNS propagation check | | `ARVANCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `ARVANCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.arvancloud.com/docs/api/cdn/4.0) lego-4.9.1/docs/content/dns/zz_gen_auroradns.md000066400000000000000000000037251434020463500215450ustar00rootroot00000000000000--- title: "Aurora DNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: auroradns dnsprovider: since: "v0.4.0" code: "auroradns" url: "https://www.pcextreme.com/dns-health-checks" --- Configuration for [Aurora DNS](https://www.pcextreme.com/dns-health-checks). - Code: `auroradns` - Since: v0.4.0 Here is an example bash command using the Aurora DNS provider: ```bash AURORA_API_KEY=xxxxx \ AURORA_SECRET=yyyyyy \ lego --email you@example.com --dns auroradns --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `AURORA_API_KEY` | API key or username to used | | `AURORA_SECRET` | Secret password to be used | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `AURORA_ENDPOINT` | API endpoint URL | | `AURORA_POLLING_INTERVAL` | Time between DNS propagation check | | `AURORA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `AURORA_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://libcloud.readthedocs.io/en/latest/dns/drivers/auroradns.html#api-docs) - [Go client](https://github.com/nrdcg/auroradns) lego-4.9.1/docs/content/dns/zz_gen_autodns.md000066400000000000000000000040641434020463500212210ustar00rootroot00000000000000--- title: "Autodns" date: 2019-03-03T16:39:46+01:00 draft: false slug: autodns dnsprovider: since: "v3.2.0" code: "autodns" url: "https://www.internetx.com/domains/autodns/" --- Configuration for [Autodns](https://www.internetx.com/domains/autodns/). - Code: `autodns` - Since: v3.2.0 Here is an example bash command using the Autodns provider: ```bash AUTODNS_API_USER=username \ AUTODNS_API_PASSWORD=supersecretpassword \ lego --email you@example.com --dns autodns --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `AUTODNS_API_PASSWORD` | User Password | | `AUTODNS_API_USER` | Username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `AUTODNS_CONTEXT` | API context (4 for production, 1 for testing. Defaults to 4) | | `AUTODNS_ENDPOINT` | API endpoint URL, defaults to https://api.autodns.com/v1/ | | `AUTODNS_HTTP_TIMEOUT` | API request timeout, defaults to 30 seconds | | `AUTODNS_POLLING_INTERVAL` | Time between DNS propagation check | | `AUTODNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `AUTODNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://help.internetx.com/display/APIJSONEN) lego-4.9.1/docs/content/dns/zz_gen_azure.md000066400000000000000000000046441434020463500206760ustar00rootroot00000000000000--- title: "Azure" date: 2019-03-03T16:39:46+01:00 draft: false slug: azure dnsprovider: since: "v0.4.0" code: "azure" url: "https://azure.microsoft.com/services/dns/" --- Configuration for [Azure](https://azure.microsoft.com/services/dns/). - Code: `azure` - Since: v0.4.0 {{% notice note %}} _Please contribute by adding a CLI example._ {{% /notice %}} ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `AZURE_CLIENT_ID` | Client ID | | `AZURE_CLIENT_SECRET` | Client secret | | `AZURE_ENVIRONMENT` | Azure environment, one of: public, usgovernment, german, and china | | `AZURE_RESOURCE_GROUP` | Resource group | | `AZURE_SUBSCRIPTION_ID` | Subscription ID | | `AZURE_TENANT_ID` | Tenant ID | | `instance metadata service` | If the credentials are **not** set via the environment, then it will attempt to get a bearer token via the [instance metadata service](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service). | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `AZURE_METADATA_ENDPOINT` | Metadata Service endpoint URL | | `AZURE_POLLING_INTERVAL` | Time between DNS propagation check | | `AZURE_PRIVATE_ZONE` | Set to true to use Azure Private DNS Zones and not public | | `AZURE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `AZURE_TTL` | The TTL of the TXT record used for the DNS challenge | | `AZURE_ZONE_NAME` | Zone name to use inside Azure DNS service to add the TXT record in | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://docs.microsoft.com/en-us/go/azure/) - [Go client](https://github.com/Azure/azure-sdk-for-go) lego-4.9.1/docs/content/dns/zz_gen_bindman.md000066400000000000000000000036351434020463500211570ustar00rootroot00000000000000--- title: "Bindman" date: 2019-03-03T16:39:46+01:00 draft: false slug: bindman dnsprovider: since: "v2.6.0" code: "bindman" url: "https://github.com/labbsr0x/bindman-dns-webhook" --- Configuration for [Bindman](https://github.com/labbsr0x/bindman-dns-webhook). - Code: `bindman` - Since: v2.6.0 Here is an example bash command using the Bindman provider: ```bash BINDMAN_MANAGER_ADDRESS= \ lego --email you@example.com --dns bindman --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `BINDMAN_MANAGER_ADDRESS` | The server URL, should have scheme, hostname, and port (if required) of the Bindman-DNS Manager server | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `BINDMAN_HTTP_TIMEOUT` | API request timeout | | `BINDMAN_POLLING_INTERVAL` | Time between DNS propagation check | | `BINDMAN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://gitlab.isc.org/isc-projects/bind9) - [Go client](https://github.com/labbsr0x/bindman-dns-webhook) lego-4.9.1/docs/content/dns/zz_gen_bluecat.md000066400000000000000000000043201434020463500211560ustar00rootroot00000000000000--- title: "Bluecat" date: 2019-03-03T16:39:46+01:00 draft: false slug: bluecat dnsprovider: since: "v0.5.0" code: "bluecat" url: "https://www.bluecatnetworks.com" --- Configuration for [Bluecat](https://www.bluecatnetworks.com). - Code: `bluecat` - Since: v0.5.0 Here is an example bash command using the Bluecat provider: ```bash BLUECAT_PASSWORD=mypassword \ BLUECAT_DNS_VIEW=myview \ BLUECAT_USER_NAME=myusername \ BLUECAT_CONFIG_NAME=myconfig \ BLUECAT_SERVER_URL=https://bam.example.com \ BLUECAT_TTL=30 \ lego --email you@example.com --dns bluecat --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `BLUECAT_CONFIG_NAME` | Configuration name | | `BLUECAT_DNS_VIEW` | External DNS View Name | | `BLUECAT_PASSWORD` | API password | | `BLUECAT_SERVER_URL` | The server URL, should have scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve | | `BLUECAT_USER_NAME` | API username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `BLUECAT_HTTP_TIMEOUT` | API request timeout | | `BLUECAT_POLLING_INTERVAL` | Time between DNS propagation check | | `BLUECAT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `BLUECAT_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/REST-API/9.1.0) lego-4.9.1/docs/content/dns/zz_gen_checkdomain.md000066400000000000000000000036531434020463500220140ustar00rootroot00000000000000--- title: "Checkdomain" date: 2019-03-03T16:39:46+01:00 draft: false slug: checkdomain dnsprovider: since: "v3.3.0" code: "checkdomain" url: "https://checkdomain.de/" --- Configuration for [Checkdomain](https://checkdomain.de/). - Code: `checkdomain` - Since: v3.3.0 Here is an example bash command using the Checkdomain provider: ```bash CHECKDOMAIN_TOKEN=yoursecrettoken \ lego --email you@example.com --dns checkdomain --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `CHECKDOMAIN_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `CHECKDOMAIN_ENDPOINT` | API endpoint URL, defaults to https://api.checkdomain.de | | `CHECKDOMAIN_HTTP_TIMEOUT` | API request timeout, defaults to 30 seconds | | `CHECKDOMAIN_POLLING_INTERVAL` | Time between DNS propagation check | | `CHECKDOMAIN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `CHECKDOMAIN_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://developer.checkdomain.de/reference/) lego-4.9.1/docs/content/dns/zz_gen_civo.md000066400000000000000000000031751434020463500205060ustar00rootroot00000000000000--- title: "Civo" date: 2019-03-03T16:39:46+01:00 draft: false slug: civo dnsprovider: since: "v4.9.0" code: "civo" url: "https://civo.com" --- Configuration for [Civo](https://civo.com). - Code: `civo` - Since: v4.9.0 Here is an example bash command using the Civo provider: ```bash CIVO_TOKEN=xxxxxx \ lego --email you@example.com --dns civo --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `CIVO_TOKEN` | Authentication token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `CIVO_POLLING_INTERVAL` | Time between DNS propagation check | | `CIVO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `CIVO_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.civo.com/api/dns) lego-4.9.1/docs/content/dns/zz_gen_clouddns.md000066400000000000000000000036751434020463500213660ustar00rootroot00000000000000--- title: "CloudDNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: clouddns dnsprovider: since: "v3.6.0" code: "clouddns" url: "https://vshosting.eu/" --- Configuration for [CloudDNS](https://vshosting.eu/). - Code: `clouddns` - Since: v3.6.0 Here is an example bash command using the CloudDNS provider: ```bash CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \ CLOUDDNS_EMAIL=you@example.com \ CLOUDDNS_PASSWORD=b9841238feb177a84330f \ lego --email you@example.com --dns clouddns --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `CLOUDDNS_CLIENT_ID` | Client ID | | `CLOUDDNS_EMAIL` | Account email | | `CLOUDDNS_PASSWORD` | Account password | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `CLOUDDNS_HTTP_TIMEOUT` | API request timeout | | `CLOUDDNS_POLLING_INTERVAL` | Time between DNS propagation check | | `CLOUDDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `CLOUDDNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://admin.vshosting.cloud/clouddns/swagger/) lego-4.9.1/docs/content/dns/zz_gen_cloudflare.md000066400000000000000000000104741434020463500216660ustar00rootroot00000000000000--- title: "Cloudflare" date: 2019-03-03T16:39:46+01:00 draft: false slug: cloudflare dnsprovider: since: "v0.3.0" code: "cloudflare" url: "https://www.cloudflare.com/dns/" --- Configuration for [Cloudflare](https://www.cloudflare.com/dns/). - Code: `cloudflare` - Since: v0.3.0 Here is an example bash command using the Cloudflare provider: ```bash CLOUDFLARE_EMAIL=you@example.com \ CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ lego --email you@example.com --dns cloudflare --domains my.example.org run # or CLOUDFLARE_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ lego --email you@example.com --dns cloudflare --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `CF_API_EMAIL` | Account email | | `CF_API_KEY` | API key | | `CF_DNS_API_TOKEN` | API token with DNS:Edit permission (since v3.1.0) | | `CF_ZONE_API_TOKEN` | API token with Zone:Read permission (since v3.1.0) | | `CLOUDFLARE_API_KEY` | Alias to CF_API_KEY | | `CLOUDFLARE_DNS_API_TOKEN` | Alias to CF_DNS_API_TOKEN | | `CLOUDFLARE_EMAIL` | Alias to CF_API_EMAIL | | `CLOUDFLARE_ZONE_API_TOKEN` | Alias to CF_ZONE_API_TOKEN | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `CLOUDFLARE_HTTP_TIMEOUT` | API request timeout | | `CLOUDFLARE_POLLING_INTERVAL` | Time between DNS propagation check | | `CLOUDFLARE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `CLOUDFLARE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description You 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`. ### API keys If using API keys (`CF_API_EMAIL` and `CF_API_KEY`), the Global API Key needs to be used, not the Origin CA Key. Please be aware, that this in principle allows Lego to read and change *everything* related to this account. ### API tokens With API tokens (`CF_DNS_API_TOKEN`, and optionally `CF_ZONE_API_TOKEN`), very specific access can be granted to your resources at Cloudflare. See this [Cloudflare announcement](https://blog.cloudflare.com/api-tokens-general-availability/) for details. The main resources Lego cares for are the DNS entries for your Zones. It also need to resolve a domain name to an internal Zone ID in order to manipulate DNS entries. Hence, you should create an API token with the following permissions: * Zone / Zone / Read * Zone / DNS / Edit You also need to scope the access to all your domains for this to work. Then pass the API token as `CF_DNS_API_TOKEN` to Lego. **Alternatively,** if you prefer a more strict set of privileges, you can split the access tokens: * Create one with *Zone / Zone / Read* permissions and scope it to all your zones. This is needed to resolve domain names to Zone IDs and can be shared among multiple Lego installations. Pass this API token as `CF_ZONE_API_TOKEN` to Lego. * Create another API token with *Zone / DNS / Edit* permissions and set the scope to the domains you want to manage with a single Lego installation. Pass this token as `CF_DNS_API_TOKEN` to Lego. * Repeat the previous step for each host you want to run Lego on. This "paranoid" setup is mainly interesting for users who manage many zones/domains with a single Cloudflare account. It follows the principle of least privilege and limits the possible damage, should one of the hosts become compromised. ## More information - [API documentation](https://api.cloudflare.com/) - [Go client](https://github.com/cloudflare/cloudflare-go) lego-4.9.1/docs/content/dns/zz_gen_cloudns.md000066400000000000000000000035751434020463500212210ustar00rootroot00000000000000--- title: "ClouDNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: cloudns dnsprovider: since: "v2.3.0" code: "cloudns" url: "https://www.cloudns.net" --- Configuration for [ClouDNS](https://www.cloudns.net). - Code: `cloudns` - Since: v2.3.0 Here is an example bash command using the ClouDNS provider: ```bash CLOUDNS_AUTH_ID=xxxx \ CLOUDNS_AUTH_PASSWORD=yyyy \ lego --email you@example.com --dns cloudns --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `CLOUDNS_AUTH_ID` | The API user ID | | `CLOUDNS_AUTH_PASSWORD` | The password for API user ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `CLOUDNS_HTTP_TIMEOUT` | API request timeout | | `CLOUDNS_POLLING_INTERVAL` | Time between DNS propagation check | | `CLOUDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `CLOUDNS_SUB_AUTH_ID` | The API sub user ID | | `CLOUDNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.cloudns.net/wiki/article/42/) lego-4.9.1/docs/content/dns/zz_gen_cloudxns.md000066400000000000000000000035511434020463500214030ustar00rootroot00000000000000--- title: "CloudXNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: cloudxns dnsprovider: since: "v0.5.0" code: "cloudxns" url: "https://www.cloudxns.net/" --- Configuration for [CloudXNS](https://www.cloudxns.net/). - Code: `cloudxns` - Since: v0.5.0 Here is an example bash command using the CloudXNS provider: ```bash CLOUDXNS_API_KEY=xxxx \ CLOUDXNS_SECRET_KEY=yyyy \ lego --email you@example.com --dns cloudxns --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `CLOUDXNS_API_KEY` | The API key | | `CLOUDXNS_SECRET_KEY` | The API secret key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `CLOUDXNS_HTTP_TIMEOUT` | API request timeout | | `CLOUDXNS_POLLING_INTERVAL` | Time between DNS propagation check | | `CLOUDXNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `CLOUDXNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.cloudxns.net/Public/Doc/CloudXNS_api2.0_doc_zh-cn.zip) lego-4.9.1/docs/content/dns/zz_gen_conoha.md000066400000000000000000000036401434020463500210120ustar00rootroot00000000000000--- title: "ConoHa" date: 2019-03-03T16:39:46+01:00 draft: false slug: conoha dnsprovider: since: "v1.2.0" code: "conoha" url: "https://www.conoha.jp/" --- Configuration for [ConoHa](https://www.conoha.jp/). - Code: `conoha` - Since: v1.2.0 Here is an example bash command using the ConoHa provider: ```bash CONOHA_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \ CONOHA_API_USERNAME=xxxx \ CONOHA_API_PASSWORD=yyyy \ lego --email you@example.com --dns conoha --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `CONOHA_API_PASSWORD` | The API password | | `CONOHA_API_USERNAME` | The API username | | `CONOHA_TENANT_ID` | Tenant ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `CONOHA_HTTP_TIMEOUT` | API request timeout | | `CONOHA_POLLING_INTERVAL` | Time between DNS propagation check | | `CONOHA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `CONOHA_REGION` | The region | | `CONOHA_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.conoha.jp/docs/) lego-4.9.1/docs/content/dns/zz_gen_constellix.md000066400000000000000000000036451434020463500217340ustar00rootroot00000000000000--- title: "Constellix" date: 2019-03-03T16:39:46+01:00 draft: false slug: constellix dnsprovider: since: "v3.4.0" code: "constellix" url: "https://constellix.com" --- Configuration for [Constellix](https://constellix.com). - Code: `constellix` - Since: v3.4.0 Here is an example bash command using the Constellix provider: ```bash CONSTELLIX_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ CONSTELLIX_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ lego --email you@example.com --dns constellix --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `CONSTELLIX_API_KEY` | User API key | | `CONSTELLIX_SECRET_KEY` | User secret key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `CONSTELLIX_HTTP_TIMEOUT` | API request timeout | | `CONSTELLIX_POLLING_INTERVAL` | Time between DNS propagation check | | `CONSTELLIX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `CONSTELLIX_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://api-docs.constellix.com) lego-4.9.1/docs/content/dns/zz_gen_desec.md000066400000000000000000000033361434020463500206300ustar00rootroot00000000000000--- title: "deSEC.io" date: 2019-03-03T16:39:46+01:00 draft: false slug: desec dnsprovider: since: "v3.7.0" code: "desec" url: "https://desec.io" --- Configuration for [deSEC.io](https://desec.io). - Code: `desec` - Since: v3.7.0 Here is an example bash command using the deSEC.io provider: ```bash DESEC_TOKEN=x-xxxxxxxxxxxxxxxxxxxxxxxxxx \ lego --email you@example.com --dns desec --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `DESEC_TOKEN` | Domain token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `DESEC_HTTP_TIMEOUT` | API request timeout | | `DESEC_POLLING_INTERVAL` | Time between DNS propagation check | | `DESEC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `DESEC_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://desec.readthedocs.io/en/latest/) lego-4.9.1/docs/content/dns/zz_gen_designate.md000066400000000000000000000076441434020463500215160ustar00rootroot00000000000000--- title: "Designate DNSaaS for Openstack" date: 2019-03-03T16:39:46+01:00 draft: false slug: designate dnsprovider: since: "v2.2.0" code: "designate" url: "https://docs.openstack.org/designate/latest/" --- Configuration for [Designate DNSaaS for Openstack](https://docs.openstack.org/designate/latest/). - Code: `designate` - Since: v2.2.0 Here is an example bash command using the Designate DNSaaS for Openstack provider: ```bash # With a `clouds.yaml` OS_CLOUD=my_openstack \ lego --email you@example.com --dns designate --domains my.example.org run # or OS_AUTH_URL=https://openstack.example.org \ OS_REGION_NAME=RegionOne \ OS_PROJECT_ID=23d4522a987d4ab529f722a007c27846 OS_USERNAME=myuser \ OS_PASSWORD=passw0rd \ lego --email you@example.com --dns designate --domains my.example.org run # or OS_AUTH_URL=https://openstack.example.org \ OS_REGION_NAME=RegionOne \ OS_AUTH_TYPE=v3applicationcredential \ OS_APPLICATION_CREDENTIAL_ID=imn74uq0or7dyzz20dwo1ytls4me8dry \ OS_APPLICATION_CREDENTIAL_SECRET=68FuSPSdQqkFQYH5X1OoriEIJOwyLtQ8QSqXZOc9XxFK1A9tzZT6He2PfPw0OMja \ lego --email you@example.com --dns designate --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `OS_APPLICATION_CREDENTIAL_ID` | Application credential ID | | `OS_APPLICATION_CREDENTIAL_NAME` | Application credential name | | `OS_APPLICATION_CREDENTIAL_SECRET` | Application credential secret | | `OS_AUTH_URL` | Identity endpoint URL | | `OS_PASSWORD` | Password | | `OS_PROJECT_NAME` | Project name | | `OS_REGION_NAME` | Region name | | `OS_USERNAME` | Username | | `OS_USER_ID` | User ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `DESIGNATE_POLLING_INTERVAL` | Time between DNS propagation check | | `DESIGNATE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `DESIGNATE_TTL` | The TTL of the TXT record used for the DNS challenge | | `OS_PROJECT_ID` | Project ID | | `OS_TENANT_NAME` | Tenant name (deprecated see OS_PROJECT_NAME and OS_PROJECT_ID) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description There are three main ways of authenticating with Designate: 1. The first one is by using the `OS_CLOUD` environment variable and a `clouds.yaml` file. 2. The second one is using your username and password, via the `OS_USERNAME`, `OS_PASSWORD` and `OS_PROJECT_NAME` environment variables. 3. The third one is by using an application credential, via the `OS_APPLICATION_CREDENTIAL_*` and `OS_USER_ID` environment variables. For the username/password and application methods, the `OS_AUTH_URL` and `OS_REGION_NAME` environment variables are required. For more information, you can read about the different methods of authentication with OpenStack in the Keystone's documentation and the gophercloud documentation: - [Keystone username/password](https://docs.openstack.org/keystone/latest/user/supported_clients.html) - [Keystone application credentials](https://docs.openstack.org/keystone/latest/user/application_credentials.html) ## More information - [API documentation](https://docs.openstack.org/designate/latest/) - [Go client](https://godoc.org/github.com/gophercloud/gophercloud/openstack/dns/v2) lego-4.9.1/docs/content/dns/zz_gen_digitalocean.md000066400000000000000000000036241434020463500221700ustar00rootroot00000000000000--- title: "Digital Ocean" date: 2019-03-03T16:39:46+01:00 draft: false slug: digitalocean dnsprovider: since: "v0.3.0" code: "digitalocean" url: "https://www.digitalocean.com/docs/networking/dns/" --- Configuration for [Digital Ocean](https://www.digitalocean.com/docs/networking/dns/). - Code: `digitalocean` - Since: v0.3.0 Here is an example bash command using the Digital Ocean provider: ```bash DO_AUTH_TOKEN=xxxxxx \ lego --email you@example.com --dns digitalocean --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `DO_AUTH_TOKEN` | Authentication token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `DO_API_URL` | The URL of the API | | `DO_HTTP_TIMEOUT` | API request timeout | | `DO_POLLING_INTERVAL` | Time between DNS propagation check | | `DO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `DO_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://developers.digitalocean.com/documentation/v2/#domain-records) lego-4.9.1/docs/content/dns/zz_gen_dnsimple.md000066400000000000000000000051011434020463500213500ustar00rootroot00000000000000--- title: "DNSimple" date: 2019-03-03T16:39:46+01:00 draft: false slug: dnsimple dnsprovider: since: "v0.3.0" code: "dnsimple" url: "https://dnsimple.com/" --- Configuration for [DNSimple](https://dnsimple.com/). - Code: `dnsimple` - Since: v0.3.0 Here is an example bash command using the DNSimple provider: ```bash DNSIMPLE_OAUTH_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ lego --email you@example.com --dns dnsimple --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `DNSIMPLE_OAUTH_TOKEN` | OAuth token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `DNSIMPLE_BASE_URL` | API endpoint URL | | `DNSIMPLE_POLLING_INTERVAL` | Time between DNS propagation check | | `DNSIMPLE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `DNSIMPLE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description `DNSIMPLE_BASE_URL` is optional and must be set to production (https://api.dnsimple.com). if `DNSIMPLE_BASE_URL` is not defined or empty, the production URL is used by default. While you can manage DNS records in the [DNSimple Sandbox environment](https://developer.dnsimple.com/sandbox/), DNS records will not resolve and you will not be able to satisfy the ACME DNS challenge. To authenticate you need to provide a valid API token. HTTP Basic Authentication is intentionally not supported. ### API tokens You can [generate a new API token](https://support.dnsimple.com/articles/api-access-token/) from your account page. Only Account API tokens are supported, if you try to use an User API token you will receive an error message. ## More information - [API documentation](https://developer.dnsimple.com/v2/) - [Go client](https://github.com/dnsimple/dnsimple-go) lego-4.9.1/docs/content/dns/zz_gen_dnsmadeeasy.md000066400000000000000000000037041434020463500220410ustar00rootroot00000000000000--- title: "DNS Made Easy" date: 2019-03-03T16:39:46+01:00 draft: false slug: dnsmadeeasy dnsprovider: since: "v0.4.0" code: "dnsmadeeasy" url: "https://dnsmadeeasy.com/" --- Configuration for [DNS Made Easy](https://dnsmadeeasy.com/). - Code: `dnsmadeeasy` - Since: v0.4.0 Here is an example bash command using the DNS Made Easy provider: ```bash DNSMADEEASY_API_KEY=xxxxxx \ DNSMADEEASY_API_SECRET=yyyyy \ lego --email you@example.com --dns dnsmadeeasy --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `DNSMADEEASY_API_KEY` | The API key | | `DNSMADEEASY_API_SECRET` | The API Secret key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `DNSMADEEASY_HTTP_TIMEOUT` | API request timeout | | `DNSMADEEASY_POLLING_INTERVAL` | Time between DNS propagation check | | `DNSMADEEASY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `DNSMADEEASY_SANDBOX` | Activate the sandbox (boolean) | | `DNSMADEEASY_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://api-docs.dnsmadeeasy.com/) lego-4.9.1/docs/content/dns/zz_gen_dnspod.md000066400000000000000000000034301434020463500210270ustar00rootroot00000000000000--- title: "DNSPod (deprecated)" date: 2019-03-03T16:39:46+01:00 draft: false slug: dnspod dnsprovider: since: "v0.4.0" code: "dnspod" url: "https://www.dnspod.com/" --- Use the Tencent Cloud provider instead. - Code: `dnspod` - Since: v0.4.0 Here is an example bash command using the DNSPod (deprecated) provider: ```bash DNSPOD_API_KEY=xxxxxx \ lego --email you@example.com --dns dnspod --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `DNSPOD_API_KEY` | The user token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `DNSPOD_HTTP_TIMEOUT` | API request timeout | | `DNSPOD_POLLING_INTERVAL` | Time between DNS propagation check | | `DNSPOD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `DNSPOD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://docs.dnspod.com/api/) - [Go client](https://github.com/nrdcg/dnspod-go) lego-4.9.1/docs/content/dns/zz_gen_dode.md000066400000000000000000000034631434020463500204610ustar00rootroot00000000000000--- title: "Domain Offensive (do.de)" date: 2019-03-03T16:39:46+01:00 draft: false slug: dode dnsprovider: since: "v2.4.0" code: "dode" url: "https://www.do.de/" --- Configuration for [Domain Offensive (do.de)](https://www.do.de/). - Code: `dode` - Since: v2.4.0 Here is an example bash command using the Domain Offensive (do.de) provider: ```bash DODE_TOKEN=xxxxxx \ lego --email you@example.com --dns dode --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `DODE_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `DODE_HTTP_TIMEOUT` | API request timeout | | `DODE_POLLING_INTERVAL` | Time between DNS propagation check | | `DODE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `DODE_SEQUENCE_INTERVAL` | Time between sequential requests | | `DODE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.do.de/wiki/LetsEncrypt_-_Entwickler) lego-4.9.1/docs/content/dns/zz_gen_domeneshop.md000066400000000000000000000037001434020463500217010ustar00rootroot00000000000000--- title: "Domeneshop" date: 2019-03-03T16:39:46+01:00 draft: false slug: domeneshop dnsprovider: since: "v4.3.0" code: "domeneshop" url: "https://domene.shop" --- Configuration for [Domeneshop](https://domene.shop). - Code: `domeneshop` - Since: v4.3.0 Here is an example bash command using the Domeneshop provider: ```bash DOMENESHOP_API_TOKEN= \ DOMENESHOP_API_SECRET= \ lego --email example@example.com --dns domeneshop --domains example.com run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `DOMENESHOP_API_SECRET` | API secret | | `DOMENESHOP_API_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `DOMENESHOP_HTTP_TIMEOUT` | API request timeout | | `DOMENESHOP_POLLING_INTERVAL` | Time between DNS propagation check | | `DOMENESHOP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ### API credentials Visit the following page for information on how to create API credentials with Domeneshop: https://api.domeneshop.no/docs/#section/Authentication ## More information - [API documentation](https://api.domeneshop.no/docs) lego-4.9.1/docs/content/dns/zz_gen_dreamhost.md000066400000000000000000000034721434020463500215340ustar00rootroot00000000000000--- title: "DreamHost" date: 2019-03-03T16:39:46+01:00 draft: false slug: dreamhost dnsprovider: since: "v1.1.0" code: "dreamhost" url: "https://www.dreamhost.com" --- Configuration for [DreamHost](https://www.dreamhost.com). - Code: `dreamhost` - Since: v1.1.0 Here is an example bash command using the DreamHost provider: ```bash DREAMHOST_API_KEY="YOURAPIKEY" \ lego --email you@example.com --dns dreamhost --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `DREAMHOST_API_KEY` | The API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `DREAMHOST_HTTP_TIMEOUT` | API request timeout | | `DREAMHOST_POLLING_INTERVAL` | Time between DNS propagation check | | `DREAMHOST_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `DREAMHOST_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://help.dreamhost.com/hc/en-us/articles/217560167-API_overview) lego-4.9.1/docs/content/dns/zz_gen_duckdns.md000066400000000000000000000034611434020463500211770ustar00rootroot00000000000000--- title: "Duck DNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: duckdns dnsprovider: since: "v0.5.0" code: "duckdns" url: "https://www.duckdns.org/" --- Configuration for [Duck DNS](https://www.duckdns.org/). - Code: `duckdns` - Since: v0.5.0 Here is an example bash command using the Duck DNS provider: ```bash DUCKDNS_TOKEN=xxxxxx \ lego --email you@example.com --dns duckdns --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `DUCKDNS_TOKEN` | Account token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `DUCKDNS_HTTP_TIMEOUT` | API request timeout | | `DUCKDNS_POLLING_INTERVAL` | Time between DNS propagation check | | `DUCKDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `DUCKDNS_SEQUENCE_INTERVAL` | Time between sequential requests | | `DUCKDNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.duckdns.org/spec.jsp) lego-4.9.1/docs/content/dns/zz_gen_dyn.md000066400000000000000000000034111434020463500203310ustar00rootroot00000000000000--- title: "Dyn" date: 2019-03-03T16:39:46+01:00 draft: false slug: dyn dnsprovider: since: "v0.3.0" code: "dyn" url: "https://dyn.com/" --- Configuration for [Dyn](https://dyn.com/). - Code: `dyn` - Since: v0.3.0 Here is an example bash command using the Dyn provider: ```bash DYN_CUSTOMER_NAME=xxxxxx \ DYN_USER_NAME=yyyyy \ DYN_PASSWORD=zzzz \ lego --email you@example.com --dns dyn --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `DYN_CUSTOMER_NAME` | Customer name | | `DYN_PASSWORD` | Password | | `DYN_USER_NAME` | User name | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `DYN_HTTP_TIMEOUT` | API request timeout | | `DYN_POLLING_INTERVAL` | Time between DNS propagation check | | `DYN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `DYN_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://help.dyn.com/rest/) lego-4.9.1/docs/content/dns/zz_gen_dynu.md000066400000000000000000000033241434020463500205210ustar00rootroot00000000000000--- title: "Dynu" date: 2019-03-03T16:39:46+01:00 draft: false slug: dynu dnsprovider: since: "v3.5.0" code: "dynu" url: "https://www.dynu.com/" --- Configuration for [Dynu](https://www.dynu.com/). - Code: `dynu` - Since: v3.5.0 Here is an example bash command using the Dynu provider: ```bash DYNU_API_KEY=1234567890abcdefghijklmnopqrstuvwxyz \ lego --email you@example.com --dns dynu --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `DYNU_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `DYNU_HTTP_TIMEOUT` | API request timeout | | `DYNU_POLLING_INTERVAL` | Time between DNS propagation check | | `DYNU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `DYNU_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.dynu.com/en-US/Support/API) lego-4.9.1/docs/content/dns/zz_gen_easydns.md000066400000000000000000000037771434020463500212240ustar00rootroot00000000000000--- title: "EasyDNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: easydns dnsprovider: since: "v2.6.0" code: "easydns" url: "https://easydns.com/" --- Configuration for [EasyDNS](https://easydns.com/). - Code: `easydns` - Since: v2.6.0 Here is an example bash command using the EasyDNS provider: ```bash EASYDNS_TOKEN= \ EASYDNS_KEY= \ lego --email you@example.com --dns easydns --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `EASYDNS_KEY` | API Key | | `EASYDNS_TOKEN` | API Token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `EASYDNS_ENDPOINT` | The endpoint URL of the API Server | | `EASYDNS_HTTP_TIMEOUT` | API request timeout | | `EASYDNS_POLLING_INTERVAL` | Time between DNS propagation check | | `EASYDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `EASYDNS_SEQUENCE_INTERVAL` | Time between sequential requests | | `EASYDNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). To test with the sandbox environment set ```EASYDNS_ENDPOINT=https://sandbox.rest.easydns.net``` ## More information - [API documentation](https://docs.sandbox.rest.easydns.net) lego-4.9.1/docs/content/dns/zz_gen_edgedns.md000066400000000000000000000074571434020463500211660ustar00rootroot00000000000000--- title: "Akamai EdgeDNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: edgedns dnsprovider: since: "v3.9.0" code: "edgedns" url: "https://www.akamai.com/us/en/products/security/edge-dns.jsp" --- Akamai edgedns supersedes FastDNS; implementing a DNS provider for solving the DNS-01 challenge using Akamai EdgeDNS - Code: `edgedns` - Since: v3.9.0 Here is an example bash command using the Akamai EdgeDNS provider: ```bash AKAMAI_CLIENT_SECRET=abcdefghijklmnopqrstuvwxyz1234567890ABCDEFG= \ AKAMAI_CLIENT_TOKEN=akab-mnbvcxzlkjhgfdsapoiuytrewq1234567 \ AKAMAI_HOST=akab-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.luna.akamaiapis.net \ AKAMAI_ACCESS_TOKEN=akab-1234567890qwerty-asdfghjklzxcvtnu \ lego --email you@example.com --dns edgedns --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `AKAMAI_ACCESS_TOKEN` | Access token, managed by the Akamai EdgeGrid client | | `AKAMAI_CLIENT_SECRET` | Client secret, managed by the Akamai EdgeGrid client | | `AKAMAI_CLIENT_TOKEN` | Client token, managed by the Akamai EdgeGrid client | | `AKAMAI_EDGERC` | Path to the .edgerc file, managed by the Akamai EdgeGrid client | | `AKAMAI_EDGERC_SECTION` | Configuration section, managed by the Akamai EdgeGrid client | | `AKAMAI_HOST` | API host, managed by the Akamai EdgeGrid client | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `AKAMAI_POLLING_INTERVAL` | Time between DNS propagation check. Default: 15 seconds | | `AKAMAI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation. Default: 3 minutes | | `AKAMAI_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). Akamai credentials are automatically detected in the following locations and prioritized in the following order: 1. Section-specific environment variables (where `{SECTION}` is specified using `AKAMAI_EDGERC_SECTION`): - `AKAMAI_{SECTION}_HOST` - `AKAMAI_{SECTION}_ACCESS_TOKEN` - `AKAMAI_{SECTION}_CLIENT_TOKEN` - `AKAMAI_{SECTION}_CLIENT_SECRET` 2. If `AKAMAI_EDGERC_SECTION` is not defined or is set to `default`, environment variables: - `AKAMAI_HOST` - `AKAMAI_ACCESS_TOKEN` - `AKAMAI_CLIENT_TOKEN` - `AKAMAI_CLIENT_SECRET` 3. `.edgerc` file located at `AKAMAI_EDGERC` - defaults to `~/.edgerc`, sections can be specified using `AKAMAI_EDGERC_SECTION` 4. Default environment variables: - `AKAMAI_HOST` - `AKAMAI_ACCESS_TOKEN` - `AKAMAI_CLIENT_TOKEN` - `AKAMAI_CLIENT_SECRET` See also: - [Setting up Akamai credentials](https://developer.akamai.com/api/getting-started) - [.edgerc Format](https://developer.akamai.com/legacy/introduction/Conf_Client.html#edgercformat) - [API Client Authentication](https://developer.akamai.com/legacy/introduction/Client_Auth.html) - [Config from Env](https://github.com/akamai/AkamaiOPEN-edgegrid-golang/blob/master/edgegrid/config.go#L118) ## More information - [API documentation](https://developer.akamai.com/api/cloud_security/edge_dns_zone_management/v2.html) - [Go client](https://github.com/akamai/AkamaiOPEN-edgegrid-golang) lego-4.9.1/docs/content/dns/zz_gen_epik.md000066400000000000000000000034111434020463500204670ustar00rootroot00000000000000--- title: "Epik" date: 2019-03-03T16:39:46+01:00 draft: false slug: epik dnsprovider: since: "v4.5.0" code: "epik" url: "https://www.epik.com/" --- Configuration for [Epik](https://www.epik.com/). - Code: `epik` - Since: v4.5.0 Here is an example bash command using the Epik provider: ```bash EPIK_SIGNATURE=xxxxxxxxxxxxxxxxxxxxxxxxxx \ lego --email you@example.com --dns epik --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `EPIK_SIGNATURE` | Epik API signature (https://registrar.epik.com/account/api-settings/) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `EPIK_HTTP_TIMEOUT` | API request timeout | | `EPIK_POLLING_INTERVAL` | Time between DNS propagation check | | `EPIK_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `EPIK_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://docs.userapi.epik.com/v2/#/) lego-4.9.1/docs/content/dns/zz_gen_exec.md000066400000000000000000000105111434020463500204620ustar00rootroot00000000000000--- title: "External program" date: 2019-03-03T16:39:46+01:00 draft: false slug: exec dnsprovider: since: "v0.5.0" code: "exec" url: "/dns/exec" --- Solving the DNS-01 challenge using an external program. - Code: `exec` - Since: v0.5.0 Here is an example bash command using the External program provider: ```bash EXEC_PATH=/the/path/to/myscript.sh \ lego --email you@example.com --dns exec --domains my.example.org run ``` ## Base Configuration | Environment Variable Name | Description | |---------------------------|---------------------------------------| | `EXEC_MODE` | `RAW`, none | | `EXEC_PATH` | The path of the the external program. | ## Additional Configuration | Environment Variable Name | Description | |----------------------------|-------------------------------------------| | `EXEC_POLLING_INTERVAL` | Time between DNS propagation check. | | `EXEC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation. | | `EXEC_SEQUENCE_INTERVAL` | Time between sequential requests. | ## Description The file name of the external program is specified in the environment variable `EXEC_PATH`. When it is run by lego, three command-line parameters are passed to it: The action ("present" or "cleanup"), the fully-qualified domain name and the value for the record. For example, requesting a certificate for the domain 'my.example.org' can be achieved by calling lego as follows: ```bash EXEC_PATH=./update-dns.sh \ lego --email you@example.com \ --dns exec \ --domains my.example.org run ``` It will then call the program './update-dns.sh' with like this: ```bash ./update-dns.sh "present" "_acme-challenge.my.example.org." "MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI" ``` The program then needs to make sure the record is inserted. When it returns an error via a non-zero exit code, lego aborts. When the record is to be removed again, the program is called with the first command-line parameter set to `cleanup` instead of `present`. If you want to use the raw domain, token, and keyAuth values with your program, you can set `EXEC_MODE=RAW`: ```bash EXEC_MODE=RAW \ EXEC_PATH=./update-dns.sh \ lego --email you@example.com \ --dns exec \ --domains my.example.org run ``` It will then call the program `./update-dns.sh` like this: ```bash ./update-dns.sh "present" "my.example.org." "--" "some-token" "KxAy-J3NwUmg9ZQuM-gP_Mq1nStaYSaP9tYQs5_-YsE.ksT-qywTd8058G-SHHWA3RAN72Pr0yWtPYmmY5UBpQ8" ``` ## Commands {{% notice note %}} The `--` is because the token MAY start with a `-`, and the called program may try and interpret a `-` as indicating a flag. In the case of urfave, which is commonly used, you can use the `--` delimiter to specify the start of positional arguments, and handle such a string safely. {{% /notice %}} ### Present | Mode | Command | |---------|----------------------------------------------------| | default | `myprogram present -- ` | | `RAW` | `myprogram present -- ` | ### Cleanup | Mode | Command | |---------|----------------------------------------------------| | default | `myprogram cleanup -- ` | | `RAW` | `myprogram cleanup -- ` | ### Timeout The command have to display propagation timeout and polling interval into Stdout. The values must be formatted as JSON, and times are in seconds. Example: `{"timeout": 30, "interval": 5}` If an error occurs or if the command is not provided: the default display propagation timeout and polling interval are used. | Mode | Command | |---------|----------------------------------------------------| | default | `myprogram timeout` | | `RAW` | `myprogram timeout` | lego-4.9.1/docs/content/dns/zz_gen_exoscale.md000066400000000000000000000037421434020463500213510ustar00rootroot00000000000000--- title: "Exoscale" date: 2019-03-03T16:39:46+01:00 draft: false slug: exoscale dnsprovider: since: "v0.4.0" code: "exoscale" url: "https://www.exoscale.com/" --- Configuration for [Exoscale](https://www.exoscale.com/). - Code: `exoscale` - Since: v0.4.0 Here is an example bash command using the Exoscale provider: ```bash EXOSCALE_API_KEY=abcdefghijklmnopqrstuvwx \ EXOSCALE_API_SECRET=xxxxxxx \ lego --email you@example.com --dns exoscale --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `EXOSCALE_API_KEY` | API key | | `EXOSCALE_API_SECRET` | API secret | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `EXOSCALE_API_ZONE` | API zone | | `EXOSCALE_ENDPOINT` | API endpoint URL | | `EXOSCALE_HTTP_TIMEOUT` | API request timeout | | `EXOSCALE_POLLING_INTERVAL` | Time between DNS propagation check | | `EXOSCALE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `EXOSCALE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://openapi-v2.exoscale.com/#endpoint-dns) - [Go client](https://github.com/exoscale/egoscale) lego-4.9.1/docs/content/dns/zz_gen_freemyip.md000066400000000000000000000034771434020463500213730ustar00rootroot00000000000000--- title: "freemyip.com" date: 2019-03-03T16:39:46+01:00 draft: false slug: freemyip dnsprovider: since: "v4.5.0" code: "freemyip" url: "https://freemyip.com/" --- Configuration for [freemyip.com](https://freemyip.com/). - Code: `freemyip` - Since: v4.5.0 Here is an example bash command using the freemyip.com provider: ```bash FREEMYIP_TOKEN=xxxxxx \ lego --email you@example.com --dns freemyip --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `FREEMYIP_TOKEN` | Account token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `FREEMYIP_HTTP_TIMEOUT` | API request timeout | | `FREEMYIP_POLLING_INTERVAL` | Time between DNS propagation check | | `FREEMYIP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `FREEMYIP_SEQUENCE_INTERVAL` | Time between sequential requests | | `FREEMYIP_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://freemyip.com/help) lego-4.9.1/docs/content/dns/zz_gen_gandi.md000066400000000000000000000033271434020463500206270ustar00rootroot00000000000000--- title: "Gandi" date: 2019-03-03T16:39:46+01:00 draft: false slug: gandi dnsprovider: since: "v0.3.0" code: "gandi" url: "https://www.gandi.net" --- Configuration for [Gandi](https://www.gandi.net). - Code: `gandi` - Since: v0.3.0 Here is an example bash command using the Gandi provider: ```bash GANDI_API_KEY=abcdefghijklmnopqrstuvwx \ lego --email you@example.com --dns gandi --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `GANDI_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `GANDI_HTTP_TIMEOUT` | API request timeout | | `GANDI_POLLING_INTERVAL` | Time between DNS propagation check | | `GANDI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `GANDI_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://doc.rpc.gandi.net/index.html) lego-4.9.1/docs/content/dns/zz_gen_gandiv5.md000066400000000000000000000034341434020463500211010ustar00rootroot00000000000000--- title: "Gandi Live DNS (v5)" date: 2019-03-03T16:39:46+01:00 draft: false slug: gandiv5 dnsprovider: since: "v0.5.0" code: "gandiv5" url: "https://www.gandi.net" --- Configuration for [Gandi Live DNS (v5)](https://www.gandi.net). - Code: `gandiv5` - Since: v0.5.0 Here is an example bash command using the Gandi Live DNS (v5) provider: ```bash GANDIV5_API_KEY=abcdefghijklmnopqrstuvwx \ lego --email you@example.com --dns gandiv5 --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `GANDIV5_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `GANDIV5_HTTP_TIMEOUT` | API request timeout | | `GANDIV5_POLLING_INTERVAL` | Time between DNS propagation check | | `GANDIV5_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `GANDIV5_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://api.gandi.net/docs/livedns/) lego-4.9.1/docs/content/dns/zz_gen_gcloud.md000066400000000000000000000041511434020463500210160ustar00rootroot00000000000000--- title: "Google Cloud" date: 2019-03-03T16:39:46+01:00 draft: false slug: gcloud dnsprovider: since: "v0.3.0" code: "gcloud" url: "https://cloud.google.com" --- Configuration for [Google Cloud](https://cloud.google.com). - Code: `gcloud` - Since: v0.3.0 {{% notice note %}} _Please contribute by adding a CLI example._ {{% /notice %}} ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `Application Default Credentials` | [Documentation](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application) | | `GCE_PROJECT` | Project name (by default, the project name is auto-detected by using the metadata service) | | `GCE_SERVICE_ACCOUNT` | Account | | `GCE_SERVICE_ACCOUNT_FILE` | Account file path | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `GCE_ALLOW_PRIVATE_ZONE` | Allows requested domain to be in private DNS zone, works only with a private ACME server (by default: false) | | `GCE_POLLING_INTERVAL` | Time between DNS propagation check | | `GCE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `GCE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://cloud.google.com/dns/api/v1/) - [Go client](https://github.com/googleapis/google-api-go-client) lego-4.9.1/docs/content/dns/zz_gen_gcore.md000066400000000000000000000035121434020463500206400ustar00rootroot00000000000000--- title: "G-Core Labs" date: 2019-03-03T16:39:46+01:00 draft: false slug: gcore dnsprovider: since: "v4.5.0" code: "gcore" url: "https://gcorelabs.com/dns/" --- Configuration for [G-Core Labs](https://gcorelabs.com/dns/). - Code: `gcore` - Since: v4.5.0 Here is an example bash command using the G-Core Labs provider: ```bash GCORE_PERMANENT_API_TOKEN=xxxxx \ lego --email you@example.com --dns gcore --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `GCORE_PERMANENT_API_TOKEN` | Permanent API tokene (https://gcorelabs.com/blog/permanent-api-token-explained/) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `GCORE_HTTP_TIMEOUT` | API request timeout | | `GCORE_POLLING_INTERVAL` | Time between DNS propagation check | | `GCORE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `GCORE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://dnsapi.gcorelabs.com/docs#tag/zonesV2) lego-4.9.1/docs/content/dns/zz_gen_glesys.md000066400000000000000000000034321434020463500210500ustar00rootroot00000000000000--- title: "Glesys" date: 2019-03-03T16:39:46+01:00 draft: false slug: glesys dnsprovider: since: "v0.5.0" code: "glesys" url: "https://glesys.com/" --- Configuration for [Glesys](https://glesys.com/). - Code: `glesys` - Since: v0.5.0 Here is an example bash command using the Glesys provider: ```bash GLESYS_API_USER=xxxxx \ GLESYS_API_KEY=yyyyy \ lego --email you@example.com --dns glesys --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `GLESYS_API_KEY` | API key | | `GLESYS_API_USER` | API user | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `GLESYS_HTTP_TIMEOUT` | API request timeout | | `GLESYS_POLLING_INTERVAL` | Time between DNS propagation check | | `GLESYS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `GLESYS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://github.com/GleSYS/API/wiki/API-Documentation) lego-4.9.1/docs/content/dns/zz_gen_godaddy.md000066400000000000000000000034721434020463500211610ustar00rootroot00000000000000--- title: "Go Daddy" date: 2019-03-03T16:39:46+01:00 draft: false slug: godaddy dnsprovider: since: "v0.5.0" code: "godaddy" url: "https://godaddy.com" --- Configuration for [Go Daddy](https://godaddy.com). - Code: `godaddy` - Since: v0.5.0 Here is an example bash command using the Go Daddy provider: ```bash GODADDY_API_KEY=xxxxxxxx \ GODADDY_API_SECRET=yyyyyyyy \ lego --email you@example.com --dns godaddy --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `GODADDY_API_KEY` | API key | | `GODADDY_API_SECRET` | API secret | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `GODADDY_HTTP_TIMEOUT` | API request timeout | | `GODADDY_POLLING_INTERVAL` | Time between DNS propagation check | | `GODADDY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `GODADDY_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://developer.godaddy.com/doc/endpoint/domains) lego-4.9.1/docs/content/dns/zz_gen_hetzner.md000066400000000000000000000033751434020463500212270ustar00rootroot00000000000000--- title: "Hetzner" date: 2019-03-03T16:39:46+01:00 draft: false slug: hetzner dnsprovider: since: "v3.7.0" code: "hetzner" url: "https://hetzner.com" --- Configuration for [Hetzner](https://hetzner.com). - Code: `hetzner` - Since: v3.7.0 Here is an example bash command using the Hetzner provider: ```bash HETZNER_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ lego --email you@example.com --dns hetzner --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `HETZNER_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `HETZNER_HTTP_TIMEOUT` | API request timeout | | `HETZNER_POLLING_INTERVAL` | Time between DNS propagation check | | `HETZNER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `HETZNER_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://dns.hetzner.com/api-docs) lego-4.9.1/docs/content/dns/zz_gen_hostingde.md000066400000000000000000000035011434020463500215230ustar00rootroot00000000000000--- title: "Hosting.de" date: 2019-03-03T16:39:46+01:00 draft: false slug: hostingde dnsprovider: since: "v1.1.0" code: "hostingde" url: "https://www.hosting.de/" --- Configuration for [Hosting.de](https://www.hosting.de/). - Code: `hostingde` - Since: v1.1.0 Here is an example bash command using the Hosting.de provider: ```bash HOSTINGDE_API_KEY=xxxxxxxx \ lego --email you@example.com --dns hostingde --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `HOSTINGDE_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `HOSTINGDE_HTTP_TIMEOUT` | API request timeout | | `HOSTINGDE_POLLING_INTERVAL` | Time between DNS propagation check | | `HOSTINGDE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `HOSTINGDE_TTL` | The TTL of the TXT record used for the DNS challenge | | `HOSTINGDE_ZONE_NAME` | Zone name in ACE format | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.hosting.de/api/#dns) lego-4.9.1/docs/content/dns/zz_gen_hosttech.md000066400000000000000000000035041434020463500213630ustar00rootroot00000000000000--- title: "Hosttech" date: 2019-03-03T16:39:46+01:00 draft: false slug: hosttech dnsprovider: since: "v4.5.0" code: "hosttech" url: "https://www.hosttech.eu/" --- Configuration for [Hosttech](https://www.hosttech.eu/). - Code: `hosttech` - Since: v4.5.0 Here is an example bash command using the Hosttech provider: ```bash HOSTTECH_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ lego --email you@example.com --dns hosttech --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `HOSTTECH_API_KEY` | API login | | `HOSTTECH_PASSWORD` | API password | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `HOSTTECH_HTTP_TIMEOUT` | API request timeout | | `HOSTTECH_POLLING_INTERVAL` | Time between DNS propagation check | | `HOSTTECH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `HOSTTECH_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://api.ns1.hosttech.eu/api/documentation) lego-4.9.1/docs/content/dns/zz_gen_httpreq.md000066400000000000000000000045641434020463500212400ustar00rootroot00000000000000--- title: "HTTP request" date: 2019-03-03T16:39:46+01:00 draft: false slug: httpreq dnsprovider: since: "v2.0.0" code: "httpreq" url: "/lego/dns/httpreq/" --- Configuration for [HTTP request](/lego/dns/httpreq/). - Code: `httpreq` - Since: v2.0.0 Here is an example bash command using the HTTP request provider: ```bash HTTPREQ_ENDPOINT=http://my.server.com:9090 \ lego --email you@example.com --dns httpreq --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `HTTPREQ_ENDPOINT` | The URL of the server | | `HTTPREQ_MODE` | `RAW`, none | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `HTTPREQ_HTTP_TIMEOUT` | API request timeout | | `HTTPREQ_PASSWORD` | Basic authentication password | | `HTTPREQ_POLLING_INTERVAL` | Time between DNS propagation check | | `HTTPREQ_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `HTTPREQ_USERNAME` | Basic authentication username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description The server must provide: - `POST` `/present` - `POST` `/cleanup` The URL of the server must be define by `HTTPREQ_ENDPOINT`. ### Mode There are 2 modes (`HTTPREQ_MODE`): - default mode: ```json { "fqdn": "_acme-challenge.domain.", "value": "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM" } ``` - `RAW` ```json { "domain": "domain", "token": "token", "keyAuth": "key" } ``` ### Authentication Basic authentication (optional) can be set with some environment variables: - `HTTPREQ_USERNAME` and `HTTPREQ_PASSWORD` - both values must be set, otherwise basic authentication is not defined. lego-4.9.1/docs/content/dns/zz_gen_hurricane.md000066400000000000000000000046521434020463500215270ustar00rootroot00000000000000--- title: "Hurricane Electric DNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: hurricane dnsprovider: since: "v4.3.0" code: "hurricane" url: "https://dns.he.net/" --- Configuration for [Hurricane Electric DNS](https://dns.he.net/). - Code: `hurricane` - Since: v4.3.0 Here is an example bash command using the Hurricane Electric DNS provider: ```bash HURRICANE_TOKENS=example.org:token \ lego --email you@example.com --dns hurricane --domains example.org --domains '*.example.org run' HURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2 \ lego --email you@example.com --dns hurricane --domains my.example.org --domains demo.example.org ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `HURRICANE_TOKENS` | TXT record names and tokens | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). Before using lego to request a certificate for a given domain or wildcard (such as `my.example.org` or `*.my.example.org`), create a TXT record named `_acme-challenge.my.example.org`, and enable dynamic updates on it. Generate a token for each URL with Hurricane Electric's UI, and copy it down. Stick to alphanumeric tokens for greatest reliability. To authenticate with the Hurricane Electric API, add each record name/token pair you want to update to the `HURRICANE_TOKENS` environment variable, as shown in the examples. Record names (without the `_acme-challenge.` component) and their tokens are separated with colons, while the credential pairs are concatenated into a comma-separated list, like so: ``` HURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2 ``` If you are issuing both a wildcard certificate and a standard certificate for a given subdomain, you should not have repeat entries for that name, as both will use the same credential. ``` HURRICANE_TOKENS=example.org:token ``` ## More information - [API documentation](https://dns.he.net/) lego-4.9.1/docs/content/dns/zz_gen_hyperone.md000066400000000000000000000046661434020463500214050ustar00rootroot00000000000000--- title: "HyperOne" date: 2019-03-03T16:39:46+01:00 draft: false slug: hyperone dnsprovider: since: "v3.9.0" code: "hyperone" url: "https://www.hyperone.com" --- Configuration for [HyperOne](https://www.hyperone.com). - Code: `hyperone` - Since: v3.9.0 Here is an example bash command using the HyperOne provider: ```bash lego --email you@example.com --dns hyperone --domains my.example.org run ``` ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `HYPERONE_API_URL` | Allows to pass custom API Endpoint to be used in the challenge (default https://api.hyperone.com/v2) | | `HYPERONE_LOCATION_ID` | Specifies location (region) to be used in API calls. (default pl-waw-1) | | `HYPERONE_PASSPORT_LOCATION` | Allows to pass custom passport file location (default ~/.h1/passport.json) | | `HYPERONE_POLLING_INTERVAL` | Time between DNS propagation check | | `HYPERONE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `HYPERONE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description Default configuration does not require any additional environment variables, just a passport file in `~/.h1/passport.json` location. ### Generating passport file using H1 CLI To use this application you have to generate passport file for `sa`: ``` h1 iam project sa credential generate --name my-passport --project --sa --passport-output-file ~/.h1/passport.json ``` ### Required permissions The application requires following permissions: - `dns/zone/list` - `dns/zone.recordset/list` - `dns/zone.recordset/create` - `dns/zone.recordset/delete` - `dns/zone.record/create` - `dns/zone.record/list` - `dns/zone.record/delete` All required permissions are available via platform role `tool.lego`. ## More information - [API documentation](https://api.hyperone.com/v2/docs) lego-4.9.1/docs/content/dns/zz_gen_ibmcloud.md000066400000000000000000000040101434020463500213310ustar00rootroot00000000000000--- title: "IBM Cloud (SoftLayer)" date: 2019-03-03T16:39:46+01:00 draft: false slug: ibmcloud dnsprovider: since: "v4.5.0" code: "ibmcloud" url: "https://www.ibm.com/cloud/" --- Configuration for [IBM Cloud (SoftLayer)](https://www.ibm.com/cloud/). - Code: `ibmcloud` - Since: v4.5.0 Here is an example bash command using the IBM Cloud (SoftLayer) provider: ```bash SOFTLAYER_USERNAME=xxxxx \ SOFTLAYER_API_KEY=yyyyy \ lego --email you@example.com --dns ibmcloud --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `SOFTLAYER_API_KEY` | Classic Infrastructure API key | | `SOFTLAYER_USERNAME` | User name (IBM Cloud is _) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `SOFTLAYER_POLLING_INTERVAL` | Time between DNS propagation check | | `SOFTLAYER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `SOFTLAYER_TIMEOUT` | API request timeout | | `SOFTLAYER_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://cloud.ibm.com/docs/dns?topic=dns-getting-started-with-the-dns-api) - [Go client](https://github.com/softlayer/softlayer-go) lego-4.9.1/docs/content/dns/zz_gen_iij.md000066400000000000000000000036161434020463500203210ustar00rootroot00000000000000--- title: "Internet Initiative Japan" date: 2019-03-03T16:39:46+01:00 draft: false slug: iij dnsprovider: since: "v1.1.0" code: "iij" url: "https://www.iij.ad.jp/en/" --- Configuration for [Internet Initiative Japan](https://www.iij.ad.jp/en/). - Code: `iij` - Since: v1.1.0 Here is an example bash command using the Internet Initiative Japan provider: ```bash IIJ_API_ACCESS_KEY=xxxxxxxx \ IIJ_API_SECRET_KEY=yyyyyy \ IIJ_DO_SERVICE_CODE=zzzzzz \ lego --email you@example.com --dns iij --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `IIJ_API_ACCESS_KEY` | API access key | | `IIJ_API_SECRET_KEY` | API secret key | | `IIJ_DO_SERVICE_CODE` | DO service code | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `IIJ_POLLING_INTERVAL` | Time between DNS propagation check | | `IIJ_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `IIJ_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://manual.iij.jp/p2/pubapi/) - [Go client](https://github.com/iij/doapi) lego-4.9.1/docs/content/dns/zz_gen_iijdpf.md000066400000000000000000000041051434020463500210050ustar00rootroot00000000000000--- title: "IIJ DNS Platform Service" date: 2019-03-03T16:39:46+01:00 draft: false slug: iijdpf dnsprovider: since: "v4.7.0" code: "iijdpf" url: "https://www.iij.ad.jp/en/biz/dns-pfm/" --- Configuration for [IIJ DNS Platform Service](https://www.iij.ad.jp/en/biz/dns-pfm/). - Code: `iijdpf` - Since: v4.7.0 Here is an example bash command using the IIJ DNS Platform Service provider: ```bash IIJ_DPF_API_TOKEN=xxxxxxxx \ IIJ_DPF_DPM_SERVICE_CODE=yyyyyy \ lego --email you@example.com --dns iijdpf --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `IIJ_DPF_API_TOKEN` | API token | | `IIJ_DPF_DPM_SERVICE_CODE` | IIJ Managed DNS Service's service code | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `IIJ_DPF_API_ENDPOINT` | API endpoint URL, defaults to https://api.dns-platform.jp/dpf/v1 | | `IIJ_DPF_POLLING_INTERVAL` | Time between DNS propagation check, defaults to 5 second | | `IIJ_DPF_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation, defaults to 660 second | | `IIJ_DPF_TTL` | The TTL of the TXT record used for the DNS challenge, default to 300 | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://manual.iij.jp/dpf/dpfapi/) - [Go client](https://github.com/mimuret/golang-iij-dpf) lego-4.9.1/docs/content/dns/zz_gen_infoblox.md000066400000000000000000000046411434020463500213650ustar00rootroot00000000000000--- title: "Infoblox" date: 2019-03-03T16:39:46+01:00 draft: false slug: infoblox dnsprovider: since: "v4.4.0" code: "infoblox" url: "https://www.infoblox.com/" --- Configuration for [Infoblox](https://www.infoblox.com/). - Code: `infoblox` - Since: v4.4.0 Here is an example bash command using the Infoblox provider: ```bash INFOBLOX_USERNAME=api-user-529 \ INFOBLOX_PASSWORD=b9841238feb177a84330febba8a83208921177bffe733 \ INFOBLOX_HOST=infoblox.example.org lego --email you@example.com --dns infoblox --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `INFOBLOX_HOST` | Host URI | | `INFOBLOX_PASSWORD` | Account Password | | `INFOBLOX_USERNAME` | Account Username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `INFOBLOX_DNS_VIEW` | The view for the TXT records, default: External | | `INFOBLOX_HTTP_TIMEOUT` | HTTP request timeout | | `INFOBLOX_POLLING_INTERVAL` | Time between DNS propagation check | | `INFOBLOX_PORT` | The port for the infoblox grid manager, default: 443 | | `INFOBLOX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `INFOBLOX_SSL_VERIFY` | Whether or not to verify the TLS certificate, default: true | | `INFOBLOX_TTL` | The TTL of the TXT record used for the DNS challenge | | `INFOBLOX_WAPI_VERSION` | The version of WAPI being used, default: 2.11 | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). When creating an API's user ensure it has the proper permissions for the view you are working with. ## More information - [API documentation](https://your.infoblox.server/wapidoc/) - [Go client](https://github.com/infobloxopen/infoblox-go-client) lego-4.9.1/docs/content/dns/zz_gen_infomaniak.md000066400000000000000000000040011434020463500216470ustar00rootroot00000000000000--- title: "Infomaniak" date: 2019-03-03T16:39:46+01:00 draft: false slug: infomaniak dnsprovider: since: "v4.1.0" code: "infomaniak" url: "https://www.infomaniak.com/" --- Configuration for [Infomaniak](https://www.infomaniak.com/). - Code: `infomaniak` - Since: v4.1.0 Here is an example bash command using the Infomaniak provider: ```bash INFOMANIAK_ACCESS_TOKEN=1234567898765432 \ lego --email you@example.com --dns infomaniak --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `INFOMANIAK_ACCESS_TOKEN` | Access token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `INFOMANIAK_ENDPOINT` | https://api.infomaniak.com | | `INFOMANIAK_HTTP_TIMEOUT` | API request timeout | | `INFOMANIAK_POLLING_INTERVAL` | Time between DNS propagation check | | `INFOMANIAK_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `INFOMANIAK_TTL` | The TTL of the TXT record used for the DNS challenge in seconds | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Access token Access token can be created at the url https://manager.infomaniak.com/v3/infomaniak-api. You will need domain scope. ## More information - [API documentation](https://api.infomaniak.com/doc) lego-4.9.1/docs/content/dns/zz_gen_internetbs.md000066400000000000000000000036331434020463500217220ustar00rootroot00000000000000--- title: "Internet.bs" date: 2019-03-03T16:39:46+01:00 draft: false slug: internetbs dnsprovider: since: "v4.5.0" code: "internetbs" url: "https://internetbs.net" --- Configuration for [Internet.bs](https://internetbs.net). - Code: `internetbs` - Since: v4.5.0 Here is an example bash command using the Internet.bs provider: ```bash INTERNET_BS_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ INTERNET_BS_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ lego --email you@example.com --dns internetbs --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `INTERNET_BS_API_KEY` | API key | | `INTERNET_BS_PASSWORD` | API password | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `INTERNET_BS_HTTP_TIMEOUT` | API request timeout | | `INTERNET_BS_POLLING_INTERVAL` | Time between DNS propagation check | | `INTERNET_BS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `INTERNET_BS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://internetbs.net/internet-bs-api.pdf) lego-4.9.1/docs/content/dns/zz_gen_inwx.md000066400000000000000000000040231434020463500205240ustar00rootroot00000000000000--- title: "INWX" date: 2019-03-03T16:39:46+01:00 draft: false slug: inwx dnsprovider: since: "v2.0.0" code: "inwx" url: "https://www.inwx.de/en" --- Configuration for [INWX](https://www.inwx.de/en). - Code: `inwx` - Since: v2.0.0 Here is an example bash command using the INWX provider: ```bash INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ lego --email you@example.com --dns inwx --domains my.example.org run # 2FA INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ INWX_SHARED_SECRET=zzzzzzzzzz \ lego --email you@example.com --dns inwx --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `INWX_PASSWORD` | Password | | `INWX_USERNAME` | Username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `INWX_POLLING_INTERVAL` | Time between DNS propagation check | | `INWX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation (default 360s) | | `INWX_SANDBOX` | Activate the sandbox (boolean) | | `INWX_SHARED_SECRET` | shared secret related to 2FA | | `INWX_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.inwx.de/en/help/apidoc) - [Go client](https://github.com/nrdcg/goinwx) lego-4.9.1/docs/content/dns/zz_gen_ionos.md000066400000000000000000000034171434020463500206740ustar00rootroot00000000000000--- title: "Ionos" date: 2019-03-03T16:39:46+01:00 draft: false slug: ionos dnsprovider: since: "v4.2.0" code: "ionos" url: "https://ionos.com" --- Configuration for [Ionos](https://ionos.com). - Code: `ionos` - Since: v4.2.0 Here is an example bash command using the Ionos provider: ```bash IONOS_API_KEY=xxxxxxxx \ lego --email you@example.com --dns ionos --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `IONOS_API_KEY` | API key `.` https://developer.hosting.ionos.com/docs/getstarted | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `IONOS_HTTP_TIMEOUT` | API request timeout | | `IONOS_POLLING_INTERVAL` | Time between DNS propagation check | | `IONOS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `IONOS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://developer.hosting.ionos.com/docs/dns) lego-4.9.1/docs/content/dns/zz_gen_iwantmyname.md000066400000000000000000000036161434020463500220770ustar00rootroot00000000000000--- title: "iwantmyname" date: 2019-03-03T16:39:46+01:00 draft: false slug: iwantmyname dnsprovider: since: "v4.7.0" code: "iwantmyname" url: "https://iwantmyname.com" --- Configuration for [iwantmyname](https://iwantmyname.com). - Code: `iwantmyname` - Since: v4.7.0 Here is an example bash command using the iwantmyname provider: ```bash IWANTMYNAME_USERNAME=xxxxxxxx \ IWANTMYNAME_PASSWORD=xxxxxxxx \ lego --email you@example.com --dns iwantmyname --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `IWANTMYNAME_PASSWORD` | API password | | `IWANTMYNAME_USERNAME` | API username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `IWANTMYNAME_HTTP_TIMEOUT` | API request timeout | | `IWANTMYNAME_POLLING_INTERVAL` | Time between DNS propagation check | | `IWANTMYNAME_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `IWANTMYNAME_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://iwantmyname.com/developer/domain-dns-api) lego-4.9.1/docs/content/dns/zz_gen_joker.md000066400000000000000000000062171434020463500206600ustar00rootroot00000000000000--- title: "Joker" date: 2019-03-03T16:39:46+01:00 draft: false slug: joker dnsprovider: since: "v2.6.0" code: "joker" url: "https://joker.com" --- Configuration for [Joker](https://joker.com). - Code: `joker` - Since: v2.6.0 Here is an example bash command using the Joker provider: ```bash # SVC JOKER_API_MODE=SVC \ JOKER_USERNAME= \ JOKER_PASSWORD= \ lego --email you@example.com --dns joker --domains my.example.org run # DMAPI JOKER_API_MODE=DMAPI \ JOKER_USERNAME= \ JOKER_PASSWORD= \ lego --email you@example.com --dns joker --domains my.example.org run ## or JOKER_API_MODE=DMAPI \ JOKER_API_KEY= \ lego --email you@example.com --dns joker --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `JOKER_API_KEY` | API key (only with DMAPI mode) | | `JOKER_API_MODE` | 'DMAPI' or 'SVC'. DMAPI is for resellers accounts. (Default: DMAPI) | | `JOKER_PASSWORD` | Joker.com password | | `JOKER_USERNAME` | Joker.com username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `JOKER_HTTP_TIMEOUT` | API request timeout | | `JOKER_POLLING_INTERVAL` | Time between DNS propagation check | | `JOKER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `JOKER_SEQUENCE_INTERVAL` | Time between sequential requests (only with 'SVC' mode) | | `JOKER_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## SVC mode In 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. As per [Joker.com documentation](https://joker.com/faq/content/6/496/en/let_s-encrypt-support.html): > 1. please login at Joker.com, visit 'My Domains', > find the domain you want to add Let's Encrypt certificate for, and chose "DNS" in the menu > > 2. on the top right, you will find the setting for 'Dynamic DNS'. > If not already active, please activate it. > It will not affect any other already existing DNS records of this domain. > > 3. please take a note of the credentials which are now shown as 'Dynamic DNS Authentication', consisting of a 'username' and a 'password'. > > 4. this is all you have to do here - and only once per domain. ## More information - [API documentation](https://joker.com/faq/category/39/22-dmapi.html) lego-4.9.1/docs/content/dns/zz_gen_lightsail.md000066400000000000000000000064731434020463500215320ustar00rootroot00000000000000--- title: "Amazon Lightsail" date: 2019-03-03T16:39:46+01:00 draft: false slug: lightsail dnsprovider: since: "v0.5.0" code: "lightsail" url: "https://aws.amazon.com/lightsail/" --- Configuration for [Amazon Lightsail](https://aws.amazon.com/lightsail/). - Code: `lightsail` - Since: v0.5.0 {{% notice note %}} _Please contribute by adding a CLI example._ {{% /notice %}} ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `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) | | `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) | | `DNS_ZONE` | Domain name of the DNS zone | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `AWS_SHARED_CREDENTIALS_FILE` | Managed by the AWS client. Shared credentials file. | | `LIGHTSAIL_POLLING_INTERVAL` | Time between DNS propagation check | | `LIGHTSAIL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description AWS Credentials are automatically detected in the following locations and prioritized in the following order: 1. Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, [`AWS_SESSION_TOKEN`] 2. Shared credentials file (defaults to `~/.aws/credentials`, profiles can be specified using `AWS_PROFILE`) 3. Amazon EC2 IAM role AWS region is not required to set as the Lightsail DNS zone is in global (us-east-1) region. ## Policy The following AWS IAM policy document describes the minimum permissions required for lego to complete the DNS challenge. ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "lightsail:DeleteDomainEntry", "lightsail:CreateDomainEntry" ], "Resource": "" } ] } ``` Replace the `Resource` value with your Lightsail DNS zone ARN. You 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). It should be in the format of `arn:aws:lightsail:global::Domain/`. You also need to replace the region in the ARN to `us-east-1` (instead of `global`). Alternatively, you can also set the `Resource` to `*` (wildcard), which allow to access all domain, but this is not recommended. ## More information - [Go client](https://github.com/aws/aws-sdk-go/) lego-4.9.1/docs/content/dns/zz_gen_linode.md000066400000000000000000000034301434020463500210120ustar00rootroot00000000000000--- title: "Linode (v4)" date: 2019-03-03T16:39:46+01:00 draft: false slug: linode dnsprovider: since: "v1.1.0" code: "linode" url: "https://www.linode.com/" --- Configuration for [Linode (v4)](https://www.linode.com/). - Code: `linode` - Since: v1.1.0 Here is an example bash command using the Linode (v4) provider: ```bash LINODE_TOKEN=xxxxx \ lego --email you@example.com --dns linode --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `LINODE_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `LINODE_HTTP_TIMEOUT` | API request timeout | | `LINODE_POLLING_INTERVAL` | Time between DNS propagation check | | `LINODE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `LINODE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://developers.linode.com/api/v4) - [Go client](https://github.com/linode/linodego) lego-4.9.1/docs/content/dns/zz_gen_liquidweb.md000066400000000000000000000041201434020463500215220ustar00rootroot00000000000000--- title: "Liquid Web" date: 2019-03-03T16:39:46+01:00 draft: false slug: liquidweb dnsprovider: since: "v3.1.0" code: "liquidweb" url: "https://liquidweb.com" --- Configuration for [Liquid Web](https://liquidweb.com). - Code: `liquidweb` - Since: v3.1.0 Here is an example bash command using the Liquid Web provider: ```bash LIQUID_WEB_USERNAME=someuser \ LIQUID_WEB_PASSWORD=somepass \ LIQUID_WEB_ZONE=tacoman.com.net \ lego --email you@example.com --dns liquidweb --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `LIQUID_WEB_PASSWORD` | Storm API Password | | `LIQUID_WEB_USERNAME` | Storm API Username | | `LIQUID_WEB_ZONE` | DNS Zone | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `LIQUID_WEB_HTTP_TIMEOUT` | Maximum waiting time for the DNS records to be created (not verified) | | `LIQUID_WEB_POLLING_INTERVAL` | Time between DNS propagation check | | `LIQUID_WEB_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `LIQUID_WEB_TTL` | The TTL of the TXT record used for the DNS challenge | | `LIQUID_WEB_URL` | Storm API endpoint | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://cart.liquidweb.com/storm/api/docs/v1/) - [Go client](https://github.com/liquidweb/liquidweb-go) lego-4.9.1/docs/content/dns/zz_gen_loopia.md000066400000000000000000000041351434020463500210260ustar00rootroot00000000000000--- title: "Loopia" date: 2019-03-03T16:39:46+01:00 draft: false slug: loopia dnsprovider: since: "v4.2.0" code: "loopia" url: "https://loopia.com" --- Configuration for [Loopia](https://loopia.com). - Code: `loopia` - Since: v4.2.0 Here is an example bash command using the Loopia provider: ```bash LOOPIA_API_USER=xxxxxxxx \ LOOPIA_API_PASSWORD=yyyyyyyy \ lego --email my@email.com --dns loopia --domains my.domain.com run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `LOOPIA_API_PASSWORD` | API password | | `LOOPIA_API_USER` | API username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `LOOPIA_API_URL` | API endpoint. Ex: https://api.loopia.se/RPCSERV or https://api.loopia.rs/RPCSERV | | `LOOPIA_HTTP_TIMEOUT` | API request timeout | | `LOOPIA_POLLING_INTERVAL` | Time between DNS propagation check | | `LOOPIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `LOOPIA_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ### API user You can [generate a new API user](https://customerzone.loopia.com/api/) from your account page. It needs to have the following permissions: * addZoneRecord * getZoneRecords * removeZoneRecord * removeSubdomain ## More information - [API documentation](https://www.loopia.com/api) lego-4.9.1/docs/content/dns/zz_gen_luadns.md000066400000000000000000000034411434020463500210300ustar00rootroot00000000000000--- title: "LuaDNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: luadns dnsprovider: since: "v3.7.0" code: "luadns" url: "https://luadns.com" --- Configuration for [LuaDNS](https://luadns.com). - Code: `luadns` - Since: v3.7.0 Here is an example bash command using the LuaDNS provider: ```bash LUADNS_API_USERNAME=youremail \ LUADNS_API_TOKEN=xxxxxxxx \ lego --email you@example.com --dns luadns --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `LUADNS_API_TOKEN` | API token | | `LUADNS_API_USERNAME` | Username (your email) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `LUADNS_HTTP_TIMEOUT` | API request timeout | | `LUADNS_POLLING_INTERVAL` | Time between DNS propagation check | | `LUADNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `LUADNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://luadns.com/api.html) lego-4.9.1/docs/content/dns/zz_gen_mydnsjp.md000066400000000000000000000034421434020463500212270ustar00rootroot00000000000000--- title: "MyDNS.jp" date: 2019-03-03T16:39:46+01:00 draft: false slug: mydnsjp dnsprovider: since: "v1.2.0" code: "mydnsjp" url: "https://www.mydns.jp" --- Configuration for [MyDNS.jp](https://www.mydns.jp). - Code: `mydnsjp` - Since: v1.2.0 Here is an example bash command using the MyDNS.jp provider: ```bash MYDNSJP_MASTER_ID=xxxxx \ MYDNSJP_PASSWORD=xxxxx \ lego --email you@example.com --dns mydnsjp --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `MYDNSJP_MASTER_ID` | Master ID | | `MYDNSJP_PASSWORD` | Password | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `MYDNSJP_HTTP_TIMEOUT` | API request timeout | | `MYDNSJP_POLLING_INTERVAL` | Time between DNS propagation check | | `MYDNSJP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `MYDNSJP_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.mydns.jp/?MENU=030) lego-4.9.1/docs/content/dns/zz_gen_mythicbeasts.md000066400000000000000000000043641434020463500222460ustar00rootroot00000000000000--- title: "MythicBeasts" date: 2019-03-03T16:39:46+01:00 draft: false slug: mythicbeasts dnsprovider: since: "v0.3.7" code: "mythicbeasts" url: "https://www.mythic-beasts.com/" --- Configuration for [MythicBeasts](https://www.mythic-beasts.com/). - Code: `mythicbeasts` - Since: v0.3.7 Here is an example bash command using the MythicBeasts provider: ```bash MYTHICBEASTS_USERNAME=myuser \ MYTHICBEASTS_PASSWORD=mypass \ lego --email you@example.com --dns mythicbeasts --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `MYTHICBEASTS_PASSWORD` | Password | | `MYTHICBEASTS_USERNAME` | User name | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `MYTHICBEASTS_API_ENDPOINT` | The endpoint for the API (must implement v2) | | `MYTHICBEASTS_AUTH_API_ENDPOINT` | The endpoint for Mythic Beasts' Authentication | | `MYTHICBEASTS_HTTP_TIMEOUT` | API request timeout | | `MYTHICBEASTS_POLLING_INTERVAL` | Time between DNS propagation check | | `MYTHICBEASTS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `MYTHICBEASTS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). If you are using specific API keys, then the username is the API ID for your API key, and the password is the API secret. Your API key name is not needed to operate lego. ## More information - [API documentation](https://www.mythic-beasts.com/support/api/dnsv2) lego-4.9.1/docs/content/dns/zz_gen_namecheap.md000066400000000000000000000043661434020463500214720ustar00rootroot00000000000000--- title: "Namecheap" date: 2019-03-03T16:39:46+01:00 draft: false slug: namecheap dnsprovider: since: "v0.3.0" code: "namecheap" url: "https://www.namecheap.com" --- Configuration for [Namecheap](https://www.namecheap.com). **To enable API access on the Namecheap production environment, some opaque requirements must be met.** More information in the section [Enabling API Access](https://www.namecheap.com/support/api/intro/) of the Namecheap documentation. (2020-08: Account balance of $50+, 20+ domains in your account, or purchases totaling $50+ within the last 2 years.) - Code: `namecheap` - Since: v0.3.0 Here is an example bash command using the Namecheap provider: ```bash NAMECHEAP_API_USER=user \ NAMECHEAP_API_KEY=key \ lego --email you@example.com --dns namecheap --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `NAMECHEAP_API_KEY` | API key | | `NAMECHEAP_API_USER` | API user | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `NAMECHEAP_HTTP_TIMEOUT` | API request timeout | | `NAMECHEAP_POLLING_INTERVAL` | Time between DNS propagation check | | `NAMECHEAP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `NAMECHEAP_SANDBOX` | Activate the sandbox (boolean) | | `NAMECHEAP_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.namecheap.com/support/api/methods.aspx) lego-4.9.1/docs/content/dns/zz_gen_namedotcom.md000066400000000000000000000036221434020463500216710ustar00rootroot00000000000000--- title: "Name.com" date: 2019-03-03T16:39:46+01:00 draft: false slug: namedotcom dnsprovider: since: "v0.5.0" code: "namedotcom" url: "https://www.name.com" --- Configuration for [Name.com](https://www.name.com). - Code: `namedotcom` - Since: v0.5.0 Here is an example bash command using the Name.com provider: ```bash NAMECOM_USERNAME=foo.bar \ NAMECOM_API_TOKEN=a379a6f6eeafb9a55e378c118034e2751e682fab \ lego --email you@example.com --dns namedotcom --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `NAMECOM_API_TOKEN` | API token | | `NAMECOM_USERNAME` | Username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `NAMECOM_HTTP_TIMEOUT` | API request timeout | | `NAMECOM_POLLING_INTERVAL` | Time between DNS propagation check | | `NAMECOM_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `NAMECOM_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.name.com/api-docs/DNS) - [Go client](https://github.com/namedotcom/go) lego-4.9.1/docs/content/dns/zz_gen_namesilo.md000066400000000000000000000035611434020463500213540ustar00rootroot00000000000000--- title: "Namesilo" date: 2019-03-03T16:39:46+01:00 draft: false slug: namesilo dnsprovider: since: "v2.7.0" code: "namesilo" url: "https://www.namesilo.com/" --- Configuration for [Namesilo](https://www.namesilo.com/). - Code: `namesilo` - Since: v2.7.0 Here is an example bash command using the Namesilo provider: ```bash NAMESILO_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ lego --email you@example.com --dns namesilo --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `NAMESILO_API_KEY` | Client ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `NAMESILO_POLLING_INTERVAL` | Time between DNS propagation check | | `NAMESILO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation, it is better to set larger than 15m | | `NAMESILO_TTL` | The TTL of the TXT record used for the DNS challenge, should be in [3600, 2592000] | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.namesilo.com/api_reference.php) - [Go client](https://github.com/nrdcg/namesilo) lego-4.9.1/docs/content/dns/zz_gen_nearlyfreespeech.md000066400000000000000000000041431434020463500230660ustar00rootroot00000000000000--- title: "NearlyFreeSpeech.NET" date: 2019-03-03T16:39:46+01:00 draft: false slug: nearlyfreespeech dnsprovider: since: "v4.8.0" code: "nearlyfreespeech" url: "https://nearlyfreespeech.net/" --- Configuration for [NearlyFreeSpeech.NET](https://nearlyfreespeech.net/). - Code: `nearlyfreespeech` - Since: v4.8.0 Here is an example bash command using the NearlyFreeSpeech.NET provider: ```bash NEARLYFREESPEECH_API_KEY=xxxxxx \ NEARLYFREESPEECH_LOGIN=xxxx \ lego --email you@example.com --dns nearlyfreespeech --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `NEARLYFREESPEECH_API_KEY` | API Key for API requests | | `NEARLYFREESPEECH_LOGIN` | Username for API requests | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `NEARLYFREESPEECH_HTTP_TIMEOUT` | API request timeout | | `NEARLYFREESPEECH_POLLING_INTERVAL` | Time between DNS propagation check | | `NEARLYFREESPEECH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `NEARLYFREESPEECH_SEQUENCE_INTERVAL` | Time between sequential requests | | `NEARLYFREESPEECH_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://members.nearlyfreespeech.net/wiki/API/Reference) lego-4.9.1/docs/content/dns/zz_gen_netcup.md000066400000000000000000000035521434020463500210430ustar00rootroot00000000000000--- title: "Netcup" date: 2019-03-03T16:39:46+01:00 draft: false slug: netcup dnsprovider: since: "v1.1.0" code: "netcup" url: "https://www.netcup.eu/" --- Configuration for [Netcup](https://www.netcup.eu/). - Code: `netcup` - Since: v1.1.0 Here is an example bash command using the Netcup provider: ```bash NETCUP_CUSTOMER_NUMBER=xxxx \ NETCUP_API_KEY=yyyy \ NETCUP_API_PASSWORD=zzzz \ lego --email you@example.com --dns netcup --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `NETCUP_API_KEY` | API key | | `NETCUP_API_PASSWORD` | API password | | `NETCUP_CUSTOMER_NUMBER` | Customer number | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `NETCUP_HTTP_TIMEOUT` | API request timeout | | `NETCUP_POLLING_INTERVAL` | Time between DNS propagation check | | `NETCUP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `NETCUP_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.netcup-wiki.de/wiki/DNS_API) lego-4.9.1/docs/content/dns/zz_gen_netlify.md000066400000000000000000000034301434020463500212120ustar00rootroot00000000000000--- title: "Netlify" date: 2019-03-03T16:39:46+01:00 draft: false slug: netlify dnsprovider: since: "v3.7.0" code: "netlify" url: "https://www.netlify.com" --- Configuration for [Netlify](https://www.netlify.com). - Code: `netlify` - Since: v3.7.0 Here is an example bash command using the Netlify provider: ```bash NETLIFY_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ lego --email you@example.com --dns netlify --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `NETLIFY_TOKEN` | Token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `NETLIFY_HTTP_TIMEOUT` | API request timeout | | `NETLIFY_POLLING_INTERVAL` | Time between DNS propagation check | | `NETLIFY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `NETLIFY_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://open-api.netlify.com/) lego-4.9.1/docs/content/dns/zz_gen_nicmanager.md000066400000000000000000000054441434020463500216530ustar00rootroot00000000000000--- title: "Nicmanager" date: 2019-03-03T16:39:46+01:00 draft: false slug: nicmanager dnsprovider: since: "v4.5.0" code: "nicmanager" url: "https://www.nicmanager.com/" --- Configuration for [Nicmanager](https://www.nicmanager.com/). - Code: `nicmanager` - Since: v4.5.0 Here is an example bash command using the Nicmanager provider: ```bash ## Login using email NICMANAGER_API_EMAIL = "you@example.com" \ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ lego --email you@example.com --dns nicmanager --domains my.example.org run ## Login using account name + username NICMANAGER_API_LOGIN = "myaccount" \ NICMANAGER_API_USERNAME = "myuser" \ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ lego --email you@example.com --dns nicmanager --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `NICMANAGER_API_EMAIL` | Email-based login | | `NICMANAGER_API_LOGIN` | Login, used for Username-based login | | `NICMANAGER_API_PASSWORD` | Password, always required | | `NICMANAGER_API_USERNAME` | Username, used for Username-based login | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `NICMANAGER_API_MODE` | mode: 'anycast' or 'zone' (default: 'anycast') | | `NICMANAGER_API_OTP` | TOTP Secret (optional) | | `NICMANAGER_HTTP_TIMEOUT` | API request timeout | | `NICMANAGER_POLLING_INTERVAL` | Time between DNS propagation check | | `NICMANAGER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `NICMANAGER_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description You can login using your account name + username or using your email address. Optionally if TOTP is configured for your account, set `NICMANAGER_API_OTP`. ## More information - [API documentation](https://api.nicmanager.com/docs/v1/) lego-4.9.1/docs/content/dns/zz_gen_nifcloud.md000066400000000000000000000035761434020463500213560ustar00rootroot00000000000000--- title: "NIFCloud" date: 2019-03-03T16:39:46+01:00 draft: false slug: nifcloud dnsprovider: since: "v1.1.0" code: "nifcloud" url: "https://www.nifcloud.com/" --- Configuration for [NIFCloud](https://www.nifcloud.com/). - Code: `nifcloud` - Since: v1.1.0 Here is an example bash command using the NIFCloud provider: ```bash NIFCLOUD_ACCESS_KEY_ID=xxxx \ NIFCLOUD_SECRET_ACCESS_KEY=yyyy \ lego --email you@example.com --dns nifcloud --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `NIFCLOUD_ACCESS_KEY_ID` | Access key | | `NIFCLOUD_SECRET_ACCESS_KEY` | Secret access key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `NIFCLOUD_HTTP_TIMEOUT` | API request timeout | | `NIFCLOUD_POLLING_INTERVAL` | Time between DNS propagation check | | `NIFCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `NIFCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://mbaas.nifcloud.com/doc/current/rest/common/format.html) lego-4.9.1/docs/content/dns/zz_gen_njalla.md000066400000000000000000000033141434020463500210020ustar00rootroot00000000000000--- title: "Njalla" date: 2019-03-03T16:39:46+01:00 draft: false slug: njalla dnsprovider: since: "v4.3.0" code: "njalla" url: "https://njal.la" --- Configuration for [Njalla](https://njal.la). - Code: `njalla` - Since: v4.3.0 Here is an example bash command using the Njalla provider: ```bash NJALLA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ lego --email you@example.com --dns njalla --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `NJALLA_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `NJALLA_HTTP_TIMEOUT` | API request timeout | | `NJALLA_POLLING_INTERVAL` | Time between DNS propagation check | | `NJALLA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `NJALLA_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://njal.la/api/) lego-4.9.1/docs/content/dns/zz_gen_ns1.md000066400000000000000000000032611434020463500202430ustar00rootroot00000000000000--- title: "NS1" date: 2019-03-03T16:39:46+01:00 draft: false slug: ns1 dnsprovider: since: "v0.4.0" code: "ns1" url: "https://ns1.com" --- Configuration for [NS1](https://ns1.com). - Code: `ns1` - Since: v0.4.0 Here is an example bash command using the NS1 provider: ```bash NS1_API_KEY=xxxx \ lego --email you@example.com --dns ns1 --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `NS1_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `NS1_HTTP_TIMEOUT` | API request timeout | | `NS1_POLLING_INTERVAL` | Time between DNS propagation check | | `NS1_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `NS1_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://ns1.com/api) - [Go client](https://github.com/ns1/ns1-go) lego-4.9.1/docs/content/dns/zz_gen_oraclecloud.md000066400000000000000000000045431434020463500220420ustar00rootroot00000000000000--- title: "Oracle Cloud" date: 2019-03-03T16:39:46+01:00 draft: false slug: oraclecloud dnsprovider: since: "v2.3.0" code: "oraclecloud" url: "https://cloud.oracle.com/home" --- Configuration for [Oracle Cloud](https://cloud.oracle.com/home). - Code: `oraclecloud` - Since: v2.3.0 Here is an example bash command using the Oracle Cloud provider: ```bash OCI_PRIVKEY_FILE="~/.oci/oci_api_key.pem" \ OCI_PRIVKEY_PASS="secret" \ OCI_TENANCY_OCID="ocid1.tenancy.oc1..secret" \ OCI_USER_OCID="ocid1.user.oc1..secret" \ OCI_PUBKEY_FINGERPRINT="00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" \ OCI_REGION="us-phoenix-1" \ OCI_COMPARTMENT_OCID="ocid1.tenancy.oc1..secret" \ lego --email you@example.com --dns oraclecloud --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `OCI_COMPARTMENT_OCID` | Compartment OCID | | `OCI_PRIVKEY_FILE` | Private key file | | `OCI_PRIVKEY_PASS` | Private key password | | `OCI_PUBKEY_FINGERPRINT` | Public key fingerprint | | `OCI_REGION` | Region | | `OCI_TENANCY_OCID` | Tenancy OCID | | `OCI_USER_OCID` | User OCID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `OCI_POLLING_INTERVAL` | Time between DNS propagation check | | `OCI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `OCI_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm) - [Go client](https://github.com/oracle/oci-go-sdk) lego-4.9.1/docs/content/dns/zz_gen_otc.md000066400000000000000000000034571434020463500203360ustar00rootroot00000000000000--- title: "Open Telekom Cloud" date: 2019-03-03T16:39:46+01:00 draft: false slug: otc dnsprovider: since: "v0.4.1" code: "otc" url: "https://cloud.telekom.de/en" --- Configuration for [Open Telekom Cloud](https://cloud.telekom.de/en). - Code: `otc` - Since: v0.4.1 {{% notice note %}} _Please contribute by adding a CLI example._ {{% /notice %}} ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `OTC_DOMAIN_NAME` | Domain name | | `OTC_IDENTITY_ENDPOINT` | Identity endpoint URL | | `OTC_PASSWORD` | Password | | `OTC_PROJECT_NAME` | Project name | | `OTC_USER_NAME` | User name | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `OTC_HTTP_TIMEOUT` | API request timeout | | `OTC_POLLING_INTERVAL` | Time between DNS propagation check | | `OTC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `OTC_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://docs.otc.t-systems.com/en-us/dns/index.html) lego-4.9.1/docs/content/dns/zz_gen_ovh.md000066400000000000000000000046401434020463500203400ustar00rootroot00000000000000--- title: "OVH" date: 2019-03-03T16:39:46+01:00 draft: false slug: ovh dnsprovider: since: "v0.4.0" code: "ovh" url: "https://www.ovh.com/" --- Configuration for [OVH](https://www.ovh.com/). - Code: `ovh` - Since: v0.4.0 Here is an example bash command using the OVH provider: ```bash OVH_APPLICATION_KEY=1234567898765432 \ OVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \ OVH_CONSUMER_KEY=256vfsd347245sdfg \ OVH_ENDPOINT=ovh-eu \ lego --email you@example.com --dns ovh --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `OVH_APPLICATION_KEY` | Application key | | `OVH_APPLICATION_SECRET` | Application secret | | `OVH_CONSUMER_KEY` | Consumer key | | `OVH_ENDPOINT` | Endpoint URL (ovh-eu or ovh-ca) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `OVH_HTTP_TIMEOUT` | API request timeout | | `OVH_POLLING_INTERVAL` | Time between DNS propagation check | | `OVH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `OVH_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Application Key and Secret Application key and secret can be created by following the [OVH guide](https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/). When requesting the consumer key, the following configuration can be use to define access rights: ```json { "accessRules": [ { "method": "POST", "path": "/domain/zone/*" }, { "method": "DELETE", "path": "/domain/zone/*" } ] } ``` ## More information - [API documentation](https://eu.api.ovh.com/) - [Go client](https://github.com/ovh/go-ovh) lego-4.9.1/docs/content/dns/zz_gen_pdns.md000066400000000000000000000047341434020463500205140ustar00rootroot00000000000000--- title: "PowerDNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: pdns dnsprovider: since: "v0.4.0" code: "pdns" url: "https://www.powerdns.com/" --- Configuration for [PowerDNS](https://www.powerdns.com/). - Code: `pdns` - Since: v0.4.0 Here is an example bash command using the PowerDNS provider: ```bash PDNS_API_URL=http://pdns-server:80/ \ PDNS_API_KEY=xxxx \ lego --email you@example.com --dns pdns --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `PDNS_API_KEY` | API key | | `PDNS_API_URL` | API URL | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `PDNS_HTTP_TIMEOUT` | API request timeout | | `PDNS_POLLING_INTERVAL` | Time between DNS propagation check | | `PDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `PDNS_SERVER_NAME` | Name of the server in the URL, 'localhost' by default | | `PDNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Information Tested 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. PowerDNS Notes: - 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. - 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 ## More information - [API documentation](https://doc.powerdns.com/md/httpapi/README/) lego-4.9.1/docs/content/dns/zz_gen_porkbun.md000066400000000000000000000034741434020463500212300ustar00rootroot00000000000000--- title: "Porkbun" date: 2019-03-03T16:39:46+01:00 draft: false slug: porkbun dnsprovider: since: "v4.4.0" code: "porkbun" url: "https://porkbun.com/" --- Configuration for [Porkbun](https://porkbun.com/). - Code: `porkbun` - Since: v4.4.0 Here is an example bash command using the Porkbun provider: ```bash PORKBUN_SECRET_API_KEY=xxxxxx \ PORKBUN_API_KEY=yyyyyy \ lego --email you@example.com --dns porkbun --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `PORKBUN_API_KEY` | API key | | `PORKBUN_SECRET_API_KEY` | secret API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `PORKBUN_HTTP_TIMEOUT` | API request timeout | | `PORKBUN_POLLING_INTERVAL` | Time between DNS propagation check | | `PORKBUN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `PORKBUN_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://porkbun.com/api/json/v3/documentation) lego-4.9.1/docs/content/dns/zz_gen_rackspace.md000066400000000000000000000035251434020463500215010ustar00rootroot00000000000000--- title: "Rackspace" date: 2019-03-03T16:39:46+01:00 draft: false slug: rackspace dnsprovider: since: "v0.4.0" code: "rackspace" url: "https://www.rackspace.com/" --- Configuration for [Rackspace](https://www.rackspace.com/). - Code: `rackspace` - Since: v0.4.0 Here is an example bash command using the Rackspace provider: ```bash RACKSPACE_USER=xxxx \ RACKSPACE_API_KEY=yyyy \ lego --email you@example.com --dns rackspace --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `RACKSPACE_API_KEY` | API key | | `RACKSPACE_USER` | API user | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `RACKSPACE_HTTP_TIMEOUT` | API request timeout | | `RACKSPACE_POLLING_INTERVAL` | Time between DNS propagation check | | `RACKSPACE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `RACKSPACE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://developer.rackspace.com/docs/cloud-dns/v1/) lego-4.9.1/docs/content/dns/zz_gen_regru.md000066400000000000000000000034071434020463500206700ustar00rootroot00000000000000--- title: "reg.ru" date: 2019-03-03T16:39:46+01:00 draft: false slug: regru dnsprovider: since: "v3.5.0" code: "regru" url: "https://www.reg.ru/" --- Configuration for [reg.ru](https://www.reg.ru/). - Code: `regru` - Since: v3.5.0 Here is an example bash command using the reg.ru provider: ```bash REGRU_USERNAME=xxxxxx \ REGRU_PASSWORD=yyyyyy \ lego --email you@example.com --dns regru --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `REGRU_PASSWORD` | API password | | `REGRU_USERNAME` | API username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `REGRU_HTTP_TIMEOUT` | API request timeout | | `REGRU_POLLING_INTERVAL` | Time between DNS propagation check | | `REGRU_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `REGRU_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.reg.ru/support/help/api2) lego-4.9.1/docs/content/dns/zz_gen_rfc2136.md000066400000000000000000000055661434020463500206420ustar00rootroot00000000000000--- title: "RFC2136" date: 2019-03-03T16:39:46+01:00 draft: false slug: rfc2136 dnsprovider: since: "v0.3.0" code: "rfc2136" url: "https://www.rfc-editor.org/rfc/rfc2136.html" --- Configuration for [RFC2136](https://www.rfc-editor.org/rfc/rfc2136.html). - Code: `rfc2136` - Since: v0.3.0 Here is an example bash command using the RFC2136 provider: ```bash RFC2136_NAMESERVER=127.0.0.1 \ RFC2136_TSIG_KEY=lego \ RFC2136_TSIG_ALGORITHM=hmac-sha256. \ RFC2136_TSIG_SECRET=YWJjZGVmZGdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNDU= \ lego --email you@example.com --dns rfc2136 --domains my.example.org run ## --- keyname=lego; keyfile=lego.key; tsig-keygen $keyname > $keyfile RFC2136_NAMESERVER=127.0.0.1 \ RFC2136_TSIG_KEY="$keyname" \ RFC2136_TSIG_ALGORITHM="$( awk -F'[ ";]' '/algorithm/ { print $2 }' $keyfile )." \ RFC2136_TSIG_SECRET="$( awk -F'[ ";]' '/secret/ { print $3 }' $keyfile )" \ lego --email you@example.com --dns rfc2136 --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `RFC2136_NAMESERVER` | Network address in the form "host" or "host:port" | | `RFC2136_TSIG_ALGORITHM` | TSIG algorithm. See [miekg/dns#tsig.go](https://github.com/miekg/dns/blob/master/tsig.go) for supported values. To disable TSIG authentication, leave the `RFC2136_TSIG*` variables unset. | | `RFC2136_TSIG_KEY` | Name of the secret key as defined in DNS server configuration. To disable TSIG authentication, leave the `RFC2136_TSIG*` variables unset. | | `RFC2136_TSIG_SECRET` | Secret key payload. To disable TSIG authentication, leave the` RFC2136_TSIG*` variables unset. | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `RFC2136_DNS_TIMEOUT` | API request timeout | | `RFC2136_POLLING_INTERVAL` | Time between DNS propagation check | | `RFC2136_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `RFC2136_SEQUENCE_INTERVAL` | Time between sequential requests | | `RFC2136_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.rfc-editor.org/rfc/rfc2136.html) lego-4.9.1/docs/content/dns/zz_gen_rimuhosting.md000066400000000000000000000035261434020463500221160ustar00rootroot00000000000000--- title: "RimuHosting" date: 2019-03-03T16:39:46+01:00 draft: false slug: rimuhosting dnsprovider: since: "v0.3.5" code: "rimuhosting" url: "https://rimuhosting.com" --- Configuration for [RimuHosting](https://rimuhosting.com). - Code: `rimuhosting` - Since: v0.3.5 Here is an example bash command using the RimuHosting provider: ```bash RIMUHOSTING_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ lego --email you@example.com --dns rimuhosting --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `RIMUHOSTING_API_KEY` | User API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `RIMUHOSTING_HTTP_TIMEOUT` | API request timeout | | `RIMUHOSTING_POLLING_INTERVAL` | Time between DNS propagation check | | `RIMUHOSTING_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `RIMUHOSTING_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://rimuhosting.com/dns/dyndns.jsp) lego-4.9.1/docs/content/dns/zz_gen_route53.md000066400000000000000000000137411434020463500210540ustar00rootroot00000000000000--- title: "Amazon Route 53" date: 2019-03-03T16:39:46+01:00 draft: false slug: route53 dnsprovider: since: "v0.3.0" code: "route53" url: "https://aws.amazon.com/route53/" --- Configuration for [Amazon Route 53](https://aws.amazon.com/route53/). - Code: `route53` - Since: v0.3.0 {{% notice note %}} _Please contribute by adding a CLI example._ {{% /notice %}} ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `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) | | `AWS_ASSUME_ROLE_ARN` | Managed by the AWS Role ARN (`AWS_ASSUME_ROLE_ARN` is not supported) | | `AWS_HOSTED_ZONE_ID` | Override the hosted zone ID. | | `AWS_PROFILE` | Managed by the AWS client (`AWS_PROFILE_FILE` is not supported) | | `AWS_REGION` | Managed by the AWS client (`AWS_REGION_FILE` is not supported) | | `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) | | `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) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `AWS_MAX_RETRIES` | The number of maximum returns the service will use to make an individual API request | | `AWS_POLLING_INTERVAL` | Time between DNS propagation check | | `AWS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `AWS_SHARED_CREDENTIALS_FILE` | Managed by the AWS client. Shared credentials file. | | `AWS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Description AWS Credentials are automatically detected in the following locations and prioritized in the following order: 1. Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, [`AWS_SESSION_TOKEN`] 2. Shared credentials file (defaults to `~/.aws/credentials`, profiles can be specified using `AWS_PROFILE`) 3. Amazon EC2 IAM role The AWS Region is automatically detected in the following locations and prioritized in the following order: 1. Environment variables: `AWS_REGION` 2. Shared configuration file if `AWS_SDK_LOAD_CONFIG` is set (defaults to `~/.aws/config`, profiles can be specified using `AWS_PROFILE`) If `AWS_HOSTED_ZONE_ID` is not set, Lego tries to determine the correct public hosted zone via the FQDN. See also: - [sessions](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/sessions.html) - [Setting AWS Credentials](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials) - [Setting AWS Region](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-the-region) ## IAM Policy Examples ### Broad privileges for testing purposes The 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. A word of caution: These permissions grant write access to any DNS record in any hosted zone, so it is recommended to narrow them down as much as possible if you are using this policy in production. ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "route53:GetChange", "route53:ChangeResourceRecordSets", "route53:ListResourceRecordSets" ], "Resource": [ "arn:aws:route53:::hostedzone/*", "arn:aws:route53:::change/*" ] }, { "Effect": "Allow", "Action": "route53:ListHostedZonesByName", "Resource": "*" } ] } ``` ### Least privilege policy for production purposes The following AWS IAM policy document describes least privilege permissions required for lego to complete the DNS challenge. Write access is limited to a specified hosted zone's DNS TXT records with a key of `_acme-challenge.example.com`. Replace `Z11111112222222333333` with your hosted zone ID and `example.com` with your domain name to use this policy. ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "route53:GetChange", "Resource": "arn:aws:route53:::change/*" }, { "Effect": "Allow", "Action": "route53:ListHostedZonesByName", "Resource": "*" }, { "Effect": "Allow", "Action": [ "route53:ListResourceRecordSets" ], "Resource": [ "arn:aws:route53:::hostedzone/Z11111112222222333333" ] }, { "Effect": "Allow", "Action": [ "route53:ChangeResourceRecordSets" ], "Resource": [ "arn:aws:route53:::hostedzone/Z11111112222222333333" ], "Condition": { "ForAllValues:StringEquals": { "route53:ChangeResourceRecordSetsNormalizedRecordNames": [ "_acme-challenge.example.com" ], "route53:ChangeResourceRecordSetsRecordTypes": [ "TXT" ] } } } ] } ``` ## More information - [API documentation](https://docs.aws.amazon.com/Route53/latest/APIReference/API_Operations_Amazon_Route_53.html) - [Go client](https://github.com/aws/aws-sdk-go/aws) lego-4.9.1/docs/content/dns/zz_gen_safedns.md000066400000000000000000000035051434020463500211660ustar00rootroot00000000000000--- title: "UKFast SafeDNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: safedns dnsprovider: since: "v4.6.0" code: "safedns" url: "https://www.ukfast.co.uk/dns-hosting.html" --- Configuration for [UKFast SafeDNS](https://www.ukfast.co.uk/dns-hosting.html). - Code: `safedns` - Since: v4.6.0 Here is an example bash command using the UKFast SafeDNS provider: ```bash SAFEDNS_AUTH_TOKEN=xxxxxx \ lego --email you@example.com --dns safedns --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `SAFEDNS_AUTH_TOKEN` | Authentication token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `SAFEDNS_HTTP_TIMEOUT` | API request timeout | | `SAFEDNS_POLLING_INTERVAL` | Time between DNS propagation check | | `SAFEDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `SAFEDNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://developers.ukfast.io/documentation/safedns) lego-4.9.1/docs/content/dns/zz_gen_sakuracloud.md000066400000000000000000000037531434020463500220650ustar00rootroot00000000000000--- title: "Sakura Cloud" date: 2019-03-03T16:39:46+01:00 draft: false slug: sakuracloud dnsprovider: since: "v1.1.0" code: "sakuracloud" url: "https://cloud.sakura.ad.jp/" --- Configuration for [Sakura Cloud](https://cloud.sakura.ad.jp/). - Code: `sakuracloud` - Since: v1.1.0 Here is an example bash command using the Sakura Cloud provider: ```bash SAKURACLOUD_ACCESS_TOKEN=xxxxx \ SAKURACLOUD_ACCESS_TOKEN_SECRET=yyyyy \ lego --email you@example.com --dns sakuracloud --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `SAKURACLOUD_ACCESS_TOKEN` | Access token | | `SAKURACLOUD_ACCESS_TOKEN_SECRET` | Access token secret | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `SAKURACLOUD_HTTP_TIMEOUT` | API request timeout | | `SAKURACLOUD_POLLING_INTERVAL` | Time between DNS propagation check | | `SAKURACLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `SAKURACLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://developer.sakura.ad.jp/cloud/api/1.1/) - [Go client](https://github.com/sacloud/iaas-api-go) lego-4.9.1/docs/content/dns/zz_gen_scaleway.md000066400000000000000000000035061434020463500213540ustar00rootroot00000000000000--- title: "Scaleway" date: 2019-03-03T16:39:46+01:00 draft: false slug: scaleway dnsprovider: since: "v3.4.0" code: "scaleway" url: "https://developers.scaleway.com/" --- Configuration for [Scaleway](https://developers.scaleway.com/). - Code: `scaleway` - Since: v3.4.0 Here is an example bash command using the Scaleway provider: ```bash SCALEWAY_API_TOKEN=xxxxxxx-xxxxx-xxxx-xxx-xxxxxx \ lego --email you@example.com --dns scaleway --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `SCALEWAY_API_TOKEN` | API token | | `SCALEWAY_PROJECT_ID` | Project to use (optional) | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `SCALEWAY_POLLING_INTERVAL` | Time between DNS propagation check | | `SCALEWAY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `SCALEWAY_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://developers.scaleway.com/en/products/domain/dns/api/) lego-4.9.1/docs/content/dns/zz_gen_selectel.md000066400000000000000000000034571434020463500213510ustar00rootroot00000000000000--- title: "Selectel" date: 2019-03-03T16:39:46+01:00 draft: false slug: selectel dnsprovider: since: "v1.2.0" code: "selectel" url: "https://kb.selectel.com/" --- Configuration for [Selectel](https://kb.selectel.com/). - Code: `selectel` - Since: v1.2.0 Here is an example bash command using the Selectel provider: ```bash SELECTEL_API_TOKEN=xxxxx \ lego --email you@example.com --dns selectel --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `SELECTEL_API_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `SELECTEL_BASE_URL` | API endpoint URL | | `SELECTEL_HTTP_TIMEOUT` | API request timeout | | `SELECTEL_POLLING_INTERVAL` | Time between DNS propagation check | | `SELECTEL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `SELECTEL_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://kb.selectel.com/23136054.html) lego-4.9.1/docs/content/dns/zz_gen_servercow.md000066400000000000000000000036151434020463500215640ustar00rootroot00000000000000--- title: "Servercow" date: 2019-03-03T16:39:46+01:00 draft: false slug: servercow dnsprovider: since: "v3.4.0" code: "servercow" url: "https://servercow.de/" --- Configuration for [Servercow](https://servercow.de/). - Code: `servercow` - Since: v3.4.0 Here is an example bash command using the Servercow provider: ```bash SERVERCOW_USERNAME=xxxxxxxx \ SERVERCOW_PASSWORD=xxxxxxxx \ lego --email you@example.com --dns servercow --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `SERVERCOW_PASSWORD` | API password | | `SERVERCOW_USERNAME` | API username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `SERVERCOW_HTTP_TIMEOUT` | API request timeout | | `SERVERCOW_POLLING_INTERVAL` | Time between DNS propagation check | | `SERVERCOW_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `SERVERCOW_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://cp.servercow.de/client/plugin/support_manager/knowledgebase/view/34/dns-api-v1/7/) lego-4.9.1/docs/content/dns/zz_gen_simply.md000066400000000000000000000035011434020463500210540ustar00rootroot00000000000000--- title: "Simply.com" date: 2019-03-03T16:39:46+01:00 draft: false slug: simply dnsprovider: since: "v4.4.0" code: "simply" url: "https://www.simply.com/en/domains/" --- Configuration for [Simply.com](https://www.simply.com/en/domains/). - Code: `simply` - Since: v4.4.0 Here is an example bash command using the Simply.com provider: ```bash SIMPLY_ACCOUNT_NAME=xxxxxx \ SIMPLY_API_KEY=yyyyyy \ lego --email you@example.com --dns simply --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `SIMPLY_ACCOUNT_NAME` | Account name | | `SIMPLY_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `SIMPLY_HTTP_TIMEOUT` | API request timeout | | `SIMPLY_POLLING_INTERVAL` | Time between DNS propagation check | | `SIMPLY_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `SIMPLY_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.simply.com/en/docs/api/) lego-4.9.1/docs/content/dns/zz_gen_sonic.md000066400000000000000000000050171434020463500206560ustar00rootroot00000000000000--- title: "Sonic" date: 2019-03-03T16:39:46+01:00 draft: false slug: sonic dnsprovider: since: "v4.4.0" code: "sonic" url: "https://www.sonic.com/" --- Configuration for [Sonic](https://www.sonic.com/). - Code: `sonic` - Since: v4.4.0 Here is an example bash command using the Sonic provider: ```bash SONIC_USER_ID=12345 \ SONIC_API_KEY=4d6fbf2f9ab0fa11697470918d37625851fc0c51 \ lego --email you@example.com --dns sonic --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `SONIC_API_KEY` | API Key | | `SONIC_USER_ID` | User ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `SONIC_HTTP_TIMEOUT` | API request timeout | | `SONIC_POLLING_INTERVAL` | Time between DNS propagation check | | `SONIC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `SONIC_SEQUENCE_INTERVAL` | Time between sequential requests | | `SONIC_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## API keys The API keys must be generated by calling the `dyndns/api_key` endpoint. Example: ```bash $ curl -X POST -H "Content-Type: application/json" --data '{"username":"notarealuser","password":"notarealpassword","hostname":"example.com"}' https://public-api.sonic.net/dyndns/api_key {"userid":"12345","apikey":"4d6fbf2f9ab0fa11697470918d37625851fc0c51","result":200,"message":"OK"} ``` See https://public-api.sonic.net/dyndns/#requesting_an_api_key for additional details. This `userid` and `apikey` combo allow modifications to any DNS entries connected to the managed domain (hostname). Hostname should be the toplevel domain managed e.g `example.com` not `www.example.com`. ## More information - [API documentation](https://public-api.sonic.net/dyndns/) lego-4.9.1/docs/content/dns/zz_gen_stackpath.md000066400000000000000000000036021434020463500215230ustar00rootroot00000000000000--- title: "Stackpath" date: 2019-03-03T16:39:46+01:00 draft: false slug: stackpath dnsprovider: since: "v1.1.0" code: "stackpath" url: "https://www.stackpath.com/" --- Configuration for [Stackpath](https://www.stackpath.com/). - Code: `stackpath` - Since: v1.1.0 Here is an example bash command using the Stackpath provider: ```bash STACKPATH_CLIENT_ID=xxxxx \ STACKPATH_CLIENT_SECRET=yyyyy \ STACKPATH_STACK_ID=zzzzz \ lego --email you@example.com --dns stackpath --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `STACKPATH_CLIENT_ID` | Client ID | | `STACKPATH_CLIENT_SECRET` | Client secret | | `STACKPATH_STACK_ID` | Stack ID | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `STACKPATH_POLLING_INTERVAL` | Time between DNS propagation check | | `STACKPATH_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `STACKPATH_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://developer.stackpath.com/en/api/dns/#tag/Zone) lego-4.9.1/docs/content/dns/zz_gen_tencentcloud.md000066400000000000000000000042161434020463500222320ustar00rootroot00000000000000--- title: "Tencent Cloud DNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: tencentcloud dnsprovider: since: "v4.6.0" code: "tencentcloud" url: "https://cloud.tencent.com/product/cns" --- Configuration for [Tencent Cloud DNS](https://cloud.tencent.com/product/cns). - Code: `tencentcloud` - Since: v4.6.0 Here is an example bash command using the Tencent Cloud DNS provider: ```bash TENCENTCLOUD_SECRET_ID=abcdefghijklmnopqrstuvwx \ TENCENTCLOUD_SECRET_KEY=your-secret-key \ lego --email you@example.com --dns tencentcloud --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `TENCENTCLOUD_SECRET_ID` | Access key ID | | `TENCENTCLOUD_SECRET_KEY` | Access Key secret | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `TENCENTCLOUD_HTTP_TIMEOUT` | API request timeout | | `TENCENTCLOUD_POLLING_INTERVAL` | Time between DNS propagation check | | `TENCENTCLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `TENCENTCLOUD_REGION` | Region | | `TENCENTCLOUD_SESSION_TOKEN` | Access Key token | | `TENCENTCLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://cloud.tencent.com/document/product/1427/56153) - [Go client](https://github.com/tencentcloud/tencentcloud-sdk-go) lego-4.9.1/docs/content/dns/zz_gen_transip.md000066400000000000000000000035451434020463500212270ustar00rootroot00000000000000--- title: "TransIP" date: 2019-03-03T16:39:46+01:00 draft: false slug: transip dnsprovider: since: "v2.0.0" code: "transip" url: "https://www.transip.nl/" --- Configuration for [TransIP](https://www.transip.nl/). - Code: `transip` - Since: v2.0.0 Here is an example bash command using the TransIP provider: ```bash TRANSIP_ACCOUNT_NAME = "Account name" \ TRANSIP_PRIVATE_KEY_PATH = "transip.key" \ lego --email you@example.com --dns transip --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `TRANSIP_ACCOUNT_NAME` | Account name | | `TRANSIP_PRIVATE_KEY_PATH` | Private key path | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `TRANSIP_POLLING_INTERVAL` | Time between DNS propagation check | | `TRANSIP_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `TRANSIP_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://api.transip.eu/rest/docs.html) - [Go client](https://github.com/transip/gotransip) lego-4.9.1/docs/content/dns/zz_gen_variomedia.md000066400000000000000000000035631434020463500216670ustar00rootroot00000000000000--- title: "Variomedia" date: 2019-03-03T16:39:46+01:00 draft: false slug: variomedia dnsprovider: since: "v4.8.0" code: "variomedia" url: "https://www.variomedia.de/" --- Configuration for [Variomedia](https://www.variomedia.de/). - Code: `variomedia` - Since: v4.8.0 Here is an example bash command using the Variomedia provider: ```bash VARIOMEDIA_API_TOKEN=xxxx \ lego --email you@example.com --dns variomedia --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `VARIOMEDIA_API_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `DODE_SEQUENCE_INTERVAL` | Time between sequential requests | | `VARIOMEDIA_HTTP_TIMEOUT` | API request timeout | | `VARIOMEDIA_POLLING_INTERVAL` | Time between DNS propagation check | | `VARIOMEDIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `VARIOMEDIA_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://api.variomedia.de/docs/dns-records.html) lego-4.9.1/docs/content/dns/zz_gen_vegadns.md000066400000000000000000000034051434020463500211710ustar00rootroot00000000000000--- title: "VegaDNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: vegadns dnsprovider: since: "v1.1.0" code: "vegadns" url: "https://github.com/shupp/VegaDNS-API" --- Configuration for [VegaDNS](https://github.com/shupp/VegaDNS-API). - Code: `vegadns` - Since: v1.1.0 {{% notice note %}} _Please contribute by adding a CLI example._ {{% /notice %}} ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `SECRET_VEGADNS_KEY` | API key | | `SECRET_VEGADNS_SECRET` | API secret | | `VEGADNS_URL` | API endpoint URL | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `VEGADNS_POLLING_INTERVAL` | Time between DNS propagation check | | `VEGADNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `VEGADNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://github.com/shupp/VegaDNS-API) - [Go client](https://github.com/OpenDNS/vegadns2client) lego-4.9.1/docs/content/dns/zz_gen_vercel.md000066400000000000000000000034561434020463500210300ustar00rootroot00000000000000--- title: "Vercel" date: 2019-03-03T16:39:46+01:00 draft: false slug: vercel dnsprovider: since: "v4.7.0" code: "vercel" url: "https://vercel.com" --- Configuration for [Vercel](https://vercel.com). - Code: `vercel` - Since: v4.7.0 Here is an example bash command using the Vercel provider: ```bash VERCEL_API_TOKEN=xxxxxx \ lego --email you@example.com --dns vercel --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `VERCEL_API_TOKEN` | Authentication token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `VERCEL_HTTP_TIMEOUT` | API request timeout | | `VERCEL_POLLING_INTERVAL` | Time between DNS propagation check | | `VERCEL_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `VERCEL_TEAM_ID` | Team ID (ex: team_xxxxxxxxxxxxxxxxxxxxxxxx) | | `VERCEL_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://vercel.com/docs/rest-api#endpoints/dns) lego-4.9.1/docs/content/dns/zz_gen_versio.md000066400000000000000000000041551434020463500210540ustar00rootroot00000000000000--- title: "Versio.[nl|eu|uk]" date: 2019-03-03T16:39:46+01:00 draft: false slug: versio dnsprovider: since: "v2.7.0" code: "versio" url: "https://www.versio.nl/domeinnamen" --- Configuration for [Versio.[nl|eu|uk]](https://www.versio.nl/domeinnamen). - Code: `versio` - Since: v2.7.0 Here is an example bash command using the Versio.[nl|eu|uk] provider: ```bash VERSIO_USERNAME= \ VERSIO_PASSWORD= \ lego --email you@example.com --dns versio --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `VERSIO_PASSWORD` | Basic authentication password | | `VERSIO_USERNAME` | Basic authentication username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `VERSIO_ENDPOINT` | The endpoint URL of the API Server | | `VERSIO_HTTP_TIMEOUT` | API request timeout | | `VERSIO_POLLING_INTERVAL` | Time between DNS propagation check | | `VERSIO_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `VERSIO_SEQUENCE_INTERVAL` | Time between sequential requests, default 60s | | `VERSIO_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). To test with the sandbox environment set ```VERSIO_ENDPOINT=https://www.versio.nl/testapi/v1/``` ## More information - [API documentation](https://www.versio.nl/RESTapidoc/) lego-4.9.1/docs/content/dns/zz_gen_vinyldns.md000066400000000000000000000042161434020463500214110ustar00rootroot00000000000000--- title: "VinylDNS" date: 2019-03-03T16:39:46+01:00 draft: false slug: vinyldns dnsprovider: since: "v4.4.0" code: "vinyldns" url: "https://www.vinyldns.io" --- Configuration for [VinylDNS](https://www.vinyldns.io). - Code: `vinyldns` - Since: v4.4.0 Here is an example bash command using the VinylDNS provider: ```bash VINYLDNS_ACCESS_KEY=xxxxxx \ VINYLDNS_SECRET_KEY=yyyyy \ VINYLDNS_HOST=https://api.vinyldns.example.org:9443 \ lego --email you@example.com --dns vinyldns --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `VINYLDNS_ACCESS_KEY` | The VinylDNS API key | | `VINYLDNS_HOST` | The VinylDNS API URL | | `VINYLDNS_SECRET_KEY` | The VinylDNS API Secret key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `VINYLDNS_POLLING_INTERVAL` | Time between DNS propagation check | | `VINYLDNS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `VINYLDNS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). The vinyldns integration makes use of dotted hostnames to ease permission management. Users are required to have DELETE ACL level or zone admin permissions on the VinylDNS zone containing the target host. ## More information - [API documentation](https://www.vinyldns.io/api/) - [Go client](https://github.com/vinyldns/go-vinyldns) lego-4.9.1/docs/content/dns/zz_gen_vkcloud.md000066400000000000000000000055231434020463500212140ustar00rootroot00000000000000--- title: "VK Cloud" date: 2019-03-03T16:39:46+01:00 draft: false slug: vkcloud dnsprovider: since: "v4.9.0" code: "vkcloud" url: "https://mcs.mail.ru/" --- Configuration for [VK Cloud](https://mcs.mail.ru/). - Code: `vkcloud` - Since: v4.9.0 Here is an example bash command using the VK Cloud provider: ```bash VK_CLOUD_PROJECT_ID="" \ VK_CLOUD_USERNAME="" \ VK_CLOUD_PASSWORD="" \ lego --email you@example.com --dns vkcloud --domains "example.org" --domains "*.example.org" run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `VK_CLOUD_PASSWORD` | Password for VK Cloud account | | `VK_CLOUD_PROJECT_ID` | String ID of project in VK Cloud | | `VK_CLOUD_USERNAME` | Email of VK Cloud account | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `VK_CLOUD_DNS_ENDPOINT` | URL of DNS API. Defaults to https://mcs.mail.ru/public-dns but can be changed for usage with private clouds | | `VK_CLOUD_DOMAIN_NAME` | Openstack users domain name. Defaults to `users` but can be changed for usage with private clouds | | `VK_CLOUD_IDENTITY_ENDPOINT` | URL of OpenStack Auth API, Defaults to https://infra.mail.ru:35357/v3/ but can be changed for usage with private clouds | | `VK_CLOUD_POLLING_INTERVAL` | Time between DNS propagation check | | `VK_CLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `VK_CLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Credential inforamtion You can find all required and additional information on ["Project/Keys" page](https://mcs.mail.ru/app/en/project/keys) of your cloud. | ENV Variable | Parameter from page | |----------------------------|---------------------| | VK_CLOUD_PROJECT_ID | Project ID | | VK_CLOUD_USERNAME | Username | | VK_CLOUD_DOMAIN_NAME | User Domain Name | | VK_CLOUD_IDENTITY_ENDPOINT | Identity endpoint | ## More information - [API documentation](https://mcs.mail.ru/docs/networks/vnet/networks/publicdns/api) lego-4.9.1/docs/content/dns/zz_gen_vscale.md000066400000000000000000000034401434020463500210160ustar00rootroot00000000000000--- title: "Vscale" date: 2019-03-03T16:39:46+01:00 draft: false slug: vscale dnsprovider: since: "v2.0.0" code: "vscale" url: "https://vscale.io/" --- Configuration for [Vscale](https://vscale.io/). - Code: `vscale` - Since: v2.0.0 Here is an example bash command using the Vscale provider: ```bash VSCALE_API_TOKEN=xxxxx \ lego --email you@example.com --dns vscale --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `VSCALE_API_TOKEN` | API token | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `VSCALE_BASE_URL` | API endpoint URL | | `VSCALE_HTTP_TIMEOUT` | API request timeout | | `VSCALE_POLLING_INTERVAL` | Time between DNS propagation check | | `VSCALE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `VSCALE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://developers.vscale.io/documentation/api/v1/#api-Domains_Records) lego-4.9.1/docs/content/dns/zz_gen_vultr.md000066400000000000000000000033601434020463500207160ustar00rootroot00000000000000--- title: "Vultr" date: 2019-03-03T16:39:46+01:00 draft: false slug: vultr dnsprovider: since: "v0.3.1" code: "vultr" url: "https://www.vultr.com/" --- Configuration for [Vultr](https://www.vultr.com/). - Code: `vultr` - Since: v0.3.1 Here is an example bash command using the Vultr provider: ```bash VULTR_API_KEY=xxxxx \ lego --email you@example.com --dns vultr --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `VULTR_API_KEY` | API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `VULTR_HTTP_TIMEOUT` | API request timeout | | `VULTR_POLLING_INTERVAL` | Time between DNS propagation check | | `VULTR_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `VULTR_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://www.vultr.com/api/#dns) - [Go client](https://github.com/vultr/govultr) lego-4.9.1/docs/content/dns/zz_gen_wedos.md000066400000000000000000000036101434020463500206610ustar00rootroot00000000000000--- title: "WEDOS" date: 2019-03-03T16:39:46+01:00 draft: false slug: wedos dnsprovider: since: "v4.4.0" code: "wedos" url: "https://www.wedos.com" --- Configuration for [WEDOS](https://www.wedos.com). - Code: `wedos` - Since: v4.4.0 Here is an example bash command using the WEDOS provider: ```bash WEDOS_USERNAME=xxxxxxxx \ WEDOS_WAPI_PASSWORD=xxxxxxxx \ lego --email you@example.com --dns wedos --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `WEDOS_USERNAME` | Username is the same as for the admin account | | `WEDOS_WAPI_PASSWORD` | Password needs to be generated and IP allowed in the admin interface | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `WEDOS_HTTP_TIMEOUT` | API request timeout | | `WEDOS_POLLING_INTERVAL` | Time between DNS propagation check | | `WEDOS_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `WEDOS_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://kb.wedos.com/en/kategorie/wapi-api-interface/wdns-en/) lego-4.9.1/docs/content/dns/zz_gen_yandex.md000066400000000000000000000034331434020463500210330ustar00rootroot00000000000000--- title: "Yandex PDD" date: 2019-03-03T16:39:46+01:00 draft: false slug: yandex dnsprovider: since: "v3.7.0" code: "yandex" url: "https://pdd.yandex.com" --- Configuration for [Yandex PDD](https://pdd.yandex.com). - Code: `yandex` - Since: v3.7.0 Here is an example bash command using the Yandex PDD provider: ```bash YANDEX_PDD_TOKEN= \ lego --email you@example.com --dns yandex --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `YANDEX_PDD_TOKEN` | Basic authentication username | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `YANDEX_HTTP_TIMEOUT` | API request timeout | | `YANDEX_POLLING_INTERVAL` | Time between DNS propagation check | | `YANDEX_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `YANDEX_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://yandex.com/dev/domain/doc/concepts/api-dns.html) lego-4.9.1/docs/content/dns/zz_gen_yandexcloud.md000066400000000000000000000055111434020463500220610ustar00rootroot00000000000000--- title: "Yandex Cloud" date: 2019-03-03T16:39:46+01:00 draft: false slug: yandexcloud dnsprovider: since: "v4.9.0" code: "yandexcloud" url: "https://cloud.yandex.com" --- Configuration for [Yandex Cloud](https://cloud.yandex.com). - Code: `yandexcloud` - Since: v4.9.0 Here is an example bash command using the Yandex Cloud provider: ```bash YANDEX_CLOUD_IAM_TOKEN= \ YANDEX_CLOUD_FOLDER_ID= \ lego --email you@example.com --dns yandexcloud --domains "example.org" --domains "*.example.org" run # --- YANDEX_CLOUD_IAM_TOKEN=$(echo '{ \ "id": "", \ "service_account_id": "", \ "created_at": "", \ "key_algorithm": "RSA_2048", \ "public_key": "-----BEGIN PUBLIC KEY----------END PUBLIC KEY-----", \ "private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----" \ }' | base64) \ YANDEX_CLOUD_FOLDER_ID= \ lego --email you@example.com --dns yandexcloud --domains "example.org" --domains "*.example.org" run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `YANDEX_CLOUD_FOLDER_ID` | The string id of folder (aka project) in Yandex Cloud | | `YANDEX_CLOUD_IAM_TOKEN` | The base64 encoded json which contains inforamtion about iam token of serivce account with `dns.admin` permissions | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `YANDEX_CLOUD_POLLING_INTERVAL` | Time between DNS propagation check | | `YANDEX_CLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `YANDEX_CLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## IAM Token The simplest way to retrieve IAM access token is usage of yc-cli, follow [docs](https://cloud.yandex.ru/docs/iam/operations/iam-token/create-for-sa) to get it ```bash yc iam key create --service-account-name my-robot --output key.json cat key.json | base64 ``` ## More information - [API documentation](https://cloud.yandex.com/en/docs/dns/quickstart) lego-4.9.1/docs/content/dns/zz_gen_zoneee.md000066400000000000000000000034521434020463500210310ustar00rootroot00000000000000--- title: "Zone.ee" date: 2019-03-03T16:39:46+01:00 draft: false slug: zoneee dnsprovider: since: "v2.1.0" code: "zoneee" url: "https://www.zone.ee/" --- Configuration for [Zone.ee](https://www.zone.ee/). - Code: `zoneee` - Since: v2.1.0 Here is an example bash command using the Zone.ee provider: ```bash ZONEEE_API_USER=xxxxx \ ZONEEE_API_KEY=yyyyy \ lego --email you@example.com --dns zoneee --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `ZONEEE_API_KEY` | API key | | `ZONEEE_API_USER` | API user | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `ZONEEE_ENDPOINT` | API endpoint URL | | `ZONEEE_HTTP_TIMEOUT` | API request timeout | | `ZONEEE_POLLING_INTERVAL` | Time between DNS propagation check | | `ZONEEE_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `ZONEEE_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://api.zone.eu/v2) lego-4.9.1/docs/content/dns/zz_gen_zonomi.md000066400000000000000000000033661434020463500210630ustar00rootroot00000000000000--- title: "Zonomi" date: 2019-03-03T16:39:46+01:00 draft: false slug: zonomi dnsprovider: since: "v3.5.0" code: "zonomi" url: "https://zonomi.com" --- Configuration for [Zonomi](https://zonomi.com). - Code: `zonomi` - Since: v3.5.0 Here is an example bash command using the Zonomi provider: ```bash ZONOMI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ lego --email you@example.com --dns zonomi --domains my.example.org run ``` ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| | `ZONOMI_API_KEY` | User API key | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| | `ZONOMI_HTTP_TIMEOUT` | API request timeout | | `ZONOMI_POLLING_INTERVAL` | Time between DNS propagation check | | `ZONOMI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | | `ZONOMI_TTL` | The TTL of the TXT record used for the DNS challenge | The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{< ref "dns#configuration-and-credentials" >}}). ## More information - [API documentation](https://zonomi.com/app/dns/dyndns.jsp) lego-4.9.1/docs/content/installation/000077500000000000000000000000001434020463500175575ustar00rootroot00000000000000lego-4.9.1/docs/content/installation/_index.md000066400000000000000000000020261434020463500213470ustar00rootroot00000000000000--- title: "Installation" date: 2019-03-03T16:39:46+01:00 weight: 1 draft: false --- ## Binaries To 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. lego does not assume anything about the location you run it from. ## From Docker ```bash docker run goacme/lego -h ``` ## From package managers - [ArchLinux (AUR)](https://aur.archlinux.org/packages/lego) (official): ```bash yay -S lego ``` - [FreeBSD (Ports)](https://www.freshports.org/security/lego) (unofficial): ```bash cd /usr/ports/security/lego && make install clean ``` or ```bash pkg install lego ``` ## From sources Requirements: - go1.17+ - environment variable: `GO111MODULE=on` To install the latest version from sources, just run: ```bash go install github.com/go-acme/lego/v4/cmd/lego@latest ``` or ```bash git clone git@github.com:go-acme/lego.git cd lego make # tests + doc + build make build # only build ``` lego-4.9.1/docs/content/usage/000077500000000000000000000000001434020463500161625ustar00rootroot00000000000000lego-4.9.1/docs/content/usage/_index.md000066400000000000000000000001761434020463500177560ustar00rootroot00000000000000--- title: "Usage" date: 2019-03-03T16:39:46+01:00 draft: false weight: 2 --- {{% children style="h2" description="true" %}} lego-4.9.1/docs/content/usage/cli/000077500000000000000000000000001434020463500167315ustar00rootroot00000000000000lego-4.9.1/docs/content/usage/cli/General-Instructions.md000066400000000000000000000021111434020463500233250ustar00rootroot00000000000000--- title: General Instructions date: 2019-03-03T16:39:46+01:00 draft: false summary: Read this first to clarify some assumptions made by the following guides. weight: 1 --- These examples assume you have [lego installed]({{< ref "installation" >}}). You can get a pre-built binary from the [releases](https://github.com/go-acme/lego/releases) page. The web server examples require that the `lego` binary has permission to bind to ports 80 and 443. If your environment does not allow you to bind to these ports, please read [Running without root privileges]({{< ref "usage/cli/Options#running-without-root-privileges" >}}) and [Port Usage]({{< ref "usage/cli/Options#port-usage" >}}). Unless otherwise instructed with the `--path` command line flag, lego will look for a directory named `.lego` in the *current working directory*. If you run `cd /dir/a && lego ... run`, lego will create a directory `/dir/a/.lego` where it will save account registration and certificate files into. If you later try to renew a certificate with `cd /dir/b && lego ... renew`, lego will likely produce an error. lego-4.9.1/docs/content/usage/cli/Obtain-a-Certificate.md000066400000000000000000000113401434020463500231240ustar00rootroot00000000000000--- title: Obtain a Certificate date: 2019-03-03T16:39:46+01:00 draft: false weight: 2 --- This guide explains various ways to obtain a new certificate. ## Using the built-in web server Open a terminal, and execute the following command (insert your own email address and domain): ```bash lego --email="you@example.com" --domains="example.com" --http run ``` You will find your certificate in the `.lego` folder of the current working directory: ```console $ ls -1 ./.lego/certificates example.com.crt example.com.issuer.crt example.com.json example.com.key [maybe more files for different domains...] ``` where - `example.com.crt` is the server certificate (including the CA certificate), - `example.com.key` is the private key needed for the server certificate, - `example.com.issuer.crt` is the CA certificate, and - `example.com.json` contains some JSON encoded meta information. For each domain, you will have a set of these four files. For wildcard certificates (`*.example.com`), the filenames will look like `_.example.com.crt`. The `.crt` and `.key` files are PEM-encoded x509 certificates and private keys. If you're looking for a `cert.pem` and `privkey.pem`, you can just use `example.com.crt` and `example.com.key`. ## Using a DNS provider If you can't or don't want to start a web server, you need to use a DNS provider. lego comes with [support for many]({{< ref "dns#dns-providers" >}}) providers, and you need to pick the one where your domain's DNS settings are set up. Typically, this is the registrar where you bought the domain, but in some cases this can be another third-party provider. For this example, let's assume you have setup CloudFlare for your domain. Execute this command: ```bash CLOUDFLARE_EMAIL="you@example.com" \ CLOUDFLARE_API_KEY="yourprivatecloudflareapikey" \ lego --email "you@example.com" --dns cloudflare --domains "example.org" run ``` ## Using a custom certificate signing request (CSR) The first step in the process of obtaining certificates involves creating a signing request. This CSR bundles various information, including the domain name(s) and a public key. By default, lego will hide this step from you, but if you already have a CSR, you can easily reuse it: ```bash lego --email="you@example.com" --http --csr="/path/to/csr.pem" run ``` lego 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. ## Using an existing, running web server If you have an existing server running on port 80, the `--http` option also requires the `--http.webroot` option. This just writes the http-01 challenge token to the given directory in the folder `.well-known/acme-challenge` and does not start a server. The given directory **should** be publicly served as `/` on the domain(s) for the validation to complete. If the given directory is not publicly served you will have to support rewriting the request to the directory; You could also implement a rewrite to rewrite `.well-known/acme-challenge` to the given directory `.well-known/acme-challenge`. You 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 `/.well-known/acme-challenge/` by running something like: ```bash lego --accept-tos --email you@example.com --http --http.webroot /path/to/webroot --domains example.com run ``` ## Running a script afterward You can easily hook into the certificate-obtaining process by providing the path to a script: ```bash lego --email="you@example.com" --domains="example.com" --http run --run-hook="./myscript.sh" ``` Some information is provided through environment variables: - `LEGO_ACCOUNT_EMAIL`: the email of the account. - `LEGO_CERT_DOMAIN`: the main domain of the certificate. - `LEGO_CERT_PATH`: the path of the certificate. - `LEGO_CERT_KEY_PATH`: the path of the certificate key. ### Use case A typical use case is distribute the certificate for other services and reload them if necessary. Since PEM-formatted TLS certificates are understood by many programs, it is relatively simple to use certificates for more than a web server. This example script installs the new certificate for a mail server, and reloads it. Beware: this is just a starting point, error checking is omitted for brevity. ```bash #!/bin/bash # copy certificates to a directory controlled by Postfix postfix_cert_dir="/etc/postfix/certificates" # our Postfix server only handles mail for @example.com domain if [ "$LEGO_CERT_DOMAIN" = "example.com" ]; then install -u postfix -g postfix -m 0644 "$LEGO_CERT_PATH" "$postfix_cert_dir" install -u postfix -g postfix -m 0640 "$LEGO_KEY_PATH" "$postfix_cert_dir" systemctl reload postfix@-service fi ``` lego-4.9.1/docs/content/usage/cli/Options.md000066400000000000000000000222511434020463500207100ustar00rootroot00000000000000--- title: "Options" date: 2019-03-03T16:39:46+01:00 draft: false summary: This page describes various command line options. weight: 4 --- ## Usage {{< tabs >}} {{% tab name="lego --help" %}} ```slim NAME: lego - Let's Encrypt client written in Go USAGE: lego [global options] command [command options] [arguments...] COMMANDS: run Register an account, then create and install a certificate revoke Revoke a certificate renew Renew a certificate dnshelp Shows additional help for the '--dns' global option list Display certificates and accounts information. help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --domains value, -d value Add a domain to the process. Can be specified multiple times. --server value, -s value CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. (default: "https://acme-v02.api.letsencrypt.org/directory") --accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service. (default: false) --email value, -m value Email used for registration and recovery contact. --csr value, -c value Certificate signing request filename, if an external CSR is to be used. --eab Use External Account Binding for account registration. Requires --kid and --hmac. (default: false) --kid value Key identifier from External CA. Used for External Account Binding. --hmac value MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding. --key-type value, -k value Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384. (default: "ec256") --filename value (deprecated) Filename of the generated certificate. --path value Directory to use for storing the data. (default: "./.lego") [$LEGO_PATH] --http Use the HTTP challenge to solve challenges. Can be mixed with other types of challenges. (default: false) --http.port value Set the port and interface to use for HTTP based challenges to listen on.Supported: interface:port or :port. (default: ":80") --http.proxy-header value Validate against this HTTP header when solving HTTP based challenges behind a reverse proxy. (default: "Host") --http.webroot value Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge --http.memcached-host value Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts. --tls Use the TLS challenge to solve challenges. Can be mixed with other types of challenges. (default: false) --tls.port value Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port. (default: ":443") --dns value Solve a DNS challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage. --dns.disable-cp By setting this flag to true, disables the need to wait the propagation of the TXT record to all authoritative name servers. (default: false) --dns.resolvers value Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined. --http-timeout value Set the HTTP timeout value to a specific value in seconds. (default: 0) --dns-timeout value Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries. (default: 10) --pem Generate a .pem file by concatenating the .key and .crt files together. (default: false) --pfx Generate a .pfx (PKCS#12) file by with the .key and .crt and issuer .crt files together. (default: false) --pfx.pass value The password used to encrypt the .pfx (PCKS#12) file. (default: "changeit") --cert.timeout value Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30) --help, -h show help (default: false) --version, -v print the version (default: false) ``` {{% /tab %}} {{% tab name="lego run --help" %}} ```slim NAME: lego run - Register an account, then create and install a certificate USAGE: lego run [command options] [arguments...] OPTIONS: --no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate. (default: false) --must-staple Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego. (default: false) --run-hook value Define a hook. The hook is executed when the certificates are effectively created. --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. --always-deactivate-authorizations value Force the authorizations to be relinquished even if the certificate request was successful. ``` {{% /tab %}} {{% tab name="lego renew --help" %}} ```slim NAME: lego renew - Renew a certificate USAGE: lego renew [command options] [arguments...] OPTIONS: --days value The number of days left on a certificate to renew it. (default: 30) --reuse-key Used to indicate you want to reuse your current private key for the new certificate. (default: false) --no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate. (default: false) --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) --renew-hook value Define a hook. The hook is executed only when the certificates are effectively renewed. --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. --always-deactivate-authorizations value Force the authorizations to be relinquished even if the certificate request was successful. --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) ``` {{% /tab %}} {{< /tabs >}} When using the standard `--path` option, all certificates and account configurations are saved to a folder `.lego` in the current working directory. ## Let's Encrypt ACME server lego defaults to communicating with the production Let's Encrypt ACME server. If you'd like to test something without issuing real certificates, consider using the staging endpoint instead: ```bash lego --server=https://acme-staging-v02.api.letsencrypt.org/directory … ``` ## Running without root privileges The CLI does not require root permissions but needs to bind to port 80 and 443 for certain challenges. To run the CLI without `sudo`, you have four options: - Use `setcap 'cap_net_bind_service=+ep' /path/to/lego` (Linux only) - 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)). - 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. - Pass the `--dns` option and specify a DNS provider. ## Port Usage By default lego assumes it is able to bind to ports 80 and 443 to solve challenges. If this is not possible in your environment, you can use the `--http.port` and `--tls.port` options to instruct lego to listen on that interface:port for any incoming challenges. If you are using this option, make sure you proxy all of the following traffic to these ports. **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] **TLS Port:** All TLS handshakes on port **443** for the TLS-ALPN challenge. This traffic redirection is only needed as long as lego solves challenges. As soon as you have received your certificates you can deactivate the forwarding. [^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`. lego-4.9.1/docs/content/usage/cli/Renew-a-Certificate.md000066400000000000000000000072511434020463500227760ustar00rootroot00000000000000--- title: Renew a Certificate date: 2019-03-03T16:39:46+01:00 draft: false weight: 3 --- This guide describes how to renew existing certificates. Certificates issues by Let's Encrypt are valid for a period of 90 days. To avoid certificate errors, you need to ensure that you renew your certificate *before* it expires. In order to renew a certificate, follow the general instructions laid out under [Obtain a Certificate]({{< ref "usage/cli/Obtain-a-Certificate" >}}), and replace `lego ... run` with `lego ... renew`. Note that the `renew` sub-command supports a slightly different set of some command line flags. ## Using the built-in web server By default, and following best practices, a certificate is only renewed if its expiry date is less than 30 days in the future. ```bash lego --email="you@example.com" --domains="example.com" --http renew ``` If the certificate needs to renewed earlier, you can specify the number of remaining days: ```bash lego --email="you@example.com" --domains="example.com" --http renew --days 45 ``` ## Using a DNS provider If you can't or don't want to start a web server, you need to use a DNS provider. lego comes with [support for many]({{< ref "dns#dns-providers" >}}) providers, and you need to pick the one where your domain's DNS settings are set up. Typically, this is the registrar where you bought the domain, but in some cases this can be another third-party provider. For this example, let's assume you have setup CloudFlare for your domain. Execute this command: ```bash CLOUDFLARE_EMAIL="you@example.com" \ CLOUDFLARE_API_KEY="yourprivatecloudflareapikey" \ lego --email "you@example.com" --dns cloudflare --domains "example.org" renew ``` ## Running a script afterward You can easily hook into the certificate-obtaining process by providing the path to a script. The hook is executed only when the certificates are effectively renewed. ```bash lego --email="you@example.com" --domains="example.com" --http renew --renew-hook="./myscript.sh" ``` Some information is provided through environment variables: - `LEGO_ACCOUNT_EMAIL`: the email of the account. - `LEGO_CERT_DOMAIN`: the main domain of the certificate. - `LEGO_CERT_PATH`: the path of the certificate. - `LEGO_CERT_KEY_PATH`: the path of the certificate key. See [Obtain a Certificate → Use case]({{< ref "usage/cli/Obtain-a-Certificate#use-case" >}}) for an example script. ## Automatic renewal It is tempting to create a cron job (or systemd timer) to automatically renew all you certificates. When doing so, please note that some cron defaults will cause measurable load on the ACME provider's infrastructure. Notably `@daily` jobs run at midnight. To 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] Since v4.8.0, lego will pause for up to 8 minutes to help spread the load. You can help further, by adjusting your crontab entry, like so: ```ruby # avoid: #@daily /usr/bin/lego ... renew #@midnight /usr/bin/lego ... renew #0 0 * * * /usr/bin/lego ... renew # instead, use a randomly chosen time: 3 35 * * * /usr/bin/lego ... renew ``` If you use systemd timers, consider doing something similar, and/or introduce a `RandomizedDelaySec`: ```ini [Unit] Description=Renew certificates [Timer] Persistent=true # avoid: #OnCalendar=*-*-* 00:00:00 #OnCalendar=daily # instead, use a randomly chosen time: OnCalendar=*-*-* 3:35 # add extra delay, here up to 1 hour: RandomizedDelaySec=1h [Install] WantedBy=timers.target ``` [^loadspikes]: See [Github issue #1656](https://github.com/go-acme/lego/issues/1656) for an excellent problem description. lego-4.9.1/docs/content/usage/cli/_index.md000066400000000000000000000002321434020463500205160ustar00rootroot00000000000000--- title: "CLI" date: 2019-03-03T16:39:46+01:00 draft: false --- Lego can be use as a CLI. {{% children style="h2" description="true" %}} lego-4.9.1/docs/content/usage/cli/examples.md000066400000000000000000000015521434020463500210740ustar00rootroot00000000000000--- title: Examples date: 2019-03-03T16:39:46+01:00 draft: false hidden: true --- {{% notice note %}} **Heads up!** We've restructured the content a bit. {{% /notice %}} You'll find the content now at one of these pages: - Guide: [**How to obtain a certificate**]({{< ref "usage/cli/Obtain-a-Certificate" >}}) - Using the built-in web server - Using a DNS provider - Using a custom certificate signing request (CSR) - Using an existing, running web server - Running a script afterward - Use case - Guide: [**How to renew a certificate**]({{< ref "usage/cli/Renew-a-Certificate" >}}) - Using the built-in web server - Using a DNS provider - Running a script afterward - Automatic renewal - Reference: [**Command line options**]({{< ref "usage/cli/Options" >}}) - Usage - Let's Encrypt ACME server - Running without root privileges - Port Usage lego-4.9.1/docs/content/usage/library/000077500000000000000000000000001434020463500176265ustar00rootroot00000000000000lego-4.9.1/docs/content/usage/library/Writing-a-Challenge-Solver.md000066400000000000000000000102361434020463500251430ustar00rootroot00000000000000--- title: "Writing a Challenge Solver" date: 2019-03-03T16:39:46+01:00 draft: false --- Lego can solve multiple ACME challenge types out of the box, but sometimes you have custom requirements. For 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). The DNS-01 challenge is advantageous when other challenge types are impossible. For 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. But even if using HTTP-01 or TLS-ALPN-01 challenges, you may have specific needs that lego does not consider by default. You can write something called a `challenge.Provider` that implements [this interface](https://pkg.go.dev/github.com/go-acme/lego/v4/challenge#Provider): ```go type Provider interface { Present(domain, token, keyAuth string) error CleanUp(domain, token, keyAuth string) error } ``` This provides the means to solve a challenge. First 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. ## Writing a challenge.Provider Pretend we want to write our own DNS-01 challenge provider (other challenge types have different requirements but the same principles apply). This 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. BestDNS has an API that, given an authentication token, allows us to manipulate DNS records. This simplistic example has only one field to store the auth token, but in reality you may need to keep more state. ```go type DNSProviderBestDNS struct { apiAuthToken string } ``` We should provide a constructor that returns a *pointer* to the `struct`. This is important in case we need to maintain state in the `struct`. ```go func NewDNSProviderBestDNS(apiAuthToken string) (*DNSProviderBestDNS, error) { return &DNSProviderBestDNS{apiAuthToken: apiAuthToken}, nil } ``` Now we need to implement the interface. We'll start with the `Present` method. You'll be passed the `domain` name for which you're proving ownership, a `token`, and a `keyAuth` string. How your provider uses `token` and `keyAuth`, or if you even use them at all, depends on the challenge type. For DNS-01, we'll just use `domain` and `keyAuth`. ```go func (d *DNSProviderBestDNS) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) // make API request to set a TXT record on fqdn with value and TTL return nil } ``` After calling `dns01.GetRecord(domain, keyAuth)`, we now have the information we need to make our API request and set the TXT record: - `fqdn` is the fully qualified domain name on which to set the TXT record. - `value` is the record's value to set on the record. So then you make an API request to the DNS service according to their docs. Once the TXT record is set on the domain, you may return and the challenge will proceed. The 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. In our case, we want to remove the TXT record we just created. ```go func (d *DNSProviderBestDNS) CleanUp(domain, token, keyAuth string) error { // clean up any state you created in Present, like removing the TXT record } ``` In 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. ## Using your new challenge.Provider To 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". In our case: ```go bestDNS, err := NewDNSProviderBestDNS("my-auth-token") if err != nil { return err } client.Challenge.SetDNS01Provider(bestDNS) ``` Then, 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. That's really all there is to it. Go make awesome things! lego-4.9.1/docs/content/usage/library/_index.md000066400000000000000000000053671434020463500214310ustar00rootroot00000000000000--- title: "Library" date: 2019-03-03T16:39:46+01:00 draft: false --- Lego can be use as a Go Library. ## GoDoc The GoDoc can be found here: [GoDoc](https://pkg.go.dev/mod/github.com/go-acme/lego/v4). ## Usage A valid, but bare-bones example use of the acme package: ```go package main import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "fmt" "log" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/registration" ) // You'll need a user or account type that implements acme.User type MyUser struct { Email string Registration *registration.Resource key crypto.PrivateKey } func (u *MyUser) GetEmail() string { return u.Email } func (u MyUser) GetRegistration() *registration.Resource { return u.Registration } func (u *MyUser) GetPrivateKey() crypto.PrivateKey { return u.key } func main() { // Create a user. New accounts need an email and private key to start. privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { log.Fatal(err) } myUser := MyUser{ Email: "you@yours.com", key: privateKey, } config := lego.NewConfig(&myUser) // This CA URL is configured for a local dev instance of Boulder running in Docker in a VM. config.CADirURL = "http://192.168.99.100:4000/directory" config.Certificate.KeyType = certcrypto.RSA2048 // A client facilitates communication with the CA server. client, err := lego.NewClient(config) if err != nil { log.Fatal(err) } // We specify an HTTP port of 5002 and an TLS port of 5001 on all interfaces // because we aren't running as root and can't bind a listener to port 80 and 443 // (used later when we attempt to pass challenges). Keep in mind that you still // need to proxy challenge traffic to port 5002 and 5001. err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "5002")) if err != nil { log.Fatal(err) } err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", "5001")) if err != nil { log.Fatal(err) } // New users will need to register reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) if err != nil { log.Fatal(err) } myUser.Registration = reg request := certificate.ObtainRequest{ Domains: []string{"mydomain.com"}, Bundle: true, } certificates, err := client.Certificate.Obtain(request) if err != nil { log.Fatal(err) } // Each certificate comes back with the cert bytes, the bytes of the client's // private key, and a certificate URL. SAVE THESE TO DISK. fmt.Printf("%#v\n", certificates) // ... all done. } ``` lego-4.9.1/docs/layouts/000077500000000000000000000000001434020463500151045ustar00rootroot00000000000000lego-4.9.1/docs/layouts/partials/000077500000000000000000000000001434020463500167235ustar00rootroot00000000000000lego-4.9.1/docs/layouts/partials/logo.html000066400000000000000000000001371434020463500205520ustar00rootroot00000000000000 lego-4.9.1/docs/layouts/shortcodes/000077500000000000000000000000001434020463500172615ustar00rootroot00000000000000lego-4.9.1/docs/layouts/shortcodes/tableofdnsproviders.html000066400000000000000000000014051434020463500242260ustar00rootroot00000000000000{{ $_hugo_config := `{ "version": 1 }` }} {{- range .Site.AllPages.ByWeight -}} {{- if .Params.dnsprovider -}} {{- $params := .Params.dnsprovider -}} {{ end }} {{ end }}
Provider name CLI flag name Required lego version
{{ .Title }} {{- if $params.url -}} Website {{- end -}} {{ $params.code }} {{ $params.since }}
lego-4.9.1/docs/static/000077500000000000000000000000001434020463500146735ustar00rootroot00000000000000lego-4.9.1/docs/static/css/000077500000000000000000000000001434020463500154635ustar00rootroot00000000000000lego-4.9.1/docs/static/css/theme-custom.css000066400000000000000000000001301434020463500206010ustar00rootroot00000000000000#top-bar-sticky-wrapper, #top-bar, #body-inner { max-width: 72em; margin: 0 auto; } lego-4.9.1/docs/static/images/000077500000000000000000000000001434020463500161405ustar00rootroot00000000000000lego-4.9.1/docs/static/images/lego-logo-white.min.svg000066400000000000000000000064211434020463500224500ustar00rootroot00000000000000 lego-4.9.1/docs/static/images/lego-logo.min.svg000066400000000000000000000066101434020463500213320ustar00rootroot00000000000000 lego-4.9.1/e2e/000077500000000000000000000000001434020463500131275ustar00rootroot00000000000000lego-4.9.1/e2e/challenges_test.go000066400000000000000000000247331434020463500166330ustar00rootroot00000000000000package e2e import ( "crypto" "crypto/rand" "crypto/rsa" "crypto/x509" "fmt" "os" "testing" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/go-acme/lego/v4/e2e/loader" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/registration" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var load = loader.EnvLoader{ PebbleOptions: &loader.CmdOption{ HealthCheckURL: "https://localhost:14000/dir", Args: []string{"-strict", "-config", "fixtures/pebble-config.json"}, Env: []string{"PEBBLE_VA_NOSLEEP=1", "PEBBLE_WFE_NONCEREJECT=20"}, }, LegoOptions: []string{ "LEGO_CA_CERTIFICATES=./fixtures/certs/pebble.minica.pem", }, } func TestMain(m *testing.M) { os.Exit(load.MainTest(m)) } func TestHelp(t *testing.T) { output, err := load.RunLego("-h") if err != nil { fmt.Fprintf(os.Stderr, "%s\n", output) t.Fatal(err) } fmt.Fprintf(os.Stdout, "%s\n", output) } func TestChallengeHTTP_Run(t *testing.T) { loader.CleanLegoFiles() output, err := load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", "-d", "acme.wtf", "--http", "--http.port", ":5002", "run") if len(output) > 0 { fmt.Fprintf(os.Stdout, "%s\n", output) } if err != nil { t.Fatal(err) } } func TestChallengeTLS_Run_Domains(t *testing.T) { loader.CleanLegoFiles() output, err := load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", "-d", "acme.wtf", "--tls", "--tls.port", ":5001", "run") if len(output) > 0 { fmt.Fprintf(os.Stdout, "%s\n", output) } if err != nil { t.Fatal(err) } } func TestChallengeTLS_Run_CSR(t *testing.T) { loader.CleanLegoFiles() output, err := load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", "-csr", "./fixtures/csr.raw", "--tls", "--tls.port", ":5001", "run") if len(output) > 0 { fmt.Fprintf(os.Stdout, "%s\n", output) } if err != nil { t.Fatal(err) } } func TestChallengeTLS_Run_CSR_PEM(t *testing.T) { loader.CleanLegoFiles() output, err := load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", "-csr", "./fixtures/csr.cert", "--tls", "--tls.port", ":5001", "run") if len(output) > 0 { fmt.Fprintf(os.Stdout, "%s\n", output) } if err != nil { t.Fatal(err) } } func TestChallengeTLS_Run_Revoke(t *testing.T) { loader.CleanLegoFiles() output, err := load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", "-d", "lego.wtf", "-d", "acme.lego.wtf", "--tls", "--tls.port", ":5001", "run") if len(output) > 0 { fmt.Fprintf(os.Stdout, "%s\n", output) } if err != nil { t.Fatal(err) } output, err = load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", "-d", "lego.wtf", "--tls", "--tls.port", ":5001", "revoke") if len(output) > 0 { fmt.Fprintf(os.Stdout, "%s\n", output) } if err != nil { t.Fatal(err) } } func TestChallengeTLS_Run_Revoke_Non_ASCII(t *testing.T) { loader.CleanLegoFiles() output, err := load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", "-d", "légô.wtf", "--tls", "--tls.port", ":5001", "run") if len(output) > 0 { fmt.Fprintf(os.Stdout, "%s\n", output) } if err != nil { t.Fatal(err) } output, err = load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", "-s", "https://localhost:14000/dir", "-d", "légô.wtf", "--tls", "--tls.port", ":5001", "revoke") if len(output) > 0 { fmt.Fprintf(os.Stdout, "%s\n", output) } if err != nil { t.Fatal(err) } } func TestChallengeHTTP_Client_Obtain(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") require.NoError(t, err) defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() privateKey, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") user := &fakeUser{privateKey: privateKey} config := lego.NewConfig(user) config.CADirURL = load.PebbleOptions.HealthCheckURL client, err := lego.NewClient(config) require.NoError(t, err) err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "5002")) require.NoError(t, err) reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) require.NoError(t, err) user.registration = reg request := certificate.ObtainRequest{ Domains: []string{"acme.wtf"}, Bundle: true, } resource, err := client.Certificate.Obtain(request) require.NoError(t, err) require.NotNil(t, resource) assert.Equal(t, "acme.wtf", resource.Domain) assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL) assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL) assert.NotEmpty(t, resource.Certificate) assert.NotEmpty(t, resource.IssuerCertificate) assert.Empty(t, resource.CSR) } func TestChallengeHTTP_Client_Registration_QueryRegistration(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") require.NoError(t, err) defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() privateKey, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") user := &fakeUser{privateKey: privateKey} config := lego.NewConfig(user) config.CADirURL = load.PebbleOptions.HealthCheckURL client, err := lego.NewClient(config) require.NoError(t, err) err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "5002")) require.NoError(t, err) reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) require.NoError(t, err) user.registration = reg resource, err := client.Registration.QueryRegistration() require.NoError(t, err) require.NotNil(t, resource) assert.Equal(t, "valid", resource.Body.Status) assert.Regexp(t, `https://localhost:14000/list-orderz/[\w\d]+`, resource.Body.Orders) assert.Regexp(t, `https://localhost:14000/my-account/[\w\d]+`, resource.URI) } func TestChallengeTLS_Client_Obtain(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") require.NoError(t, err) defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() privateKey, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") user := &fakeUser{privateKey: privateKey} config := lego.NewConfig(user) config.CADirURL = load.PebbleOptions.HealthCheckURL client, err := lego.NewClient(config) require.NoError(t, err) err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", "5001")) require.NoError(t, err) reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) require.NoError(t, err) user.registration = reg // https://github.com/letsencrypt/pebble/issues/285 privateKeyCSR, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") request := certificate.ObtainRequest{ Domains: []string{"acme.wtf"}, Bundle: true, PrivateKey: privateKeyCSR, } resource, err := client.Certificate.Obtain(request) require.NoError(t, err) require.NotNil(t, resource) assert.Equal(t, "acme.wtf", resource.Domain) assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL) assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL) assert.NotEmpty(t, resource.Certificate) assert.NotEmpty(t, resource.IssuerCertificate) assert.Empty(t, resource.CSR) } func TestChallengeTLS_Client_ObtainForCSR(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") require.NoError(t, err) defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() privateKey, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") user := &fakeUser{privateKey: privateKey} config := lego.NewConfig(user) config.CADirURL = load.PebbleOptions.HealthCheckURL client, err := lego.NewClient(config) require.NoError(t, err) err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", "5001")) require.NoError(t, err) reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) require.NoError(t, err) user.registration = reg csrRaw, err := os.ReadFile("./fixtures/csr.raw") require.NoError(t, err) csr, err := x509.ParseCertificateRequest(csrRaw) require.NoError(t, err) resource, err := client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{ CSR: csr, Bundle: true, }) require.NoError(t, err) require.NotNil(t, resource) assert.Equal(t, "acme.wtf", resource.Domain) assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL) assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL) assert.NotEmpty(t, resource.Certificate) assert.NotEmpty(t, resource.IssuerCertificate) assert.NotEmpty(t, resource.CSR) } func TestRegistrar_UpdateAccount(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") require.NoError(t, err) defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() privateKey, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") user := &fakeUser{ privateKey: privateKey, email: "foo@example.com", } config := lego.NewConfig(user) config.CADirURL = load.PebbleOptions.HealthCheckURL client, err := lego.NewClient(config) require.NoError(t, err) regOptions := registration.RegisterOptions{TermsOfServiceAgreed: true} reg, err := client.Registration.Register(regOptions) require.NoError(t, err) require.Equal(t, []string{"mailto:foo@example.com"}, reg.Body.Contact) user.registration = reg user.email = "bar@example.com" resource, err := client.Registration.UpdateRegistration(regOptions) require.NoError(t, err) require.Equal(t, []string{"mailto:bar@example.com"}, resource.Body.Contact) require.Equal(t, reg.URI, resource.URI) } type fakeUser struct { email string privateKey crypto.PrivateKey registration *registration.Resource } func (f *fakeUser) GetEmail() string { return f.email } func (f *fakeUser) GetRegistration() *registration.Resource { return f.registration } func (f *fakeUser) GetPrivateKey() crypto.PrivateKey { return f.privateKey } lego-4.9.1/e2e/dnschallenge/000077500000000000000000000000001434020463500155565ustar00rootroot00000000000000lego-4.9.1/e2e/dnschallenge/dns_challenges_test.go000066400000000000000000000074571434020463500221320ustar00rootroot00000000000000package dnschallenge import ( "crypto" "crypto/rand" "crypto/rsa" "fmt" "os" "testing" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/e2e/loader" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/providers/dns" "github.com/go-acme/lego/v4/registration" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var load = loader.EnvLoader{ PebbleOptions: &loader.CmdOption{ HealthCheckURL: "https://localhost:15000/dir", Args: []string{"-strict", "-config", "fixtures/pebble-config-dns.json", "-dnsserver", "localhost:8053"}, Env: []string{"PEBBLE_VA_NOSLEEP=1", "PEBBLE_WFE_NONCEREJECT=20"}, Dir: "../", }, LegoOptions: []string{ "LEGO_CA_CERTIFICATES=../fixtures/certs/pebble.minica.pem", "EXEC_PATH=../fixtures/update-dns.sh", }, ChallSrv: &loader.CmdOption{ Args: []string{"-http01", ":5012", "-tlsalpn01", ":5011"}, }, } func TestMain(m *testing.M) { os.Exit(load.MainTest(m)) } func TestDNSHelp(t *testing.T) { output, err := load.RunLego("dnshelp") if err != nil { fmt.Fprintf(os.Stderr, "%s\n", output) t.Fatal(err) } fmt.Fprintf(os.Stdout, "%s\n", output) } func TestChallengeDNS_Run(t *testing.T) { loader.CleanLegoFiles() output, err := load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", "--dns", "exec", "--dns.resolvers", ":8053", "--dns.disable-cp", "-s", "https://localhost:15000/dir", "-d", "*.légo.acme", "-d", "légo.acme", "run") if len(output) > 0 { fmt.Fprintf(os.Stdout, "%s\n", output) } if err != nil { t.Fatal(err) } } func TestChallengeDNS_Client_Obtain(t *testing.T) { err := os.Setenv("LEGO_CA_CERTIFICATES", "../fixtures/certs/pebble.minica.pem") require.NoError(t, err) defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() err = os.Setenv("EXEC_PATH", "../fixtures/update-dns.sh") require.NoError(t, err) defer func() { _ = os.Unsetenv("EXEC_PATH") }() privateKey, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") user := &fakeUser{privateKey: privateKey} config := lego.NewConfig(user) config.CADirURL = "https://localhost:15000/dir" client, err := lego.NewClient(config) require.NoError(t, err) provider, err := dns.NewDNSChallengeProviderByName("exec") require.NoError(t, err) err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers([]string{":8053"}), dns01.DisableCompletePropagationRequirement()) require.NoError(t, err) reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) require.NoError(t, err) user.registration = reg domains := []string{"*.légo.acme", "légo.acme"} // https://github.com/letsencrypt/pebble/issues/285 privateKeyCSR, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "Could not generate test key") request := certificate.ObtainRequest{ Domains: domains, Bundle: true, PrivateKey: privateKeyCSR, } resource, err := client.Certificate.Obtain(request) require.NoError(t, err) require.NotNil(t, resource) assert.Equal(t, "*.xn--lgo-bma.acme", resource.Domain) assert.Regexp(t, `https://localhost:15000/certZ/[\w\d]{14,}`, resource.CertURL) assert.Regexp(t, `https://localhost:15000/certZ/[\w\d]{14,}`, resource.CertStableURL) assert.NotEmpty(t, resource.Certificate) assert.NotEmpty(t, resource.IssuerCertificate) assert.Empty(t, resource.CSR) } type fakeUser struct { email string privateKey crypto.PrivateKey registration *registration.Resource } func (f *fakeUser) GetEmail() string { return f.email } func (f *fakeUser) GetRegistration() *registration.Resource { return f.registration } func (f *fakeUser) GetPrivateKey() crypto.PrivateKey { return f.privateKey } lego-4.9.1/e2e/fixtures/000077500000000000000000000000001434020463500150005ustar00rootroot00000000000000lego-4.9.1/e2e/fixtures/certs/000077500000000000000000000000001434020463500161205ustar00rootroot00000000000000lego-4.9.1/e2e/fixtures/certs/README.md000066400000000000000000000020471434020463500174020ustar00rootroot00000000000000# certs/ This directory contains a CA certificate (`pebble.minica.pem`) and a private key (`pebble.minica.key.pem`) that are used to issue a end-entity certificate (See `certs/localhost`) for the Pebble HTTPS server. To get your **testing code** to use Pebble without HTTPS errors you should configure your ACME client to trust the `pebble.minica.pem` CA certificate. Your ACME client should offer a runtime option to specify a list of root CAs that you can configure to include the `pebble.minica.pem` file. **Do not** add this CA certificate to the system trust store or in production code!!! The CA's private key is **public** and anyone can use it to issue certificates that will be trusted by a system with the Pebble CA in the trust store. To re-create all of the Pebble certificates run: minica -ca-cert pebble.minica.pem \ -ca-key pebble.minica.key.pem \ -domains localhost,pebble \ -ip-addresses 127.0.0.1 From the `test/certs/` directory after [installing MiniCA](https://github.com/jsha/minica#installation) lego-4.9.1/e2e/fixtures/certs/localhost/000077500000000000000000000000001434020463500201105ustar00rootroot00000000000000lego-4.9.1/e2e/fixtures/certs/localhost/README.md000066400000000000000000000003511434020463500213660ustar00rootroot00000000000000# certs/localhost This directory contains an end-entity (leaf) certificate (`cert.pem`) and a private key (`key.pem`) for the Pebble HTTPS server. It includes `127.0.0.1` as an IP address SAN, and `[localhost, pebble]` as DNS SANs. lego-4.9.1/e2e/fixtures/certs/localhost/cert.pem000066400000000000000000000021631434020463500215520ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDGzCCAgOgAwIBAgIIbEfayDFsBtwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMDcx MjA2MTk0MjEwWjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQCbFMW3DXXdErvQf2lCZ0qz0DGEWadDoF0O2neM5mVa VQ7QGW0xc5Qwvn3Tl62C0JtwLpF0pG2BICIN+DHdVaIUwkf77iBS2doH1I3waE1I 8GkV9JrYmFY+j0dA1SwBmqUZNXhLNwZGq1a91nFSI59DZNy/JciqxoPX2K++ojU2 FPpuXe2t51NmXMsszpa+TDqF/IeskA9A/ws6UIh4Mzhghx7oay2/qqj2IIPjAmJj i73kdUvtEry3wmlkBvtVH50+FscS9WmPC5h3lDTk5nbzSAXKuFusotuqy3XTgY5B PiRAwkZbEY43JNfqenQPHo7mNTt29i+NVVrBsnAa5ovrAgMBAAGjYzBhMA4GA1Ud DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T AQH/BAIwADAiBgNVHREEGzAZgglsb2NhbGhvc3SCBnBlYmJsZYcEfwAAATANBgkq hkiG9w0BAQsFAAOCAQEAYIkXff8H28KS0KyLHtbbSOGU4sujHHVwiVXSATACsNAE D0Qa8hdtTQ6AUqA6/n8/u1tk0O4rPE/cTpsM3IJFX9S3rZMRsguBP7BSr1Lq/XAB 7JP/CNHt+Z9aKCKcg11wIX9/B9F7pyKM3TdKgOpqXGV6TMuLjg5PlYWI/07lVGFW /mSJDRs8bSCFmbRtEqc4lpwlrpz+kTTnX6G7JDLfLWYw/xXVqwFfdengcDTHCc8K wtgGq/Gu6vcoBxIO3jaca+OIkMfxxXmGrcNdseuUCa3RMZ8Qy03DqGu6Y6XQyK4B W8zIG6H9SVKkAznM2yfYhW8v2ktcaZ95/OBHY97ZIw== -----END CERTIFICATE----- lego-4.9.1/e2e/fixtures/certs/localhost/key.pem000066400000000000000000000032131434020463500214020ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAmxTFtw113RK70H9pQmdKs9AxhFmnQ6BdDtp3jOZlWlUO0Blt MXOUML5905etgtCbcC6RdKRtgSAiDfgx3VWiFMJH++4gUtnaB9SN8GhNSPBpFfSa 2JhWPo9HQNUsAZqlGTV4SzcGRqtWvdZxUiOfQ2TcvyXIqsaD19ivvqI1NhT6bl3t redTZlzLLM6Wvkw6hfyHrJAPQP8LOlCIeDM4YIce6Gstv6qo9iCD4wJiY4u95HVL 7RK8t8JpZAb7VR+dPhbHEvVpjwuYd5Q05OZ280gFyrhbrKLbqst104GOQT4kQMJG WxGONyTX6np0Dx6O5jU7dvYvjVVawbJwGuaL6wIDAQABAoIBAGW9W/S6lO+DIcoo PHL+9sg+tq2gb5ZzN3nOI45BfI6lrMEjXTqLG9ZasovFP2TJ3J/dPTnrwZdr8Et/ 357YViwORVFnKLeSCnMGpFPq6YEHj7mCrq+YSURjlRhYgbVPsi52oMOfhrOIJrEG ZXPAwPRi0Ftqu1omQEqz8qA7JHOkjB2p0i2Xc/uOSJccCmUDMlksRYz8zFe8wHuD XvUL2k23n2pBZ6wiez6Xjr0wUQ4ESI02x7PmYgA3aqF2Q6ECDwHhjVeQmAuypMF6 IaTjIJkWdZCW96pPaK1t+5nTNZ+Mg7tpJ/PRE4BkJvqcfHEOOl6wAE8gSk5uVApY ZRKGmGkCgYEAzF9iRXYo7A/UphL11bR0gqxB6qnQl54iLhqS/E6CVNcmwJ2d9pF8 5HTfSo1/lOXT3hGV8gizN2S5RmWBrc9HBZ+dNrVo7FYeeBiHu+opbX1X/C1HC0m1 wJNsyoXeqD1OFc1WbDpHz5iv4IOXzYdOdKiYEcTv5JkqE7jomqBLQk8CgYEAwkG/ rnwr4ThUo/DG5oH+l0LVnHkrJY+BUSI33g3eQ3eM0MSbfJXGT7snh5puJW0oXP7Z Gw88nK3Vnz2nTPesiwtO2OkUVgrIgWryIvKHaqrYnapZHuM+io30jbZOVaVTMR9c X/7/d5/evwXuP7p2DIdZKQKKFgROm1XnhNqVgaUCgYBD/ogHbCR5RVsOVciMbRlG UGEt3YmUp/vfMuAsKUKbT2mJM+dWHVlb+LZBa4pC06QFgfxNJi/aAhzSGvtmBEww xsXbaceauZwxgJfIIUPfNZCMSdQVIVTi2Smcx6UofBz6i/Jw14MEwlvhamaa7qVf kqflYYwelga1wRNCPopLaQKBgQCWsZqZKQqBNMm0Q9yIhN+TR+2d7QFjqeePoRPl 1qxNejhq25ojE607vNv1ff9kWUGuoqSZMUC76r6FQba/JoNbefI4otd7x/GzM9uS 8MHMJazU4okwROkHYwgLxxkNp6rZuJJYheB4VDTfyyH/ng5lubmY7rdgTQcNyZ5I majRYQKBgAMKJ3RlII0qvAfNFZr4Y2bNIq+60Z+Qu2W5xokIHCFNly3W1XDDKGFe CCPHSvQljinke3P9gPt2HVdXxcnku9VkTti+JygxuLkVg7E0/SWwrWfGsaMJs+84 fK+mTZay2d3v24r9WKEKwLykngYPyZw5+BdWU0E+xx5lGUd3U4gG -----END RSA PRIVATE KEY----- lego-4.9.1/e2e/fixtures/certs/pebble.minica.key.pem000066400000000000000000000032171434020463500221050ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAuVoGTaFSWp3Y+N5JC8lOdL8wmWpaM73UaNzhYiqA7ZqijzVk TTtoQvQFDcUwyXKOdWHONrv1ld3z224Us504jjlbZwI5uoquCOZ2WJbRhmXrRgzk Fq+/MtoFmPkhtO/DLjjtocgyIirVXN8Yl2APvB5brvRfCm6kktYeecsWfW/O3ikf gdM7tmocwQiBypiloHOjdd5e2g8cWNw+rqvILSUVNLaLpsi23cxnLqVb424wz9dZ 5dO0REg1gSxtf4N5LSb6iGuAVoFNhzIeKzQ+svDg9x8tx/DGOghJS/jDgmxSY1qo bTsXhcmWVfat5GJ5PQgLkCSjBBrjeBlOrc4VtQIDAQABAoIBAQCAoRoou6C0ZEDU DScyN8TrvlcS0LzClaWYFFmRT5/jxOG1cr8l3elwNXpgYQ2Hb6mvim2ajHxVQg/e oxlYwO4jvWhSJzg63c0DPjS5LAlCNO6+0Wlk2RheSPGDhLlAoPeZ10YKdS1dis5B Qk4Fl1O0IHlOBCcEzV4GzPOfYDI+X6/f4xY7qz1s+CgoIxjIeiG+1/WpZQpYhobY 7CfSDdYDKtksXi7iQkc5earUAHBqZ1gQTq6e5LVm9AjRzENhMctFgcPs5zOjp2ak PluixrA8LTAfu9wQzvxDkPl0UarZVxCerw6nlAziILpQ+U6PtoPZj49VpntTc+cq 1qjzkbhBAoGBANElJmFWY2X6LgBpszeqt0ZOSbkFg2bC0wHCJrMlRzUMEn83w9e8 Z2Fqml9eCC5qxJcyxWDVQeoAX6090m0qgP8xNmGdafcVic2cUlrqtkqhhst2OHCO MCQEB7cdsjiidNNrOgLbQ3i1bYID8BVLf/TDhEbRgvTewDaz6XPdoSIRAoGBAOLg RuOec5gn50SrVycx8BLFO8AXjXojpZb1Xg26V5miz1IavSfDcgae/699ppSz+UWi jGMFr/PokY2JxDVs3PyQLu7ahMzyFHr16Agvp5g5kq056XV+uI/HhqLHOWSQ09DS 1Vrj7FOYpKRzge3/AC7ty9Vr35uMiebpm4/CLFVlAoGALnsIJZfSbWaFdLgJCXUa WDir77/G7T6dMIXanfPJ+IMfVUCqeLa5bxAHEOzP+qjl2giBjzy18nB00warTnGk y5I/WMBoPW5++sAkGWqSatGtKGi0sGcZUdfHcy3ZXvbT6eyprtrWCuyfUsbXQ5RM 8rPFIQwNA6jBpSak2ohF+FECgYEAn+6IKncNd6pRfnfmdSvf1+uPxkcUJZCxb2xC xByjGhvKWE+fHkPJwt8c0SIbZuJEC5Gds0RUF/XPfV4roZm/Yo9ldl02lp7kTxXA XtzxIP8c5d5YM8qD4l8+Csu0Kq9pkeC+JFddxkRpc8A1TIehInPhZ+6mb6mvoMb3 MW0pAX0CgYATT74RYuIYWZvx0TK4ZXIKTw2i6HObLF63Y6UwyPXXdEVie/ToYRNH JIxE1weVpHvnHZvVD6D3yGk39ZsCIt31VvKpatWXlWBm875MbBc6kuIGsYT+mSSj y9TXaE89E5zfL27nZe15QLJ+Xw8Io6PMLZ/jtC5TYoEixSZ9J8v6HA== -----END RSA PRIVATE KEY----- lego-4.9.1/e2e/fixtures/certs/pebble.minica.pem000066400000000000000000000021331434020463500213120ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDCTCCAfGgAwIBAgIIJOLbes8sTr4wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMTcx MjA2MTk0MjEwWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAyNGUyZGIwggEi MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5WgZNoVJandj43kkLyU50vzCZ alozvdRo3OFiKoDtmqKPNWRNO2hC9AUNxTDJco51Yc42u/WV3fPbbhSznTiOOVtn Ajm6iq4I5nZYltGGZetGDOQWr78y2gWY+SG078MuOO2hyDIiKtVc3xiXYA+8Hluu 9F8KbqSS1h55yxZ9b87eKR+B0zu2ahzBCIHKmKWgc6N13l7aDxxY3D6uq8gtJRU0 toumyLbdzGcupVvjbjDP11nl07RESDWBLG1/g3ktJvqIa4BWgU2HMh4rND6y8OD3 Hy3H8MY6CElL+MOCbFJjWqhtOxeFyZZV9q3kYnk9CAuQJKMEGuN4GU6tzhW1AgMB AAGjRTBDMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB BQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAF85v d40HK1ouDAtWeO1PbnWfGEmC5Xa478s9ddOd9Clvp2McYzNlAFfM7kdcj6xeiNhF WPIfaGAi/QdURSL/6C1KsVDqlFBlTs9zYfh2g0UXGvJtj1maeih7zxFLvet+fqll xseM4P9EVJaQxwuK/F78YBt0tCNfivC6JNZMgxKF59h0FBpH70ytUSHXdz7FKwix Mfn3qEb9BXSk0Q3prNV5sOV3vgjEtB4THfDxSz9z3+DepVnW3vbbqwEbkXdk3j82 2muVldgOUgTwK8eT+XdofVdntzU/kzygSAtAQwLJfn51fS1GvEcYGBc1bDryIqmF p9BI7gVKtWSZYegicA== -----END CERTIFICATE----- lego-4.9.1/e2e/fixtures/csr.cert000066400000000000000000000016541434020463500164540ustar00rootroot00000000000000-----BEGIN CERTIFICATE REQUEST----- MIICfjCCAWYCAQAwEzERMA8GA1UEAxMIYWNtZS53dGYwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQDAhXnho1w9OPHWs4YSMahYbG4Ui1K6hsHytBZfhsz0 09igSWzHMEFZYHZJVuSr60enuJSZRhgwDjfhQWSUgHgKItLPnlNVYM6RhVaW0WfT w6CpmE2AuH3WuQbrR2he1Nt0xfUJla+VWOFZuW7GhgBiV5iWBvdLv6Ztgh8eATjo 2vG2R+KuSUzrm6h+sb3nUR28OYunZ3vESjNwnL3/D/1th2rFpe3EA3em1HArJdXN F4eclciun5Js17AS9tdoHEEZMMBWyViiuz3CQlh+YD2qAvqaubanWNa+r+iijMvd 4HlDHC99LTk6TJoSKoL+E/OGKmntLqmBJ1UrCFgvnw3DAgMBAAGgJjAkBgkqhkiG 9w0BCQ4xFzAVMBMGA1UdEQQMMAqCCGFjbWUud3RmMA0GCSqGSIb3DQEBCwUAA4IB AQAfBLR8njftxf15V49szNsgNaG7Y5UQFwgl8pyiIaanGvX1DE0BtU1RB/w7itzX wW5W/wjielEbs1XkI2uz3hkebvHVA1QpA7bbrX01WonS18xCkiRDj8ZqFEG4vEGa HswzGUfq2v0gCOIPpVGE+8Q2Y7In5zwEfev+5DkHox4/vgwMhyPMI+y7jKtdG/dV U58SFnt/F1raoSmR6vfDcAFXm/L8LXEkxqqefFbhiRHRqQar1Wr15BH//swmNzEW 5SVCCHcyIqreSua8uPjBcJ8aYVLniX6DMRyYv4ij/PSvSQy9xJDewLqR235WfTd/ tk4hhJaqizKDpsvB+UFod5o5 -----END CERTIFICATE REQUEST----- lego-4.9.1/e2e/fixtures/csr.raw000066400000000000000000000012021434020463500162750ustar00rootroot000000000000000~0f010Uacme.wtf0"0  *H 0 y\=8ֳ1XlnR_ؠIl0AY`vIVGF07Adx "ϞSU`ΑVgàM}ֹGh^t XYnƆbWKm8GIL뛨~Q9g{J3pmjťwp+%ȮlװhA0VX=BX~`=X־袌yC/}-9:L**i.'U+X/ &0$ *H  100U 0 acme.wtf0  *H  |7yWl 5c%! MMQ;nVzQU#knT)ۭ}5ZB$CjAA3G Q6c'<}9? ##컌]US{Zڡ)pW-q$ƪ|Vѩj&71%Bw2"J漸paR~1I Đ~V}7N!2Ahw9lego-4.9.1/e2e/fixtures/pebble-config-dns.json000066400000000000000000000003241434020463500211500ustar00rootroot00000000000000{ "pebble": { "listenAddress": "0.0.0.0:15000", "certificate": "fixtures/certs/localhost/cert.pem", "privateKey": "fixtures/certs/localhost/key.pem", "httpPort": 5004, "tlsPort": 5003 } } lego-4.9.1/e2e/fixtures/pebble-config.json000066400000000000000000000003241434020463500203660ustar00rootroot00000000000000{ "pebble": { "listenAddress": "0.0.0.0:14000", "certificate": "fixtures/certs/localhost/cert.pem", "privateKey": "fixtures/certs/localhost/key.pem", "httpPort": 5002, "tlsPort": 5001 } } lego-4.9.1/e2e/fixtures/update-dns.sh000077500000000000000000000010351434020463500174020ustar00rootroot00000000000000#!/usr/bin/env bash # Simple DNS challenge exec solver. # Use challtestsrv https://github.com/letsencrypt/boulder/tree/master/test/challtestsrv set -e case "$1" in "present") echo "Present" payload="{\"host\":\"$2\", \"value\":\"$3\"}" echo "payload=${payload}" curl -s -X POST -d "${payload}" localhost:8055/set-txt ;; "cleanup") echo "cleanup" payload="{\"host\":\"$2\"}" echo "payload=${payload}" curl -s -X POST -d "${payload}" localhost:8055/clear-txt ;; *) echo "OOPS" ;; esac lego-4.9.1/e2e/loader/000077500000000000000000000000001434020463500143755ustar00rootroot00000000000000lego-4.9.1/e2e/loader/loader.go000066400000000000000000000136161434020463500162010ustar00rootroot00000000000000package loader import ( "bytes" "crypto/tls" "errors" "fmt" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "time" "github.com/go-acme/lego/v4/platform/wait" ) const ( cmdNamePebble = "pebble" cmdNameChallSrv = "pebble-challtestsrv" ) type CmdOption struct { HealthCheckURL string Args []string Env []string Dir string } type EnvLoader struct { PebbleOptions *CmdOption LegoOptions []string ChallSrv *CmdOption lego string } func (l *EnvLoader) MainTest(m *testing.M) int { if _, e2e := os.LookupEnv("LEGO_E2E_TESTS"); !e2e { fmt.Fprintln(os.Stderr, "skipping test: e2e tests are disabled. (no 'LEGO_E2E_TESTS' env var)") fmt.Println("PASS") return 0 } if _, err := exec.LookPath("git"); err != nil { fmt.Fprintln(os.Stderr, "skipping because git command not found") fmt.Println("PASS") return 0 } if l.PebbleOptions != nil { if _, err := exec.LookPath(cmdNamePebble); err != nil { fmt.Fprintln(os.Stderr, "skipping because pebble binary not found") fmt.Println("PASS") return 0 } } if l.ChallSrv != nil { if _, err := exec.LookPath(cmdNameChallSrv); err != nil { fmt.Fprintln(os.Stderr, "skipping because challtestsrv binary not found") fmt.Println("PASS") return 0 } } pebbleTearDown := l.launchPebble() defer pebbleTearDown() challSrvTearDown := l.launchChallSrv() defer challSrvTearDown() legoBinary, tearDown, err := buildLego() defer tearDown() if err != nil { fmt.Fprintln(os.Stderr, err) return 1 } l.lego = legoBinary if l.PebbleOptions != nil && l.PebbleOptions.HealthCheckURL != "" { pebbleHealthCheck(l.PebbleOptions) } return m.Run() } func (l *EnvLoader) RunLego(arg ...string) ([]byte, error) { cmd := exec.Command(l.lego, arg...) cmd.Env = l.LegoOptions fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) return cmd.CombinedOutput() } func (l *EnvLoader) launchPebble() func() { if l.PebbleOptions == nil { return func() {} } pebble, outPebble := l.cmdPebble() go func() { err := pebble.Run() if err != nil { fmt.Println(err) } }() return func() { err := pebble.Process.Kill() if err != nil { fmt.Println(err) } fmt.Println(outPebble.String()) } } func (l *EnvLoader) cmdPebble() (*exec.Cmd, *bytes.Buffer) { cmd := exec.Command(cmdNamePebble, l.PebbleOptions.Args...) cmd.Env = l.PebbleOptions.Env dir, err := filepath.Abs(l.PebbleOptions.Dir) if err != nil { panic(err) } cmd.Dir = dir fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) var b bytes.Buffer cmd.Stdout = &b cmd.Stderr = &b return cmd, &b } func pebbleHealthCheck(options *CmdOption) { client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} err := wait.For("pebble", 10*time.Second, 500*time.Millisecond, func() (bool, error) { resp, err := client.Get(options.HealthCheckURL) if err != nil { return false, err } if resp.StatusCode != http.StatusOK { return false, nil } return true, nil }) if err != nil { panic(err) } } func (l *EnvLoader) launchChallSrv() func() { if l.ChallSrv == nil { return func() {} } challtestsrv, outChalSrv := l.cmdChallSrv() go func() { err := challtestsrv.Run() if err != nil { fmt.Println(err) } }() return func() { err := challtestsrv.Process.Kill() if err != nil { fmt.Println(err) } fmt.Println(outChalSrv.String()) } } func (l *EnvLoader) cmdChallSrv() (*exec.Cmd, *bytes.Buffer) { cmd := exec.Command(cmdNameChallSrv, l.ChallSrv.Args...) fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) var b bytes.Buffer cmd.Stdout = &b cmd.Stderr = &b return cmd, &b } func buildLego() (string, func(), error) { here, err := os.Getwd() if err != nil { return "", func() {}, err } defer func() { _ = os.Chdir(here) }() buildPath, err := os.MkdirTemp("", "lego_test") if err != nil { return "", func() {}, err } projectRoot, err := getProjectRoot() if err != nil { return "", func() {}, err } mainFolder := filepath.Join(projectRoot, "cmd", "lego") err = os.Chdir(mainFolder) if err != nil { return "", func() {}, err } binary := filepath.Join(buildPath, "lego") err = build(binary) if err != nil { return "", func() {}, err } err = os.Chdir(here) if err != nil { return "", func() {}, err } return binary, func() { _ = os.RemoveAll(buildPath) CleanLegoFiles() }, nil } func getProjectRoot() (string, error) { git := exec.Command("git", "rev-parse", "--show-toplevel") output, err := git.CombinedOutput() if err != nil { fmt.Fprintf(os.Stderr, "%s\n", output) return "", err } return strings.TrimSpace(string(output)), nil } func build(binary string) error { toolPath, err := goToolPath() if err != nil { return err } cmd := exec.Command(toolPath, "build", "-o", binary) output, err := cmd.CombinedOutput() if err != nil { fmt.Fprintf(os.Stderr, "%s\n", output) return err } return nil } func goToolPath() (string, error) { // inspired by go1.11.1/src/internal/testenv/testenv.go if os.Getenv("GO_GCFLAGS") != "" { return "", errors.New("'go build' not compatible with setting $GO_GCFLAGS") } if runtime.GOOS == "darwin" && strings.HasPrefix(runtime.GOARCH, "arm") { return "", fmt.Errorf("skipping test: 'go build' not available on %s/%s", runtime.GOOS, runtime.GOARCH) } return goTool() } func goTool() (string, error) { var exeSuffix string if runtime.GOOS == "windows" { exeSuffix = ".exe" } path := filepath.Join(runtime.GOROOT(), "bin", "go"+exeSuffix) if _, err := os.Stat(path); err == nil { return path, nil } goBin, err := exec.LookPath("go" + exeSuffix) if err != nil { return "", fmt.Errorf("cannot find go tool: %w", err) } return goBin, nil } func CleanLegoFiles() { cmd := exec.Command("rm", "-rf", ".lego") fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) output, err := cmd.CombinedOutput() if err != nil { fmt.Println(string(output)) } } lego-4.9.1/e2e/readme.md000066400000000000000000000005341434020463500147100ustar00rootroot00000000000000# E2E tests How to run: - Add the following entries to your `/etc/hosts`: ``` 127.0.0.1 acme.wtf 127.0.0.1 lego.wtf 127.0.0.1 acme.lego.wtf 127.0.0.1 légô.wtf 127.0.0.1 xn--lg-bja9b.wtf ``` - Install [Pebble](https://github.com/letsencrypt/pebble): ```bash go get -u github.com/letsencrypt/pebble/... ``` - Launch tests: ```bash make e2e ``` lego-4.9.1/go.mod000066400000000000000000000144041434020463500135650ustar00rootroot00000000000000module github.com/go-acme/lego/v4 go 1.18 // github.com/exoscale/egoscale v1.19.0 => It is an error, please don't use it. require ( cloud.google.com/go v0.54.0 github.com/Azure/azure-sdk-for-go v32.4.0+incompatible github.com/Azure/go-autorest/autorest v0.11.24 github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 github.com/Azure/go-autorest/autorest/to v0.4.0 github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/BurntSushi/toml v1.2.0 github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.1 github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 github.com/aws/aws-sdk-go v1.39.0 github.com/cenkalti/backoff/v4 v4.1.3 github.com/civo/civogo v0.3.11 github.com/cloudflare/cloudflare-go v0.49.0 github.com/cpu/goacmedns v0.1.1 github.com/dnsimple/dnsimple-go v0.71.1 github.com/exoscale/egoscale v0.90.0 github.com/google/go-querystring v1.1.0 github.com/gophercloud/gophercloud v1.0.0 github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df github.com/infobloxopen/infoblox-go-client v1.1.1 github.com/labbsr0x/bindman-dns-webhook v1.0.2 github.com/linode/linodego v1.9.1 github.com/liquidweb/liquidweb-go v1.6.3 github.com/mattn/go-isatty v0.0.16 github.com/miekg/dns v1.1.50 github.com/mimuret/golang-iij-dpf v0.7.1 github.com/mitchellh/mapstructure v1.5.0 github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 github.com/nrdcg/auroradns v1.1.0 github.com/nrdcg/desec v0.6.0 github.com/nrdcg/dnspod-go v0.4.0 github.com/nrdcg/freemyip v0.2.0 github.com/nrdcg/goinwx v0.8.1 github.com/nrdcg/namesilo v0.2.1 github.com/nrdcg/porkbun v0.1.1 github.com/oracle/oci-go-sdk v24.3.0+incompatible github.com/ovh/go-ovh v1.1.0 github.com/pquerna/otp v1.3.0 github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 github.com/sacloud/api-client-go v0.2.1 github.com/sacloud/iaas-api-go v1.3.2 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 github.com/softlayer/softlayer-go v1.0.6 github.com/stretchr/testify v1.8.0 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 github.com/transip/gotransip/v6 v6.17.0 github.com/urfave/cli/v2 v2.14.0 github.com/vinyldns/go-vinyldns v0.9.16 github.com/vultr/govultr/v2 v2.17.2 github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 golang.org/x/net v0.0.0-20220722155237-a158d28d115b golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 google.golang.org/api v0.20.0 gopkg.in/ns1/ns1-go.v2 v2.6.5 gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v2 v2.4.0 software.sslmate.com/src/go-pkcs12 v0.2.0 ) require ( github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deepmap/oapi-codegen v1.9.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.0.1 // indirect github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 // indirect github.com/golang-jwt/jwt/v4 v4.2.0 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/uuid v1.3.0 // indirect github.com/googleapis/gax-go/v2 v2.0.5 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b // indirect github.com/labbsr0x/goh v1.0.1 // indirect github.com/liquidweb/go-lwApi v0.0.5 // indirect github.com/liquidweb/liquidweb-cli v0.6.9 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sacloud/go-http v0.1.2 // indirect github.com/sacloud/packages-go v0.0.5 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/smartystreets/assertions v1.0.1 // indirect github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect github.com/spf13/cast v1.3.1 // indirect github.com/stretchr/objx v0.4.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opencensus.io v0.22.3 // indirect go.uber.org/ratelimit v0.2.0 // indirect golang.org/x/mod v0.4.2 // indirect golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20211021150943-2b146023228c // indirect google.golang.org/grpc v1.41.0 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) lego-4.9.1/go.sum000066400000000000000000002607521434020463500136230ustar00rootroot00000000000000cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0 h1:3ithwDMr7/3vpAMXiH+ZQnYbuIsh+OPhUPMFC9enmn0= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go v32.4.0+incompatible h1:1JP8SKfroEakYiQU2ZyPDosh8w2Tg9UopKt88VyQPt4= github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.24 h1:1fIGgHKqVm54KIPT+q8Zmd1QlVsmHqeUGso5qm2BqqE= github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 h1:P6bYXFoao05z5uhOQzbC3Qd8JqF3jUoocoTeIxkp2cA= github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 h1:0W/yGmFdTIT77fvdlGZ0LMISoLHFJ7Tx4U0yeB+uFs4= github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24= github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks= github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.1 h1:5BIsppVPdWJA29Yb5cYawQYeh5geN413WxAgBZvEtdA= github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.1/go.mod h1:kX6YddBkXqqywAe8c9LyvgTCyFuZCTMF4cRPQhc3Fy8= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 h1:J45/QHgrzUdqe/Vco/Vxk0wRvdS2nKUxmf/zLgvfass= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go v1.39.0 h1:74BBwkEmiqBbi2CGflEh34l0YNtIibTjZsibGarkNjo= github.com/aws/aws-sdk-go v1.39.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/civo/civogo v0.3.11 h1:mON/fyrV946Sbk6paRtOSGsN+asCgCmHCgArf5xmGxM= github.com/civo/civogo v0.3.11/go.mod h1:7+GeeFwc4AYTULaEshpT2vIcl3Qq8HPoxA17viX3l6g= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cloudflare-go v0.49.0 h1:KqJYk/YQ5ZhmyYz1oa4kGDskfF1gVuZfqesaJ/XDLto= github.com/cloudflare/cloudflare-go v0.49.0/go.mod h1:h0QgcIZ3qEXwFiwfBO8sQxjVdYsLX+PfD7NFEnANaKg= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpu/goacmedns v0.1.1 h1:DM3H2NiN2oam7QljgGY5ygy4yDXhK5Z4JUnqaugs2C4= github.com/cpu/goacmedns v0.1.1/go.mod h1:MuaouqEhPAHxsbqjgnck5zeghuwBP1dLnPoobeGqugQ= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= github.com/deepmap/oapi-codegen v1.9.1 h1:yHmEnA7jSTUMQgV+uN02WpZtwHnz2CBW3mZRIxr1vtI= github.com/deepmap/oapi-codegen v1.9.1/go.mod h1:PLqNAhdedP8ttRpBBkzLKU3bp+Fpy+tTgeAMlztR2cw= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/dnsimple/dnsimple-go v0.71.1 h1:1hGoBA3CIjpjZj5DM3081xfxr4e2jYmYnkO2VuBF8Qc= github.com/dnsimple/dnsimple-go v0.71.1/go.mod h1:F9WHww9cC76hrnwGFfAfrqdW99j3MOYasQcIwTS/aUk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/exoscale/egoscale v0.90.0 h1:DZBXVU3iHqu5Ju5lQ5jWVlPo0IpI98SUo8Aa1UQVrmo= github.com/exoscale/egoscale v0.90.0/go.mod h1:wyXE5zrnFynMXA0jMhwQqSe24CfUhmBk2WI5wFZcq6Y= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/getkin/kin-openapi v0.87.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 h1:JVrqSeQfdhYRFk24TvhTZWU0q8lfCojxZQFi3Ou7+uY= github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gophercloud/gophercloud v0.15.1-0.20210202035223-633d73521055/go.mod h1:wRtmUelyIIv3CSSDI47aUwbs075O6i+LY+pXsKCBsb4= github.com/gophercloud/gophercloud v1.0.0 h1:9nTGx0jizmHxDobe4mck89FyQHVyA3CaXLIUSGJjP9k= github.com/gophercloud/gophercloud v1.0.0/go.mod h1:Q8fZtyi5zZxPS/j9aj3sSxtvj41AdQMDwyo1myduD5c= github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae h1:Hi3IgB9RQDE15Kfovd8MTZrcana+UlQqNbOif8dLpA0= github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae/go.mod h1:wx8HMD8oQD0Ryhz6+6ykq75PJ79iPyEqYHfwZ4l7OsA= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5uPK3wHh96wCsqe3hCMKh8E= github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/infobloxopen/infoblox-go-client v1.1.1 h1:728A6LbLjptj/7kZjHyIxQnm768PWHfGFm0HH8FnbtU= github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI= github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k= github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b h1:DzHy0GlWeF0KAglaTMY7Q+khIFoG8toHP+wLFBVBQJc= github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labbsr0x/bindman-dns-webhook v1.0.2 h1:I7ITbmQPAVwrDdhd6dHKi+MYJTJqPCK0jE6YNBAevnk= github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA= github.com/labbsr0x/goh v1.0.1 h1:97aBJkDjpyBZGPbQuOK5/gHcSFbcr5aRsq3RSRJFpPk= github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w= github.com/labstack/echo/v4 v4.6.3/go.mod h1:Hk5OiHj0kDqmFq7aHe7eDqI7CUhuCrfpupQtLGGLm7A= github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= github.com/lestrrat-go/codegen v1.0.2/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM= github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7kd09ZGA= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/linode/linodego v1.9.1 h1:29UpEPpYcGFnbwiJW8mbk/bjBZpgd/pv68io2IKTo34= github.com/linode/linodego v1.9.1/go.mod h1:h6AuFR/JpqwwM/vkj7s8KV3iGN8/jxn+zc437F8SZ8w= github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= github.com/liquidweb/go-lwApi v0.0.5 h1:CT4cdXzJXmo0bon298kS7NeSk+Gt8/UHpWBBol1NGCA= github.com/liquidweb/go-lwApi v0.0.5/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM= github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ= github.com/liquidweb/liquidweb-go v1.6.3 h1:NVHvcnX3eb3BltiIoA+gLYn15nOpkYkdizOEYGSKrk4= github.com/liquidweb/liquidweb-go v1.6.3/go.mod h1:SuXXp+thr28LnjEw18AYtWwIbWMHSUiajPQs8T9c/Rc= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/mimuret/golang-iij-dpf v0.7.1 h1:MHEZKx6gNGTvq1+3PYUNfTZ/qtGNNK4+zo+0Rdo4jY4= github.com/mimuret/golang-iij-dpf v0.7.1/go.mod h1:IXWYcQVIHYzuM+W7kDWX0mseHDfUoqMuarxMXHVTir0= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo= github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk= github.com/nrdcg/desec v0.6.0 h1:kZ9JtsYEW3LNfuPIM+2tXoxoQlF9koWfQTWTQsA7Sr8= github.com/nrdcg/desec v0.6.0/go.mod h1:wybWg5cRrNmtXLYpUCPCLvz4jfFNEGZQEnoUiX9WqcY= github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U= github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= github.com/nrdcg/freemyip v0.2.0 h1:/GscavT4GVqAY13HExl5UyoB4wlchv6Cg5NYDGsUoJ8= github.com/nrdcg/freemyip v0.2.0/go.mod h1:HjF0Yz0lSb37HD2ihIyGz9esyGcxbCrrGFLPpKevbx4= github.com/nrdcg/goinwx v0.8.1 h1:20EQ/JaGFnSKwiDH2JzjIpicffl3cPk6imJBDqVBVtU= github.com/nrdcg/goinwx v0.8.1/go.mod h1:tILVc10gieBp/5PMvbcYeXM6pVQ+c9jxDZnpaR1UW7c= github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg= github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= github.com/nrdcg/porkbun v0.1.1 h1:gxVzQYfFUGXhnBax/aVugoE3OIBAdHgrJgyMPyY5Sjo= github.com/nrdcg/porkbun v0.1.1/go.mod h1:JWl/WKnguWos4mjfp4YizvvToigk9qpQwrodOk+CPoA= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/oracle/oci-go-sdk v24.3.0+incompatible h1:x4mcfb4agelf1O4/1/auGlZ1lr97jXRSSN5MxTgG/zU= github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= github.com/ovh/go-ovh v1.1.0 h1:bHXZmw8nTgZin4Nv7JuaLs0KG5x54EQR7migYTd1zrk= github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs= github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 h1:dq90+d51/hQRaHEqRAsQ1rE/pC1GUS4sc2rCbbFsAIY= github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sacloud/api-client-go v0.2.1 h1:jl02ZG6cM+mcH4eDYg0cxCFFuTOVTOjUCLYL4UbP09U= github.com/sacloud/api-client-go v0.2.1/go.mod h1:8fmYy5OpT3W8ltV5ZxF8evultNwKpduGN4YKmU9Af7w= github.com/sacloud/go-http v0.1.2 h1:a84HkeDHxDD1vIA6HiOT72a3fwwJueZBwuGP6zVtEJU= github.com/sacloud/go-http v0.1.2/go.mod h1:gvWaT8LFBFnSBFVrznOQXC62uad46bHZQM8w+xoH3eE= github.com/sacloud/iaas-api-go v1.3.2 h1:03obrdVdv/bGHK9p6CV7Uzg+ot2gLsddUMevm9DDZqQ= github.com/sacloud/iaas-api-go v1.3.2/go.mod h1:CoqpRYBG2NRB5xfqTfZNyh2lVLKyLkE/HV9ISqmbhGc= github.com/sacloud/packages-go v0.0.5 h1:NXTQNyyp/3ugM4CANtLBJLejFESzfWu4GPUURN4NJrA= github.com/sacloud/packages-go v0.0.5/go.mod h1:XWMBSNHT9YKY3lCh6yJsx1o1RRQQGpuhNqJA6bSHdD4= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 h1:0roa6gXKgyta64uqh52AQG3wzZXH21unn+ltzQSXML0= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA= github.com/softlayer/softlayer-go v1.0.6 h1:wMyWmnTm0y3iNwwUJLacgSpMjxAW42MaVqWW4CwYb3c= github.com/softlayer/softlayer-go v1.0.6/go.mod h1:6HepcfAXROz0Rf63krk5hPZyHT6qyx2MNvYyHof7ik4= github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ= github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 h1:mmz27tVi2r70JYnm5y0Zk8w0Qzsx+vfUw3oqSyrEfP8= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 h1:g9SWTaTy/rEuhMErC2jWq9Qt5ci+jBYSvXnJsLq4adg= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490/go.mod h1:l9q4vc1QiawUB1m3RU+87yLvrrxe54jc0w/kEl4DbSQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/transip/gotransip/v6 v6.17.0 h1:2RCyqYqz5+Ej8z96EyE4sf6tQrrfEBaFDO0LliSl6+8= github.com/transip/gotransip/v6 v6.17.0/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= github.com/urfave/cli/v2 v2.14.0 h1:sFRL29Dm9JhXSMYb96raDeo/Q/JRyPXPs8u+4CkMlI8= github.com/urfave/cli/v2 v2.14.0/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ= github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f h1:cG+ehPRJSlqljSufLf1KXeXpUd1dLNjnzA18mZcB/O0= github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 h1:2wzke3JH7OtN20WsNDZx2VH/TCmsbqtDEbXzjF+i05E= github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997/go.mod h1:2CHKs/YGbCcNn/BPaCkEBwKz/FNCELi+MLILjR9RaTA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA= golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs= golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20211021150943-2b146023228c h1:FqrtZMB5Wr+/RecOM3uPJNPfWR8Upb5hAPnt7PU6i4k= google.golang.org/genproto v0.0.0-20211021150943-2b146023228c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0= gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ns1/ns1-go.v2 v2.6.5 h1:nzf3RXP4TEZLeZl7q9t6eav4htlNlWuYX+pXVUitlf0= gopkg.in/ns1/ns1-go.v2 v2.6.5/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= lego-4.9.1/internal/000077500000000000000000000000001434020463500142705ustar00rootroot00000000000000lego-4.9.1/internal/dnsdocs/000077500000000000000000000000001434020463500157255ustar00rootroot00000000000000lego-4.9.1/internal/dnsdocs/dns.go.tmpl000066400000000000000000000030301434020463500200070ustar00rootroot00000000000000package cmd // CODE GENERATED AUTOMATICALLY // THIS FILE MUST NOT BE EDITED BY HAND import ( "fmt" "os" "sort" "strings" "text/tabwriter" ) func allDNSCodes() string { providers := []string{ "manual", {{- range $provider := .Providers }} "{{ $provider.Code }}", {{- end}} } sort.Strings(providers) return strings.Join(providers, ", ") } func displayDNSHelp(name string) error { w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) ew := &errWriter{w: w} switch name { {{- range $provider := .Providers }} case "{{ $provider.Code }}": // generated from: {{ .GeneratedFrom }} ew.writeln(`Configuration for {{ $provider.Name }}.`) ew.writeln(`Code: '{{ $provider.Code }}'`) ew.writeln(`Since: '{{ $provider.Since }}'`) ew.writeln() {{if $provider.Configuration }}{{if $provider.Configuration.Credentials }} ew.writeln(`Credentials:`) {{- range $k, $v := $provider.Configuration.Credentials }} ew.writeln(` - "{{ $k }}": {{ safe $v }}`) {{- end}} ew.writeln() {{end}}{{if $provider.Configuration.Additional }} ew.writeln(`Additional Configuration:`) {{- range $k, $v := $provider.Configuration.Additional }} ew.writeln(` - "{{ $k }}": {{ safe $v }}`) {{- end}} {{end}}{{end}} ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/{{ $provider.Code }}`) {{end}} case "manual": ew.writeln(`Solving the DNS-01 challenge using CLI prompt.`) default: return fmt.Errorf("%q is not yet supported", name) } if ew.err != nil { return fmt.Errorf("error: %w", ew.err) } return w.Flush() }lego-4.9.1/internal/dnsdocs/dns.md.tmpl000066400000000000000000000036751434020463500200210ustar00rootroot00000000000000--- title: "{{ .Name }}" date: 2019-03-03T16:39:46+01:00 draft: false slug: {{ .Code }} dnsprovider: since: "{{ .Since }}" code: "{{ .Code }}" url: "{{ .URL }}" --- {{if .Description -}} {{ .Description }} {{else}} Configuration for [{{ .Name }}]({{ .URL }}). {{end}} - Code: `{{ .Code }}` - Since: {{ .Since }} {{if .Example }} Here is an example bash command using the {{ .Name }} provider: ```bash {{ .Example -}} ``` {{else}} {{ "{{" }}% notice note %}} _Please contribute by adding a CLI example._ {{ "{{" }}% /notice %}} {{end}} {{if .Configuration }} {{if .Configuration.Credentials }} ## Credentials | Environment Variable Name | Description | |-----------------------|-------------| {{- range $k, $v := .Configuration.Credentials }} | `{{$k}}` | {{$v}} | {{- end}} The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{ `{{< ref "dns#configuration-and-credentials" >}}` }}). {{- end}} {{if .Configuration.Additional }} ## Additional Configuration | Environment Variable Name | Description | |--------------------------------|-------------| {{- range $k, $v := .Configuration.Additional }} | `{{$k}}` | {{$v}} | {{- end}} The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. More information [here]({{ `{{< ref "dns#configuration-and-credentials" >}}` }}). {{- end}} {{- end}} {{ .Additional }} {{if .Links }} ## More information {{if .Links.API -}} - [API documentation]({{ .Links.API }}) {{- end}} {{- if .Links.GoClient }} - [Go client]({{ .Links.GoClient }}) {{- end}} {{- end}} lego-4.9.1/internal/dnsdocs/generator.go000066400000000000000000000127411434020463500202470ustar00rootroot00000000000000package main //go:generate go run . import ( "bufio" "bytes" "errors" "fmt" "go/format" "io" "log" "os" "path/filepath" "sort" "strings" "text/template" "github.com/BurntSushi/toml" ) const ( root = "../../" dnsPackage = root + "providers/dns" mdTemplate = root + "internal/dnsdocs/dns.md.tmpl" cliTemplate = root + "internal/dnsdocs/dns.go.tmpl" cliOutput = root + "cmd/zz_gen_cmd_dnshelp.go" docOutput = root + "docs/content/dns" readmePath = root + "README.md" ) const ( startLine = "" endLine = "" ) type Model struct { Name string // Real name of the DNS provider Code string // DNS code Since string // First lego version URL string // DNS provider URL Description string // Provider summary Example string // CLI example Configuration *Configuration // Environment variables Links *Links // Links Additional string // Extra documentation GeneratedFrom string // Source file } type Configuration struct { Credentials map[string]string Additional map[string]string } type Links struct { API string GoClient string } type Providers struct { Providers []Model } func main() { models := &Providers{} err := filepath.Walk(dnsPackage, walker(models)) if err != nil { log.Fatal(err) } // generate CLI help err = generateCLIHelp(models) if err != nil { log.Fatal(err) } // generate README.md err = generateReadMe(models) if err != nil { log.Fatal(err) } fmt.Printf("Documentation for %d DNS providers has been generated.\n", len(models.Providers)+1) } func walker(prs *Providers) func(string, os.FileInfo, error) error { return func(path string, _ os.FileInfo, err error) error { if err != nil { return err } if filepath.Ext(path) == ".toml" { m := Model{} m.GeneratedFrom, err = filepath.Rel(root, path) if err != nil { return err } _, err := toml.DecodeFile(path, &m) if err != nil { return err } prs.Providers = append(prs.Providers, m) // generate documentation return generateDocumentation(m) } return nil } } func generateDocumentation(m Model) error { filename := filepath.Join(docOutput, "zz_gen_"+m.Code+".md") file, err := os.Create(filename) if err != nil { return err } return template.Must(template.ParseFiles(mdTemplate)).Execute(file, m) } func generateCLIHelp(models *Providers) error { filename := filepath.Clean(cliOutput) file, err := os.Create(filename) if err != nil { return err } tlt := template.New(filepath.Base(cliTemplate)).Funcs(map[string]interface{}{ "safe": func(src string) string { return strings.ReplaceAll(src, "`", "'") }, }) b := &bytes.Buffer{} err = template.Must(tlt.ParseFiles(cliTemplate)).Execute(b, models) if err != nil { return err } // gofmt source, err := format.Source(b.Bytes()) if err != nil { return err } _, err = file.Write(source) return err } func generateReadMe(models *Providers) error { max, lines := extractTableData(models) file, err := os.Open(readmePath) if err != nil { return err } defer func() { _ = file.Close() }() var skip bool buffer := bytes.NewBufferString("") fileScanner := bufio.NewScanner(file) for fileScanner.Scan() { text := fileScanner.Text() if text == startLine { _, _ = fmt.Fprintln(buffer, text) err = writeDNSTable(buffer, lines, max) if err != nil { return err } skip = true } if text == endLine { skip = false } if skip { continue } _, _ = fmt.Fprintln(buffer, text) } if fileScanner.Err() != nil { return fileScanner.Err() } if skip { return errors.New("missing end tag") } return os.WriteFile(readmePath, buffer.Bytes(), 0o666) } func extractTableData(models *Providers) (int, [][]string) { readmePattern := "[%s](https://go-acme.github.io/lego/dns/%s/)" items := []string{fmt.Sprintf(readmePattern, "Manual", "manual")} var max int for _, pvd := range models.Providers { item := fmt.Sprintf(readmePattern, strings.ReplaceAll(pvd.Name, "|", "/"), pvd.Code) items = append(items, item) if max < len(item) { max = len(item) } } const nbCol = 4 sort.Slice(items, func(i, j int) bool { return strings.ToLower(items[i]) < strings.ToLower(items[j]) }) var lines [][]string var line []string for i, item := range items { switch { case len(line) == nbCol: lines = append(lines, line) line = []string{item} case i == len(items)-1: line = append(line, item) for j := len(line); j < nbCol; j++ { line = append(line, "") } lines = append(lines, line) default: line = append(line, item) } } if len(line) < nbCol { for j := len(line); j < nbCol; j++ { line = append(line, "") } lines = append(lines, line) } return max, lines } func writeDNSTable(w io.Writer, lines [][]string, size int) error { _, err := fmt.Fprintf(w, "\n") if err != nil { return err } _, err = fmt.Fprintf(w, "|%[1]s|%[1]s|%[1]s|%[1]s|\n", strings.Repeat(" ", size+2)) if err != nil { return err } _, err = fmt.Fprintf(w, "|%[1]s|%[1]s|%[1]s|%[1]s|\n", strings.Repeat("-", size+2)) if err != nil { return err } linePattern := fmt.Sprintf("| %%-%[1]ds | %%-%[1]ds | %%-%[1]ds | %%-%[1]ds |\n", size) for _, line := range lines { _, err = fmt.Fprintf(w, linePattern, line[0], line[1], line[2], line[3]) if err != nil { return err } } _, err = fmt.Fprintf(w, "\n") return err } lego-4.9.1/internal/release.go000066400000000000000000000105751434020463500162470ustar00rootroot00000000000000package main import ( "bytes" "fmt" "go/ast" "go/format" "go/parser" "go/token" "log" "os" "regexp" "strconv" "strings" "text/template" "github.com/urfave/cli/v2" ) const sourceFile = "./acme/api/internal/sender/useragent.go" const uaTemplate = `package sender // CODE GENERATED AUTOMATICALLY // THIS FILE MUST NOT BE EDITED BY HAND const ( // ourUserAgent is the User-Agent of this underlying library package. ourUserAgent = "xenolf-acme/{{ .version }}" // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. // values: detach|release // NOTE: Update this with each tagged release. ourUserAgentComment = "{{ .comment }}" ) ` func main() { app := cli.NewApp() app.Name = "lego-releaser" app.Usage = "Lego releaser" app.HelpName = "releaser" app.Commands = []*cli.Command{ { Name: "release", Usage: "Update file for a release", Action: release, Before: func(ctx *cli.Context) error { mode := ctx.String("mode") switch mode { case "patch", "minor", "major": return nil default: return fmt.Errorf("invalid mode: %s", mode) } }, Flags: []cli.Flag{ &cli.StringFlag{ Name: "mode", Aliases: []string{"m"}, Value: "patch", Usage: "The release mode: patch|minor|major", }, }, }, { Name: "detach", Usage: "Update file post release", Action: detach, }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } } func release(ctx *cli.Context) error { mode := ctx.String("mode") // Read file data, err := readUserAgentFile(sourceFile) if err != nil { return err } // Bump version newVersion, err := bumpVersion(data["ourUserAgent"], mode) if err != nil { return err } // Write file comment := "release" // detach|release return writeUserAgentFile(sourceFile, newVersion, comment) } func detach(_ *cli.Context) error { // Read file data, err := readUserAgentFile(sourceFile) if err != nil { return err } // Write file version := strings.TrimPrefix(data["ourUserAgent"], "xenolf-acme/") comment := "detach" return writeUserAgentFile(sourceFile, version, comment) } type visitor struct { data map[string]string } func (v visitor) Visit(n ast.Node) ast.Visitor { if n == nil { return nil } switch d := n.(type) { case *ast.GenDecl: if d.Tok == token.CONST { for _, spec := range d.Specs { valueSpec, ok := spec.(*ast.ValueSpec) if !ok { continue } if len(valueSpec.Names) != 1 || len(valueSpec.Values) != 1 { continue } va, ok := valueSpec.Values[0].(*ast.BasicLit) if !ok { continue } if va.Kind != token.STRING { continue } s, err := strconv.Unquote(va.Value) if err != nil { continue } v.data[valueSpec.Names[0].String()] = s } } default: // noop } return v } func readUserAgentFile(filename string) (map[string]string, error) { fset := token.NewFileSet() file, err := parser.ParseFile(fset, filename, nil, parser.AllErrors) if err != nil { return nil, err } v := visitor{data: make(map[string]string)} ast.Walk(v, file) return v.data, nil } func writeUserAgentFile(filename, version, comment string) error { tmpl, err := template.New("ua").Parse(uaTemplate) if err != nil { return err } b := &bytes.Buffer{} err = tmpl.Execute(b, map[string]string{ "version": version, "comment": comment, }) if err != nil { return err } source, err := format.Source(b.Bytes()) if err != nil { return err } return os.WriteFile(filename, source, 0o644) } func bumpVersion(userAgent, mode string) (string, error) { prevVersion := strings.TrimPrefix(userAgent, "xenolf-acme/") allString := regexp.MustCompile(`(\d+)\.(\d+)\.(\d+)`).FindStringSubmatch(prevVersion) if len(allString) != 4 { return "", fmt.Errorf("invalid version format: %s", prevVersion) } switch mode { case "patch": patch, err := strconv.Atoi(allString[3]) if err != nil { return "", err } return fmt.Sprintf("%s.%s.%d", allString[1], allString[2], patch+1), nil case "minor": minor, err := strconv.Atoi(allString[2]) if err != nil { return "", err } return fmt.Sprintf("%s.%d.0", allString[1], minor+1), nil case "major": major, err := strconv.Atoi(allString[1]) if err != nil { return "", err } return fmt.Sprintf("%d.0.0", major+1), nil default: return "", fmt.Errorf("invalid mode: %s", mode) } } lego-4.9.1/lego/000077500000000000000000000000001434020463500134025ustar00rootroot00000000000000lego-4.9.1/lego/client.go000066400000000000000000000041531434020463500152120ustar00rootroot00000000000000package lego import ( "errors" "net/url" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/challenge/resolver" "github.com/go-acme/lego/v4/registration" ) // Client is the user-friendly way to ACME. type Client struct { Certificate *certificate.Certifier Challenge *resolver.SolverManager Registration *registration.Registrar core *api.Core } // NewClient creates a new ACME client on behalf of the user. // The client will depend on the ACME directory located at CADirURL for the rest of its actions. // A private key of type keyType (see KeyType constants) will be generated when requesting a new certificate if one isn't provided. func NewClient(config *Config) (*Client, error) { if config == nil { return nil, errors.New("a configuration must be provided") } _, err := url.Parse(config.CADirURL) if err != nil { return nil, err } if config.HTTPClient == nil { return nil, errors.New("the HTTP client cannot be nil") } privateKey := config.User.GetPrivateKey() if privateKey == nil { return nil, errors.New("private key was nil") } var kid string if reg := config.User.GetRegistration(); reg != nil { kid = reg.URI } core, err := api.New(config.HTTPClient, config.UserAgent, config.CADirURL, kid, privateKey) if err != nil { return nil, err } solversManager := resolver.NewSolversManager(core) prober := resolver.NewProber(solversManager) certifier := certificate.NewCertifier(core, prober, certificate.CertifierOptions{KeyType: config.Certificate.KeyType, Timeout: config.Certificate.Timeout}) return &Client{ Certificate: certifier, Challenge: solversManager, Registration: registration.NewRegistrar(core, config.User), core: core, }, nil } // GetToSURL returns the current ToS URL from the Directory. func (c *Client) GetToSURL() string { return c.core.GetDirectory().Meta.TermsOfService } // GetExternalAccountRequired returns the External Account Binding requirement of the Directory. func (c *Client) GetExternalAccountRequired() bool { return c.core.GetDirectory().Meta.ExternalAccountRequired } lego-4.9.1/lego/client_config.go000066400000000000000000000077161434020463500165470ustar00rootroot00000000000000package lego import ( "crypto/tls" "crypto/x509" "fmt" "net" "net/http" "os" "strconv" "strings" "time" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/registration" ) const ( // caCertificatesEnvVar is the environment variable name that can be used to // specify the path to PEM encoded CA Certificates that can be used to // authenticate an ACME server with an HTTPS certificate not issued by a CA in // the system-wide trusted root list. // Multiple file paths can be added by using os.PathListSeparator as a separator. caCertificatesEnvVar = "LEGO_CA_CERTIFICATES" // caSystemCertPool is the environment variable name that can be used to define // if the certificates pool must use a copy of the system cert pool. caSystemCertPool = "LEGO_CA_SYSTEM_CERT_POOL" // caServerNameEnvVar is the environment variable name that can be used to // specify the CA server name that can be used to // authenticate an ACME server with a HTTPS certificate not issued by a CA in // the system-wide trusted root list. caServerNameEnvVar = "LEGO_CA_SERVER_NAME" // LEDirectoryProduction URL to the Let's Encrypt production. LEDirectoryProduction = "https://acme-v02.api.letsencrypt.org/directory" // LEDirectoryStaging URL to the Let's Encrypt staging. LEDirectoryStaging = "https://acme-staging-v02.api.letsencrypt.org/directory" ) type Config struct { CADirURL string User registration.User UserAgent string HTTPClient *http.Client Certificate CertificateConfig } func NewConfig(user registration.User) *Config { return &Config{ CADirURL: LEDirectoryProduction, User: user, HTTPClient: createDefaultHTTPClient(), Certificate: CertificateConfig{ KeyType: certcrypto.RSA2048, Timeout: 30 * time.Second, }, } } type CertificateConfig struct { KeyType certcrypto.KeyType Timeout time.Duration } // createDefaultHTTPClient Creates an HTTP client with a reasonable timeout value // and potentially a custom *x509.CertPool // based on the caCertificatesEnvVar environment variable (see the `initCertPool` function). func createDefaultHTTPClient() *http.Client { return &http.Client{ Timeout: 2 * time.Minute, Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, TLSHandshakeTimeout: 30 * time.Second, ResponseHeaderTimeout: 30 * time.Second, TLSClientConfig: &tls.Config{ ServerName: os.Getenv(caServerNameEnvVar), RootCAs: initCertPool(), }, }, } } // initCertPool creates a *x509.CertPool populated with the PEM certificates // found in the filepath specified in the caCertificatesEnvVar OS environment variable. // If the caCertificatesEnvVar is not set then initCertPool will return nil. // If there is an error creating a *x509.CertPool from the provided caCertificatesEnvVar value then initCertPool will panic. // 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. // caSystemCertPool requires caCertificatesEnvVar to be set. func initCertPool() *x509.CertPool { customCACertsPath := os.Getenv(caCertificatesEnvVar) if customCACertsPath == "" { return nil } certPool := getCertPool() for _, customPath := range strings.Split(customCACertsPath, string(os.PathListSeparator)) { customCAs, err := os.ReadFile(customPath) if err != nil { panic(fmt.Sprintf("error reading %s=%q: %v", caCertificatesEnvVar, customPath, err)) } if ok := certPool.AppendCertsFromPEM(customCAs); !ok { panic(fmt.Sprintf("error creating x509 cert pool from %s=%q: %v", caCertificatesEnvVar, customPath, err)) } } return certPool } func getCertPool() *x509.CertPool { useSystemCertPool, _ := strconv.ParseBool(os.Getenv(caSystemCertPool)) if !useSystemCertPool { return x509.NewCertPool() } pool, err := x509.SystemCertPool() if err == nil { return pool } return x509.NewCertPool() } lego-4.9.1/lego/client_test.go000066400000000000000000000021301434020463500162420ustar00rootroot00000000000000package lego import ( "crypto" "crypto/rand" "crypto/rsa" "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/go-acme/lego/v4/registration" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewClient(t *testing.T) { _, apiURL := tester.SetupFakeAPI(t) keyBits := 32 // small value keeps test fast key, err := rsa.GenerateKey(rand.Reader, keyBits) require.NoError(t, err, "Could not generate test key") user := mockUser{ email: "test@test.com", regres: new(registration.Resource), privatekey: key, } config := NewConfig(user) config.CADirURL = apiURL + "/dir" client, err := NewClient(config) require.NoError(t, err, "Could not create client") assert.NotNil(t, client) } type mockUser struct { email string regres *registration.Resource privatekey *rsa.PrivateKey } func (u mockUser) GetEmail() string { return u.email } func (u mockUser) GetRegistration() *registration.Resource { return u.regres } func (u mockUser) GetPrivateKey() crypto.PrivateKey { return u.privatekey } lego-4.9.1/log/000077500000000000000000000000001434020463500132355ustar00rootroot00000000000000lego-4.9.1/log/logger.go000066400000000000000000000027501434020463500150470ustar00rootroot00000000000000package log import ( "log" "os" ) // Logger is an optional custom logger. var Logger StdLogger = log.New(os.Stderr, "", log.LstdFlags) // StdLogger interface for Standard Logger. type StdLogger interface { Fatal(args ...interface{}) Fatalln(args ...interface{}) Fatalf(format string, args ...interface{}) Print(args ...interface{}) Println(args ...interface{}) Printf(format string, args ...interface{}) } // Fatal writes a log entry. // It uses Logger if not nil, otherwise it uses the default log.Logger. func Fatal(args ...interface{}) { Logger.Fatal(args...) } // Fatalf writes a log entry. // It uses Logger if not nil, otherwise it uses the default log.Logger. func Fatalf(format string, args ...interface{}) { Logger.Fatalf(format, args...) } // Print writes a log entry. // It uses Logger if not nil, otherwise it uses the default log.Logger. func Print(args ...interface{}) { Logger.Print(args...) } // Println writes a log entry. // It uses Logger if not nil, otherwise it uses the default log.Logger. func Println(args ...interface{}) { Logger.Println(args...) } // Printf writes a log entry. // It uses Logger if not nil, otherwise it uses the default log.Logger. func Printf(format string, args ...interface{}) { Logger.Printf(format, args...) } // Warnf writes a log entry. func Warnf(format string, args ...interface{}) { Printf("[WARN] "+format, args...) } // Infof writes a log entry. func Infof(format string, args ...interface{}) { Printf("[INFO] "+format, args...) } lego-4.9.1/platform/000077500000000000000000000000001434020463500143005ustar00rootroot00000000000000lego-4.9.1/platform/config/000077500000000000000000000000001434020463500155455ustar00rootroot00000000000000lego-4.9.1/platform/config/env/000077500000000000000000000000001434020463500163355ustar00rootroot00000000000000lego-4.9.1/platform/config/env/env.go000066400000000000000000000076101434020463500174600ustar00rootroot00000000000000package env import ( "errors" "fmt" "os" "strconv" "strings" "time" "github.com/go-acme/lego/v4/log" ) // Get environment variables. func Get(names ...string) (map[string]string, error) { values := map[string]string{} var missingEnvVars []string for _, envVar := range names { value := GetOrFile(envVar) if value == "" { missingEnvVars = append(missingEnvVars, envVar) } values[envVar] = value } if len(missingEnvVars) > 0 { return nil, fmt.Errorf("some credentials information are missing: %s", strings.Join(missingEnvVars, ",")) } return values, nil } // GetWithFallback Get environment variable values. // The first name in each group is use as key in the result map. // // case 1: // // // LEGO_ONE="ONE" // // LEGO_TWO="TWO" // env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"}) // // => "LEGO_ONE" = "ONE" // // case 2: // // // LEGO_ONE="" // // LEGO_TWO="TWO" // env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"}) // // => "LEGO_ONE" = "TWO" // // case 3: // // // LEGO_ONE="" // // LEGO_TWO="" // env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"}) // // => error func GetWithFallback(groups ...[]string) (map[string]string, error) { values := map[string]string{} var missingEnvVars []string for _, names := range groups { if len(names) == 0 { return nil, errors.New("undefined environment variable names") } value, envVar := getOneWithFallback(names[0], names[1:]...) if value == "" { missingEnvVars = append(missingEnvVars, envVar) continue } values[envVar] = value } if len(missingEnvVars) > 0 { return nil, fmt.Errorf("some credentials information are missing: %s", strings.Join(missingEnvVars, ",")) } return values, nil } func getOneWithFallback(main string, names ...string) (string, string) { value := GetOrFile(main) if len(value) > 0 { return value, main } for _, name := range names { value := GetOrFile(name) if len(value) > 0 { return value, main } } return "", main } // GetOrDefaultInt returns the given environment variable value as an integer. // Returns the default if the envvar cannot be coopered to an int, or is not found. func GetOrDefaultInt(envVar string, defaultValue int) int { v, err := strconv.Atoi(GetOrFile(envVar)) if err != nil { return defaultValue } return v } // GetOrDefaultSecond returns the given environment variable value as an time.Duration (second). // Returns the default if the envvar cannot be coopered to an int, or is not found. func GetOrDefaultSecond(envVar string, defaultValue time.Duration) time.Duration { v := GetOrDefaultInt(envVar, -1) if v < 0 { return defaultValue } return time.Duration(v) * time.Second } // GetOrDefaultString returns the given environment variable value as a string. // Returns the default if the envvar cannot be find. func GetOrDefaultString(envVar, defaultValue string) string { v := GetOrFile(envVar) if v == "" { return defaultValue } return v } // GetOrDefaultBool returns the given environment variable value as a boolean. // Returns the default if the envvar cannot be coopered to a boolean, or is not found. func GetOrDefaultBool(envVar string, defaultValue bool) bool { v, err := strconv.ParseBool(GetOrFile(envVar)) if err != nil { return defaultValue } return v } // GetOrFile Attempts to resolve 'key' as an environment variable. // Failing that, it will check to see if '_FILE' exists. // If so, it will attempt to read from the referenced file to populate a value. func GetOrFile(envVar string) string { envVarValue := os.Getenv(envVar) if envVarValue != "" { return envVarValue } fileVar := envVar + "_FILE" fileVarValue := os.Getenv(fileVar) if fileVarValue == "" { return envVarValue } fileContents, err := os.ReadFile(fileVarValue) if err != nil { log.Printf("Failed to read the file %s (defined by env var %s): %s", fileVarValue, fileVar, err) return "" } return strings.TrimSuffix(string(fileContents), "\n") } lego-4.9.1/platform/config/env/env_test.go000066400000000000000000000167411434020463500205240ustar00rootroot00000000000000package env import ( "os" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGetWithFallback(t *testing.T) { var1Exist := os.Getenv("TEST_LEGO_VAR_EXIST_1") var2Exist := os.Getenv("TEST_LEGO_VAR_EXIST_2") var1Missing := os.Getenv("TEST_LEGO_VAR_MISSING_1") var2Missing := os.Getenv("TEST_LEGO_VAR_MISSING_2") defer func() { _ = os.Setenv("TEST_LEGO_VAR_EXIST_1", var1Exist) _ = os.Setenv("TEST_LEGO_VAR_EXIST_2", var2Exist) _ = os.Setenv("TEST_LEGO_VAR_MISSING_1", var1Missing) _ = os.Setenv("TEST_LEGO_VAR_MISSING_2", var2Missing) }() err := os.Setenv("TEST_LEGO_VAR_EXIST_1", "VAR1") require.NoError(t, err) err = os.Setenv("TEST_LEGO_VAR_EXIST_2", "VAR2") require.NoError(t, err) err = os.Unsetenv("TEST_LEGO_VAR_MISSING_1") require.NoError(t, err) err = os.Unsetenv("TEST_LEGO_VAR_MISSING_2") require.NoError(t, err) type expected struct { value map[string]string error string } testCases := []struct { desc string groups [][]string expected expected }{ { desc: "no groups", groups: nil, expected: expected{ value: map[string]string{}, }, }, { desc: "empty groups", groups: [][]string{{}, {}}, expected: expected{ error: "undefined environment variable names", }, }, { desc: "missing env var", groups: [][]string{{"TEST_LEGO_VAR_MISSING_1"}}, expected: expected{ error: "some credentials information are missing: TEST_LEGO_VAR_MISSING_1", }, }, { desc: "all env var in a groups are missing", groups: [][]string{{"TEST_LEGO_VAR_MISSING_1", "TEST_LEGO_VAR_MISSING_2"}}, expected: expected{ error: "some credentials information are missing: TEST_LEGO_VAR_MISSING_1", }, }, { desc: "only the first env var have a value", groups: [][]string{{"TEST_LEGO_VAR_EXIST_1", "TEST_LEGO_VAR_MISSING_1"}}, expected: expected{ value: map[string]string{"TEST_LEGO_VAR_EXIST_1": "VAR1"}, }, }, { desc: "only the second env var have a value", groups: [][]string{{"TEST_LEGO_VAR_MISSING_1", "TEST_LEGO_VAR_EXIST_1"}}, expected: expected{ value: map[string]string{"TEST_LEGO_VAR_MISSING_1": "VAR1"}, }, }, { desc: "all env vars in a groups have a value", groups: [][]string{{"TEST_LEGO_VAR_EXIST_1", "TEST_LEGO_VAR_EXIST_2"}}, expected: expected{ value: map[string]string{"TEST_LEGO_VAR_EXIST_1": "VAR1"}, }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { value, err := GetWithFallback(test.groups...) if len(test.expected.error) > 0 { assert.EqualError(t, err, test.expected.error) } else { require.NoError(t, err) assert.Equal(t, test.expected.value, value) } }) } } func TestGetOrDefaultInt(t *testing.T) { testCases := []struct { desc string envValue string defaultValue int expected int }{ { desc: "valid value", envValue: "100", defaultValue: 2, expected: 100, }, { desc: "invalid content, use default value", envValue: "abc123", defaultValue: 2, expected: 2, }, { desc: "valid negative value", envValue: "-111", defaultValue: 2, expected: -111, }, { desc: "float: invalid type, use default value", envValue: "1.11", defaultValue: 2, expected: 2, }, } const key = "LEGO_ENV_TC" for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Setenv(key, test.envValue) result := GetOrDefaultInt(key, test.defaultValue) assert.Equal(t, test.expected, result) }) } } func TestGetOrDefaultSecond(t *testing.T) { testCases := []struct { desc string envValue string defaultValue time.Duration expected time.Duration }{ { desc: "valid value", envValue: "100", defaultValue: 2 * time.Second, expected: 100 * time.Second, }, { desc: "invalid content, use default value", envValue: "abc123", defaultValue: 2 * time.Second, expected: 2 * time.Second, }, { desc: "invalid content, negative value", envValue: "-111", defaultValue: 2 * time.Second, expected: 2 * time.Second, }, { desc: "float: invalid type, use default value", envValue: "1.11", defaultValue: 2 * time.Second, expected: 2 * time.Second, }, } key := "LEGO_ENV_TC" for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Setenv(key, test.envValue) result := GetOrDefaultSecond(key, test.defaultValue) assert.Equal(t, test.expected, result) }) } } func TestGetOrDefaultString(t *testing.T) { testCases := []struct { desc string envValue string defaultValue string expected string }{ { desc: "missing env var", defaultValue: "foo", expected: "foo", }, { desc: "with env var", envValue: "bar", defaultValue: "foo", expected: "bar", }, } key := "LEGO_ENV_TC" for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Setenv(key, test.envValue) actual := GetOrDefaultString(key, test.defaultValue) assert.Equal(t, test.expected, actual) }) } } func TestGetOrDefaultBool(t *testing.T) { testCases := []struct { desc string envValue string defaultValue bool expected bool }{ { desc: "missing env var", defaultValue: true, expected: true, }, { desc: "with env var", envValue: "true", defaultValue: false, expected: true, }, { desc: "invalid value", envValue: "foo", defaultValue: false, expected: false, }, } key := "LEGO_ENV_TC" for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Setenv(key, test.envValue) actual := GetOrDefaultBool(key, test.defaultValue) assert.Equal(t, test.expected, actual) }) } } func TestGetOrFile_ReadsEnvVars(t *testing.T) { t.Setenv("TEST_LEGO_ENV_VAR", "lego_env") value := GetOrFile("TEST_LEGO_ENV_VAR") assert.Equal(t, "lego_env", value) } func TestGetOrFile_ReadsFiles(t *testing.T) { varEnvFileName := "TEST_LEGO_ENV_VAR_FILE" varEnvName := "TEST_LEGO_ENV_VAR" testCases := []struct { desc string fileContent []byte }{ { desc: "simple", fileContent: []byte("lego_file"), }, { desc: "with an empty last line", fileContent: []byte("lego_file\n"), }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { err := os.Unsetenv(varEnvFileName) require.NoError(t, err) err = os.Unsetenv(varEnvName) require.NoError(t, err) file, err := os.CreateTemp("", "lego") require.NoError(t, err) defer os.Remove(file.Name()) err = os.WriteFile(file.Name(), []byte("lego_file\n"), 0o644) require.NoError(t, err) t.Setenv(varEnvFileName, file.Name()) value := GetOrFile(varEnvName) assert.Equal(t, "lego_file", value) }) } } func TestGetOrFile_PrefersEnvVars(t *testing.T) { varEnvFileName := "TEST_LEGO_ENV_VAR_FILE" varEnvName := "TEST_LEGO_ENV_VAR" err := os.Unsetenv(varEnvFileName) require.NoError(t, err) err = os.Unsetenv(varEnvName) require.NoError(t, err) file, err := os.CreateTemp("", "lego") require.NoError(t, err) defer os.Remove(file.Name()) err = os.WriteFile(file.Name(), []byte("lego_file"), 0o644) require.NoError(t, err) t.Setenv(varEnvFileName, file.Name()) t.Setenv(varEnvName, "lego_env") value := GetOrFile(varEnvName) assert.Equal(t, "lego_env", value) } lego-4.9.1/platform/tester/000077500000000000000000000000001434020463500156065ustar00rootroot00000000000000lego-4.9.1/platform/tester/api.go000066400000000000000000000030741434020463500167120ustar00rootroot00000000000000package tester import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-acme/lego/v4/acme" ) // SetupFakeAPI Minimal stub ACME server for validation. func SetupFakeAPI(t *testing.T) (*http.ServeMux, string) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/dir", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } err := WriteJSONResponse(w, acme.Directory{ NewNonceURL: server.URL + "/nonce", NewAccountURL: server.URL + "/account", NewOrderURL: server.URL + "/newOrder", RevokeCertURL: server.URL + "/revokeCert", KeyChangeURL: server.URL + "/keyChange", }) mux.HandleFunc("/nonce", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodHead { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } w.Header().Set("Replay-Nonce", "12345") w.Header().Set("Retry-After", "0") }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) return mux, server.URL } // WriteJSONResponse marshals the body as JSON and writes it to the response. func WriteJSONResponse(w http.ResponseWriter, body interface{}) error { bs, err := json.Marshal(body) if err != nil { return err } w.Header().Set("Content-Type", "application/json") if _, err := w.Write(bs); err != nil { return err } return nil } lego-4.9.1/platform/tester/env.go000066400000000000000000000064661434020463500167410ustar00rootroot00000000000000package tester import ( "fmt" "os" ) // EnvTest Environment variables manager for tests. type EnvTest struct { keys []string values map[string]string liveTestHook func() bool liveTestExtraHook func() bool domain string domainKey string } // NewEnvTest Creates an EnvTest. func NewEnvTest(keys ...string) *EnvTest { values := make(map[string]string) for _, key := range keys { value := os.Getenv(key) if value != "" { values[key] = value } } return &EnvTest{ keys: keys, values: values, } } // WithDomain Defines the name of the environment variable used to define the domain related to the DNS request. // If the domain is defined, it was considered mandatory to define a test as a "live" test. func (e *EnvTest) WithDomain(key string) *EnvTest { e.domainKey = key e.domain = os.Getenv(key) return e } // WithLiveTestRequirements Defines the environment variables required to define a test as a "live" test. // Replaces the default behavior (all keys are required). func (e *EnvTest) WithLiveTestRequirements(keys ...string) *EnvTest { var countValuedVars int for _, key := range keys { if e.domainKey != key && !e.isManagedKey(key) { panic(fmt.Sprintf("Unauthorized action, the env var %s is not managed or it's not the key of the domain.", key)) } if _, ok := e.values[key]; ok { countValuedVars++ } } live := countValuedVars != 0 && len(keys) == countValuedVars e.liveTestHook = func() bool { return live } return e } // WithLiveTestExtra Allows to define an additional condition to flag a test as "live" test. // This does not replace the default behavior. func (e *EnvTest) WithLiveTestExtra(extra func() bool) *EnvTest { e.liveTestExtraHook = extra return e } // GetDomain Gets the domain value associated with the DNS challenge (linked to WithDomain method). func (e *EnvTest) GetDomain() string { return e.domain } // IsLiveTest Checks whether environment variables allow running a "live" test. func (e *EnvTest) IsLiveTest() bool { liveTest := e.liveTestExtra() if e.liveTestHook != nil { return liveTest && e.liveTestHook() } liveTest = liveTest && len(e.values) == len(e.keys) if liveTest && e.domainKey != "" && e.domain == "" { return false } return liveTest } // RestoreEnv Restores the environment variables to the initial state. func (e *EnvTest) RestoreEnv() { for key, value := range e.values { os.Setenv(key, value) } } // ClearEnv Deletes all environment variables related to the test. func (e *EnvTest) ClearEnv() { for _, key := range e.keys { os.Unsetenv(key) } } // GetValue Gets the stored value of an environment variable. func (e *EnvTest) GetValue(key string) string { return e.values[key] } func (e *EnvTest) liveTestExtra() bool { if e.liveTestExtraHook == nil { return true } return e.liveTestExtraHook() } // Apply Sets/Unsets environment variables. // Not related to the main environment variables. func (e *EnvTest) Apply(envVars map[string]string) { for key, value := range envVars { if !e.isManagedKey(key) { panic(fmt.Sprintf("Unauthorized action, the env var %s is not managed.", key)) } if value == "" { os.Unsetenv(key) } else { os.Setenv(key, value) } } } func (e *EnvTest) isManagedKey(varName string) bool { for _, key := range e.keys { if key == varName { return true } } return false } lego-4.9.1/platform/tester/env_test.go000066400000000000000000000234561434020463500177760ustar00rootroot00000000000000package tester_test import ( "os" "strings" "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" ) const ( envNamespace = "LEGO_TEST_" envVar01 = envNamespace + "01" envVar02 = envNamespace + "02" envVarDomain = envNamespace + "DOMAIN" ) func TestMain(m *testing.M) { exitCode := m.Run() clearEnv() os.Exit(exitCode) } func applyEnv(envVars map[string]string) { for key, value := range envVars { if value == "" { os.Unsetenv(key) } else { os.Setenv(key, value) } } } func clearEnv() { environ := os.Environ() for _, key := range environ { if strings.HasPrefix(key, envNamespace) { os.Unsetenv(strings.Split(key, "=")[0]) } } os.Unsetenv("EXTRA_LEGO_TEST") } func TestEnvTest(t *testing.T) { testCases := []struct { desc string envVars map[string]string envTestSetup func() *tester.EnvTest expected func(t *testing.T, envTest *tester.EnvTest) }{ { desc: "simple", envVars: map[string]string{ envVar01: "A", envVar02: "B", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetDomain()) }, }, { desc: "missing env var", envVars: map[string]string{ envVar02: "B", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.False(t, envTest.IsLiveTest()) assert.Equal(t, "", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetDomain()) }, }, { desc: "WithDomain", envVars: map[string]string{ envVar01: "A", envVar02: "B", envVarDomain: "D", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02).WithDomain(envVarDomain) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetValue(envVarDomain)) assert.Equal(t, "D", envTest.GetDomain()) }, }, { desc: "WithDomain missing env var", envVars: map[string]string{ envVar01: "A", envVarDomain: "D", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02).WithDomain(envVarDomain) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.False(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetValue(envVarDomain)) assert.Equal(t, "D", envTest.GetDomain()) }, }, { desc: "WithDomain missing domain", envVars: map[string]string{ envVar01: "A", envVar02: "B", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02).WithDomain(envVarDomain) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.False(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetValue(envVarDomain)) assert.Equal(t, "", envTest.GetDomain()) }, }, { desc: "WithLiveTestRequirements", envVars: map[string]string{ envVar01: "A", envVar02: "B", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02).WithLiveTestRequirements(envVar02) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetDomain()) }, }, { desc: "WithLiveTestRequirements non required var missing", envVars: map[string]string{ envVar02: "B", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02).WithLiveTestRequirements(envVar02) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetDomain()) }, }, { desc: "WithLiveTestRequirements required var missing", envVars: map[string]string{ envVar01: "A", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02).WithLiveTestRequirements(envVar02) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.False(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetDomain()) }, }, { desc: "WithLiveTestRequirements WithDomain", envVars: map[string]string{ envVar01: "A", envVar02: "B", envVarDomain: "D", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02). WithDomain(envVarDomain). WithLiveTestRequirements(envVar02) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetValue(envVarDomain)) assert.Equal(t, "D", envTest.GetDomain()) }, }, { desc: "WithLiveTestRequirements WithDomain without domain", envVars: map[string]string{ envVar01: "A", envVar02: "B", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02). WithDomain(envVarDomain). WithLiveTestRequirements(envVar02) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetValue(envVarDomain)) assert.Equal(t, "", envTest.GetDomain()) }, }, { desc: "WithLiveTestExtra true", envVars: map[string]string{ envVar01: "A", envVar02: "B", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02). WithLiveTestExtra(func() bool { return true }) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetDomain()) }, }, { desc: "WithLiveTestExtra false", envVars: map[string]string{ envVar01: "A", envVar02: "B", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02). WithLiveTestExtra(func() bool { return false }) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.False(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetDomain()) }, }, { desc: "WithLiveTestRequirements WithLiveTestExtra true", envVars: map[string]string{ envVar01: "A", envVar02: "B", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02). WithLiveTestRequirements(envVar02). WithLiveTestExtra(func() bool { return true }) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.True(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetDomain()) }, }, { desc: "WithLiveTestRequirements WithLiveTestExtra false", envVars: map[string]string{ envVar01: "A", envVar02: "B", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02). WithLiveTestRequirements(envVar02). WithLiveTestExtra(func() bool { return false }) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.False(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "B", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetDomain()) }, }, { desc: "WithLiveTestRequirements require env var missing WithLiveTestExtra true", envVars: map[string]string{ envVar01: "A", }, envTestSetup: func() *tester.EnvTest { return tester.NewEnvTest(envVar01, envVar02). WithLiveTestRequirements(envVar02). WithLiveTestExtra(func() bool { return true }) }, expected: func(t *testing.T, envTest *tester.EnvTest) { assert.False(t, envTest.IsLiveTest()) assert.Equal(t, "A", envTest.GetValue(envVar01)) assert.Equal(t, "", envTest.GetValue(envVar02)) assert.Equal(t, "", envTest.GetDomain()) }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer clearEnv() applyEnv(test.envVars) envTest := test.envTestSetup() test.expected(t, envTest) }) } } func TestEnvTest_RestoreEnv(t *testing.T) { os.Setenv(envVar01, "A") os.Setenv(envVar02, "B") envTest := tester.NewEnvTest(envVar01, envVar02) clearEnv() envTest.RestoreEnv() assert.Equal(t, "A", os.Getenv(envVar01)) assert.Equal(t, "B", os.Getenv(envVar02)) } func TestEnvTest_ClearEnv(t *testing.T) { os.Setenv(envVar01, "A") os.Setenv(envVar02, "B") os.Setenv("EXTRA_LEGO_TEST", "X") envTest := tester.NewEnvTest(envVar01, envVar02) envTest.ClearEnv() assert.Equal(t, "", os.Getenv(envVar01)) assert.Equal(t, "", os.Getenv(envVar02)) assert.Equal(t, "X", os.Getenv("EXTRA_LEGO_TEST")) } lego-4.9.1/platform/wait/000077500000000000000000000000001434020463500152445ustar00rootroot00000000000000lego-4.9.1/platform/wait/wait.go000066400000000000000000000012601434020463500165360ustar00rootroot00000000000000package wait import ( "errors" "fmt" "time" "github.com/go-acme/lego/v4/log" ) // For polls the given function 'f', once every 'interval', up to 'timeout'. func For(msg string, timeout, interval time.Duration, f func() (bool, error)) error { log.Infof("Wait for %s [timeout: %s, interval: %s]", msg, timeout, interval) var lastErr error timeUp := time.After(timeout) for { select { case <-timeUp: if lastErr == nil { return errors.New("time limit exceeded") } return fmt.Errorf("time limit exceeded: last error: %w", lastErr) default: } stop, err := f() if stop { return nil } if err != nil { lastErr = err } time.Sleep(interval) } } lego-4.9.1/platform/wait/wait_test.go000066400000000000000000000006561434020463500176050ustar00rootroot00000000000000package wait import ( "testing" "time" ) func TestForTimeout(t *testing.T) { c := make(chan error) go func() { c <- For("", 3*time.Second, 1*time.Second, func() (bool, error) { return false, nil }) }() timeout := time.After(6 * time.Second) select { case <-timeout: t.Fatal("timeout exceeded") case err := <-c: if err == nil { t.Errorf("expected timeout error; got %v", err) } t.Logf("%v", err) } } lego-4.9.1/providers/000077500000000000000000000000001434020463500144715ustar00rootroot00000000000000lego-4.9.1/providers/dns/000077500000000000000000000000001434020463500152555ustar00rootroot00000000000000lego-4.9.1/providers/dns/acmedns/000077500000000000000000000000001434020463500166675ustar00rootroot00000000000000lego-4.9.1/providers/dns/acmedns/acmedns.go000066400000000000000000000142531434020463500206350ustar00rootroot00000000000000// Package acmedns implements a DNS provider for solving DNS-01 challenges using Joohoi's acme-dns project. // For more information see the ACME-DNS homepage: https://github.com/joohoi/acme-dns package acmedns import ( "errors" "fmt" "github.com/cpu/goacmedns" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) const ( // envNamespace is the prefix for ACME-DNS environment variables. envNamespace = "ACME_DNS_" // EnvAPIBase is the environment variable name for the ACME-DNS API address. // (e.g. https://acmedns.your-domain.com). EnvAPIBase = envNamespace + "API_BASE" // EnvStoragePath is the environment variable name for the ACME-DNS JSON account data file. // A per-domain account will be registered/persisted to this file and used for TXT updates. EnvStoragePath = envNamespace + "STORAGE_PATH" ) // acmeDNSClient is an interface describing the goacmedns.Client functions the DNSProvider uses. // It makes it easier for tests to shim a mock Client into the DNSProvider. type acmeDNSClient interface { // UpdateTXTRecord updates the provided account's TXT record // to the given value or returns an error. UpdateTXTRecord(goacmedns.Account, string) error // RegisterAccount registers and returns a new account // with the given allowFrom restriction or returns an error. RegisterAccount([]string) (goacmedns.Account, error) } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client acmeDNSClient storage goacmedns.Storage } // NewDNSProvider creates an ACME-DNS provider using file based account storage. // Its configuration is loaded from the environment by reading EnvAPIBase and EnvStoragePath. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIBase, EnvStoragePath) if err != nil { return nil, fmt.Errorf("acme-dns: %w", err) } client := goacmedns.NewClient(values[EnvAPIBase]) storage := goacmedns.NewFileStorage(values[EnvStoragePath], 0o600) return NewDNSProviderClient(client, storage) } // NewDNSProviderClient creates an ACME-DNS DNSProvider with the given acmeDNSClient and goacmedns.Storage. func NewDNSProviderClient(client acmeDNSClient, storage goacmedns.Storage) (*DNSProvider, error) { if client == nil { return nil, errors.New("ACME-DNS Client must be not nil") } if storage == nil { return nil, errors.New("ACME-DNS Storage must be not nil") } return &DNSProvider{ client: client, storage: storage, }, nil } // ErrCNAMERequired is returned by Present when the Domain indicated had no // existing ACME-DNS account in the Storage and additional setup is required. // The user must create a CNAME in the DNS zone for Domain that aliases FQDN // to Target in order to complete setup for the ACME-DNS account that was created. type ErrCNAMERequired struct { // The Domain that is being issued for. Domain string // The alias of the CNAME (left hand DNS label). FQDN string // The RDATA of the CNAME (right hand side, canonical name). Target string } // Error returns a descriptive message for the ErrCNAMERequired instance telling // the user that a CNAME needs to be added to the DNS zone of c.Domain before // the ACME-DNS hook will work. // The CNAME to be created should be of the form: {{ c.FQDN }} CNAME {{ c.Target }}. func (e ErrCNAMERequired) Error() string { return fmt.Sprintf("acme-dns: new account created for %q. "+ "To complete setup for %q you must provision the following "+ "CNAME in your DNS zone and re-run this provider when it is "+ "in place:\n"+ "%s CNAME %s.", e.Domain, e.Domain, e.FQDN, e.Target) } // Present creates a TXT record to fulfill the DNS-01 challenge. // If there is an existing account for the domain in the provider's storage // then it will be used to set the challenge response TXT record with the ACME-DNS server and issuance will continue. // If there is not an account for the given domain present in the DNSProvider storage // one will be created and registered with the ACME DNS server and an ErrCNAMERequired error is returned. // This will halt issuance and indicate to the user that a one-time manual setup is required for the domain. func (d *DNSProvider) Present(domain, _, keyAuth string) error { // Compute the challenge response FQDN and TXT value for the domain based // on the keyAuth. fqdn, value := dns01.GetRecord(domain, keyAuth) // Check if credentials were previously saved for this domain. // TODO(ldez) replace domain by FQDN to follow CNAME. account, err := d.storage.Fetch(domain) // Errors other than goacmeDNS.ErrDomainNotFound are unexpected. if err != nil && !errors.Is(err, goacmedns.ErrDomainNotFound) { return err } if errors.Is(err, goacmedns.ErrDomainNotFound) { // The account did not exist. Create a new one and return an error // indicating the required one-time manual CNAME setup. return d.register(domain, fqdn) } // Update the acme-dns TXT record. return d.client.UpdateTXTRecord(account, value) } // CleanUp removes the record matching the specified parameters. It is not // implemented for the ACME-DNS provider. func (d *DNSProvider) CleanUp(_, _, _ string) error { // ACME-DNS doesn't support the notion of removing a record. // For users of ACME-DNS it is expected the stale records remain in-place. return nil } // register creates a new ACME-DNS account for the given domain. // If account creation works as expected a ErrCNAMERequired error is returned describing // the one-time manual CNAME setup required to complete setup of the ACME-DNS hook for the domain. // If any other error occurs it is returned as-is. func (d *DNSProvider) register(domain, fqdn string) error { // TODO(@cpu): Read CIDR whitelists from the environment newAcct, err := d.client.RegisterAccount(nil) if err != nil { return err } // Store the new account in the storage and call save to persist the data. err = d.storage.Put(domain, newAcct) if err != nil { return err } err = d.storage.Save() if err != nil { return err } // Stop issuance by returning an error. // The user needs to perform a manual one-time CNAME setup in their DNS zone // to complete the setup of the new account we created. return ErrCNAMERequired{ Domain: domain, FQDN: fqdn, Target: newAcct.FullDomain, } } lego-4.9.1/providers/dns/acmedns/acmedns.toml000066400000000000000000000012471434020463500212020ustar00rootroot00000000000000Name = "Joohoi's ACME-DNS" Description = '''''' URL = "https://github.com/joohoi/acme-dns" Code = "acme-dns" Since = "v1.1.0" Example = ''' ACME_DNS_API_BASE=http://10.0.0.8:4443 \ ACME_DNS_STORAGE_PATH=/root/.lego-acme-dns-accounts.json \ lego --email you@example.com --dns acme-dns --domains my.example.org run ''' [Configuration] [Configuration.Credentials] ACME_DNS_API_BASE = "The ACME-DNS API address" ACME_DNS_STORAGE_PATH = "The ACME-DNS JSON account data file. A per-domain account will be registered/persisted to this file and used for TXT updates." [Links] API = "https://github.com/joohoi/acme-dns#api" GoClient = "https://github.com/cpu/goacmedns" lego-4.9.1/providers/dns/acmedns/acmedns_test.go000066400000000000000000000200101434020463500216600ustar00rootroot00000000000000package acmedns import ( "errors" "testing" "github.com/cpu/goacmedns" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( // errorClientErr is used by the Client mocks that return an error. errorClientErr = errors.New("errorClient always errors") // errorStorageErr is used by the Storage mocks that return an error. errorStorageErr = errors.New("errorStorage always errors") ) const ( // Fixed test data for unit tests. egDomain = "example.com" egFQDN = "_acme-challenge." + egDomain + "." egKeyAuth = "⚷" ) var egTestAccount = goacmedns.Account{ FullDomain: "acme-dns." + egDomain, SubDomain: "random-looking-junk." + egDomain, Username: "spooky.mulder", Password: "trustno1", } // mockClient is a mock implementing the acmeDNSClient interface that always // returns a fixed goacmedns.Account from calls to Register. type mockClient struct { mockAccount goacmedns.Account } // UpdateTXTRecord does nothing. func (c mockClient) UpdateTXTRecord(_ goacmedns.Account, _ string) error { return nil } // RegisterAccount returns c.mockAccount and no errors. func (c mockClient) RegisterAccount(_ []string) (goacmedns.Account, error) { return c.mockAccount, nil } // mockUpdateClient is a mock implementing the acmeDNSClient interface that // tracks the calls to UpdateTXTRecord in the records map. type mockUpdateClient struct { mockClient records map[goacmedns.Account]string } // UpdateTXTRecord saves a record value to c.records for the given acct. func (c mockUpdateClient) UpdateTXTRecord(acct goacmedns.Account, value string) error { c.records[acct] = value return nil } // errorRegisterClient is a mock implementing the acmeDNSClient interface that always // returns errors from errorUpdateClient. type errorUpdateClient struct { mockClient } // UpdateTXTRecord always returns an error. func (c errorUpdateClient) UpdateTXTRecord(_ goacmedns.Account, _ string) error { return errorClientErr } // errorRegisterClient is a mock implementing the acmeDNSClient interface that always // returns errors from RegisterAccount. type errorRegisterClient struct { mockClient } // RegisterAccount always returns an error. func (c errorRegisterClient) RegisterAccount(_ []string) (goacmedns.Account, error) { return goacmedns.Account{}, errorClientErr } // mockStorage is a mock implementing the goacmedns.Storage interface that // returns static account data and ignores Save. type mockStorage struct { accounts map[string]goacmedns.Account } // Save does nothing. func (m mockStorage) Save() error { return nil } // Put stores an account for the given domain in m.accounts. func (m mockStorage) Put(domain string, acct goacmedns.Account) error { m.accounts[domain] = acct return nil } // Fetch retrieves an account for the given domain from m.accounts or returns // goacmedns.ErrDomainNotFound. func (m mockStorage) Fetch(domain string) (goacmedns.Account, error) { if acct, ok := m.accounts[domain]; ok { return acct, nil } return goacmedns.Account{}, goacmedns.ErrDomainNotFound } // FetchAll returns all of m.accounts. func (m mockStorage) FetchAll() map[string]goacmedns.Account { return m.accounts } // errorPutStorage is a mock implementing the goacmedns.Storage interface that // always returns errors from Put. type errorPutStorage struct { mockStorage } // Put always errors. func (e errorPutStorage) Put(_ string, _ goacmedns.Account) error { return errorStorageErr } // errorSaveStorage is a mock implementing the goacmedns.Storage interface that // always returns errors from Save. type errorSaveStorage struct { mockStorage } // Save always errors. func (e errorSaveStorage) Save() error { return errorStorageErr } // errorFetchStorage is a mock implementing the goacmedns.Storage interface that // always returns errors from Fetch. type errorFetchStorage struct { mockStorage } // Fetch always errors. func (e errorFetchStorage) Fetch(_ string) (goacmedns.Account, error) { return goacmedns.Account{}, errorStorageErr } // FetchAll is a nop for errorFetchStorage. func (e errorFetchStorage) FetchAll() map[string]goacmedns.Account { return nil } // TestPresent tests that the ACME-DNS Present function for updating a DNS-01 // challenge response TXT record works as expected. func TestPresent(t *testing.T) { // validAccountStorage is a mockStorage configured to return the egTestAccount. validAccountStorage := mockStorage{ map[string]goacmedns.Account{ egDomain: egTestAccount, }, } // validUpdateClient is a mockClient configured with the egTestAccount that will // track TXT updates in a map. validUpdateClient := mockUpdateClient{ mockClient{egTestAccount}, make(map[goacmedns.Account]string), } testCases := []struct { Name string Client acmeDNSClient Storage goacmedns.Storage ExpectedError error }{ { Name: "present when client storage returns unexpected error", Client: mockClient{egTestAccount}, Storage: errorFetchStorage{}, ExpectedError: errorStorageErr, }, { Name: "present when client storage returns ErrDomainNotFound", Client: mockClient{egTestAccount}, ExpectedError: ErrCNAMERequired{ Domain: egDomain, FQDN: egFQDN, Target: egTestAccount.FullDomain, }, }, { Name: "present when client UpdateTXTRecord returns unexpected error", Client: errorUpdateClient{}, Storage: validAccountStorage, ExpectedError: errorClientErr, }, { Name: "present when everything works", Storage: validAccountStorage, Client: validUpdateClient, }, } for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { dp, err := NewDNSProviderClient(test.Client, mockStorage{make(map[string]goacmedns.Account)}) require.NoError(t, err) // override the storage mock if required by the test case. if test.Storage != nil { dp.storage = test.Storage } // call Present. The token argument can be garbage because the ACME-DNS // provider does not use it. err = dp.Present(egDomain, "foo", egKeyAuth) if test.ExpectedError != nil { assert.Equal(t, test.ExpectedError, err) } else { require.NoError(t, err) } }) } // Check that the success test case set a record. assert.Len(t, validUpdateClient.records, 1) // Check that the success test case set the right record for the right account. assert.Len(t, validUpdateClient.records[egTestAccount], 43) } // TestRegister tests that the ACME-DNS register function works correctly. func TestRegister(t *testing.T) { testCases := []struct { Name string Client acmeDNSClient Storage goacmedns.Storage Domain string FQDN string ExpectedError error }{ { Name: "register when acme-dns client returns an error", Client: errorRegisterClient{}, ExpectedError: errorClientErr, }, { Name: "register when acme-dns storage put returns an error", Client: mockClient{egTestAccount}, Storage: errorPutStorage{mockStorage{make(map[string]goacmedns.Account)}}, ExpectedError: errorStorageErr, }, { Name: "register when acme-dns storage save returns an error", Client: mockClient{egTestAccount}, Storage: errorSaveStorage{mockStorage{make(map[string]goacmedns.Account)}}, ExpectedError: errorStorageErr, }, { Name: "register when everything works", Client: mockClient{egTestAccount}, ExpectedError: ErrCNAMERequired{ Domain: egDomain, FQDN: egFQDN, Target: egTestAccount.FullDomain, }, }, } for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { dp, err := NewDNSProviderClient(test.Client, mockStorage{make(map[string]goacmedns.Account)}) require.NoError(t, err) // override the storage mock if required by the testcase. if test.Storage != nil { dp.storage = test.Storage } // Call register for the example domain/fqdn. err = dp.register(egDomain, egFQDN) if test.ExpectedError != nil { assert.Equal(t, test.ExpectedError, err) } else { require.NoError(t, err) } }) } } lego-4.9.1/providers/dns/alidns/000077500000000000000000000000001434020463500165275ustar00rootroot00000000000000lego-4.9.1/providers/dns/alidns/alidns.go000066400000000000000000000174471434020463500203450ustar00rootroot00000000000000// Package alidns implements a DNS provider for solving the DNS-01 challenge using Alibaba Cloud DNS. package alidns import ( "errors" "fmt" "strings" "time" "github.com/aliyun/alibaba-cloud-sdk-go/sdk" "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth" "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials" "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "golang.org/x/net/idna" ) const defaultRegionID = "cn-hangzhou" // Environment variables names. const ( envNamespace = "ALICLOUD_" EnvRAMRole = envNamespace + "RAM_ROLE" EnvAccessKey = envNamespace + "ACCESS_KEY" EnvSecretKey = envNamespace + "SECRET_KEY" EnvSecurityToken = envNamespace + "SECURITY_TOKEN" EnvRegionID = envNamespace + "REGION_ID" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { RAMRole string APIKey string SecretKey string SecurityToken string RegionID string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPTimeout time.Duration } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *alidns.Client } // NewDNSProvider returns a DNSProvider instance configured for Alibaba Cloud DNS. // - If you're using the instance RAM role, the RAM role environment variable must be passed in: ALICLOUD_RAM_ROLE. // - Other than that, credentials must be passed in the environment variables: // ALICLOUD_ACCESS_KEY, ALICLOUD_SECRET_KEY, and optionally ALICLOUD_SECURITY_TOKEN. func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.RegionID = env.GetOrFile(EnvRegionID) values, err := env.Get(EnvRAMRole) if err == nil { config.RAMRole = values[EnvRAMRole] return NewDNSProviderConfig(config) } values, err = env.Get(EnvAccessKey, EnvSecretKey) if err != nil { return nil, fmt.Errorf("alicloud: %w", err) } config.APIKey = values[EnvAccessKey] config.SecretKey = values[EnvSecretKey] config.SecurityToken = env.GetOrFile(EnvSecurityToken) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for alidns. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("alicloud: the configuration of the DNS provider is nil") } if config.RegionID == "" { config.RegionID = defaultRegionID } var credential auth.Credential switch { case config.RAMRole != "": credential = credentials.NewEcsRamRoleCredential(config.RAMRole) case config.APIKey != "" && config.SecretKey != "" && config.SecurityToken != "": credential = credentials.NewStsTokenCredential(config.APIKey, config.SecretKey, config.SecurityToken) case config.APIKey != "" && config.SecretKey != "": credential = credentials.NewAccessKeyCredential(config.APIKey, config.SecretKey) default: return nil, fmt.Errorf("alicloud: ram role or credentials missing") } conf := sdk.NewConfig().WithTimeout(config.HTTPTimeout) client, err := alidns.NewClientWithOptions(config.RegionID, conf, credential) if err != nil { return nil, fmt.Errorf("alicloud: credentials failed: %w", err) } return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zoneName, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("alicloud: %w", err) } recordAttributes, err := d.newTxtRecord(zoneName, fqdn, value) if err != nil { return err } _, err = d.client.AddDomainRecord(recordAttributes) if err != nil { return fmt.Errorf("alicloud: API call failed: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) records, err := d.findTxtRecords(fqdn) if err != nil { return fmt.Errorf("alicloud: %w", err) } _, err = d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("alicloud: %w", err) } for _, rec := range records { request := alidns.CreateDeleteDomainRecordRequest() request.RecordId = rec.RecordId _, err = d.client.DeleteDomainRecord(request) if err != nil { return fmt.Errorf("alicloud: %w", err) } } return nil } func (d *DNSProvider) getHostedZone(domain string) (string, error) { request := alidns.CreateDescribeDomainsRequest() var domains []alidns.DomainInDescribeDomains startPage := 1 for { request.PageNumber = requests.NewInteger(startPage) response, err := d.client.DescribeDomains(request) if err != nil { return "", fmt.Errorf("API call failed: %w", err) } domains = append(domains, response.Domains.Domain...) if response.PageNumber*response.PageSize >= response.TotalCount { break } startPage++ } authZone, err := dns01.FindZoneByFqdn(domain) if err != nil { return "", err } var hostedZone alidns.DomainInDescribeDomains for _, zone := range domains { if zone.DomainName == dns01.UnFqdn(authZone) || zone.PunyCode == dns01.UnFqdn(authZone) { hostedZone = zone } } if hostedZone.DomainId == "" { return "", fmt.Errorf("zone %s not found in AliDNS for domain %s", authZone, domain) } return hostedZone.DomainName, nil } func (d *DNSProvider) newTxtRecord(zone, fqdn, value string) (*alidns.AddDomainRecordRequest, error) { request := alidns.CreateAddDomainRecordRequest() request.Type = "TXT" request.DomainName = zone var err error request.RR, err = extractRecordName(fqdn, zone) if err != nil { return nil, err } request.Value = value request.TTL = requests.NewInteger(d.config.TTL) return request, nil } func (d *DNSProvider) findTxtRecords(fqdn string) ([]alidns.Record, error) { zoneName, err := d.getHostedZone(fqdn) if err != nil { return nil, err } request := alidns.CreateDescribeDomainRecordsRequest() request.DomainName = zoneName request.PageSize = requests.NewInteger(500) var records []alidns.Record result, err := d.client.DescribeDomainRecords(request) if err != nil { return records, fmt.Errorf("API call has failed: %w", err) } recordName, err := extractRecordName(fqdn, zoneName) if err != nil { return nil, err } for _, record := range result.DomainRecords.Record { if record.RR == recordName { records = append(records, record) } } return records, nil } func extractRecordName(fqdn, zone string) (string, error) { asciiDomain, err := idna.ToASCII(zone) if err != nil { return "", fmt.Errorf("fail to convert punycode: %w", err) } name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+asciiDomain); idx != -1 { return name[:idx], nil } return name, nil } lego-4.9.1/providers/dns/alidns/alidns.toml000066400000000000000000000023261434020463500207010ustar00rootroot00000000000000Name = "Alibaba Cloud DNS" Description = '''''' URL = "https://www.alibabacloud.com/product/dns" Code = "alidns" Since = "v1.1.0" Example = ''' # Setup using instance RAM role ALICLOUD_RAM_ROLE=lego \ lego --email you@example.com --dns alidns --domains my.example.org run # Or, using credentials ALICLOUD_ACCESS_KEY=abcdefghijklmnopqrstuvwx \ ALICLOUD_SECRET_KEY=your-secret-key \ ALICLOUD_SECURITY_TOKEN=your-sts-token \ lego --email you@example.com --dns alidns --domains my.example.org run ''' [Configuration] [Configuration.Credentials] ALICLOUD_RAM_ROLE = "Your instance RAM role (https://www.alibabacloud.com/help/doc-detail/54579.htm)" ALICLOUD_ACCESS_KEY = "Access key ID" ALICLOUD_SECRET_KEY = "Access Key secret" ALICLOUD_SECURITY_TOKEN = "STS Security Token (optional)" [Configuration.Additional] ALICLOUD_POLLING_INTERVAL = "Time between DNS propagation check" ALICLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" ALICLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" ALICLOUD_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.alibabacloud.com/help/doc-detail/42875.htm" GoClient = "https://github.com/aliyun/alibaba-cloud-sdk-go" lego-4.9.1/providers/dns/alidns/alidns_test.go000066400000000000000000000065171434020463500214000ustar00rootroot00000000000000package alidns import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAccessKey, EnvSecretKey, EnvRAMRole). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAccessKey: "123", EnvSecretKey: "456", }, }, { desc: "success (RAM role)", envVars: map[string]string{ EnvRAMRole: "LegoInstanceRole", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAccessKey: "", EnvSecretKey: "", }, expected: "alicloud: some credentials information are missing: ALICLOUD_ACCESS_KEY,ALICLOUD_SECRET_KEY", }, { desc: "missing access key", envVars: map[string]string{ EnvAccessKey: "", EnvSecretKey: "456", }, expected: "alicloud: some credentials information are missing: ALICLOUD_ACCESS_KEY", }, { desc: "missing secret key", envVars: map[string]string{ EnvAccessKey: "123", EnvSecretKey: "", }, expected: "alicloud: some credentials information are missing: ALICLOUD_SECRET_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string ramRole string apiKey string secretKey string expected string }{ { desc: "success", apiKey: "123", secretKey: "456", }, { desc: "success", ramRole: "LegoInstanceRole", }, { desc: "missing credentials", expected: "alicloud: ram role or credentials missing", }, { desc: "missing api key", secretKey: "456", expected: "alicloud: ram role or credentials missing", }, { desc: "missing secret key", apiKey: "123", expected: "alicloud: ram role or credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.SecretKey = test.secretKey config.RAMRole = test.ramRole p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/allinkl/000077500000000000000000000000001434020463500167035ustar00rootroot00000000000000lego-4.9.1/providers/dns/allinkl/allinkl.go000066400000000000000000000105151434020463500206620ustar00rootroot00000000000000// Package allinkl implements a DNS provider for solving the DNS-01 challenge using all-inkl. package allinkl import ( "errors" "fmt" "net/http" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/allinkl/internal" ) // Environment variables names. const ( envNamespace = "ALL_INKL_" EnvLogin = envNamespace + "LOGIN" EnvPassword = envNamespace + "PASSWORD" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Login string Password string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client recordIDs map[string]string recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for all-inkl. // Credentials must be passed in the environment variable: ALL_INKL_LOGIN, ALL_INKL_PASSWORD. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvLogin, EnvPassword) if err != nil { return nil, fmt.Errorf("allinkl: %w", err) } config := NewDefaultConfig() config.Login = values[EnvLogin] config.Password = values[EnvPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for all-inkl. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("allinkl: the configuration of the DNS provider is nil") } if config.Login == "" || config.Password == "" { return nil, errors.New("allinkl: missing credentials") } client := internal.NewClient(config.Login, config.Password) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]string), }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("allinkl: could not determine zone for domain %q: %w", domain, err) } credential, err := d.client.Authentication(60, true) if err != nil { return fmt.Errorf("allinkl: %w", err) } subDomain := dns01.UnFqdn(strings.TrimSuffix(fqdn, authZone)) record := internal.DNSRequest{ ZoneHost: authZone, RecordType: "TXT", RecordName: subDomain, RecordData: value, } recordID, err := d.client.AddDNSSettings(credential, record) if err != nil { return fmt.Errorf("allinkl: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = recordID d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) credential, err := d.client.Authentication(60, true) if err != nil { return fmt.Errorf("allinkl: %w", err) } // gets the record's unique ID from when we created it d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("allinkl: unknown record ID for '%s' '%s'", fqdn, token) } _, err = d.client.DeleteDNSSettings(credential, recordID) if err != nil { return fmt.Errorf("allinkl: %w", err) } return nil } lego-4.9.1/providers/dns/allinkl/allinkl.toml000066400000000000000000000013751434020463500212340ustar00rootroot00000000000000Name = "all-inkl" Description = '''''' URL = "https://all-inkl.com" Code = "allinkl" Since = "v4.5.0" Example = ''' ALL_INKL_LOGIN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ ALL_INKL_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ lego --email you@example.com --dns allinkl --domains my.example.org run ''' [Configuration] [Configuration.Credentials] ALL_INKL_LOGIN = "KAS login" ALL_INKL_PASSWORD = "KAS password" [Configuration.Additional] ALL_INKL_POLLING_INTERVAL = "Time between DNS propagation check" ALL_INKL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" ALL_INKL_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://kasapi.kasserver.com/dokumentation/phpdoc/index.html" Guide = "https://kasapi.kasserver.com/dokumentation/" lego-4.9.1/providers/dns/allinkl/allinkl_test.go000066400000000000000000000056661434020463500217340ustar00rootroot00000000000000package allinkl import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvLogin, EnvPassword).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvLogin: "user", EnvPassword: "secret", }, }, { desc: "missing credentials: account name", envVars: map[string]string{ EnvLogin: "", EnvPassword: "secret", }, expected: "allinkl: some credentials information are missing: ALL_INKL_LOGIN", }, { desc: "missing credentials: api key", envVars: map[string]string{ EnvLogin: "user", EnvPassword: "", }, expected: "allinkl: some credentials information are missing: ALL_INKL_PASSWORD", }, { desc: "missing credentials: all", envVars: map[string]string{ EnvLogin: "", EnvPassword: "", }, expected: "allinkl: some credentials information are missing: ALL_INKL_LOGIN,ALL_INKL_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string login string password string expected string }{ { desc: "success", login: "user", password: "secret", }, { desc: "missing account name", password: "secret", expected: "allinkl: missing credentials", }, { desc: "missing api key", login: "user", expected: "allinkl: missing credentials", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Login = test.login config.Password = test.password p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/allinkl/internal/000077500000000000000000000000001434020463500205175ustar00rootroot00000000000000lego-4.9.1/providers/dns/allinkl/internal/client.go000066400000000000000000000147011434020463500223270ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "encoding/xml" "fmt" "io" "net/http" "strconv" "strings" "time" "github.com/mitchellh/mapstructure" ) const ( authEndpoint = "https://kasapi.kasserver.com/soap/KasAuth.php" apiEndpoint = "https://kasapi.kasserver.com/soap/KasApi.php" ) // Client a KAS server client. type Client struct { login string password string authEndpoint string apiEndpoint string HTTPClient *http.Client floodTime time.Time } // NewClient creates a new Client. func NewClient(login string, password string) *Client { return &Client{ login: login, password: password, authEndpoint: authEndpoint, apiEndpoint: apiEndpoint, HTTPClient: &http.Client{Timeout: 10 * time.Second}, } } // Authentication Creates a credential token. // - sessionLifetime: Validity of the token in seconds. // - sessionUpdateLifetime: with `true` the session is extended with every request. func (c Client) Authentication(sessionLifetime int, sessionUpdateLifetime bool) (string, error) { sul := "N" if sessionUpdateLifetime { sul = "Y" } ar := AuthRequest{ Login: c.login, AuthData: c.password, AuthType: "plain", SessionLifetime: sessionLifetime, SessionUpdateLifetime: sul, } body, err := json.Marshal(ar) if err != nil { return "", fmt.Errorf("request marshal: %w", err) } payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAuthEnvelope, body))) req, err := http.NewRequest(http.MethodPost, c.authEndpoint, bytes.NewReader(payload)) if err != nil { return "", fmt.Errorf("request creation: %w", err) } resp, err := c.HTTPClient.Do(req) if err != nil { return "", fmt.Errorf("request execution: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { data, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("invalid status code: %d %s", resp.StatusCode, string(data)) } data, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("response read: %w", err) } var e KasAuthEnvelope decoder := xml.NewTokenDecoder(Trimmer{decoder: xml.NewDecoder(bytes.NewReader(data))}) err = decoder.Decode(&e) if err != nil { return "", fmt.Errorf("response xml decode: %w", err) } if e.Body.Fault != nil { return "", e.Body.Fault } return e.Body.KasAuthResponse.Return.Text, nil } // GetDNSSettings Reading out the DNS settings of a zone. // - zone: host zone. // - recordID: the ID of the resource record (optional). func (c *Client) GetDNSSettings(credentialToken, zone, recordID string) ([]ReturnInfo, error) { requestParams := map[string]string{"zone_host": zone} if recordID != "" { requestParams["record_id"] = recordID } item, err := c.do(credentialToken, "get_dns_settings", requestParams) if err != nil { return nil, err } raw := getValue(item) var g GetDNSSettingsAPIResponse err = mapstructure.Decode(raw, &g) if err != nil { return nil, fmt.Errorf("response struct decode: %w", err) } c.updateFloodTime(g.Response.KasFloodDelay) return g.Response.ReturnInfo, nil } // AddDNSSettings Creation of a DNS resource record. func (c *Client) AddDNSSettings(credentialToken string, record DNSRequest) (string, error) { item, err := c.do(credentialToken, "add_dns_settings", record) if err != nil { return "", err } raw := getValue(item) var g AddDNSSettingsAPIResponse err = mapstructure.Decode(raw, &g) if err != nil { return "", fmt.Errorf("response struct decode: %w", err) } c.updateFloodTime(g.Response.KasFloodDelay) return g.Response.ReturnInfo, nil } // DeleteDNSSettings Deleting a DNS Resource Record. func (c *Client) DeleteDNSSettings(credentialToken, recordID string) (bool, error) { requestParams := map[string]string{"record_id": recordID} item, err := c.do(credentialToken, "delete_dns_settings", requestParams) if err != nil { return false, err } raw := getValue(item) var g DeleteDNSSettingsAPIResponse err = mapstructure.Decode(raw, &g) if err != nil { return false, fmt.Errorf("response struct decode: %w", err) } c.updateFloodTime(g.Response.KasFloodDelay) return g.Response.ReturnInfo, nil } func (c Client) do(credentialToken, action string, requestParams interface{}) (*Item, error) { time.Sleep(time.Until(c.floodTime)) ar := KasRequest{ Login: c.login, AuthType: "session", AuthData: credentialToken, Action: action, RequestParams: requestParams, } body, err := json.Marshal(ar) if err != nil { return nil, fmt.Errorf("request marshal: %w", err) } payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAPIEnvelope, body))) req, err := http.NewRequest(http.MethodPost, c.apiEndpoint, bytes.NewReader(payload)) if err != nil { return nil, fmt.Errorf("request creation: %w", err) } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("request execution: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { data, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("invalid status code: %d %s", resp.StatusCode, string(data)) } data, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("response read: %w", err) } var e KasAPIResponseEnvelope decoder := xml.NewTokenDecoder(Trimmer{decoder: xml.NewDecoder(bytes.NewReader(data))}) err = decoder.Decode(&e) if err != nil { return nil, fmt.Errorf("response xml decode: %w", err) } if e.Body.Fault != nil { return nil, e.Body.Fault } return e.Body.KasAPIResponse.Return, nil } func (c *Client) updateFloodTime(delay float64) { c.floodTime = time.Now().Add(time.Duration(delay * float64(time.Second))) } func getValue(item *Item) interface{} { switch { case item.Raw != "": v, _ := strconv.ParseBool(item.Raw) return v case item.Text != "": switch item.Type { case "xsd:string": return item.Text case "xsd:float": v, _ := strconv.ParseFloat(item.Text, 64) return v case "xsd:int": v, _ := strconv.ParseInt(item.Text, 10, 64) return v default: return item.Text } case item.Value != nil: return getValue(item.Value) case len(item.Items) > 0 && item.Type == "SOAP-ENC:Array": var v []interface{} for _, i := range item.Items { v = append(v, getValue(i)) } return v case len(item.Items) > 0: v := map[string]interface{}{} for _, i := range item.Items { v[getKey(i)] = getValue(i) } return v default: return "" } } func getKey(item *Item) string { if item.Key == nil { return "" } return item.Key.Text } lego-4.9.1/providers/dns/allinkl/internal/client_test.go000066400000000000000000000101021434020463500233550ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_Authentication(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/", testHandler("auth.xml")) client := NewClient("user", "secret") client.authEndpoint = server.URL credentialToken, err := client.Authentication(60, false) require.NoError(t, err) assert.Equal(t, "593959ca04f0de9689b586c6a647d15d", credentialToken) } func TestClient_Authentication_error(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/", testHandler("auth_fault.xml")) client := NewClient("user", "secret") client.authEndpoint = server.URL _, err := client.Authentication(60, false) require.Error(t, err) } func TestClient_GetDNSSettings(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/", testHandler("get_dns_settings.xml")) client := NewClient("user", "secret") client.apiEndpoint = server.URL token := "sha1secret" records, err := client.GetDNSSettings(token, "example.com", "") require.NoError(t, err) expected := []ReturnInfo{ { ID: "57297429", Zone: "example.org", Name: "", Type: "A", Data: "10.0.0.1", Changeable: "Y", Aux: 0, }, { ID: int64(0), Zone: "example.org", Name: "", Type: "NS", Data: "ns5.kasserver.com.", Changeable: "N", Aux: 0, }, { ID: int64(0), Zone: "example.org", Name: "", Type: "NS", Data: "ns6.kasserver.com.", Changeable: "N", Aux: 0, }, { ID: "57297479", Zone: "example.org", Name: "*", Type: "A", Data: "10.0.0.1", Changeable: "Y", Aux: 0, }, { ID: "57297481", Zone: "example.org", Name: "", Type: "MX", Data: "user.kasserver.com.", Changeable: "Y", Aux: 10, }, { ID: "57297483", Zone: "example.org", Name: "", Type: "TXT", Data: "v=spf1 mx a ?all", Changeable: "Y", Aux: 0, }, { ID: "57297485", Zone: "example.org", Name: "_dmarc", Type: "TXT", Data: "v=DMARC1; p=none;", Changeable: "Y", Aux: 0, }, } assert.Equal(t, expected, records) } func TestClient_AddDNSSettings(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/", testHandler("add_dns_settings.xml")) client := NewClient("user", "secret") client.apiEndpoint = server.URL token := "sha1secret" record := DNSRequest{ ZoneHost: "42cnc.de.", RecordType: "TXT", RecordName: "lego", RecordData: "abcdefgh", } recordID, err := client.AddDNSSettings(token, record) require.NoError(t, err) assert.Equal(t, "57347444", recordID) } func TestClient_DeleteDNSSettings(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/", testHandler("delete_dns_settings.xml")) client := NewClient("user", "secret") client.apiEndpoint = server.URL token := "sha1secret" r, err := client.DeleteDNSSettings(token, "57347450") require.NoError(t, err) assert.True(t, r) } func testHandler(filename string) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } file, err := os.Open(filepath.Join("fixtures", filename)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } } } lego-4.9.1/providers/dns/allinkl/internal/fixtures/000077500000000000000000000000001434020463500223705ustar00rootroot00000000000000lego-4.9.1/providers/dns/allinkl/internal/fixtures/add_dns_settings.json000066400000000000000000000005601434020463500266000ustar00rootroot00000000000000{ "Request": { "KasRequestParams": { "record_aux": 0, "record_data": "abcdefgh", "record_name": "lego", "record_type": "TXT", "zone_host": "example.org." }, "KasRequestTime": 1625014992, "KasRequestType": true }, "Response": { "KasFloodDelay": 0.5, "ReturnInfo": "57347444", "ReturnString": "TRUE" } } lego-4.9.1/providers/dns/allinkl/internal/fixtures/add_dns_settings.xml000066400000000000000000000066061434020463500264360ustar00rootroot00000000000000 Request KasRequestTime 1625014992 KasRequestType KasRequestParams zone_host example.org. record_type TXT record_name lego record_data abcdefgh record_aux 0 Response KasFloodDelay 0.5 ReturnString TRUE ReturnInfo 57347444 lego-4.9.1/providers/dns/allinkl/internal/fixtures/auth.xml000066400000000000000000000011361434020463500240540ustar00rootroot00000000000000 593959ca04f0de9689b586c6a647d15d lego-4.9.1/providers/dns/allinkl/internal/fixtures/auth_fault.xml000066400000000000000000000006111434020463500252440ustar00rootroot00000000000000 SOAP-ENV:Client kas_login_syntax_incorrect KasAuth lego-4.9.1/providers/dns/allinkl/internal/fixtures/delete_dns_settings.json000066400000000000000000000003651434020463500273150ustar00rootroot00000000000000{ "Request": { "KasRequestParams": { "record_id": "57347444" }, "KasRequestTime": 1625016066, "KasRequestType": true }, "Response": { "KasFloodDelay": 0.5, "ReturnInfo": true, "ReturnString": "TRUE" } } lego-4.9.1/providers/dns/allinkl/internal/fixtures/delete_dns_settings.xml000066400000000000000000000046701434020463500271470ustar00rootroot00000000000000 Request KasRequestTime 1625016066 KasRequestType KasRequestParams record_id 57347444 Response KasFloodDelay 0.5 ReturnString TRUE ReturnInfo lego-4.9.1/providers/dns/allinkl/internal/fixtures/get_dns_settings.json000066400000000000000000000036331434020463500266330ustar00rootroot00000000000000{ "Request": { "KasRequestParams": { "zone_host": "example.org" }, "KasRequestTime": 1625012975, "KasRequestType": true }, "Response": { "KasFloodDelay": 0.5, "ReturnInfo": [ { "record_aux": 0, "record_changeable": "Y", "record_data": "10.0.0.1", "record_id": "57297429", "record_name": "", "record_type": "A", "record_zone": "example.org" }, { "record_aux": 0, "record_changeable": "N", "record_data": "ns5.kasserver.com.", "record_id": 0, "record_name": "", "record_type": "NS", "record_zone": "example.org" }, { "record_aux": 0, "record_changeable": "N", "record_data": "ns6.kasserver.com.", "record_id": 0, "record_name": "", "record_type": "NS", "record_zone": "example.org" }, { "record_aux": 0, "record_changeable": "Y", "record_data": "10.0.0.1", "record_id": "57297479", "record_name": "*", "record_type": "A", "record_zone": "example.org" }, { "record_aux": 10, "record_changeable": "Y", "record_data": "user.kasserver.com.", "record_id": "57297481", "record_name": "", "record_type": "MX", "record_zone": "example.org" }, { "record_aux": 0, "record_changeable": "Y", "record_data": "v=spf1 mx a ?all", "record_id": "57297483", "record_name": "", "record_type": "TXT", "record_zone": "example.org" }, { "record_aux": 0, "record_changeable": "Y", "record_data": "v=DMARC1; p=none;", "record_id": "57297485", "record_name": "_dmarc", "record_type": "TXT", "record_zone": "example.org" } ], "ReturnString": "TRUE" } } lego-4.9.1/providers/dns/allinkl/internal/fixtures/get_dns_settings.xml000066400000000000000000000367121434020463500264660ustar00rootroot00000000000000 Request KasRequestTime 1624993260 KasRequestType KasRequestParams zone_host example.org Response KasFloodDelay 0.5 ReturnString TRUE ReturnInfo record_zone example.org record_name record_type A record_data 10.0.0.1 record_aux 0 record_id 57297429 record_changeable Y record_zone example.org record_name record_type NS record_data ns5.kasserver.com. record_aux 0 record_id 0 record_changeable N record_zone example.org record_name record_type NS record_data ns6.kasserver.com. record_aux 0 record_id 0 record_changeable N record_zone example.org record_name * record_type A record_data 10.0.0.1 record_aux 0 record_id 57297479 record_changeable Y record_zone example.org record_name record_type MX record_data user.kasserver.com. record_aux 10 record_id 57297481 record_changeable Y record_zone example.org record_name record_type TXT record_data v=spf1 mx a ?all record_aux 0 record_id 57297483 record_changeable Y record_zone example.org record_name _dmarc record_type TXT record_data v=DMARC1; p=none; record_aux 0 record_id 57297485 record_changeable Y lego-4.9.1/providers/dns/allinkl/internal/types.go000066400000000000000000000020121434020463500222050ustar00rootroot00000000000000package internal import ( "bytes" "encoding/xml" "fmt" ) // Trimmer trim all XML fields. type Trimmer struct { decoder *xml.Decoder } func (tr Trimmer) Token() (xml.Token, error) { t, err := tr.decoder.Token() if cd, ok := t.(xml.CharData); ok { t = xml.CharData(bytes.TrimSpace(cd)) } return t, err } // Fault a SOAP fault. type Fault struct { Code string `xml:"faultcode"` Message string `xml:"faultstring"` Actor string `xml:"faultactor"` } func (f Fault) Error() string { return fmt.Sprintf("%s: %s: %s", f.Actor, f.Code, f.Message) } // KasResponse a KAS SOAP response. type KasResponse struct { Return *Item `xml:"return"` } // Item an item of the KAS SOAP response. type Item struct { Text string `xml:",chardata" json:"text,omitempty"` Type string `xml:"type,attr" json:"type,omitempty"` Raw string `xml:"nil,attr" json:"raw,omitempty"` Key *Item `xml:"key" json:"key,omitempty"` Value *Item `xml:"value" json:"value,omitempty"` Items []*Item `xml:"item" json:"item,omitempty"` } lego-4.9.1/providers/dns/allinkl/internal/types_api.go000066400000000000000000000061611434020463500230470ustar00rootroot00000000000000package internal import "encoding/xml" // kasAPIEnvelope a KAS API request envelope. const kasAPIEnvelope = ` %s ` // KasAPIResponseEnvelope a KAS envelope of the API response. type KasAPIResponseEnvelope struct { XMLName xml.Name `xml:"Envelope"` Body KasAPIBody `xml:"Body"` } type KasAPIBody struct { KasAPIResponse *KasResponse `xml:"KasApiResponse"` Fault *Fault `xml:"Fault"` } // --- type KasRequest struct { // Login the relevant KAS login. Login string `json:"kas_login,omitempty"` // AuthType the authentication type. AuthType string `json:"kas_auth_type,omitempty"` // AuthData the authentication data. AuthData string `json:"kas_auth_data,omitempty"` // Action API function. Action string `json:"kas_action,omitempty"` // RequestParams Parameters to the API function. RequestParams interface{} `json:"KasRequestParams,omitempty"` } type DNSRequest struct { // ZoneHost the zone in question (must be a FQDN). ZoneHost string `json:"zone_host"` // RecordType the TYPE of the resource record (MX, A, AAAA etc.). RecordType string `json:"record_type"` // RecordName the NAME of the resource record. RecordName string `json:"record_name"` // RecordData the DATA of the resource record. RecordData string `json:"record_data"` // RecordAux the AUX of the resource record. RecordAux int `json:"record_aux"` } // --- type GetDNSSettingsAPIResponse struct { Response GetDNSSettingsResponse `json:"Response" mapstructure:"Response"` } type GetDNSSettingsResponse struct { KasFloodDelay float64 `json:"KasFloodDelay" mapstructure:"KasFloodDelay"` ReturnInfo []ReturnInfo `json:"ReturnInfo" mapstructure:"ReturnInfo"` ReturnString string `json:"ReturnString"` } type ReturnInfo struct { ID interface{} `json:"record_id,omitempty" mapstructure:"record_id"` Zone string `json:"record_zone,omitempty" mapstructure:"record_zone"` Name string `json:"record_name,omitempty" mapstructure:"record_name"` Type string `json:"record_type,omitempty" mapstructure:"record_type"` Data string `json:"record_data,omitempty" mapstructure:"record_data"` Changeable string `json:"record_changeable,omitempty" mapstructure:"record_changeable"` Aux int `json:"record_aux,omitempty" mapstructure:"record_aux"` } type AddDNSSettingsAPIResponse struct { Response AddDNSSettingsResponse `json:"Response" mapstructure:"Response"` } type AddDNSSettingsResponse struct { KasFloodDelay float64 `json:"KasFloodDelay" mapstructure:"KasFloodDelay"` ReturnInfo string `json:"ReturnInfo" mapstructure:"ReturnInfo"` ReturnString string `json:"ReturnString" mapstructure:"ReturnString"` } type DeleteDNSSettingsAPIResponse struct { Response DeleteDNSSettingsResponse `json:"Response"` } type DeleteDNSSettingsResponse struct { KasFloodDelay float64 `json:"KasFloodDelay"` ReturnInfo bool `json:"ReturnInfo"` ReturnString string `json:"ReturnString"` } lego-4.9.1/providers/dns/allinkl/internal/types_auth.go000066400000000000000000000017031434020463500232340ustar00rootroot00000000000000package internal import "encoding/xml" // kasAuthEnvelope a KAS authentication request envelope. const kasAuthEnvelope = ` %s ` // KasAuthEnvelope a KAS envelope of the authentication response. type KasAuthEnvelope struct { XMLName xml.Name `xml:"Envelope"` Body KasAuthBody `xml:"Body"` } type KasAuthBody struct { KasAuthResponse *KasResponse `xml:"KasAuthResponse"` Fault *Fault `xml:"Fault"` } // --- type AuthRequest struct { Login string `json:"kas_login,omitempty"` AuthData string `json:"kas_auth_data,omitempty"` AuthType string `json:"kas_auth_type,omitempty"` SessionLifetime int `json:"session_lifetime,omitempty"` SessionUpdateLifetime string `json:"session_update_lifetime,omitempty"` } lego-4.9.1/providers/dns/arvancloud/000077500000000000000000000000001434020463500174135ustar00rootroot00000000000000lego-4.9.1/providers/dns/arvancloud/arvancloud.go000066400000000000000000000115211434020463500221000ustar00rootroot00000000000000// Package arvancloud implements a DNS provider for solving the DNS-01 challenge using ArvanCloud DNS. package arvancloud import ( "errors" "fmt" "net/http" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/arvancloud/internal" ) const minTTL = 600 // Environment variables names. const ( envNamespace = "ARVANCLOUD_" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client recordIDs map[string]string recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for ArvanCloud. // Credentials must be passed in the environment variable: ARVANCLOUD_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("arvancloud: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for ArvanCloud. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("arvancloud: the configuration of the DNS provider is nil") } if config.APIKey == "" { return nil, errors.New("arvancloud: credentials missing") } if config.TTL < minTTL { return nil, fmt.Errorf("arvancloud: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } client := internal.NewClient(config.APIKey) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]string), }, nil } // Timeout returns the timeout and interval to use when checking for DNS // propagation. Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := getZone(fqdn) if err != nil { return err } record := internal.DNSRecord{ Type: "txt", Name: extractRecordName(fqdn, authZone), Value: internal.TXTRecordValue{Text: value}, TTL: d.config.TTL, UpstreamHTTPS: "default", IPFilterMode: &internal.IPFilterMode{ Count: "single", GeoFilter: "none", Order: "none", }, } newRecord, err := d.client.CreateRecord(authZone, record) if err != nil { return fmt.Errorf("arvancloud: failed to add TXT record: fqdn=%s: %w", fqdn, err) } d.recordIDsMu.Lock() d.recordIDs[token] = newRecord.ID d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) authZone, err := getZone(fqdn) if err != nil { return err } // gets the record's unique ID from when we created it d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("arvancloud: unknown record ID for '%s' '%s'", fqdn, token) } if err := d.client.DeleteRecord(authZone, recordID); err != nil { return fmt.Errorf("arvancloud: failed to delate TXT record: id=%s: %w", recordID, err) } // deletes record ID from map d.recordIDsMu.Lock() delete(d.recordIDs, token) d.recordIDsMu.Unlock() return nil } func getZone(fqdn string) (string, error) { authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", err } return dns01.UnFqdn(authZone), nil } func extractRecordName(fqdn, zone string) string { name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+zone); idx != -1 { return name[:idx] } return name } lego-4.9.1/providers/dns/arvancloud/arvancloud.toml000066400000000000000000000013231434020463500224450ustar00rootroot00000000000000Name = "ArvanCloud" Description = '''''' URL = "https://arvancloud.com" Code = "arvancloud" Since = "v3.8.0" Example = ''' ARVANCLOUD_API_KEY="Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \ lego --email you@example.com --dns arvancloud --domains my.example.org run ''' [Configuration] [Configuration.Credentials] ARVANCLOUD_API_KEY = "API key" [Configuration.Additional] ARVANCLOUD_POLLING_INTERVAL = "Time between DNS propagation check" ARVANCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" ARVANCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" ARVANCLOUD_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.arvancloud.com/docs/api/cdn/4.0" lego-4.9.1/providers/dns/arvancloud/arvancloud_test.go000066400000000000000000000046121434020463500231420ustar00rootroot00000000000000package arvancloud import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIKey: "", }, expected: "arvancloud: some credentials information are missing: ARVANCLOUD_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string ttl int expected string }{ { desc: "success", ttl: minTTL, apiKey: "123", }, { desc: "missing credentials", ttl: minTTL, expected: "arvancloud: credentials missing", }, { desc: "invalid TTL", apiKey: "123", ttl: 60, expected: "arvancloud: invalid TTL, TTL (60) must be greater than 600", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.TTL = test.ttl p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/arvancloud/internal/000077500000000000000000000000001434020463500212275ustar00rootroot00000000000000lego-4.9.1/providers/dns/arvancloud/internal/client.go000066400000000000000000000115451434020463500230420ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "path" "strings" ) // defaultBaseURL represents the API endpoint to call. const defaultBaseURL = "https://napi.arvancloud.com" const authHeader = "Authorization" // Client the ArvanCloud client. type Client struct { HTTPClient *http.Client BaseURL string apiKey string } // NewClient Creates a new ArvanCloud client. func NewClient(apiKey string) *Client { return &Client{ HTTPClient: http.DefaultClient, BaseURL: defaultBaseURL, apiKey: apiKey, } } // GetTxtRecord gets a TXT record. func (c *Client) GetTxtRecord(domain, name, value string) (*DNSRecord, error) { records, err := c.getRecords(domain, name) if err != nil { return nil, err } for _, record := range records { if equalsTXTRecord(record, name, value) { return &record, nil } } return nil, fmt.Errorf("could not find record: Domain: %s; Record: %s", domain, name) } // https://www.arvancloud.com/docs/api/cdn/4.0#operation/dns_records.list func (c *Client) getRecords(domain, search string) ([]DNSRecord, error) { endpoint, err := c.createEndpoint("cdn", "4.0", "domains", domain, "dns-records") if err != nil { return nil, fmt.Errorf("failed to create endpoint: %w", err) } if search != "" { query := endpoint.Query() query.Set("search", strings.ReplaceAll(search, "_", "")) endpoint.RawQuery = query.Encode() } resp, err := c.do(http.MethodGet, endpoint.String(), nil) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("could not get records %s: Domain: %s; Status: %s; Body: %s", search, domain, resp.Status, string(body)) } response := &apiResponse{} err = json.Unmarshal(body, response) if err != nil { return nil, fmt.Errorf("failed to decode response body: %w", err) } var records []DNSRecord err = json.Unmarshal(response.Data, &records) if err != nil { return nil, fmt.Errorf("failed to decode records: %w", err) } return records, nil } // CreateRecord creates a DNS record. // https://www.arvancloud.com/docs/api/cdn/4.0#operation/dns_records.create func (c *Client) CreateRecord(domain string, record DNSRecord) (*DNSRecord, error) { reqBody, err := json.Marshal(record) if err != nil { return nil, err } endpoint, err := c.createEndpoint("cdn", "4.0", "domains", domain, "dns-records") if err != nil { return nil, fmt.Errorf("failed to create endpoint: %w", err) } resp, err := c.do(http.MethodPost, endpoint.String(), bytes.NewReader(reqBody)) if err != nil { return nil, err } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } if resp.StatusCode != http.StatusCreated { return nil, fmt.Errorf("could not create record %s; Domain: %s; Status: %s; Body: %s", string(reqBody), domain, resp.Status, string(body)) } response := &apiResponse{} err = json.Unmarshal(body, response) if err != nil { return nil, fmt.Errorf("failed to decode response body: %w", err) } var newRecord DNSRecord err = json.Unmarshal(response.Data, &newRecord) if err != nil { return nil, fmt.Errorf("failed to decode record: %w", err) } return &newRecord, nil } // DeleteRecord deletes a DNS record. // https://www.arvancloud.com/docs/api/cdn/4.0#operation/dns_records.remove func (c *Client) DeleteRecord(domain, id string) error { endpoint, err := c.createEndpoint("cdn", "4.0", "domains", domain, "dns-records", id) if err != nil { return fmt.Errorf("failed to create endpoint: %w", err) } resp, err := c.do(http.MethodDelete, endpoint.String(), nil) if err != nil { return err } if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("could not delete record %s; Domain: %s; Status: %s; Body: %s", id, domain, resp.Status, string(body)) } return nil } func (c *Client) do(method, endpoint string, body io.Reader) (*http.Response, error) { req, err := http.NewRequest(method, endpoint, body) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") if body != nil { req.Header.Set("Content-Type", "application/json") } req.Header.Set(authHeader, c.apiKey) return c.HTTPClient.Do(req) } func (c *Client) createEndpoint(parts ...string) (*url.URL, error) { baseURL, err := url.Parse(c.BaseURL) if err != nil { return nil, err } endpoint, err := baseURL.Parse(path.Join(parts...)) if err != nil { return nil, err } return endpoint, nil } func equalsTXTRecord(record DNSRecord, name, value string) bool { if record.Type != "txt" { return false } if record.Name != name { return false } data, ok := record.Value.(map[string]interface{}) if !ok { return false } return data["text"] == value } lego-4.9.1/providers/dns/arvancloud/internal/client_test.go000066400000000000000000000071301434020463500240740ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_GetTxtRecord(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) const domain = "example.com" const apiKey = "myKeyA" mux.HandleFunc("/cdn/4.0/domains/"+domain+"/dns-records", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } auth := req.Header.Get(authHeader) if auth != apiKey { http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) return } file, err := os.Open("./fixtures/get_txt_record.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient(apiKey) client.BaseURL = server.URL _, err := client.GetTxtRecord(domain, "_acme-challenge", "txtxtxt") require.NoError(t, err) } func TestClient_CreateRecord(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) const domain = "example.com" const apiKey = "myKeyB" mux.HandleFunc("/cdn/4.0/domains/"+domain+"/dns-records", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } auth := req.Header.Get(authHeader) if auth != apiKey { http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) return } file, err := os.Open("./fixtures/create_txt_record.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() rw.WriteHeader(http.StatusCreated) _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient(apiKey) client.BaseURL = server.URL record := DNSRecord{ Name: "_acme-challenge", Type: "txt", Value: &TXTRecordValue{Text: "txtxtxt"}, TTL: 600, } newRecord, err := client.CreateRecord(domain, record) require.NoError(t, err) expected := &DNSRecord{ ID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", Type: "txt", Value: map[string]interface{}{"text": "txtxtxt"}, Name: "_acme-challenge", TTL: 120, UpstreamHTTPS: "default", IPFilterMode: &IPFilterMode{ Count: "single", Order: "none", GeoFilter: "none", }, } assert.Equal(t, expected, newRecord) } func TestClient_DeleteRecord(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) const domain = "example.com" const apiKey = "myKeyC" const recordID = "recordId" mux.HandleFunc("/cdn/4.0/domains/"+domain+"/dns-records/"+recordID, func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodDelete { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } auth := req.Header.Get(authHeader) if auth != apiKey { http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) return } }) client := NewClient(apiKey) client.BaseURL = server.URL err := client.DeleteRecord(domain, recordID) require.NoError(t, err) } lego-4.9.1/providers/dns/arvancloud/internal/fixtures/000077500000000000000000000000001434020463500231005ustar00rootroot00000000000000lego-4.9.1/providers/dns/arvancloud/internal/fixtures/create_txt_record.json000066400000000000000000000011631434020463500274740ustar00rootroot00000000000000{ "data": { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "type": "txt", "name": "_acme-challenge", "value": { "text": "txtxtxt" }, "ttl": 120, "cloud": false, "upstream_https": "default", "ip_filter_mode": { "count": "single", "order": "none", "geo_filter": "none" }, "can_delete": true, "health_check_status": false, "health_check_setting": { "protocol": "http", "port": "", "uri": "" }, "created_at": "2020-05-27T23:57:02Z", "updated_at": "2020-05-27T23:57:02Z" }, "message": "DNS record created successfully" } lego-4.9.1/providers/dns/arvancloud/internal/fixtures/get_txt_record.json000066400000000000000000000132701434020463500270120ustar00rootroot00000000000000{ "data": [ { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "type": "a", "name": "@", "value": [ { "ip": "xx.xxx.xxx.xxx", "port": null, "weight": 1, "country": "" } ], "ttl": 120, "cloud": true, "upstream_https": "default", "ip_filter_mode": { "count": "single", "order": "none", "geo_filter": "none" }, "can_delete": true, "health_check_status": false, "health_check_setting": { "protocol": "http", "port": "", "uri": "" }, "created_at": "2020-05-19T15:05:12Z", "updated_at": "2020-05-23T22:06:00Z" }, { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "type": "a", "name": "www", "value": [ { "ip": "xx.xxx.xxx.xxx", "port": null, "weight": 1, "country": "" } ], "ttl": 120, "cloud": true, "upstream_https": "default", "ip_filter_mode": { "count": "single", "order": "none", "geo_filter": "none" }, "can_delete": true, "health_check_status": false, "health_check_setting": { "protocol": "http", "port": "", "uri": "" }, "created_at": "2020-05-19T15:05:12Z", "updated_at": "2020-05-23T22:05:55Z" }, { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "type": "a", "name": "thatcher", "value": [ { "ip": "xx.xxx.xxx.xxx", "port": null, "weight": 100, "country": "" } ], "ttl": 120, "cloud": false, "upstream_https": "default", "ip_filter_mode": { "count": "single", "order": "none", "geo_filter": "none" }, "can_delete": true, "health_check_status": false, "health_check_setting": { "protocol": "http", "port": "", "uri": "" }, "created_at": "2020-05-20T18:45:10Z", "updated_at": "2020-05-21T13:19:46Z" }, { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "type": "a", "name": "api", "value": [ { "ip": "xx.xxx.xxx.xxx", "port": null, "weight": 100, "country": "" } ], "ttl": 120, "cloud": true, "upstream_https": "default", "ip_filter_mode": { "count": "single", "order": "none", "geo_filter": "none" }, "can_delete": true, "health_check_status": false, "health_check_setting": { "protocol": "http", "port": "", "uri": "" }, "created_at": "2020-05-20T18:45:35Z", "updated_at": "2020-05-22T20:22:27Z" }, { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "type": "a", "name": "rock", "value": [ { "ip": "xx.xxx.xxx.xxx", "port": null, "weight": 100, "country": "" } ], "ttl": 120, "cloud": true, "upstream_https": "default", "ip_filter_mode": { "count": "single", "order": "none", "geo_filter": "none" }, "can_delete": true, "health_check_status": false, "health_check_setting": { "protocol": "http", "port": "", "uri": "" }, "created_at": "2020-05-22T10:29:27Z", "updated_at": "2020-05-22T13:35:26Z" }, { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "type": "ns", "name": "@", "value": { "host": "z.ns.arvancdn.com." }, "ttl": 7200, "cloud": false, "upstream_https": "default", "ip_filter_mode": { "count": "single", "order": "none", "geo_filter": "none" }, "can_delete": false, "health_check_status": false, "health_check_setting": { "protocol": "http", "port": "", "uri": "" }, "created_at": "2020-05-19T15:05:09Z", "updated_at": "2020-05-19T15:05:09Z" }, { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "type": "ns", "name": "@", "value": { "host": "g.ns.arvancdn.com." }, "ttl": 7200, "cloud": false, "upstream_https": "default", "ip_filter_mode": { "count": "single", "order": "none", "geo_filter": "none" }, "can_delete": false, "health_check_status": false, "health_check_setting": { "protocol": "http", "port": "", "uri": "" }, "created_at": "2020-05-19T15:05:12Z", "updated_at": "2020-05-19T15:05:12Z" }, { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "type": "txt", "name": "_acme-challenge", "value": { "text": "txtxtxt" }, "ttl": 120, "cloud": false, "upstream_https": "default", "ip_filter_mode": { "count": "single", "order": "none", "geo_filter": "none" }, "can_delete": true, "health_check_status": false, "health_check_setting": { "protocol": "http", "port": "", "uri": "" }, "created_at": "2020-05-27T20:53:54Z", "updated_at": "2020-05-27T20:53:54Z" } ], "links": { "first": "https://napi.arvancloud.com/4.0/domains/example.ir/dns-records?page=1", "last": "https://napi.arvancloud.com/4.0/domains/example.ir/dns-records?page=1", "prev": null, "next": null }, "meta": { "current_page": 1, "from": 1, "last_page": 1, "path": "https://napi.arvancloud.com/4.0/domains/example.ir/dns-records", "per_page": 300, "to": 8, "total": 8 } } lego-4.9.1/providers/dns/arvancloud/internal/model.go000066400000000000000000000016401434020463500226570ustar00rootroot00000000000000package internal import "encoding/json" type apiResponse struct { Message string `json:"message"` Data json.RawMessage `json:"data"` } // DNSRecord a DNS record. type DNSRecord struct { ID string `json:"id,omitempty"` Type string `json:"type"` Value interface{} `json:"value,omitempty"` Name string `json:"name,omitempty"` TTL int `json:"ttl,omitempty"` UpstreamHTTPS string `json:"upstream_https,omitempty"` IPFilterMode *IPFilterMode `json:"ip_filter_mode,omitempty"` } // TXTRecordValue represents a TXT record value. type TXTRecordValue struct { Text string `json:"text,omitempty"` // only for TXT Record. } // IPFilterMode a DNS ip_filter_mode. type IPFilterMode struct { Count string `json:"count,omitempty"` Order string `json:"order,omitempty"` GeoFilter string `json:"geo_filter,omitempty"` } lego-4.9.1/providers/dns/auroradns/000077500000000000000000000000001434020463500172535ustar00rootroot00000000000000lego-4.9.1/providers/dns/auroradns/auroradns.go000066400000000000000000000127731434020463500216120ustar00rootroot00000000000000// Package auroradns implements a DNS provider for solving the DNS-01 challenge using Aurora DNS. package auroradns import ( "errors" "fmt" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/nrdcg/auroradns" ) const defaultBaseURL = "https://api.auroradns.eu" // Environment variables names. const ( envNamespace = "AURORA_" EnvAPIKey = envNamespace + "API_KEY" EnvSecret = envNamespace + "SECRET" EnvEndpoint = envNamespace + "ENDPOINT" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string APIKey string Secret string PropagationTimeout time.Duration PollingInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 300), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { recordIDs map[string]string recordIDsMu sync.Mutex config *Config client *auroradns.Client } // NewDNSProvider returns a DNSProvider instance configured for AuroraDNS. // Credentials must be passed in the environment variables: // AURORA_API_KEY and AURORA_SECRET. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey, EnvSecret) if err != nil { return nil, fmt.Errorf("aurora: %w", err) } config := NewDefaultConfig() config.BaseURL = env.GetOrFile(EnvEndpoint) config.APIKey = values[EnvAPIKey] config.Secret = values[EnvSecret] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for AuroraDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("aurora: the configuration of the DNS provider is nil") } if config.APIKey == "" || config.Secret == "" { return nil, errors.New("aurora: some credentials information are missing") } if config.BaseURL == "" { config.BaseURL = defaultBaseURL } tr, err := auroradns.NewTokenTransport(config.APIKey, config.Secret) if err != nil { return nil, fmt.Errorf("aurora: %w", err) } client, err := auroradns.NewClient(tr.Client(), auroradns.WithBaseURL(config.BaseURL)) if err != nil { return nil, fmt.Errorf("aurora: %w", err) } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]string), }, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("aurora: could not determine zone for domain %q: %w", domain, err) } // 1. Aurora will happily create the TXT record when it is provided a fqdn, // but it will only appear in the control panel and will not be // propagated to DNS servers. Extract and use subdomain instead. // 2. A trailing dot in the fqdn will cause Aurora to add a trailing dot to // the subdomain, resulting in _acme-challenge.. rather // than _acme-challenge. subdomain := fqdn[0 : len(fqdn)-len(authZone)-1] authZone = dns01.UnFqdn(authZone) zone, err := d.getZoneInformationByName(authZone) if err != nil { return fmt.Errorf("aurora: could not create record: %w", err) } record := auroradns.Record{ RecordType: "TXT", Name: subdomain, Content: value, TTL: d.config.TTL, } newRecord, _, err := d.client.CreateRecord(zone.ID, record) if err != nil { return fmt.Errorf("aurora: could not create record: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = newRecord.ID d.recordIDsMu.Unlock() return nil } // CleanUp removes a given record that was generated by Present. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("unknown recordID for %q", fqdn) } authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(fqdn)) if err != nil { return fmt.Errorf("could not determine zone for domain %q: %w", domain, err) } authZone = dns01.UnFqdn(authZone) zone, err := d.getZoneInformationByName(authZone) if err != nil { return err } _, _, err = d.client.DeleteRecord(zone.ID, recordID) if err != nil { return err } d.recordIDsMu.Lock() delete(d.recordIDs, token) d.recordIDsMu.Unlock() return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func (d *DNSProvider) getZoneInformationByName(name string) (auroradns.Zone, error) { zs, _, err := d.client.ListZones() if err != nil { return auroradns.Zone{}, err } for _, element := range zs { if element.Name == name { return element, nil } } return auroradns.Zone{}, errors.New("could not find Zone record") } lego-4.9.1/providers/dns/auroradns/auroradns.toml000066400000000000000000000015171434020463500221520ustar00rootroot00000000000000Name = "Aurora DNS" Description = '''''' URL = "https://www.pcextreme.com/dns-health-checks" Code = "auroradns" Since = "v0.4.0" Example = ''' AURORA_API_KEY=xxxxx \ AURORA_SECRET=yyyyyy \ lego --email you@example.com --dns auroradns --domains my.example.org run ''' [Configuration] [Configuration.Credentials] AURORA_API_KEY = "API key or username to used" AURORA_SECRET = "Secret password to be used" [Configuration.Additional] AURORA_ENDPOINT = "API endpoint URL" AURORA_POLLING_INTERVAL = "Time between DNS propagation check" AURORA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" AURORA_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://libcloud.readthedocs.io/en/latest/dns/drivers/auroradns.html#api-docs" GoClient = "https://github.com/nrdcg/auroradns" lego-4.9.1/providers/dns/auroradns/auroradns_test.go000066400000000000000000000125001434020463500226350ustar00rootroot00000000000000package auroradns import ( "fmt" "io" "net/http" "net/http/httptest" "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var envTest = tester.NewEnvTest(EnvAPIKey, EnvSecret) func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) config := NewDefaultConfig() config.APIKey = "asdf1234" config.Secret = "key" config.BaseURL = server.URL provider, err := NewDNSProviderConfig(config) require.NoError(t, err) return provider, mux } func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", EnvSecret: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIKey: "", EnvSecret: "", }, expected: "aurora: some credentials information are missing: AURORA_API_KEY,AURORA_SECRET", }, { desc: "missing api key", envVars: map[string]string{ EnvAPIKey: "", EnvSecret: "456", }, expected: "aurora: some credentials information are missing: AURORA_API_KEY", }, { desc: "missing secret key", envVars: map[string]string{ EnvAPIKey: "123", EnvSecret: "", }, expected: "aurora: some credentials information are missing: AURORA_SECRET", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string secret string expected string }{ { desc: "success", apiKey: "123", secret: "456", }, { desc: "missing credentials", apiKey: "", secret: "", expected: "aurora: some credentials information are missing", }, { desc: "missing user id", apiKey: "", secret: "456", expected: "aurora: some credentials information are missing", }, { desc: "missing key", apiKey: "123", secret: "", expected: "aurora: some credentials information are missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.Secret = test.secret p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestDNSProvider_Present(t *testing.T) { provider, mux := setupTest(t) mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method, "method") w.WriteHeader(http.StatusCreated) fmt.Fprintf(w, `[{ "id": "c56a4180-65aa-42ec-a945-5fd21dec0538", "name": "example.com" }]`) }) mux.HandleFunc("/zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type") reqBody, err := io.ReadAll(r.Body) require.NoError(t, err) assert.Equal(t, `{"type":"TXT","name":"_acme-challenge","content":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":300}`, string(reqBody)) w.WriteHeader(http.StatusCreated) fmt.Fprintf(w, `{ "id": "c56a4180-65aa-42ec-a945-5fd21dec0538", "type": "TXT", "name": "_acme-challenge", "ttl": 300 }`) }) err := provider.Present("example.com", "", "foobar") require.NoError(t, err, "fail to create TXT record") } func TestDNSProvider_CleanUp(t *testing.T) { provider, mux := setupTest(t) mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) w.WriteHeader(http.StatusCreated) fmt.Fprintf(w, `[{ "id": "c56a4180-65aa-42ec-a945-5fd21dec0538", "name": "example.com" }]`) }) mux.HandleFunc("/zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) w.WriteHeader(http.StatusCreated) fmt.Fprintf(w, `{ "id": "ec56a4180-65aa-42ec-a945-5fd21dec0538", "type": "TXT", "name": "_acme-challenge", "ttl": 300 }`) }) mux.HandleFunc("/zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records/ec56a4180-65aa-42ec-a945-5fd21dec0538", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodDelete, r.Method) assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type") w.WriteHeader(http.StatusCreated) fmt.Fprintf(w, `{}`) }) err := provider.Present("example.com", "", "foobar") require.NoError(t, err, "fail to create TXT record") err = provider.CleanUp("example.com", "", "foobar") require.NoError(t, err, "fail to remove TXT record") } lego-4.9.1/providers/dns/autodns/000077500000000000000000000000001434020463500167325ustar00rootroot00000000000000lego-4.9.1/providers/dns/autodns/autodns.go000066400000000000000000000076731434020463500207530ustar00rootroot00000000000000// Package autodns implements a DNS provider for solving the DNS-01 challenge using auto DNS. package autodns import ( "errors" "fmt" "net/http" "net/url" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "AUTODNS_" EnvAPIUser = envNamespace + "API_USER" EnvAPIPassword = envNamespace + "API_PASSWORD" EnvAPIEndpoint = envNamespace + "ENDPOINT" EnvAPIEndpointContext = envNamespace + "CONTEXT" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) const ( defaultEndpointContext int = 4 defaultTTL int = 600 ) // Config is used to configure the creation of the DNSProvider. type Config struct { Endpoint *url.URL Username string Password string Context int TTL int PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { endpoint, _ := url.Parse(env.GetOrDefaultString(EnvAPIEndpoint, defaultEndpoint)) return &Config{ Endpoint: endpoint, Context: env.GetOrDefaultInt(EnvAPIEndpointContext, defaultEndpointContext), TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config } // NewDNSProvider returns a DNSProvider instance configured for autoDNS. // Credentials must be passed in the environment variables. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIUser, EnvAPIPassword) if err != nil { return nil, fmt.Errorf("autodns: %w", err) } config := NewDefaultConfig() config.Username = values[EnvAPIUser] config.Password = values[EnvAPIPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for autoDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("autodns: config is nil") } if config.Username == "" { return nil, errors.New("autodns: missing user") } if config.Password == "" { return nil, errors.New("autodns: missing password") } return &DNSProvider{config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) records := []*ResourceRecord{{ Name: fqdn, TTL: int64(d.config.TTL), Type: "TXT", Value: value, }} // TODO(ldez) replace domain by FQDN to follow CNAME. _, err := d.addTxtRecord(domain, records) if err != nil { return fmt.Errorf("autodns: %w", err) } return nil } // CleanUp removes the TXT record previously created. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) records := []*ResourceRecord{{ Name: fqdn, TTL: int64(d.config.TTL), Type: "TXT", Value: value, }} // TODO(ldez) replace domain by FQDN to follow CNAME. if err := d.removeTXTRecord(domain, records); err != nil { return fmt.Errorf("autodns: %w", err) } return nil } lego-4.9.1/providers/dns/autodns/autodns.toml000066400000000000000000000017011434020463500213030ustar00rootroot00000000000000Name = "Autodns" Description = '''''' URL = "https://www.internetx.com/domains/autodns/" Code = "autodns" Since = "v3.2.0" Example = ''' AUTODNS_API_USER=username \ AUTODNS_API_PASSWORD=supersecretpassword \ lego --email you@example.com --dns autodns --domains my.example.org run ''' [Configuration] [Configuration.Credentials] AUTODNS_API_USER = "Username" AUTODNS_API_PASSWORD = "User Password" [Configuration.Additional] AUTODNS_ENDPOINT = "API endpoint URL, defaults to https://api.autodns.com/v1/" AUTODNS_CONTEXT = "API context (4 for production, 1 for testing. Defaults to 4)" AUTODNS_TTL = "The TTL of the TXT record used for the DNS challenge" AUTODNS_POLLING_INTERVAL = "Time between DNS propagation check" AUTODNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" AUTODNS_HTTP_TIMEOUT = "API request timeout, defaults to 30 seconds" [Links] API = "https://help.internetx.com/display/APIJSONEN" lego-4.9.1/providers/dns/autodns/autodns_test.go000066400000000000000000000060471434020463500220040ustar00rootroot00000000000000package autodns import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIEndpoint, EnvAPIUser, EnvAPIPassword). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIUser: "123", EnvAPIPassword: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIUser: "", EnvAPIPassword: "", }, expected: "autodns: some credentials information are missing: AUTODNS_API_USER,AUTODNS_API_PASSWORD", }, { desc: "missing user id", envVars: map[string]string{ EnvAPIUser: "", EnvAPIPassword: "456", }, expected: "autodns: some credentials information are missing: AUTODNS_API_USER", }, { desc: "missing key", envVars: map[string]string{ EnvAPIUser: "123", EnvAPIPassword: "", }, expected: "autodns: some credentials information are missing: AUTODNS_API_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string username string password string expected string }{ { desc: "success", username: "123", password: "456", }, { desc: "missing credentials", username: "", password: "", expected: "autodns: missing user", }, { desc: "missing user id", username: "", password: "456", expected: "autodns: missing user", }, { desc: "missing key", username: "123", password: "", expected: "autodns: missing password", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Username = test.username config.Password = test.password p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() assert.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") assert.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() assert.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") assert.NoError(t, err) } lego-4.9.1/providers/dns/autodns/client.go000066400000000000000000000075471434020463500205540ustar00rootroot00000000000000package autodns import ( "bytes" "encoding/json" "fmt" "io" "net/http" "path" "strconv" ) const ( defaultEndpoint = "https://api.autodns.com/v1/" ) type ResponseMessage struct { Text string `json:"text"` Messages []string `json:"messages"` Objects []string `json:"objects"` Code string `json:"code"` Status string `json:"status"` } type ResponseStatus struct { Code string `json:"code"` Text string `json:"text"` Type string `json:"type"` } type ResponseObject struct { Type string `json:"type"` Value string `json:"value"` Summary int32 `json:"summary"` Data string } type DataZoneResponse struct { STID string `json:"stid"` CTID string `json:"ctid"` Messages []*ResponseMessage `json:"messages"` Status *ResponseStatus `json:"status"` Object interface{} `json:"object"` Data []*Zone `json:"data"` } // ResourceRecord holds a resource record. type ResourceRecord struct { Name string `json:"name"` TTL int64 `json:"ttl"` Type string `json:"type"` Value string `json:"value"` Pref int32 `json:"pref,omitempty"` } // Zone is an autodns zone record with all for us relevant fields. type Zone struct { Name string `json:"origin"` ResourceRecords []*ResourceRecord `json:"resourceRecords"` Action string `json:"action"` VirtualNameServer string `json:"virtualNameServer"` } type ZoneStream struct { Adds []*ResourceRecord `json:"adds"` Removes []*ResourceRecord `json:"rems"` } func (d *DNSProvider) addTxtRecord(domain string, records []*ResourceRecord) (*Zone, error) { zoneStream := &ZoneStream{Adds: records} return d.makeZoneUpdateRequest(zoneStream, domain) } func (d *DNSProvider) removeTXTRecord(domain string, records []*ResourceRecord) error { zoneStream := &ZoneStream{Removes: records} _, err := d.makeZoneUpdateRequest(zoneStream, domain) return err } func (d *DNSProvider) makeZoneUpdateRequest(zoneStream *ZoneStream, domain string) (*Zone, error) { reqBody := &bytes.Buffer{} if err := json.NewEncoder(reqBody).Encode(zoneStream); err != nil { return nil, err } req, err := d.makeRequest(http.MethodPost, path.Join("zone", domain, "_stream"), reqBody) if err != nil { return nil, err } var resp *Zone if err := d.sendRequest(req, &resp); err != nil { return nil, err } return resp, nil } func (d *DNSProvider) makeRequest(method, resource string, body io.Reader) (*http.Request, error) { uri, err := d.config.Endpoint.Parse(resource) if err != nil { return nil, err } req, err := http.NewRequest(method, uri.String(), body) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Domainrobot-Context", strconv.Itoa(d.config.Context)) req.SetBasicAuth(d.config.Username, d.config.Password) return req, nil } func (d *DNSProvider) sendRequest(req *http.Request, result interface{}) error { resp, err := d.config.HTTPClient.Do(req) if err != nil { return err } if err = checkResponse(resp); err != nil { return err } defer func() { _ = resp.Body.Close() }() if result == nil { return nil } raw, err := io.ReadAll(resp.Body) if err != nil { return err } err = json.Unmarshal(raw, result) if err != nil { return fmt.Errorf("unmarshaling %T error [status code=%d]: %w: %s", result, resp.StatusCode, err, string(raw)) } return err } func checkResponse(resp *http.Response) error { if resp.StatusCode < http.StatusBadRequest { return nil } if resp.Body == nil { return fmt.Errorf("response body is nil, status code=%d", resp.StatusCode) } defer func() { _ = resp.Body.Close() }() raw, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("unable to read body: status code=%d, error=%w", resp.StatusCode, err) } return fmt.Errorf("status code=%d: %s", resp.StatusCode, string(raw)) } lego-4.9.1/providers/dns/azure/000077500000000000000000000000001434020463500164035ustar00rootroot00000000000000lego-4.9.1/providers/dns/azure/azure.go000066400000000000000000000167011434020463500200650ustar00rootroot00000000000000// Package azure implements a DNS provider for solving the DNS-01 challenge using azure DNS. // Azure doesn't like trailing dots on domain names, most of the acme code does. package azure import ( "errors" "fmt" "io" "net/http" "strings" "time" "github.com/Azure/go-autorest/autorest" aazure "github.com/Azure/go-autorest/autorest/azure" "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) const defaultMetadataEndpoint = "http://169.254.169.254" // Environment variables names. const ( envNamespace = "AZURE_" EnvEnvironment = envNamespace + "ENVIRONMENT" EnvMetadataEndpoint = envNamespace + "METADATA_ENDPOINT" EnvSubscriptionID = envNamespace + "SUBSCRIPTION_ID" EnvResourceGroup = envNamespace + "RESOURCE_GROUP" EnvTenantID = envNamespace + "TENANT_ID" EnvClientID = envNamespace + "CLIENT_ID" EnvClientSecret = envNamespace + "CLIENT_SECRET" EnvZoneName = envNamespace + "ZONE_NAME" EnvPrivateZone = envNamespace + "PRIVATE_ZONE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { // optional if using instance metadata service ClientID string ClientSecret string TenantID string SubscriptionID string ResourceGroup string PrivateZone bool MetadataEndpoint string ResourceManagerEndpoint string ActiveDirectoryEndpoint string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 60), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), MetadataEndpoint: env.GetOrFile(EnvMetadataEndpoint), ResourceManagerEndpoint: aazure.PublicCloud.ResourceManagerEndpoint, ActiveDirectoryEndpoint: aazure.PublicCloud.ActiveDirectoryEndpoint, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { provider challenge.ProviderTimeout } // NewDNSProvider returns a DNSProvider instance configured for azure. // Credentials can be passed in the environment variables: // AZURE_ENVIRONMENT, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, // AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_RESOURCE_GROUP // If the credentials are _not_ set via the environment, // then it will attempt to get a bearer token via the instance metadata service. // see: https://github.com/Azure/go-autorest/blob/v10.14.0/autorest/azure/auth/auth.go#L38-L42 func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() environmentName := env.GetOrFile(EnvEnvironment) if environmentName != "" { var environment aazure.Environment switch environmentName { case "china": environment = aazure.ChinaCloud case "german": environment = aazure.GermanCloud case "public": environment = aazure.PublicCloud case "usgovernment": environment = aazure.USGovernmentCloud default: return nil, fmt.Errorf("azure: unknown environment %s", environmentName) } config.ResourceManagerEndpoint = environment.ResourceManagerEndpoint config.ActiveDirectoryEndpoint = environment.ActiveDirectoryEndpoint } config.SubscriptionID = env.GetOrFile(EnvSubscriptionID) config.ResourceGroup = env.GetOrFile(EnvResourceGroup) config.ClientSecret = env.GetOrFile(EnvClientSecret) config.ClientID = env.GetOrFile(EnvClientID) config.TenantID = env.GetOrFile(EnvTenantID) config.PrivateZone = env.GetOrDefaultBool(EnvPrivateZone, false) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Azure. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("azure: the configuration of the DNS provider is nil") } if config.HTTPClient == nil { config.HTTPClient = http.DefaultClient } authorizer, err := getAuthorizer(config) if err != nil { return nil, err } if config.SubscriptionID == "" { subsID, err := getMetadata(config, "subscriptionId") if err != nil { return nil, fmt.Errorf("azure: %w", err) } if subsID == "" { return nil, errors.New("azure: SubscriptionID is missing") } config.SubscriptionID = subsID } if config.ResourceGroup == "" { resGroup, err := getMetadata(config, "resourceGroupName") if err != nil { return nil, fmt.Errorf("azure: %w", err) } if resGroup == "" { return nil, errors.New("azure: ResourceGroup is missing") } config.ResourceGroup = resGroup } if config.PrivateZone { return &DNSProvider{provider: &dnsProviderPrivate{config: config, authorizer: authorizer}}, nil } return &DNSProvider{provider: &dnsProviderPublic{config: config, authorizer: authorizer}}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.provider.Timeout() } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { return d.provider.Present(domain, token, keyAuth) } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return d.provider.CleanUp(domain, token, keyAuth) } // Returns the relative record to the domain. func toRelativeRecord(domain, zone string) string { return dns01.UnFqdn(strings.TrimSuffix(domain, zone)) } func getAuthorizer(config *Config) (autorest.Authorizer, error) { if config.ClientID != "" && config.ClientSecret != "" && config.TenantID != "" { credentialsConfig := auth.ClientCredentialsConfig{ ClientID: config.ClientID, ClientSecret: config.ClientSecret, TenantID: config.TenantID, Resource: config.ResourceManagerEndpoint, AADEndpoint: config.ActiveDirectoryEndpoint, } spToken, err := credentialsConfig.ServicePrincipalToken() if err != nil { return nil, fmt.Errorf("failed to get oauth token from client credentials: %w", err) } spToken.SetSender(config.HTTPClient) return autorest.NewBearerAuthorizer(spToken), nil } return auth.NewAuthorizerFromEnvironment() } // Fetches metadata from environment or he instance metadata service. // borrowed from https://github.com/Microsoft/azureimds/blob/master/imdssample.go func getMetadata(config *Config, field string) (string, error) { metadataEndpoint := config.MetadataEndpoint if metadataEndpoint == "" { metadataEndpoint = defaultMetadataEndpoint } resource := fmt.Sprintf("%s/metadata/instance/compute/%s", metadataEndpoint, field) req, err := http.NewRequest(http.MethodGet, resource, nil) if err != nil { return "", err } req.Header.Set("Metadata", "True") q := req.URL.Query() q.Add("format", "text") q.Add("api-version", "2017-12-01") req.URL.RawQuery = q.Encode() resp, err := config.HTTPClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return "", err } return string(respBody), nil } lego-4.9.1/providers/dns/azure/azure.toml000066400000000000000000000024711434020463500204320ustar00rootroot00000000000000Name = "Azure" Description = '''''' URL = "https://azure.microsoft.com/services/dns/" Code = "azure" Since = "v0.4.0" Example = '''''' [Configuration] [Configuration.Credentials] AZURE_ENVIRONMENT = "Azure environment, one of: public, usgovernment, german, and china" AZURE_CLIENT_ID = "Client ID" AZURE_CLIENT_SECRET = "Client secret" AZURE_SUBSCRIPTION_ID = "Subscription ID" AZURE_TENANT_ID = "Tenant ID" AZURE_RESOURCE_GROUP = "Resource group" '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)." [Configuration.Additional] AZURE_METADATA_ENDPOINT = "Metadata Service endpoint URL" AZURE_PRIVATE_ZONE = "Set to true to use Azure Private DNS Zones and not public" AZURE_ZONE_NAME = "Zone name to use inside Azure DNS service to add the TXT record in" AZURE_POLLING_INTERVAL = "Time between DNS propagation check" AZURE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" AZURE_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://docs.microsoft.com/en-us/go/azure/" GoClient = "https://github.com/Azure/azure-sdk-for-go" lego-4.9.1/providers/dns/azure/azure_test.go000066400000000000000000000110301434020463500211120ustar00rootroot00000000000000package azure import ( "net/http" "net/http/httptest" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvEnvironment, EnvClientID, EnvClientSecret, EnvSubscriptionID, EnvTenantID, EnvResourceGroup). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvClientID: "A", EnvClientSecret: "B", EnvTenantID: "C", EnvSubscriptionID: "D", EnvResourceGroup: "E", }, }, { desc: "missing client ID", envVars: map[string]string{ EnvClientID: "", EnvClientSecret: "B", EnvTenantID: "C", EnvSubscriptionID: "D", EnvResourceGroup: "E", }, expected: "failed to get SPT from client credentials: parameter 'clientID' cannot be empty", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected != "" { require.EqualError(t, err, test.expected) return } require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.provider) assert.IsType(t, p.provider, new(dnsProviderPublic)) }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string clientID string clientSecret string subscriptionID string tenantID string resourceGroup string privateZone bool handler func(w http.ResponseWriter, r *http.Request) expected string }{ { desc: "success (public)", clientID: "A", clientSecret: "B", tenantID: "C", subscriptionID: "D", resourceGroup: "E", privateZone: false, }, { desc: "success (private)", clientID: "A", clientSecret: "B", tenantID: "C", subscriptionID: "D", resourceGroup: "E", privateZone: true, }, { desc: "SubscriptionID missing", clientID: "A", clientSecret: "B", tenantID: "C", subscriptionID: "", resourceGroup: "", expected: "azure: SubscriptionID is missing", }, { desc: "ResourceGroup missing", clientID: "A", clientSecret: "B", tenantID: "C", subscriptionID: "D", resourceGroup: "", expected: "azure: ResourceGroup is missing", }, { desc: "use metadata", clientID: "A", clientSecret: "B", tenantID: "C", subscriptionID: "", resourceGroup: "", handler: func(w http.ResponseWriter, _ *http.Request) { _, err := w.Write([]byte("foo")) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.ClientID = test.clientID config.ClientSecret = test.clientSecret config.SubscriptionID = test.subscriptionID config.TenantID = test.tenantID config.ResourceGroup = test.resourceGroup config.PrivateZone = test.privateZone mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) if test.handler == nil { mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {}) } else { mux.HandleFunc("/", test.handler) } config.MetadataEndpoint = server.URL p, err := NewDNSProviderConfig(config) if test.expected != "" { require.EqualError(t, err, test.expected) return } require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.provider) if test.privateZone { assert.IsType(t, p.provider, new(dnsProviderPrivate)) } else { assert.IsType(t, p.provider, new(dnsProviderPublic)) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/azure/private.go000066400000000000000000000074031434020463500204100ustar00rootroot00000000000000package azure import ( "context" "errors" "fmt" "net/http" "time" "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/to" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // dnsProviderPrivate implements the challenge.Provider interface for Azure Private Zone DNS. type dnsProviderPrivate struct { config *Config authorizer autorest.Authorizer } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *dnsProviderPrivate) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *dnsProviderPrivate) Present(domain, token, keyAuth string) error { ctx := context.Background() fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZoneID(ctx, fqdn) if err != nil { return fmt.Errorf("azure: %w", err) } rsc := privatedns.NewRecordSetsClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID) rsc.Authorizer = d.authorizer relative := toRelativeRecord(fqdn, dns01.ToFqdn(zone)) // Get existing record set rset, err := rsc.Get(ctx, d.config.ResourceGroup, zone, privatedns.TXT, relative) if err != nil { var detailed autorest.DetailedError if !errors.As(err, &detailed) || detailed.StatusCode != http.StatusNotFound { return fmt.Errorf("azure: %w", err) } } // Construct unique TXT records using map uniqRecords := map[string]struct{}{value: {}} if rset.RecordSetProperties != nil && rset.TxtRecords != nil { for _, txtRecord := range *rset.TxtRecords { // Assume Value doesn't contain multiple strings values := to.StringSlice(txtRecord.Value) if len(values) > 0 { uniqRecords[values[0]] = struct{}{} } } } var txtRecords []privatedns.TxtRecord for txt := range uniqRecords { txtRecords = append(txtRecords, privatedns.TxtRecord{Value: &[]string{txt}}) } rec := privatedns.RecordSet{ Name: &relative, RecordSetProperties: &privatedns.RecordSetProperties{ TTL: to.Int64Ptr(int64(d.config.TTL)), TxtRecords: &txtRecords, }, } _, err = rsc.CreateOrUpdate(ctx, d.config.ResourceGroup, zone, privatedns.TXT, relative, rec, "", "") if err != nil { return fmt.Errorf("azure: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *dnsProviderPrivate) CleanUp(domain, token, keyAuth string) error { ctx := context.Background() fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZoneID(ctx, fqdn) if err != nil { return fmt.Errorf("azure: %w", err) } relative := toRelativeRecord(fqdn, dns01.ToFqdn(zone)) rsc := privatedns.NewRecordSetsClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID) rsc.Authorizer = d.authorizer _, err = rsc.Delete(ctx, d.config.ResourceGroup, zone, privatedns.TXT, relative, "") if err != nil { return fmt.Errorf("azure: %w", err) } return nil } // Checks that azure has a zone for this domain name. func (d *dnsProviderPrivate) getHostedZoneID(ctx context.Context, fqdn string) (string, error) { if zone := env.GetOrFile(EnvZoneName); zone != "" { return zone, nil } authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", err } dc := privatedns.NewPrivateZonesClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID) dc.Authorizer = d.authorizer zone, err := dc.Get(ctx, d.config.ResourceGroup, dns01.UnFqdn(authZone)) if err != nil { return "", err } // zone.Name shouldn't have a trailing dot(.) return to.String(zone.Name), nil } lego-4.9.1/providers/dns/azure/public.go000066400000000000000000000072411434020463500202140ustar00rootroot00000000000000package azure import ( "context" "errors" "fmt" "net/http" "time" "github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2017-09-01/dns" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/to" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // dnsProviderPublic implements the challenge.Provider interface for Azure Public Zone DNS. type dnsProviderPublic struct { config *Config authorizer autorest.Authorizer } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *dnsProviderPublic) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *dnsProviderPublic) Present(domain, token, keyAuth string) error { ctx := context.Background() fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZoneID(ctx, fqdn) if err != nil { return fmt.Errorf("azure: %w", err) } rsc := dns.NewRecordSetsClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID) rsc.Authorizer = d.authorizer relative := toRelativeRecord(fqdn, dns01.ToFqdn(zone)) // Get existing record set rset, err := rsc.Get(ctx, d.config.ResourceGroup, zone, relative, dns.TXT) if err != nil { var detailed autorest.DetailedError if !errors.As(err, &detailed) || detailed.StatusCode != http.StatusNotFound { return fmt.Errorf("azure: %w", err) } } // Construct unique TXT records using map uniqRecords := map[string]struct{}{value: {}} if rset.RecordSetProperties != nil && rset.TxtRecords != nil { for _, txtRecord := range *rset.TxtRecords { // Assume Value doesn't contain multiple strings values := to.StringSlice(txtRecord.Value) if len(values) > 0 { uniqRecords[values[0]] = struct{}{} } } } var txtRecords []dns.TxtRecord for txt := range uniqRecords { txtRecords = append(txtRecords, dns.TxtRecord{Value: &[]string{txt}}) } rec := dns.RecordSet{ Name: &relative, RecordSetProperties: &dns.RecordSetProperties{ TTL: to.Int64Ptr(int64(d.config.TTL)), TxtRecords: &txtRecords, }, } _, err = rsc.CreateOrUpdate(ctx, d.config.ResourceGroup, zone, relative, dns.TXT, rec, "", "") if err != nil { return fmt.Errorf("azure: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *dnsProviderPublic) CleanUp(domain, token, keyAuth string) error { ctx := context.Background() fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZoneID(ctx, fqdn) if err != nil { return fmt.Errorf("azure: %w", err) } relative := toRelativeRecord(fqdn, dns01.ToFqdn(zone)) rsc := dns.NewRecordSetsClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID) rsc.Authorizer = d.authorizer _, err = rsc.Delete(ctx, d.config.ResourceGroup, zone, relative, dns.TXT, "") if err != nil { return fmt.Errorf("azure: %w", err) } return nil } // Checks that azure has a zone for this domain name. func (d *dnsProviderPublic) getHostedZoneID(ctx context.Context, fqdn string) (string, error) { if zone := env.GetOrFile(EnvZoneName); zone != "" { return zone, nil } authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", err } dc := dns.NewZonesClientWithBaseURI(d.config.ResourceManagerEndpoint, d.config.SubscriptionID) dc.Authorizer = d.authorizer zone, err := dc.Get(ctx, d.config.ResourceGroup, dns01.UnFqdn(authZone)) if err != nil { return "", err } // zone.Name shouldn't have a trailing dot(.) return to.String(zone.Name), nil } lego-4.9.1/providers/dns/bindman/000077500000000000000000000000001434020463500166655ustar00rootroot00000000000000lego-4.9.1/providers/dns/bindman/bindman.go000066400000000000000000000065101434020463500206260ustar00rootroot00000000000000// Package bindman implements a DNS provider for solving the DNS-01 challenge. package bindman import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/labbsr0x/bindman-dns-webhook/src/client" ) // Environment variables names. const ( envNamespace = "BINDMAN_" EnvManagerAddress = envNamespace + "MANAGER_ADDRESS" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { PropagationTimeout time.Duration PollingInterval time.Duration BaseURL string HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, time.Minute), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *client.DNSWebhookClient } // NewDNSProvider returns a DNSProvider instance configured for Bindman. // BINDMAN_MANAGER_ADDRESS should have the scheme, hostname, and port (if required) of the authoritative Bindman Manager server. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvManagerAddress) if err != nil { return nil, fmt.Errorf("bindman: %w", err) } config := NewDefaultConfig() config.BaseURL = values[EnvManagerAddress] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Bindman. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("bindman: the configuration of the DNS provider is nil") } if config.BaseURL == "" { return nil, errors.New("bindman: bindman manager address missing") } bClient, err := client.New(config.BaseURL, config.HTTPClient) if err != nil { return nil, fmt.Errorf("bindman: %w", err) } return &DNSProvider{config: config, client: bClient}, nil } // Present creates a TXT record using the specified parameters. // This will *not* create a subzone to contain the TXT record, // so make sure the FQDN specified is within an extant zone. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) if err := d.client.AddRecord(fqdn, "TXT", value); err != nil { return fmt.Errorf("bindman: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) if err := d.client.RemoveRecord(fqdn, "TXT"); err != nil { return fmt.Errorf("bindman: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/bindman/bindman.toml000066400000000000000000000014451434020463500211760ustar00rootroot00000000000000Name = "Bindman" Description = '''''' URL = "https://github.com/labbsr0x/bindman-dns-webhook" Code = "bindman" Since = "v2.6.0" Example = ''' BINDMAN_MANAGER_ADDRESS= \ lego --email you@example.com --dns bindman --domains my.example.org run ''' [Configuration] [Configuration.Credentials] BINDMAN_MANAGER_ADDRESS = "The server URL, should have scheme, hostname, and port (if required) of the Bindman-DNS Manager server" [Configuration.Additional] BINDMAN_POLLING_INTERVAL = "Time between DNS propagation check" BINDMAN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" BINDMAN_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://gitlab.isc.org/isc-projects/bind9" GoClient = "https://github.com/labbsr0x/bindman-dns-webhook" lego-4.9.1/providers/dns/bindman/bindman_test.go000066400000000000000000000134261434020463500216710ustar00rootroot00000000000000// Package bindman implements a DNS provider for solving the DNS-01 challenge. package bindman import ( "errors" "net/http" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" bindmanClient "github.com/labbsr0x/bindman-dns-webhook/src/client" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvManagerAddress).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvManagerAddress: "http://localhost", }, }, { desc: "missing bindman manager address", envVars: map[string]string{ EnvManagerAddress: "", }, expected: "bindman: some credentials information are missing: BINDMAN_MANAGER_ADDRESS", }, { desc: "empty bindman manager address", envVars: map[string]string{ EnvManagerAddress: " ", }, expected: "bindman: managerAddress parameter must be a non-empty string", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string config *Config expected string }{ { desc: "success", config: &Config{BaseURL: "http://localhost"}, }, { desc: "missing base URL", config: &Config{BaseURL: ""}, expected: "bindman: bindman manager address missing", }, { desc: "missing base URL", config: &Config{BaseURL: " "}, expected: "bindman: managerAddress parameter must be a non-empty string", }, { desc: "missing config", expected: "bindman: the configuration of the DNS provider is nil", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { p, err := NewDNSProviderConfig(test.config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestDNSProvider_Present(t *testing.T) { testCases := []struct { name string client *bindmanClient.DNSWebhookClient domain string token string keyAuth string expectError bool }{ { name: "success when add record function return no error", client: &bindmanClient.DNSWebhookClient{ ClientAPI: &MockHTTPClientAPI{Status: http.StatusNoContent}, }, domain: "hello.test.com", keyAuth: "szDTG4zmM0GsKG91QAGO2M4UYOJMwU8oFpWOP7eTjCw", expectError: false, }, { name: "error when add record function return an error", client: &bindmanClient.DNSWebhookClient{ ClientAPI: &MockHTTPClientAPI{Error: errors.New("error adding record")}, }, domain: "hello.test.com", keyAuth: "szDTG4zmM0GsKG91QAGO2M4UYOJMwU8oFpWOP7eTjCw", expectError: true, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { d := &DNSProvider{client: test.client} err := d.Present(test.domain, test.token, test.keyAuth) if test.expectError { require.Error(t, err) } else { require.NoError(t, err) } }) } } func TestDNSProvider_CleanUp(t *testing.T) { testCases := []struct { name string client *bindmanClient.DNSWebhookClient domain string token string keyAuth string expectError bool }{ { name: "success when remove record function return no error", client: &bindmanClient.DNSWebhookClient{ ClientAPI: &MockHTTPClientAPI{Status: http.StatusNoContent}, }, domain: "hello.test.com", keyAuth: "szDTG4zmM0GsKG91QAGO2M4UYOJMwU8oFpWOP7eTjCw", expectError: false, }, { name: "error when remove record function return an error", client: &bindmanClient.DNSWebhookClient{ ClientAPI: &MockHTTPClientAPI{Error: errors.New("error adding record")}, }, domain: "hello.test.com", keyAuth: "szDTG4zmM0GsKG91QAGO2M4UYOJMwU8oFpWOP7eTjCw", expectError: true, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { d := &DNSProvider{client: test.client} err := d.CleanUp(test.domain, test.token, test.keyAuth) if test.expectError { require.Error(t, err) } else { require.NoError(t, err) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } type MockHTTPClientAPI struct { Data []byte Status int Error error } func (m *MockHTTPClientAPI) Put(url string, data []byte) (*http.Response, []byte, error) { return &http.Response{StatusCode: m.Status}, m.Data, m.Error } func (m *MockHTTPClientAPI) Post(url string, data []byte) (*http.Response, []byte, error) { return &http.Response{StatusCode: m.Status}, m.Data, m.Error } func (m *MockHTTPClientAPI) Get(url string) (*http.Response, []byte, error) { return &http.Response{StatusCode: m.Status}, m.Data, m.Error } func (m *MockHTTPClientAPI) Delete(url string) (*http.Response, []byte, error) { return &http.Response{StatusCode: m.Status}, m.Data, m.Error } lego-4.9.1/providers/dns/bluecat/000077500000000000000000000000001434020463500166745ustar00rootroot00000000000000lego-4.9.1/providers/dns/bluecat/bluecat.go000066400000000000000000000142431434020463500206460ustar00rootroot00000000000000// Package bluecat implements a DNS provider for solving the DNS-01 challenge using a self-hosted Bluecat Address Manager. package bluecat import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/bluecat/internal" ) // Environment variables names. const ( envNamespace = "BLUECAT_" EnvServerURL = envNamespace + "SERVER_URL" EnvUserName = envNamespace + "USER_NAME" EnvPassword = envNamespace + "PASSWORD" EnvConfigName = envNamespace + "CONFIG_NAME" EnvDNSView = envNamespace + "DNS_VIEW" EnvDebug = envNamespace + "DEBUG" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string UserName string Password string ConfigName string DNSView string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client Debug bool } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, Debug: env.GetOrDefaultBool(EnvDebug, false), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for Bluecat DNS. // Credentials must be passed in the environment variables: // - BLUECAT_SERVER_URL // It should have the scheme, hostname, and port (if required) of the authoritative Bluecat BAM server. // The REST endpoint will be appended. // - BLUECAT_USER_NAME and BLUECAT_PASSWORD // - BLUECAT_CONFIG_NAME (the Configuration name) // - BLUECAT_DNS_VIEW (external DNS View Name) func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvServerURL, EnvUserName, EnvPassword, EnvConfigName, EnvDNSView) if err != nil { return nil, fmt.Errorf("bluecat: %w", err) } config := NewDefaultConfig() config.BaseURL = values[EnvServerURL] config.UserName = values[EnvUserName] config.Password = values[EnvPassword] config.ConfigName = values[EnvConfigName] config.DNSView = values[EnvDNSView] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Bluecat DNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("bluecat: the configuration of the DNS provider is nil") } if config.BaseURL == "" || config.UserName == "" || config.Password == "" || config.ConfigName == "" || config.DNSView == "" { return nil, errors.New("bluecat: credentials missing") } client := internal.NewClient(config.BaseURL) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{config: config, client: client}, nil } // Present creates a TXT record using the specified parameters // This will *not* create a sub-zone to contain the TXT record, // so make sure the FQDN specified is within an existent zone. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) err := d.client.Login(d.config.UserName, d.config.Password) if err != nil { return fmt.Errorf("bluecat: login: %w", err) } viewID, err := d.client.LookupViewID(d.config.ConfigName, d.config.DNSView) if err != nil { return fmt.Errorf("bluecat: lookupViewID: %w", err) } parentZoneID, name, err := d.client.LookupParentZoneID(viewID, fqdn) if err != nil { return fmt.Errorf("bluecat: lookupParentZoneID: %w", err) } if d.config.Debug { log.Infof("fqdn: %s; viewID: %d; ZoneID: %d; zone: %s", fqdn, viewID, parentZoneID, name) } txtRecord := internal.Entity{ Name: name, Type: internal.TXTType, Properties: fmt.Sprintf("ttl=%d|absoluteName=%s|txt=%s|", d.config.TTL, fqdn, value), } _, err = d.client.AddEntity(parentZoneID, txtRecord) if err != nil { return fmt.Errorf("bluecat: add TXT record: %w", err) } err = d.client.Deploy(parentZoneID) if err != nil { return fmt.Errorf("bluecat: deploy: %w", err) } err = d.client.Logout() if err != nil { return fmt.Errorf("bluecat: logout: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) err := d.client.Login(d.config.UserName, d.config.Password) if err != nil { return fmt.Errorf("bluecat: login: %w", err) } viewID, err := d.client.LookupViewID(d.config.ConfigName, d.config.DNSView) if err != nil { return fmt.Errorf("bluecat: lookupViewID: %w", err) } parentZoneID, name, err := d.client.LookupParentZoneID(viewID, fqdn) if err != nil { return fmt.Errorf("bluecat: lookupParentZoneID: %w", err) } txtRecord, err := d.client.GetEntityByName(parentZoneID, name, internal.TXTType) if err != nil { return fmt.Errorf("bluecat: get TXT record: %w", err) } err = d.client.Delete(txtRecord.ID) if err != nil { return fmt.Errorf("bluecat: delete TXT record: %w", err) } err = d.client.Deploy(parentZoneID) if err != nil { return fmt.Errorf("bluecat: deploy: %w", err) } err = d.client.Logout() if err != nil { return fmt.Errorf("bluecat: logout: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/bluecat/bluecat.toml000066400000000000000000000021501434020463500212060ustar00rootroot00000000000000Name = "Bluecat" Description = '''''' URL = "https://www.bluecatnetworks.com" Code = "bluecat" Since = "v0.5.0" Example = ''' BLUECAT_PASSWORD=mypassword \ BLUECAT_DNS_VIEW=myview \ BLUECAT_USER_NAME=myusername \ BLUECAT_CONFIG_NAME=myconfig \ BLUECAT_SERVER_URL=https://bam.example.com \ BLUECAT_TTL=30 \ lego --email you@example.com --dns bluecat --domains my.example.org run ''' [Configuration] [Configuration.Credentials] BLUECAT_SERVER_URL = "The server URL, should have scheme, hostname, and port (if required) of the authoritative Bluecat BAM serve" BLUECAT_USER_NAME = "API username" BLUECAT_PASSWORD = "API password" BLUECAT_CONFIG_NAME = "Configuration name" BLUECAT_DNS_VIEW = "External DNS View Name" [Configuration.Additional] BLUECAT_POLLING_INTERVAL = "Time between DNS propagation check" BLUECAT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" BLUECAT_TTL = "The TTL of the TXT record used for the DNS challenge" BLUECAT_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/REST-API/9.1.0" lego-4.9.1/providers/dns/bluecat/bluecat_test.go000066400000000000000000000124141434020463500217030ustar00rootroot00000000000000package bluecat import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvServerURL, EnvUserName, EnvPassword, EnvConfigName, EnvDNSView). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvServerURL: "http://localhost", EnvUserName: "A", EnvPassword: "B", EnvConfigName: "C", EnvDNSView: "D", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvServerURL: "", EnvUserName: "", EnvPassword: "", EnvConfigName: "", EnvDNSView: "", }, expected: "bluecat: some credentials information are missing: BLUECAT_SERVER_URL,BLUECAT_USER_NAME,BLUECAT_PASSWORD,BLUECAT_CONFIG_NAME,BLUECAT_DNS_VIEW", }, { desc: "missing server url", envVars: map[string]string{ EnvServerURL: "", EnvUserName: "A", EnvPassword: "B", EnvConfigName: "C", EnvDNSView: "D", }, expected: "bluecat: some credentials information are missing: BLUECAT_SERVER_URL", }, { desc: "missing username", envVars: map[string]string{ EnvServerURL: "http://localhost", EnvUserName: "", EnvPassword: "B", EnvConfigName: "C", EnvDNSView: "D", }, expected: "bluecat: some credentials information are missing: BLUECAT_USER_NAME", }, { desc: "missing password", envVars: map[string]string{ EnvServerURL: "http://localhost", EnvUserName: "A", EnvPassword: "", EnvConfigName: "C", EnvDNSView: "D", }, expected: "bluecat: some credentials information are missing: BLUECAT_PASSWORD", }, { desc: "missing config name", envVars: map[string]string{ EnvServerURL: "http://localhost", EnvUserName: "A", EnvPassword: "B", EnvConfigName: "", EnvDNSView: "D", }, expected: "bluecat: some credentials information are missing: BLUECAT_CONFIG_NAME", }, { desc: "missing DNS view", envVars: map[string]string{ EnvServerURL: "http://localhost", EnvUserName: "A", EnvPassword: "B", EnvConfigName: "C", EnvDNSView: "", }, expected: "bluecat: some credentials information are missing: BLUECAT_DNS_VIEW", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string baseURL string userName string password string configName string dnsView string expected string }{ { desc: "success", baseURL: "http://localhost", userName: "A", password: "B", configName: "C", dnsView: "D", }, { desc: "missing credentials", expected: "bluecat: credentials missing", }, { desc: "missing base URL", baseURL: "", userName: "A", password: "B", configName: "C", dnsView: "D", expected: "bluecat: credentials missing", }, { desc: "missing username", baseURL: "http://localhost", userName: "", password: "B", configName: "C", dnsView: "D", expected: "bluecat: credentials missing", }, { desc: "missing password", baseURL: "http://localhost", userName: "A", password: "", configName: "C", dnsView: "D", expected: "bluecat: credentials missing", }, { desc: "missing config name", baseURL: "http://localhost", userName: "A", password: "B", configName: "", dnsView: "D", expected: "bluecat: credentials missing", }, { desc: "missing DNS view", baseURL: "http://localhost", userName: "A", password: "B", configName: "C", dnsView: "", expected: "bluecat: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.BaseURL = test.baseURL config.UserName = test.userName config.Password = test.password config.ConfigName = test.configName config.DNSView = test.dnsView p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(time.Second * 1) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/bluecat/internal/000077500000000000000000000000001434020463500205105ustar00rootroot00000000000000lego-4.9.1/providers/dns/bluecat/internal/client.go000066400000000000000000000172531434020463500223250ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "fmt" "io" "net/http" "regexp" "strconv" "strings" "time" ) // Object types. const ( ConfigType = "Configuration" ViewType = "View" ZoneType = "Zone" TXTType = "TXTRecord" ) type Client struct { HTTPClient *http.Client baseURL string token string tokenExp *regexp.Regexp } func NewClient(baseURL string) *Client { return &Client{ HTTPClient: &http.Client{Timeout: 30 * time.Second}, baseURL: baseURL, tokenExp: regexp.MustCompile("BAMAuthToken: [^ ]+"), } } // Login Logs in as API user. // Authenticates and receives a token to be used in for subsequent requests. // https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/GET/v1/login/9.1.0 func (c *Client) Login(username, password string) error { queryArgs := map[string]string{ "username": username, "password": password, } resp, err := c.sendRequest(http.MethodGet, "login", nil, queryArgs) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { data, _ := io.ReadAll(resp.Body) return &APIError{ StatusCode: resp.StatusCode, Resource: "login", Message: string(data), } } authBytes, err := io.ReadAll(resp.Body) if err != nil { return err } authResp := string(authBytes) if strings.Contains(authResp, "Authentication Error") { return fmt.Errorf("request failed: %s", strings.Trim(authResp, `"`)) } // Upon success, API responds with "Session Token-> BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM= <- for User : username" c.token = c.tokenExp.FindString(authResp) return nil } // Logout Logs out of the current API session. // https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/GET/v1/logout/9.1.0 func (c *Client) Logout() error { if c.token == "" { // nothing to do return nil } resp, err := c.sendRequest(http.MethodGet, "logout", nil, nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { data, _ := io.ReadAll(resp.Body) return &APIError{ StatusCode: resp.StatusCode, Resource: "logout", Message: string(data), } } authBytes, err := io.ReadAll(resp.Body) if err != nil { return err } authResp := string(authBytes) if !strings.Contains(authResp, "successfully") { return fmt.Errorf("request failed to delete session: %s", strings.Trim(authResp, `"`)) } c.token = "" return nil } // Deploy the DNS config for the specified entity to the authoritative servers. // https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/POST/v1/quickDeploy/9.1.0 func (c *Client) Deploy(entityID uint) error { queryArgs := map[string]string{ "entityId": strconv.FormatUint(uint64(entityID), 10), } resp, err := c.sendRequest(http.MethodPost, "quickDeploy", nil, queryArgs) if err != nil { return err } defer resp.Body.Close() // The API doc says that 201 is expected but in the reality 200 is return. if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { data, _ := io.ReadAll(resp.Body) return &APIError{ StatusCode: resp.StatusCode, Resource: "quickDeploy", Message: string(data), } } return nil } // AddEntity A generic method for adding configurations, DNS zones, and DNS resource records. // https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/POST/v1/addEntity/9.1.0 func (c *Client) AddEntity(parentID uint, entity Entity) (uint64, error) { queryArgs := map[string]string{ "parentId": strconv.FormatUint(uint64(parentID), 10), } resp, err := c.sendRequest(http.MethodPost, "addEntity", entity, queryArgs) if err != nil { return 0, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { data, _ := io.ReadAll(resp.Body) return 0, &APIError{ StatusCode: resp.StatusCode, Resource: "addEntity", Message: string(data), } } addTxtBytes, _ := io.ReadAll(resp.Body) // addEntity responds only with body text containing the ID of the created record addTxtResp := string(addTxtBytes) id, err := strconv.ParseUint(addTxtResp, 10, 64) if err != nil { return 0, fmt.Errorf("addEntity request failed: %s", addTxtResp) } return id, nil } // GetEntityByName Returns objects from the database referenced by their database ID and with its properties fields populated. // https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/GET/v1/getEntityById/9.1.0 func (c *Client) GetEntityByName(parentID uint, name, objType string) (*EntityResponse, error) { queryArgs := map[string]string{ "parentId": strconv.FormatUint(uint64(parentID), 10), "name": name, "type": objType, } resp, err := c.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { data, _ := io.ReadAll(resp.Body) return nil, &APIError{ StatusCode: resp.StatusCode, Resource: "getEntityByName", Message: string(data), } } var txtRec EntityResponse if err = json.NewDecoder(resp.Body).Decode(&txtRec); err != nil { return nil, fmt.Errorf("JSON decode: %w", err) } return &txtRec, nil } // Delete Deletes an object using the generic delete method. // https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/DELETE/v1/delete/9.1.0 func (c *Client) Delete(objectID uint) error { queryArgs := map[string]string{ "objectId": strconv.FormatUint(uint64(objectID), 10), } resp, err := c.sendRequest(http.MethodDelete, "delete", nil, queryArgs) if err != nil { return err } defer resp.Body.Close() // The API doc says that 204 is expected but in the reality 200 is return. if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK { data, _ := io.ReadAll(resp.Body) return &APIError{ StatusCode: resp.StatusCode, Resource: "delete", Message: string(data), } } return nil } // LookupViewID Find the DNS view with the given name within. func (c *Client) LookupViewID(configName, viewName string) (uint, error) { // Lookup the entity ID of the configuration named in our properties. conf, err := c.GetEntityByName(0, configName, ConfigType) if err != nil { return 0, err } view, err := c.GetEntityByName(conf.ID, viewName, ViewType) if err != nil { return 0, err } return view.ID, nil } // LookupParentZoneID Return the entityId of the parent zone by recursing from the root view. // Also return the simple name of the host. func (c *Client) LookupParentZoneID(viewID uint, fqdn string) (uint, string, error) { if fqdn == "" { return viewID, "", nil } zones := strings.Split(strings.Trim(fqdn, "."), ".") name := zones[0] parentViewID := viewID for i := len(zones) - 1; i > -1; i-- { zone, err := c.GetEntityByName(parentViewID, zones[i], ZoneType) if err != nil { return 0, "", fmt.Errorf("could not find zone named %s: %w", name, err) } if zone == nil || zone.ID == 0 { break } if i > 0 { name = strings.Join(zones[0:i], ".") } parentViewID = zone.ID } return parentViewID, name, nil } // Send a REST request, using query parameters specified. // The Authorization header will be set if we have an active auth token. func (c *Client) sendRequest(method, resource string, payload interface{}, queryParams map[string]string) (*http.Response, error) { url := fmt.Sprintf("%s/Services/REST/v1/%s", c.baseURL, resource) body, err := json.Marshal(payload) if err != nil { return nil, err } req, err := http.NewRequest(method, url, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") if c.token != "" { req.Header.Set("Authorization", c.token) } q := req.URL.Query() for k, v := range queryParams { q.Set(k, v) } req.URL.RawQuery = q.Encode() return c.HTTPClient.Do(req) } lego-4.9.1/providers/dns/bluecat/internal/client_test.go000066400000000000000000000015671434020463500233650ustar00rootroot00000000000000package internal import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_LookupParentZoneID(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient(server.URL) mux.HandleFunc("/Services/REST/v1/getEntityByName", func(rw http.ResponseWriter, req *http.Request) { query := req.URL.Query() if query.Get("name") == "com" { _ = json.NewEncoder(rw).Encode(EntityResponse{ ID: 2, Name: "com", Type: ZoneType, Properties: "test", }) return } http.Error(rw, "{}", http.StatusOK) }) parentID, name, err := client.LookupParentZoneID(2, "foo.example.com") require.NoError(t, err) assert.EqualValues(t, 2, parentID) assert.Equal(t, "foo.example", name) } lego-4.9.1/providers/dns/bluecat/internal/types.go000066400000000000000000000012711434020463500222040ustar00rootroot00000000000000package internal import "fmt" // Entity JSON body for Bluecat entity requests. type Entity struct { ID string `json:"id,omitempty"` Name string `json:"name"` Type string `json:"type"` Properties string `json:"properties"` } // EntityResponse JSON body for Bluecat entity responses. type EntityResponse struct { ID uint `json:"id"` Name string `json:"name"` Type string `json:"type"` Properties string `json:"properties"` } type APIError struct { StatusCode int Resource string Message string } func (a APIError) Error() string { return fmt.Sprintf("resource: %s, status code: %d, message: %s", a.Resource, a.StatusCode, a.Message) } lego-4.9.1/providers/dns/checkdomain/000077500000000000000000000000001434020463500175225ustar00rootroot00000000000000lego-4.9.1/providers/dns/checkdomain/checkdomain.go000066400000000000000000000101111434020463500223100ustar00rootroot00000000000000// Package checkdomain implements a DNS provider for solving the DNS-01 challenge using CheckDomain DNS. package checkdomain import ( "errors" "fmt" "net/http" "net/url" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "CHECKDOMAIN_" EnvEndpoint = envNamespace + "ENDPOINT" EnvToken = envNamespace + "TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) const ( defaultEndpoint = "https://api.checkdomain.de" defaultTTL = 300 ) // Config is used to configure the creation of the DNSProvider. type Config struct { Endpoint *url.URL Token string TTL int PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 7*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config domainIDMu sync.Mutex domainIDMapping map[string]int } // NewDNSProvider returns a DNSProvider instance configured for CheckDomain. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvToken) if err != nil { return nil, fmt.Errorf("checkdomain: %w", err) } config := NewDefaultConfig() config.Token = values[EnvToken] endpoint, err := url.Parse(env.GetOrDefaultString(EnvEndpoint, defaultEndpoint)) if err != nil { return nil, fmt.Errorf("checkdomain: invalid %s: %w", EnvEndpoint, err) } config.Endpoint = endpoint return NewDNSProviderConfig(config) } func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config.Endpoint == nil { return nil, errors.New("checkdomain: invalid endpoint") } if config.Token == "" { return nil, errors.New("checkdomain: missing token") } if config.HTTPClient == nil { config.HTTPClient = http.DefaultClient } return &DNSProvider{ config: config, domainIDMapping: make(map[string]int), }, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { // TODO(ldez) replace domain by FQDN to follow CNAME. domainID, err := d.getDomainIDByName(domain) if err != nil { return fmt.Errorf("checkdomain: %w", err) } err = d.checkNameservers(domainID) if err != nil { return fmt.Errorf("checkdomain: %w", err) } fqdn, value := dns01.GetRecord(domain, keyAuth) err = d.createRecord(domainID, &Record{ Name: fqdn, TTL: d.config.TTL, Type: "TXT", Value: value, }) if err != nil { return fmt.Errorf("checkdomain: %w", err) } return nil } // CleanUp removes the TXT record previously created. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { // TODO(ldez) replace domain by FQDN to follow CNAME. domainID, err := d.getDomainIDByName(domain) if err != nil { return fmt.Errorf("checkdomain: %w", err) } err = d.checkNameservers(domainID) if err != nil { return fmt.Errorf("checkdomain: %w", err) } fqdn, value := dns01.GetRecord(domain, keyAuth) err = d.deleteTXTRecord(domainID, fqdn, value) if err != nil { return fmt.Errorf("checkdomain: %w", err) } d.domainIDMu.Lock() delete(d.domainIDMapping, fqdn) d.domainIDMu.Unlock() return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/checkdomain/checkdomain.toml000066400000000000000000000016351434020463500226710ustar00rootroot00000000000000Name = "Checkdomain" Description = '''''' URL = "https://checkdomain.de/" Code = "checkdomain" Since = "v3.3.0" Example = ''' CHECKDOMAIN_TOKEN=yoursecrettoken \ lego --email you@example.com --dns checkdomain --domains my.example.org run ''' [Configuration] [Configuration.Credentials] CHECKDOMAIN_TOKEN = "API token" [Configuration.Additional] CHECKDOMAIN_ENDPOINT = "API endpoint URL, defaults to https://api.checkdomain.de" CHECKDOMAIN_TTL = "The TTL of the TXT record used for the DNS challenge" CHECKDOMAIN_POLLING_INTERVAL = "Time between DNS propagation check" CHECKDOMAIN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" CHECKDOMAIN_HTTP_TIMEOUT = "API request timeout, defaults to 30 seconds" [Links] API = "https://developer.checkdomain.de/reference/" Guide = "https://developer.checkdomain.de/guide/" Settings = "https://www.checkdomain.net/en/login/data/api/" lego-4.9.1/providers/dns/checkdomain/checkdomain_test.go000066400000000000000000000047641434020463500233700ustar00rootroot00000000000000package checkdomain import ( "net/url" "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvEndpoint, EnvToken). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvToken: "dummy", }, }, { desc: "no token", envVars: map[string]string{}, expected: "checkdomain: some credentials information are missing: CHECKDOMAIN_TOKEN", }, { desc: "invalid endpoint", envVars: map[string]string{ EnvToken: "dummy", EnvEndpoint: ":", }, expected: `checkdomain: invalid CHECKDOMAIN_ENDPOINT: parse ":": missing protocol scheme`, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string token string expected string }{ { desc: "success", token: "dummy", }, { desc: "missing token", token: "", expected: "checkdomain: missing token", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Endpoint, _ = url.Parse(defaultEndpoint) if test.token != "" { config.Token = test.token } p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() assert.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") assert.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() assert.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") assert.NoError(t, err) } lego-4.9.1/providers/dns/checkdomain/client.go000066400000000000000000000240651434020463500213360ustar00rootroot00000000000000package checkdomain import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "strconv" "strings" ) const ( ns1 = "ns.checkdomain.de" ns2 = "ns2.checkdomain.de" ) const domainNotFound = -1 // max page limit that the checkdomain api allows. const maxLimit = 100 // max integer value. const maxInt = int((^uint(0)) >> 1) type ( // Some fields have been omitted from the structs // because they are not required for this application. DomainListingResponse struct { Page int `json:"page"` Limit int `json:"limit"` Pages int `json:"pages"` Total int `json:"total"` Embedded EmbeddedDomainList `json:"_embedded"` } EmbeddedDomainList struct { Domains []*Domain `json:"domains"` } Domain struct { ID int `json:"id"` Name string `json:"name"` } DomainResponse struct { ID int `json:"id"` Name string `json:"name"` Created string `json:"created"` PaidUp string `json:"payed_up"` Active bool `json:"active"` } NameserverResponse struct { General NameserverGeneral `json:"general"` Nameservers []*Nameserver `json:"nameservers"` SOA NameserverSOA `json:"soa"` } NameserverGeneral struct { IPv4 string `json:"ip_v4"` IPv6 string `json:"ip_v6"` IncludeWWW bool `json:"include_www"` } NameserverSOA struct { Mail string `json:"mail"` Refresh int `json:"refresh"` Retry int `json:"retry"` Expiry int `json:"expiry"` TTL int `json:"ttl"` } Nameserver struct { Name string `json:"name"` } RecordListingResponse struct { Page int `json:"page"` Limit int `json:"limit"` Pages int `json:"pages"` Total int `json:"total"` Embedded EmbeddedRecordList `json:"_embedded"` } EmbeddedRecordList struct { Records []*Record `json:"records"` } Record struct { Name string `json:"name"` Value string `json:"value"` TTL int `json:"ttl"` Priority int `json:"priority"` Type string `json:"type"` } ) func (d *DNSProvider) getDomainIDByName(name string) (int, error) { // Load from cache if exists d.domainIDMu.Lock() id, ok := d.domainIDMapping[name] d.domainIDMu.Unlock() if ok { return id, nil } // Find out by querying API domains, err := d.listDomains() if err != nil { return domainNotFound, err } // Linear search over all registered domains for _, domain := range domains { if domain.Name == name || strings.HasSuffix(name, "."+domain.Name) { d.domainIDMu.Lock() d.domainIDMapping[name] = domain.ID d.domainIDMu.Unlock() return domain.ID, nil } } return domainNotFound, errors.New("domain not found") } func (d *DNSProvider) listDomains() ([]*Domain, error) { req, err := d.makeRequest(http.MethodGet, "/v1/domains", http.NoBody) if err != nil { return nil, fmt.Errorf("failed to make request: %w", err) } // Checkdomain also provides a query param 'query' which allows filtering domains for a string. // But that functionality is kinda broken, // so we scan through the whole list of registered domains to later find the one that is of interest to us. q := req.URL.Query() q.Set("limit", strconv.Itoa(maxLimit)) currentPage := 1 totalPages := maxInt var domainList []*Domain for currentPage <= totalPages { q.Set("page", strconv.Itoa(currentPage)) req.URL.RawQuery = q.Encode() var res DomainListingResponse if err := d.sendRequest(req, &res); err != nil { return nil, fmt.Errorf("failed to send domain listing request: %w", err) } // This is the first response, // so we update totalPages and allocate the slice memory. if totalPages == maxInt { totalPages = res.Pages domainList = make([]*Domain, 0, res.Total) } domainList = append(domainList, res.Embedded.Domains...) currentPage++ } return domainList, nil } func (d *DNSProvider) getNameserverInfo(domainID int) (*NameserverResponse, error) { req, err := d.makeRequest(http.MethodGet, fmt.Sprintf("/v1/domains/%d/nameservers", domainID), http.NoBody) if err != nil { return nil, err } res := &NameserverResponse{} if err := d.sendRequest(req, res); err != nil { return nil, err } return res, nil } func (d *DNSProvider) checkNameservers(domainID int) error { info, err := d.getNameserverInfo(domainID) if err != nil { return err } var found1, found2 bool for _, item := range info.Nameservers { switch item.Name { case ns1: found1 = true case ns2: found2 = true } } if !found1 || !found2 { return errors.New("not using checkdomain nameservers, can not update records") } return nil } func (d *DNSProvider) createRecord(domainID int, record *Record) error { bs, err := json.Marshal(record) if err != nil { return fmt.Errorf("encoding record failed: %w", err) } req, err := d.makeRequest(http.MethodPost, fmt.Sprintf("/v1/domains/%d/nameservers/records", domainID), bytes.NewReader(bs)) if err != nil { return err } return d.sendRequest(req, nil) } // Checkdomain doesn't seem provide a way to delete records but one can replace all records at once. // The current solution is to fetch all records and then use that list minus the record deleted as the new record list. // TODO: Simplify this function once Checkdomain do provide the functionality. func (d *DNSProvider) deleteTXTRecord(domainID int, recordName, recordValue string) error { domainInfo, err := d.getDomainInfo(domainID) if err != nil { return err } nsInfo, err := d.getNameserverInfo(domainID) if err != nil { return err } allRecords, err := d.listRecords(domainID, "") if err != nil { return err } recordName = strings.TrimSuffix(recordName, "."+domainInfo.Name+".") var recordsToKeep []*Record // Find and delete matching records for _, record := range allRecords { if skipRecord(recordName, recordValue, record, nsInfo) { continue } // Checkdomain API can return records without any TTL set (indicated by the value of 0). // The API Call to replace the records would fail if we wouldn't specify a value. // Thus, we use the default TTL queried beforehand if record.TTL == 0 { record.TTL = nsInfo.SOA.TTL } recordsToKeep = append(recordsToKeep, record) } return d.replaceRecords(domainID, recordsToKeep) } func (d *DNSProvider) getDomainInfo(domainID int) (*DomainResponse, error) { req, err := d.makeRequest(http.MethodGet, fmt.Sprintf("/v1/domains/%d", domainID), http.NoBody) if err != nil { return nil, err } var res DomainResponse err = d.sendRequest(req, &res) if err != nil { return nil, err } return &res, nil } func (d *DNSProvider) listRecords(domainID int, recordType string) ([]*Record, error) { req, err := d.makeRequest(http.MethodGet, fmt.Sprintf("/v1/domains/%d/nameservers/records", domainID), http.NoBody) if err != nil { return nil, fmt.Errorf("failed to make request: %w", err) } q := req.URL.Query() q.Set("limit", strconv.Itoa(maxLimit)) if recordType != "" { q.Set("type", recordType) } currentPage := 1 totalPages := maxInt var recordList []*Record for currentPage <= totalPages { q.Set("page", strconv.Itoa(currentPage)) req.URL.RawQuery = q.Encode() var res RecordListingResponse if err := d.sendRequest(req, &res); err != nil { return nil, fmt.Errorf("failed to send record listing request: %w", err) } // This is the first response, so we update totalPages and allocate the slice memory. if totalPages == maxInt { totalPages = res.Pages recordList = make([]*Record, 0, res.Total) } recordList = append(recordList, res.Embedded.Records...) currentPage++ } return recordList, nil } func (d *DNSProvider) replaceRecords(domainID int, records []*Record) error { bs, err := json.Marshal(records) if err != nil { return fmt.Errorf("encoding record failed: %w", err) } req, err := d.makeRequest(http.MethodPut, fmt.Sprintf("/v1/domains/%d/nameservers/records", domainID), bytes.NewReader(bs)) if err != nil { return err } return d.sendRequest(req, nil) } func skipRecord(recordName, recordValue string, record *Record, nsInfo *NameserverResponse) bool { // Skip empty records if record.Value == "" { return true } // Skip some special records, otherwise we would get a "Nameserver update failed" if record.Type == "SOA" || record.Type == "NS" || record.Name == "@" || (nsInfo.General.IncludeWWW && record.Name == "www") { return true } nameMatch := recordName == "" || record.Name == recordName valueMatch := recordValue == "" || record.Value == recordValue // Skip our matching record if record.Type == "TXT" && nameMatch && valueMatch { return true } return false } func (d *DNSProvider) makeRequest(method, resource string, body io.Reader) (*http.Request, error) { uri, err := d.config.Endpoint.Parse(resource) if err != nil { return nil, err } req, err := http.NewRequest(method, uri.String(), body) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+d.config.Token) if method != http.MethodGet { req.Header.Set("Content-Type", "application/json") } return req, nil } func (d *DNSProvider) sendRequest(req *http.Request, result interface{}) error { resp, err := d.config.HTTPClient.Do(req) if err != nil { return err } if err = checkResponse(resp); err != nil { return err } defer func() { _ = resp.Body.Close() }() if result == nil { return nil } raw, err := io.ReadAll(resp.Body) if err != nil { return err } err = json.Unmarshal(raw, result) if err != nil { return fmt.Errorf("unmarshaling %T error [status code=%d]: %w: %s", result, resp.StatusCode, err, string(raw)) } return nil } func checkResponse(resp *http.Response) error { if resp.StatusCode < http.StatusBadRequest { return nil } if resp.Body == nil { return fmt.Errorf("response body is nil, status code=%d", resp.StatusCode) } defer func() { _ = resp.Body.Close() }() raw, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("unable to read body: status code=%d, error=%w", resp.StatusCode, err) } return fmt.Errorf("status code=%d: %s", resp.StatusCode, string(raw)) } lego-4.9.1/providers/dns/checkdomain/client_test.go000066400000000000000000000120171434020463500223670ustar00rootroot00000000000000package checkdomain import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "net/url" "reflect" "testing" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupTestProvider(t *testing.T) (*DNSProvider, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) config := NewDefaultConfig() config.Endpoint, _ = url.Parse(server.URL) config.Token = "secret" p, err := NewDNSProviderConfig(config) require.NoError(t, err) return p, mux } func Test_getDomainIDByName(t *testing.T) { prd, handler := setupTestProvider(t) handler.HandleFunc("/v1/domains", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } domainList := DomainListingResponse{ Embedded: EmbeddedDomainList{Domains: []*Domain{ {ID: 1, Name: "test.com"}, {ID: 2, Name: "test.org"}, }}, } err := json.NewEncoder(rw).Encode(domainList) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) id, err := prd.getDomainIDByName("test.com") require.NoError(t, err) assert.Equal(t, 1, id) } func Test_checkNameservers(t *testing.T) { prd, handler := setupTestProvider(t) handler.HandleFunc("/v1/domains/1/nameservers", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } nsResp := NameserverResponse{ Nameservers: []*Nameserver{ {Name: ns1}, {Name: ns2}, // {Name: "ns.fake.de"}, }, } err := json.NewEncoder(rw).Encode(nsResp) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) err := prd.checkNameservers(1) require.NoError(t, err) } func Test_createRecord(t *testing.T) { prd, handler := setupTestProvider(t) handler.HandleFunc("/v1/domains/1/nameservers/records", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } content, err := io.ReadAll(req.Body) if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } if string(content) != `{"name":"test.com","value":"value","ttl":300,"priority":0,"type":"TXT"}` { http.Error(rw, "invalid request body: "+string(content), http.StatusBadRequest) return } }) record := &Record{ Name: "test.com", TTL: 300, Type: "TXT", Value: "value", } err := prd.createRecord(1, record) require.NoError(t, err) } func Test_deleteTXTRecord(t *testing.T) { prd, handler := setupTestProvider(t) domainName := "lego.test" recordValue := "test" records := []*Record{ { Name: "_acme-challenge", Value: recordValue, Type: "TXT", }, { Name: "_acme-challenge", Value: recordValue, Type: "A", }, { Name: "foobar", Value: recordValue, Type: "TXT", }, } expectedRecords := []*Record{ { Name: "_acme-challenge", Value: recordValue, Type: "A", }, { Name: "foobar", Value: recordValue, Type: "TXT", }, } handler.HandleFunc("/v1/domains/1", func(rw http.ResponseWriter, req *http.Request) { resp := DomainResponse{ ID: 1, Name: domainName, } err := json.NewEncoder(rw).Encode(resp) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) handler.HandleFunc("/v1/domains/1/nameservers", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } nsResp := NameserverResponse{ Nameservers: []*Nameserver{{Name: ns1}, {Name: ns2}}, } err := json.NewEncoder(rw).Encode(nsResp) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) handler.HandleFunc("/v1/domains/1/nameservers/records", func(rw http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodGet: resp := RecordListingResponse{ Embedded: EmbeddedRecordList{ Records: records, }, } err := json.NewEncoder(rw).Encode(resp) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } case http.MethodPut: var records []*Record err := json.NewDecoder(req.Body).Decode(&records) if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } if len(records) == 0 { http.Error(rw, "empty request body", http.StatusBadRequest) return } if !reflect.DeepEqual(expectedRecords, records) { http.Error(rw, fmt.Sprintf("invalid records: %v", records), http.StatusBadRequest) return } default: http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) } }) fqdn, _ := dns01.GetRecord(domainName, "abc") err := prd.deleteTXTRecord(1, fqdn, recordValue) require.NoError(t, err) } lego-4.9.1/providers/dns/civo/000077500000000000000000000000001434020463500162155ustar00rootroot00000000000000lego-4.9.1/providers/dns/civo/civo.go000066400000000000000000000107331434020463500175100ustar00rootroot00000000000000// Package civo implements a DNS provider for solving the DNS-01 challenge using CIVO. package civo import ( "errors" "fmt" "strings" "time" "github.com/civo/civogo" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) const ( minTTL = 600 defaultPollingInterval = 30 * time.Second defaultPropagationTimeout = 300 * time.Second ) // Environment variables names. const ( envNamespace = "CIVO_" EnvAPIToken = envNamespace + "TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { ProjectID string Token string PropagationTimeout time.Duration PollingInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *civogo.Client } // NewDNSProvider returns a DNSProvider instance configured for CIVO. // Credentials must be passed in the environment variables: API_TOKEN. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIToken) if err != nil { return nil, fmt.Errorf("civo: %w", err) } config := NewDefaultConfig() config.Token = values[EnvAPIToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for CIVO. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("civo: the configuration of the DNS provider is nil") } if config.Token == "" { return nil, errors.New("civo: credentials missing") } if config.TTL < minTTL { config.TTL = minTTL } // Create a Civo client - DNS is region independent, we can use any region client, err := civogo.NewClient(config.Token, "LON1") if err != nil { return nil, fmt.Errorf("civo: %w", err) } return &DNSProvider{config: config, client: client}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := getZone(fqdn) if err != nil { return fmt.Errorf("civo: failed to find zone: fqdn=%s: %w", fqdn, err) } dnsDomain, err := d.client.GetDNSDomain(zone) if err != nil { return fmt.Errorf("civo: %w", err) } _, err = d.client.CreateDNSRecord(dnsDomain.ID, &civogo.DNSRecordConfig{ Name: extractRecordName(fqdn, zone), Value: value, Type: civogo.DNSRecordTypeTXT, TTL: d.config.TTL, }) if err != nil { return fmt.Errorf("civo: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := getZone(fqdn) if err != nil { return fmt.Errorf("civo: failed to find zone: fqdn=%s: %w", fqdn, err) } dnsDomain, err := d.client.GetDNSDomain(zone) if err != nil { return fmt.Errorf("civo: %w", err) } dnsRecords, err := d.client.ListDNSRecords(dnsDomain.ID) if err != nil { return fmt.Errorf("civo: %w", err) } var dnsRecord civogo.DNSRecord for _, entry := range dnsRecords { if entry.Name == extractRecordName(fqdn, zone) && entry.Value == value { dnsRecord = entry break } } _, err = d.client.DeleteDNSRecord(&dnsRecord) if err != nil { return fmt.Errorf("civo: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func getZone(fqdn string) (string, error) { authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", err } return dns01.UnFqdn(authZone), nil } func extractRecordName(fqdn, zone string) string { name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+zone); idx != -1 { return name[:idx] } return name } lego-4.9.1/providers/dns/civo/civo.toml000066400000000000000000000011221434020463500200460ustar00rootroot00000000000000Name = "Civo" Description = '''''' URL = "https://civo.com" Code = "civo" Since = "v4.9.0" Example = ''' CIVO_TOKEN=xxxxxx \ lego --email you@example.com --dns civo --domains my.example.org run ''' [Configuration] [Configuration.Credentials] CIVO_TOKEN = "Authentication token" [Configuration.Additional] CIVO_POLLING_INTERVAL = "Time between DNS propagation check" CIVO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" CIVO_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://www.civo.com/api/dns" lego-4.9.1/providers/dns/civo/civo_test.go000066400000000000000000000047421434020463500205520ustar00rootroot00000000000000package civo import ( "fmt" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAPIToken). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIToken: "00000000000000000000000000000000000000000000000000", }, }, { desc: "missing api key", envVars: map[string]string{ EnvAPIToken: "", }, expected: fmt.Sprintf("civo: some credentials information are missing: %s", EnvAPIToken), }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string token string ttl int expected string }{ { desc: "success", token: "00000000000000000000000000000000000000000000000000", ttl: minTTL, }, { desc: "missing api key", token: "", ttl: minTTL, expected: "civo: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.TTL = test.ttl config.Token = test.token p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/clouddns/000077500000000000000000000000001434020463500170705ustar00rootroot00000000000000lego-4.9.1/providers/dns/clouddns/clouddns.go000066400000000000000000000074141434020463500212400ustar00rootroot00000000000000// Package clouddns implements a DNS provider for solving the DNS-01 challenge using CloudDNS API. package clouddns import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/clouddns/internal" ) // Environment variables names. const ( envNamespace = "CLOUDDNS_" EnvClientID = envNamespace + "CLIENT_ID" EnvEmail = envNamespace + "EMAIL" EnvPassword = envNamespace + "PASSWORD" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the DNSProvider. type Config struct { ClientID string Email string Password string TTL int PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 300), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *internal.Client config *Config } // NewDNSProvider returns a DNSProvider instance configured for CloudDNS. // Credentials must be passed in the environment variables: // CLOUDDNS_CLIENT_ID, CLOUDDNS_EMAIL, CLOUDDNS_PASSWORD. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvClientID, EnvEmail, EnvPassword) if err != nil { return nil, fmt.Errorf("clouddns: %w", err) } config := NewDefaultConfig() config.ClientID = values[EnvClientID] config.Email = values[EnvEmail] config.Password = values[EnvPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for CloudDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("clouddns: the configuration of the DNS provider is nil") } if config.ClientID == "" || config.Email == "" || config.Password == "" { return nil, errors.New("clouddns: credentials missing") } client := internal.NewClient(config.ClientID, config.Email, config.Password, config.TTL) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ client: client, config: config, }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("clouddns: %w", err) } err = d.client.AddRecord(authZone, fqdn, value) if err != nil { return fmt.Errorf("clouddns: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("clouddns: %w", err) } err = d.client.DeleteRecord(authZone, fqdn) if err != nil { return fmt.Errorf("clouddns: %w", err) } return nil } lego-4.9.1/providers/dns/clouddns/clouddns.toml000066400000000000000000000017221434020463500216020ustar00rootroot00000000000000Name = "CloudDNS" Description = '''''' URL = "https://vshosting.eu/" Code = "clouddns" Since = "v3.6.0" Example = ''' CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \ CLOUDDNS_EMAIL=you@example.com \ CLOUDDNS_PASSWORD=b9841238feb177a84330f \ lego --email you@example.com --dns clouddns --domains my.example.org run ''' [Configuration] [Configuration.Credentials] CLOUDDNS_CLIENT_ID = "Client ID" CLOUDDNS_EMAIL = "Account email" CLOUDDNS_PASSWORD = "Account password" [Configuration.Additional] CLOUDDNS_POLLING_INTERVAL = "Time between DNS propagation check" CLOUDDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" CLOUDDNS_TTL = "The TTL of the TXT record used for the DNS challenge" CLOUDDNS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://admin.vshosting.cloud/clouddns/swagger/" APIAdmin = "https://admin.vshosting.cloud/api/public/swagger/" Documentation = "https://github.com/vshosting/clouddns" lego-4.9.1/providers/dns/clouddns/clouddns_test.go000066400000000000000000000070161434020463500222750ustar00rootroot00000000000000package clouddns import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvClientID, EnvEmail, EnvPassword). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvClientID: "client123", EnvEmail: "test@example.com", EnvPassword: "password123", }, }, { desc: "missing clientId", envVars: map[string]string{ EnvClientID: "", EnvEmail: "test@example.com", EnvPassword: "password123", }, expected: "clouddns: some credentials information are missing: CLOUDDNS_CLIENT_ID", }, { desc: "missing email", envVars: map[string]string{ EnvClientID: "client123", EnvEmail: "", EnvPassword: "password123", }, expected: "clouddns: some credentials information are missing: CLOUDDNS_EMAIL", }, { desc: "missing password", envVars: map[string]string{ EnvClientID: "client123", EnvEmail: "test@example.com", EnvPassword: "", }, expected: "clouddns: some credentials information are missing: CLOUDDNS_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string clientID string email string password string expected string }{ { desc: "success", clientID: "ID", email: "test@example.com", password: "secret", }, { desc: "missing credentials", expected: "clouddns: credentials missing", }, { desc: "missing client ID", clientID: "", email: "test@example.com", password: "secret", expected: "clouddns: credentials missing", }, { desc: "missing email", clientID: "ID", email: "", password: "secret", expected: "clouddns: credentials missing", }, { desc: "missing password", clientID: "ID", email: "test@example.com", password: "", expected: "clouddns: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.ClientID = test.clientID config.Email = test.email config.Password = test.password p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/clouddns/internal/000077500000000000000000000000001434020463500207045ustar00rootroot00000000000000lego-4.9.1/providers/dns/clouddns/internal/client.go000066400000000000000000000133631434020463500225170ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" ) const ( apiBaseURL = "https://admin.vshosting.cloud/clouddns" loginURL = "https://admin.vshosting.cloud/api/public/auth/login" ) // Client handles all communication with CloudDNS API. type Client struct { AccessToken string ClientID string Email string Password string TTL int HTTPClient *http.Client apiBaseURL string loginURL string } // NewClient returns a Client instance configured to handle CloudDNS API communication. func NewClient(clientID, email, password string, ttl int) *Client { return &Client{ ClientID: clientID, Email: email, Password: password, TTL: ttl, HTTPClient: &http.Client{}, apiBaseURL: apiBaseURL, loginURL: loginURL, } } // AddRecord is a high level method to add a new record into CloudDNS zone. func (c *Client) AddRecord(zone, recordName, recordValue string) error { domain, err := c.getDomain(zone) if err != nil { return err } record := Record{DomainID: domain.ID, Name: recordName, Value: recordValue, Type: "TXT"} err = c.addTxtRecord(record) if err != nil { return err } return c.publishRecords(domain.ID) } // DeleteRecord is a high level method to remove a record from zone. func (c *Client) DeleteRecord(zone, recordName string) error { domain, err := c.getDomain(zone) if err != nil { return err } record, err := c.getRecord(domain.ID, recordName) if err != nil { return err } err = c.deleteRecord(record) if err != nil { return err } return c.publishRecords(domain.ID) } func (c *Client) addTxtRecord(record Record) error { body, err := json.Marshal(record) if err != nil { return err } _, err = c.doAPIRequest(http.MethodPost, "record-txt", bytes.NewReader(body)) return err } func (c *Client) deleteRecord(record Record) error { endpoint := fmt.Sprintf("record/%s", record.ID) _, err := c.doAPIRequest(http.MethodDelete, endpoint, nil) return err } func (c *Client) getDomain(zone string) (Domain, error) { searchQuery := SearchQuery{ Search: []Search{ {Name: "clientId", Operator: "eq", Value: c.ClientID}, {Name: "domainName", Operator: "eq", Value: zone}, }, } body, err := json.Marshal(searchQuery) if err != nil { return Domain{}, err } resp, err := c.doAPIRequest(http.MethodPost, "domain/search", bytes.NewReader(body)) if err != nil { return Domain{}, err } var result SearchResponse err = json.Unmarshal(resp, &result) if err != nil { return Domain{}, err } if len(result.Items) == 0 { return Domain{}, fmt.Errorf("domain not found: %s", zone) } return result.Items[0], nil } func (c *Client) getRecord(domainID, recordName string) (Record, error) { endpoint := fmt.Sprintf("domain/%s", domainID) resp, err := c.doAPIRequest(http.MethodGet, endpoint, nil) if err != nil { return Record{}, err } var result DomainInfo err = json.Unmarshal(resp, &result) if err != nil { return Record{}, err } for _, record := range result.LastDomainRecordList { if record.Name == recordName && record.Type == "TXT" { return record, nil } } return Record{}, fmt.Errorf("record not found: domainID %s, name %s", domainID, recordName) } func (c *Client) publishRecords(domainID string) error { body, err := json.Marshal(DomainInfo{SoaTTL: c.TTL}) if err != nil { return err } endpoint := fmt.Sprintf("domain/%s/publish", domainID) _, err = c.doAPIRequest(http.MethodPut, endpoint, bytes.NewReader(body)) return err } func (c *Client) login() error { authorization := Authorization{Email: c.Email, Password: c.Password} body, err := json.Marshal(authorization) if err != nil { return err } req, err := http.NewRequest(http.MethodPost, c.loginURL, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") content, err := c.doRequest(req) if err != nil { return err } var result AuthResponse err = json.Unmarshal(content, &result) if err != nil { return err } c.AccessToken = result.Auth.AccessToken return nil } func (c *Client) doAPIRequest(method, endpoint string, body io.Reader) ([]byte, error) { if c.AccessToken == "" { err := c.login() if err != nil { return nil, err } } url := fmt.Sprintf("%s/%s", c.apiBaseURL, endpoint) req, err := c.newRequest(method, url, body) if err != nil { return nil, err } content, err := c.doRequest(req) if err != nil { return nil, err } return content, nil } func (c *Client) newRequest(method, reqURL string, body io.Reader) (*http.Request, error) { req, err := http.NewRequest(method, reqURL, body) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.AccessToken)) return req, nil } func (c *Client) doRequest(req *http.Request) ([]byte, error) { resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { return nil, readError(req, resp) } content, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return content, nil } func readError(req *http.Request, resp *http.Response) error { content, err := io.ReadAll(resp.Body) if err != nil { return errors.New(toUnreadableBodyMessage(req, content)) } var errInfo APIError err = json.Unmarshal(content, &errInfo) if err != nil { return fmt.Errorf("APIError unmarshaling error: %w: %s", err, toUnreadableBodyMessage(req, content)) } return fmt.Errorf("HTTP %d: code %v: %s", resp.StatusCode, errInfo.Error.Code, errInfo.Error.Message) } func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string { return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody)) } lego-4.9.1/providers/dns/clouddns/internal/client_test.go000066400000000000000000000061101434020463500235460ustar00rootroot00000000000000package internal import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/require" ) func TestClient_AddRecord(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/api/domain/search", func(rw http.ResponseWriter, req *http.Request) { response := SearchResponse{ Items: []Domain{ { ID: "A", DomainName: "example.com", }, }, } err := json.NewEncoder(rw).Encode(response) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) mux.HandleFunc("/api/record-txt", func(rw http.ResponseWriter, req *http.Request) {}) mux.HandleFunc("/api/domain/A/publish", func(rw http.ResponseWriter, req *http.Request) {}) mux.HandleFunc("/login", func(rw http.ResponseWriter, req *http.Request) { response := AuthResponse{ Auth: Auth{ AccessToken: "at", RefreshToken: "", }, } err := json.NewEncoder(rw).Encode(response) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient("clientID", "email@example.com", "secret", 300) client.apiBaseURL = server.URL + "/api" client.loginURL = server.URL + "/login" err := client.AddRecord("example.com", "_acme-challenge.example.com", "txt") require.NoError(t, err) } func TestClient_DeleteRecord(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/api/domain/search", func(rw http.ResponseWriter, req *http.Request) { response := SearchResponse{ Items: []Domain{ { ID: "A", DomainName: "example.com", }, }, } err := json.NewEncoder(rw).Encode(response) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) mux.HandleFunc("/api/domain/A", func(rw http.ResponseWriter, req *http.Request) { response := DomainInfo{ ID: "Z", DomainName: "example.com", LastDomainRecordList: []Record{ { ID: "R01", DomainID: "A", Name: "_acme-challenge.example.com", Value: "txt", Type: "TXT", }, }, SoaTTL: 300, } err := json.NewEncoder(rw).Encode(response) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) mux.HandleFunc("/api/record/R01", func(rw http.ResponseWriter, req *http.Request) {}) mux.HandleFunc("/api/domain/A/publish", func(rw http.ResponseWriter, req *http.Request) {}) mux.HandleFunc("/login", func(rw http.ResponseWriter, req *http.Request) { response := AuthResponse{ Auth: Auth{ AccessToken: "at", RefreshToken: "", }, } err := json.NewEncoder(rw).Encode(response) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient("clientID", "email@example.com", "secret", 300) client.apiBaseURL = server.URL + "/api" client.loginURL = server.URL + "/login" err := client.DeleteRecord("example.com", "_acme-challenge.example.com") require.NoError(t, err) } lego-4.9.1/providers/dns/clouddns/internal/models.go000066400000000000000000000037461434020463500225300ustar00rootroot00000000000000package internal type APIError struct { Error ErrorContent `json:"error"` } type ErrorContent struct { Code int `json:"code,omitempty"` Message string `json:"message,omitempty"` } type Authorization struct { Email string `json:"email,omitempty"` Password string `json:"password,omitempty"` } type AuthResponse struct { Auth Auth `json:"auth,omitempty"` } type Auth struct { AccessToken string `json:"accessToken,omitempty"` RefreshToken string `json:"refreshToken,omitempty"` } type SearchQuery struct { Limit int `json:"limit,omitempty"` Offset int `json:"offset,omitempty"` Search []Search `json:"search,omitempty"` Sort []Sort `json:"sort,omitempty"` } // Search used for searches in the CloudDNS API. type Search struct { Name string `json:"name,omitempty"` Operator string `json:"operator,omitempty"` Type string `json:"type,omitempty"` Value string `json:"value,omitempty"` } type Sort struct { Ascending bool `json:"ascending,omitempty"` Name string `json:"name,omitempty"` } type SearchResponse struct { Items []Domain `json:"items,omitempty"` Limit int `json:"limit,omitempty"` Offset int `json:"offset,omitempty"` TotalHits int `json:"totalHits,omitempty"` } type Domain struct { ID string `json:"id,omitempty"` DomainName string `json:"domainName,omitempty"` Status string `json:"status,omitempty"` } // Record represents a DNS record. type Record struct { ID string `json:"id,omitempty"` DomainID string `json:"domainId,omitempty"` Name string `json:"name,omitempty"` Value string `json:"value,omitempty"` Type string `json:"type,omitempty"` } type DomainInfo struct { ID string `json:"id,omitempty"` DomainName string `json:"domainName,omitempty"` LastDomainRecordList []Record `json:"lastDomainRecordList,omitempty"` SoaTTL int `json:"soaTtl,omitempty"` Status string `json:"status,omitempty"` } lego-4.9.1/providers/dns/cloudflare/000077500000000000000000000000001434020463500173755ustar00rootroot00000000000000lego-4.9.1/providers/dns/cloudflare/client.go000066400000000000000000000044451434020463500212110ustar00rootroot00000000000000package cloudflare import ( "context" "sync" "github.com/cloudflare/cloudflare-go" "github.com/go-acme/lego/v4/challenge/dns01" ) type metaClient struct { clientEdit *cloudflare.API // needs Zone/DNS/Edit permissions clientRead *cloudflare.API // needs Zone/Zone/Read permissions zones map[string]string // caches calls to ZoneIDByName, see lookupZoneID() zonesMu *sync.RWMutex } func newClient(config *Config) (*metaClient, error) { // with AuthKey/AuthEmail we can access all available APIs if config.AuthToken == "" { client, err := cloudflare.New(config.AuthKey, config.AuthEmail, cloudflare.HTTPClient(config.HTTPClient)) if err != nil { return nil, err } return &metaClient{ clientEdit: client, clientRead: client, zones: make(map[string]string), zonesMu: &sync.RWMutex{}, }, nil } dns, err := cloudflare.NewWithAPIToken(config.AuthToken, cloudflare.HTTPClient(config.HTTPClient)) if err != nil { return nil, err } if config.ZoneToken == "" || config.ZoneToken == config.AuthToken { return &metaClient{ clientEdit: dns, clientRead: dns, zones: make(map[string]string), zonesMu: &sync.RWMutex{}, }, nil } zone, err := cloudflare.NewWithAPIToken(config.ZoneToken, cloudflare.HTTPClient(config.HTTPClient)) if err != nil { return nil, err } return &metaClient{ clientEdit: dns, clientRead: zone, zones: make(map[string]string), zonesMu: &sync.RWMutex{}, }, nil } func (m *metaClient) CreateDNSRecord(ctx context.Context, zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { return m.clientEdit.CreateDNSRecord(ctx, zoneID, rr) } func (m *metaClient) DNSRecords(ctx context.Context, zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { return m.clientEdit.DNSRecords(ctx, zoneID, rr) } func (m *metaClient) DeleteDNSRecord(ctx context.Context, zoneID, recordID string) error { return m.clientEdit.DeleteDNSRecord(ctx, zoneID, recordID) } func (m *metaClient) ZoneIDByName(fdqn string) (string, error) { m.zonesMu.RLock() id := m.zones[fdqn] m.zonesMu.RUnlock() if id != "" { return id, nil } id, err := m.clientRead.ZoneIDByName(dns01.UnFqdn(fdqn)) if err != nil { return "", err } m.zonesMu.Lock() m.zones[fdqn] = id m.zonesMu.Unlock() return id, nil } lego-4.9.1/providers/dns/cloudflare/cloudflare.go000066400000000000000000000131751434020463500220530ustar00rootroot00000000000000// Package cloudflare implements a DNS provider for solving the DNS-01 challenge using cloudflare DNS. package cloudflare import ( "context" "errors" "fmt" "net/http" "sync" "time" "github.com/cloudflare/cloudflare-go" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" ) const ( minTTL = 120 ) // Config is used to configure the creation of the DNSProvider. type Config struct { AuthEmail string AuthKey string AuthToken string ZoneToken string TTL int PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt("CLOUDFLARE_TTL", minTTL), PropagationTimeout: env.GetOrDefaultSecond("CLOUDFLARE_PROPAGATION_TIMEOUT", 2*time.Minute), PollingInterval: env.GetOrDefaultSecond("CLOUDFLARE_POLLING_INTERVAL", 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("CLOUDFLARE_HTTP_TIMEOUT", 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *metaClient config *Config recordIDs map[string]string recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for Cloudflare. // Credentials must be passed in as environment variables: // // Either provide CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY, // or a CLOUDFLARE_DNS_API_TOKEN. // // For a more paranoid setup, provide CLOUDFLARE_DNS_API_TOKEN and CLOUDFLARE_ZONE_API_TOKEN. // // The email and API key should be avoided, if possible. // Instead setup a API token with both Zone:Read and DNS:Edit permission, and pass the CLOUDFLARE_DNS_API_TOKEN environment variable. // You can split the Zone:Read and DNS:Edit permissions across multiple API tokens: // in this case pass both CLOUDFLARE_ZONE_API_TOKEN and CLOUDFLARE_DNS_API_TOKEN accordingly. func NewDNSProvider() (*DNSProvider, error) { values, err := env.GetWithFallback( []string{"CLOUDFLARE_EMAIL", "CF_API_EMAIL"}, []string{"CLOUDFLARE_API_KEY", "CF_API_KEY"}, ) if err != nil { var errT error values, errT = env.GetWithFallback( []string{"CLOUDFLARE_DNS_API_TOKEN", "CF_DNS_API_TOKEN"}, []string{"CLOUDFLARE_ZONE_API_TOKEN", "CF_ZONE_API_TOKEN", "CLOUDFLARE_DNS_API_TOKEN", "CF_DNS_API_TOKEN"}, ) if errT != nil { //nolint:errorlint return nil, fmt.Errorf("cloudflare: %v or %v", err, errT) } } config := NewDefaultConfig() config.AuthEmail = values["CLOUDFLARE_EMAIL"] config.AuthKey = values["CLOUDFLARE_API_KEY"] config.AuthToken = values["CLOUDFLARE_DNS_API_TOKEN"] config.ZoneToken = values["CLOUDFLARE_ZONE_API_TOKEN"] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Cloudflare. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("cloudflare: the configuration of the DNS provider is nil") } if config.TTL < minTTL { return nil, fmt.Errorf("cloudflare: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } client, err := newClient(config) if err != nil { return nil, fmt.Errorf("cloudflare: %w", err) } return &DNSProvider{ client: client, config: config, recordIDs: make(map[string]string), }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("cloudflare: %w", err) } zoneID, err := d.client.ZoneIDByName(authZone) if err != nil { return fmt.Errorf("cloudflare: failed to find zone %s: %w", authZone, err) } dnsRecord := cloudflare.DNSRecord{ Type: "TXT", Name: dns01.UnFqdn(fqdn), Content: value, TTL: d.config.TTL, } response, err := d.client.CreateDNSRecord(context.Background(), zoneID, dnsRecord) if err != nil { return fmt.Errorf("cloudflare: failed to create TXT record: %w", err) } if !response.Success { return fmt.Errorf("cloudflare: failed to create TXT record: %+v %+v", response.Errors, response.Messages) } d.recordIDsMu.Lock() d.recordIDs[token] = response.Result.ID d.recordIDsMu.Unlock() log.Infof("cloudflare: new record for %s, ID %s", domain, response.Result.ID) return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("cloudflare: %w", err) } zoneID, err := d.client.ZoneIDByName(authZone) if err != nil { return fmt.Errorf("cloudflare: failed to find zone %s: %w", authZone, err) } // get the record's unique ID from when we created it d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("cloudflare: unknown record ID for '%s'", fqdn) } err = d.client.DeleteDNSRecord(context.Background(), zoneID, recordID) if err != nil { log.Printf("cloudflare: failed to delete TXT record: %w", err) } // Delete record ID from map d.recordIDsMu.Lock() delete(d.recordIDs, token) d.recordIDsMu.Unlock() return nil } lego-4.9.1/providers/dns/cloudflare/cloudflare.toml000066400000000000000000000063221434020463500224150ustar00rootroot00000000000000Name = "Cloudflare" Description = '''''' URL = "https://www.cloudflare.com/dns/" Code = "cloudflare" Since = "v0.3.0" Example = ''' CLOUDFLARE_EMAIL=you@example.com \ CLOUDFLARE_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ lego --email you@example.com --dns cloudflare --domains my.example.org run # or CLOUDFLARE_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ lego --email you@example.com --dns cloudflare --domains my.example.org run ''' Additional = ''' ## Description You 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`. ### API keys If using API keys (`CF_API_EMAIL` and `CF_API_KEY`), the Global API Key needs to be used, not the Origin CA Key. Please be aware, that this in principle allows Lego to read and change *everything* related to this account. ### API tokens With API tokens (`CF_DNS_API_TOKEN`, and optionally `CF_ZONE_API_TOKEN`), very specific access can be granted to your resources at Cloudflare. See this [Cloudflare announcement](https://blog.cloudflare.com/api-tokens-general-availability/) for details. The main resources Lego cares for are the DNS entries for your Zones. It also need to resolve a domain name to an internal Zone ID in order to manipulate DNS entries. Hence, you should create an API token with the following permissions: * Zone / Zone / Read * Zone / DNS / Edit You also need to scope the access to all your domains for this to work. Then pass the API token as `CF_DNS_API_TOKEN` to Lego. **Alternatively,** if you prefer a more strict set of privileges, you can split the access tokens: * Create one with *Zone / Zone / Read* permissions and scope it to all your zones. This is needed to resolve domain names to Zone IDs and can be shared among multiple Lego installations. Pass this API token as `CF_ZONE_API_TOKEN` to Lego. * Create another API token with *Zone / DNS / Edit* permissions and set the scope to the domains you want to manage with a single Lego installation. Pass this token as `CF_DNS_API_TOKEN` to Lego. * Repeat the previous step for each host you want to run Lego on. This "paranoid" setup is mainly interesting for users who manage many zones/domains with a single Cloudflare account. It follows the principle of least privilege and limits the possible damage, should one of the hosts become compromised. ''' [Configuration] [Configuration.Credentials] CF_API_EMAIL = "Account email" CF_API_KEY = "API key" CF_DNS_API_TOKEN = "API token with DNS:Edit permission (since v3.1.0)" CF_ZONE_API_TOKEN = "API token with Zone:Read permission (since v3.1.0)" CLOUDFLARE_EMAIL = "Alias to CF_API_EMAIL" CLOUDFLARE_API_KEY = "Alias to CF_API_KEY" CLOUDFLARE_DNS_API_TOKEN = "Alias to CF_DNS_API_TOKEN" CLOUDFLARE_ZONE_API_TOKEN = "Alias to CF_ZONE_API_TOKEN" [Configuration.Additional] CLOUDFLARE_POLLING_INTERVAL = "Time between DNS propagation check" CLOUDFLARE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" CLOUDFLARE_TTL = "The TTL of the TXT record used for the DNS challenge" CLOUDFLARE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.cloudflare.com/" GoClient = "https://github.com/cloudflare/cloudflare-go" lego-4.9.1/providers/dns/cloudflare/cloudflare_test.go000066400000000000000000000160661434020463500231140ustar00rootroot00000000000000package cloudflare import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var envTest = tester.NewEnvTest( "CLOUDFLARE_EMAIL", "CLOUDFLARE_API_KEY", "CLOUDFLARE_DNS_API_TOKEN", "CLOUDFLARE_ZONE_API_TOKEN"). WithDomain("CLOUDFLARE_DOMAIN") func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success email, API key", envVars: map[string]string{ "CLOUDFLARE_EMAIL": "test@example.com", "CLOUDFLARE_API_KEY": "123", }, }, { desc: "success API token", envVars: map[string]string{ "CLOUDFLARE_DNS_API_TOKEN": "012345abcdef", }, }, { desc: "success separate API tokens", envVars: map[string]string{ "CLOUDFLARE_DNS_API_TOKEN": "012345abcdef", "CLOUDFLARE_ZONE_API_TOKEN": "abcdef012345", }, }, { desc: "missing credentials", envVars: map[string]string{ "CLOUDFLARE_EMAIL": "", "CLOUDFLARE_API_KEY": "", "CLOUDFLARE_DNS_API_TOKEN": "", }, expected: "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY or some credentials information are missing: CLOUDFLARE_DNS_API_TOKEN,CLOUDFLARE_ZONE_API_TOKEN", }, { desc: "missing email", envVars: map[string]string{ "CLOUDFLARE_EMAIL": "", "CLOUDFLARE_API_KEY": "key", }, expected: "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL or some credentials information are missing: CLOUDFLARE_DNS_API_TOKEN,CLOUDFLARE_ZONE_API_TOKEN", }, { desc: "missing api key", envVars: map[string]string{ "CLOUDFLARE_EMAIL": "awesome@possum.com", "CLOUDFLARE_API_KEY": "", }, expected: "cloudflare: some credentials information are missing: CLOUDFLARE_API_KEY or some credentials information are missing: CLOUDFLARE_DNS_API_TOKEN,CLOUDFLARE_ZONE_API_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderWithToken(t *testing.T) { type expected struct { dnsToken string zoneToken string sameClient bool error string } testCases := []struct { desc string // test input envVars map[string]string // expectations expected expected }{ { desc: "same client when zone token is missing", envVars: map[string]string{ "CLOUDFLARE_DNS_API_TOKEN": "123", }, expected: expected{ dnsToken: "123", zoneToken: "123", sameClient: true, }, }, { desc: "same client when zone token equals dns token", envVars: map[string]string{ "CLOUDFLARE_DNS_API_TOKEN": "123", "CLOUDFLARE_ZONE_API_TOKEN": "123", }, expected: expected{ dnsToken: "123", zoneToken: "123", sameClient: true, }, }, { desc: "failure when only zone api given", envVars: map[string]string{ "CLOUDFLARE_ZONE_API_TOKEN": "123", }, expected: expected{ error: "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY or some credentials information are missing: CLOUDFLARE_DNS_API_TOKEN", }, }, { desc: "different clients when zone and dns token differ", envVars: map[string]string{ "CLOUDFLARE_DNS_API_TOKEN": "123", "CLOUDFLARE_ZONE_API_TOKEN": "abc", }, expected: expected{ dnsToken: "123", zoneToken: "abc", sameClient: false, }, }, { desc: "aliases work as expected", // CLOUDFLARE_* takes precedence over CF_* envVars: map[string]string{ "CLOUDFLARE_DNS_API_TOKEN": "123", "CF_DNS_API_TOKEN": "456", "CLOUDFLARE_ZONE_API_TOKEN": "abc", "CF_ZONE_API_TOKEN": "def", }, expected: expected{ dnsToken: "123", zoneToken: "abc", sameClient: false, }, }, } defer envTest.RestoreEnv() localEnvTest := tester.NewEnvTest( "CLOUDFLARE_DNS_API_TOKEN", "CF_DNS_API_TOKEN", "CLOUDFLARE_ZONE_API_TOKEN", "CF_ZONE_API_TOKEN", ).WithDomain("CLOUDFLARE_DOMAIN") envTest.ClearEnv() for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer localEnvTest.RestoreEnv() localEnvTest.ClearEnv() localEnvTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected.error != "" { require.EqualError(t, err, test.expected.error) return } require.NoError(t, err) require.NotNil(t, p) assert.Equal(t, test.expected.dnsToken, p.config.AuthToken) assert.Equal(t, test.expected.zoneToken, p.config.ZoneToken) if test.expected.sameClient { assert.Equal(t, p.client.clientRead, p.client.clientEdit) } else { assert.NotEqual(t, p.client.clientRead, p.client.clientEdit) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string authEmail string authKey string authToken string expected string }{ { desc: "success with email and api key", authEmail: "test@example.com", authKey: "123", }, { desc: "success with api token", authToken: "012345abcdef", }, { desc: "prefer api token", authToken: "012345abcdef", authEmail: "test@example.com", authKey: "123", }, { desc: "missing credentials", expected: "cloudflare: invalid credentials: key & email must not be empty", }, { desc: "missing email", authKey: "123", expected: "cloudflare: invalid credentials: key & email must not be empty", }, { desc: "missing api key", authEmail: "test@example.com", expected: "cloudflare: invalid credentials: key & email must not be empty", }, { desc: "missing api token, fallback to api key/email", authToken: "", expected: "cloudflare: invalid credentials: key & email must not be empty", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.AuthEmail = test.authEmail config.AuthKey = test.authKey config.AuthToken = test.authToken p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/cloudns/000077500000000000000000000000001434020463500167245ustar00rootroot00000000000000lego-4.9.1/providers/dns/cloudns/cloudns.go000066400000000000000000000116211434020463500207230ustar00rootroot00000000000000// Package cloudns implements a DNS provider for solving the DNS-01 challenge using ClouDNS DNS. package cloudns import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/platform/wait" "github.com/go-acme/lego/v4/providers/dns/cloudns/internal" ) // Environment variables names. const ( envNamespace = "CLOUDNS_" EnvAuthID = envNamespace + "AUTH_ID" EnvSubAuthID = envNamespace + "SUB_AUTH_ID" EnvAuthPassword = envNamespace + "AUTH_PASSWORD" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { AuthID string SubAuthID string AuthPassword string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 60), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 180*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for ClouDNS. // Credentials must be passed in the environment variables: // CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD. func NewDNSProvider() (*DNSProvider, error) { var subAuthID string authID := env.GetOrFile(EnvAuthID) if authID == "" { subAuthID = env.GetOrFile(EnvSubAuthID) } if authID == "" && subAuthID == "" { return nil, fmt.Errorf("ClouDNS: some credentials information are missing: %s or %s", EnvAuthID, EnvSubAuthID) } values, err := env.Get(EnvAuthPassword) if err != nil { return nil, fmt.Errorf("ClouDNS: %w", err) } config := NewDefaultConfig() config.AuthID = authID config.SubAuthID = subAuthID config.AuthPassword = values[EnvAuthPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for ClouDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("ClouDNS: the configuration of the DNS provider is nil") } client, err := internal.NewClient(config.AuthID, config.SubAuthID, config.AuthPassword) if err != nil { return nil, fmt.Errorf("ClouDNS: %w", err) } client.HTTPClient = config.HTTPClient return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.client.GetZone(fqdn) if err != nil { return fmt.Errorf("ClouDNS: %w", err) } err = d.client.AddTxtRecord(zone.Name, fqdn, value, d.config.TTL) if err != nil { return fmt.Errorf("ClouDNS: %w", err) } return d.waitNameservers(domain, zone) } // CleanUp removes the TXT records matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := d.client.GetZone(fqdn) if err != nil { return fmt.Errorf("ClouDNS: %w", err) } records, err := d.client.ListTxtRecords(zone.Name, fqdn) if err != nil { return fmt.Errorf("ClouDNS: %w", err) } if len(records) == 0 { return nil } for _, record := range records { err = d.client.RemoveTxtRecord(record.ID, zone.Name) if err != nil { return fmt.Errorf("ClouDNS: %w", err) } } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // waitNameservers At the time of writing 4 servers are found as authoritative, but 8 are reported during the sync. // If this is not done, the secondary verification done by Let's Encrypt server will fail quire a bit. func (d *DNSProvider) waitNameservers(domain string, zone *internal.Zone) error { return wait.For("Nameserver sync on "+domain, d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) { syncProgress, err := d.client.GetUpdateStatus(zone.Name) if err != nil { return false, err } log.Infof("[%s] Sync %d/%d complete", domain, syncProgress.Updated, syncProgress.Total) return syncProgress.Complete, nil }) } lego-4.9.1/providers/dns/cloudns/cloudns.toml000066400000000000000000000014351434020463500212730ustar00rootroot00000000000000Name = "ClouDNS" Description = '''''' URL = "https://www.cloudns.net" Code = "cloudns" Since = "v2.3.0" Example = ''' CLOUDNS_AUTH_ID=xxxx \ CLOUDNS_AUTH_PASSWORD=yyyy \ lego --email you@example.com --dns cloudns --domains my.example.org run ''' [Configuration] [Configuration.Credentials] CLOUDNS_AUTH_ID = "The API user ID" CLOUDNS_AUTH_PASSWORD = "The password for API user ID" [Configuration.Additional] CLOUDNS_SUB_AUTH_ID = "The API sub user ID" CLOUDNS_POLLING_INTERVAL = "Time between DNS propagation check" CLOUDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" CLOUDNS_TTL = "The TTL of the TXT record used for the DNS challenge" CLOUDNS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.cloudns.net/wiki/article/42/" lego-4.9.1/providers/dns/cloudns/cloudns_test.go000066400000000000000000000102201434020463500217540ustar00rootroot00000000000000package cloudns import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAuthID, EnvSubAuthID, EnvAuthPassword). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success auth-id", envVars: map[string]string{ EnvAuthID: "123", EnvSubAuthID: "", EnvAuthPassword: "456", }, }, { desc: "success sub-auth-id", envVars: map[string]string{ EnvAuthID: "", EnvSubAuthID: "123", EnvAuthPassword: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAuthID: "", EnvSubAuthID: "", EnvAuthPassword: "", }, expected: "ClouDNS: some credentials information are missing: CLOUDNS_AUTH_ID or CLOUDNS_SUB_AUTH_ID", }, { desc: "missing auth-id", envVars: map[string]string{ EnvAuthID: "", EnvSubAuthID: "", EnvAuthPassword: "456", }, expected: "ClouDNS: some credentials information are missing: CLOUDNS_AUTH_ID or CLOUDNS_SUB_AUTH_ID", }, { desc: "missing sub-auth-id", envVars: map[string]string{ EnvAuthID: "", EnvSubAuthID: "", EnvAuthPassword: "456", }, expected: "ClouDNS: some credentials information are missing: CLOUDNS_AUTH_ID or CLOUDNS_SUB_AUTH_ID", }, { desc: "missing auth-password", envVars: map[string]string{ EnvAuthID: "123", EnvSubAuthID: "", EnvAuthPassword: "", }, expected: "ClouDNS: some credentials information are missing: CLOUDNS_AUTH_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string authID string subAuthID string authPassword string expected string }{ { desc: "success auth-id", authID: "123", subAuthID: "", authPassword: "456", }, { desc: "success sub-auth-id", authID: "", subAuthID: "123", authPassword: "456", }, { desc: "missing credentials", expected: "ClouDNS: credentials missing: authID or subAuthID", }, { desc: "missing auth-id", authID: "", subAuthID: "", authPassword: "456", expected: "ClouDNS: credentials missing: authID or subAuthID", }, { desc: "missing sub-auth-id", authID: "", subAuthID: "", authPassword: "456", expected: "ClouDNS: credentials missing: authID or subAuthID", }, { desc: "missing auth-password", authID: "123", expected: "ClouDNS: credentials missing: authPassword", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.AuthID = test.authID config.SubAuthID = test.subAuthID config.AuthPassword = test.authPassword p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/cloudns/internal/000077500000000000000000000000001434020463500205405ustar00rootroot00000000000000lego-4.9.1/providers/dns/cloudns/internal/client.go000066400000000000000000000204251434020463500223500ustar00rootroot00000000000000package internal import ( "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "path" "strconv" "strings" "github.com/go-acme/lego/v4/challenge/dns01" ) const defaultBaseURL = "https://api.cloudns.net/dns/" // Client the ClouDNS client. type Client struct { authID string subAuthID string authPassword string HTTPClient *http.Client BaseURL *url.URL } // NewClient creates a ClouDNS client. func NewClient(authID, subAuthID, authPassword string) (*Client, error) { if authID == "" && subAuthID == "" { return nil, errors.New("credentials missing: authID or subAuthID") } if authPassword == "" { return nil, errors.New("credentials missing: authPassword") } baseURL, err := url.Parse(defaultBaseURL) if err != nil { return nil, err } return &Client{ authID: authID, subAuthID: subAuthID, authPassword: authPassword, HTTPClient: &http.Client{}, BaseURL: baseURL, }, nil } // GetZone Get domain name information for a FQDN. func (c *Client) GetZone(authFQDN string) (*Zone, error) { authZone, err := dns01.FindZoneByFqdn(authFQDN) if err != nil { return nil, err } authZoneName := dns01.UnFqdn(authZone) endpoint, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "get-zone-info.json")) if err != nil { return nil, fmt.Errorf("failed to parse endpoint: %w", err) } q := endpoint.Query() q.Set("domain-name", authZoneName) endpoint.RawQuery = q.Encode() result, err := c.doRequest(http.MethodGet, endpoint) if err != nil { return nil, err } var zone Zone if len(result) > 0 { if err = json.Unmarshal(result, &zone); err != nil { return nil, fmt.Errorf("failed to unmarshal zone: %w", err) } } if zone.Name == authZoneName { return &zone, nil } return nil, fmt.Errorf("zone %s not found for authFQDN %s", authZoneName, authFQDN) } // FindTxtRecord returns the TXT record a zone ID and a FQDN. func (c *Client) FindTxtRecord(zoneName, fqdn string) (*TXTRecord, error) { host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName)) reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "records.json")) if err != nil { return nil, fmt.Errorf("failed to parse endpoint: %w", err) } q := reqURL.Query() q.Set("domain-name", zoneName) q.Set("host", host) q.Set("type", "TXT") reqURL.RawQuery = q.Encode() result, err := c.doRequest(http.MethodGet, reqURL) if err != nil { return nil, err } // the API returns [] when there is no records. if string(result) == "[]" { return nil, nil } var records map[string]TXTRecord if err = json.Unmarshal(result, &records); err != nil { return nil, fmt.Errorf("failed to unmarshall TXT records: %w: %s", err, string(result)) } for _, record := range records { if record.Host == host && record.Type == "TXT" { return &record, nil } } return nil, nil } // ListTxtRecords returns the TXT records a zone ID and a FQDN. func (c *Client) ListTxtRecords(zoneName, fqdn string) ([]TXTRecord, error) { host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName)) reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "records.json")) if err != nil { return nil, fmt.Errorf("failed to parse endpoint: %w", err) } q := reqURL.Query() q.Set("domain-name", zoneName) q.Set("host", host) q.Set("type", "TXT") reqURL.RawQuery = q.Encode() result, err := c.doRequest(http.MethodGet, reqURL) if err != nil { return nil, err } // the API returns [] when there is no records. if string(result) == "[]" { return nil, nil } var raw map[string]TXTRecord if err = json.Unmarshal(result, &raw); err != nil { return nil, fmt.Errorf("failed to unmarshall TXT records: %w: %s", err, string(result)) } var records []TXTRecord for _, record := range raw { if record.Host == host && record.Type == "TXT" { records = append(records, record) } } return records, nil } // AddTxtRecord adds a TXT record. func (c *Client) AddTxtRecord(zoneName, fqdn, value string, ttl int) error { host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName)) reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "add-record.json")) if err != nil { return fmt.Errorf("failed to parse endpoint: %w", err) } q := reqURL.Query() q.Set("domain-name", zoneName) q.Set("host", host) q.Set("record", value) q.Set("ttl", strconv.Itoa(ttlRounder(ttl))) q.Set("record-type", "TXT") reqURL.RawQuery = q.Encode() raw, err := c.doRequest(http.MethodPost, reqURL) if err != nil { return err } resp := apiResponse{} if err = json.Unmarshal(raw, &resp); err != nil { return fmt.Errorf("failed to unmarshal API response: %w: %s", err, string(raw)) } if resp.Status != "Success" { return fmt.Errorf("failed to add TXT record: %s %s", resp.Status, resp.StatusDescription) } return nil } // RemoveTxtRecord removes a TXT record. func (c *Client) RemoveTxtRecord(recordID int, zoneName string) error { reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "delete-record.json")) if err != nil { return fmt.Errorf("failed to parse endpoint: %w", err) } q := reqURL.Query() q.Set("domain-name", zoneName) q.Set("record-id", strconv.Itoa(recordID)) reqURL.RawQuery = q.Encode() raw, err := c.doRequest(http.MethodPost, reqURL) if err != nil { return err } resp := apiResponse{} if err = json.Unmarshal(raw, &resp); err != nil { return fmt.Errorf("failed to unmarshal API response: %w: %s", err, string(raw)) } if resp.Status != "Success" { return fmt.Errorf("failed to remove TXT record: %s %s", resp.Status, resp.StatusDescription) } return nil } // GetUpdateStatus gets sync progress of all CloudDNS NS servers. func (c *Client) GetUpdateStatus(zoneName string) (*SyncProgress, error) { reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "update-status.json")) if err != nil { return nil, fmt.Errorf("failed to parse endpoint: %w", err) } q := reqURL.Query() q.Set("domain-name", zoneName) reqURL.RawQuery = q.Encode() result, err := c.doRequest(http.MethodGet, reqURL) if err != nil { return nil, err } // the API returns [] when there is no records. if string(result) == "[]" { return nil, errors.New("no nameservers records returned") } var records []UpdateRecord if err = json.Unmarshal(result, &records); err != nil { return nil, fmt.Errorf("failed to unmarshal UpdateRecord: %w: %s", err, string(result)) } updatedCount := 0 for _, record := range records { if record.Updated { updatedCount++ } } return &SyncProgress{Complete: updatedCount == len(records), Updated: updatedCount, Total: len(records)}, nil } func (c *Client) doRequest(method string, uri *url.URL) (json.RawMessage, error) { req, err := c.buildRequest(method, uri) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() content, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.New(toUnreadableBodyMessage(req, content)) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("invalid code (%d), error: %s", resp.StatusCode, content) } return content, nil } func (c *Client) buildRequest(method string, uri *url.URL) (*http.Request, error) { q := uri.Query() if c.subAuthID != "" { q.Set("sub-auth-id", c.subAuthID) } else { q.Set("auth-id", c.authID) } q.Set("auth-password", c.authPassword) uri.RawQuery = q.Encode() req, err := http.NewRequest(method, uri.String(), nil) if err != nil { return nil, fmt.Errorf("invalid request: %w", err) } return req, nil } func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string { return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody)) } // Rounds the given TTL in seconds to the next accepted value. // Accepted TTL values are: // - 60 = 1 minute // - 300 = 5 minutes // - 900 = 15 minutes // - 1800 = 30 minutes // - 3600 = 1 hour // - 21600 = 6 hours // - 43200 = 12 hours // - 86400 = 1 day // - 172800 = 2 days // - 259200 = 3 days // - 604800 = 1 week // - 1209600 = 2 weeks // - 2592000 = 1 month // // See https://www.cloudns.net/wiki/article/58/ for details. func ttlRounder(ttl int) int { for _, validTTL := range []int{60, 300, 900, 1800, 3600, 21600, 43200, 86400, 172800, 259200, 604800, 1209600} { if ttl <= validTTL { return validTTL } } return 2592000 } lego-4.9.1/providers/dns/cloudns/internal/client_test.go000066400000000000000000000377001434020463500234130ustar00rootroot00000000000000package internal import ( "fmt" "net/http" "net/http/httptest" "net/url" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func handlerMock(method string, jsonData []byte) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { if req.Method != method { http.Error(rw, "Incorrect method used", http.StatusBadRequest) return } _, err := rw.Write(jsonData) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } } } func TestNewClient(t *testing.T) { testCases := []struct { desc string authID string subAuthID string authPassword string expected string }{ { desc: "all provided", authID: "1000", subAuthID: "1111", authPassword: "no-secret", }, { desc: "missing authID & subAuthID", authID: "", subAuthID: "", authPassword: "no-secret", expected: "credentials missing: authID or subAuthID", }, { desc: "missing authID & subAuthID", authID: "", subAuthID: "present", authPassword: "", expected: "credentials missing: authPassword", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { client, err := NewClient(test.authID, test.subAuthID, test.authPassword) if test.expected != "" { assert.Nil(t, client) require.EqualError(t, err, test.expected) } else { assert.NotNil(t, client) require.NoError(t, err) } }) } } func TestClient_GetZone(t *testing.T) { type expected struct { zone *Zone errorMsg string } testCases := []struct { desc string authFQDN string apiResponse string expected }{ { desc: "zone found", authFQDN: "_acme-challenge.foo.com.", apiResponse: `{"name": "foo.com", "type": "master", "zone": "zone", "status": "1"}`, expected: expected{ zone: &Zone{ Name: "foo.com", Type: "master", Zone: "zone", Status: "1", }, }, }, { desc: "zone not found", authFQDN: "_acme-challenge.foo.com.", apiResponse: ``, expected: expected{ errorMsg: "zone foo.com not found for authFQDN _acme-challenge.foo.com.", }, }, { desc: "invalid json response", authFQDN: "_acme-challenge.foo.com.", apiResponse: `[{}]`, expected: expected{ errorMsg: "failed to unmarshal zone: json: cannot unmarshal array into Go value of type internal.Zone", }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { server := httptest.NewServer(handlerMock(http.MethodGet, []byte(test.apiResponse))) t.Cleanup(server.Close) client, err := NewClient("myAuthID", "", "myAuthPassword") require.NoError(t, err) client.BaseURL, _ = url.Parse(server.URL) zone, err := client.GetZone(test.authFQDN) if test.expected.errorMsg != "" { require.EqualError(t, err, test.expected.errorMsg) } else { require.NoError(t, err) assert.Equal(t, test.expected.zone, zone) } }) } } func TestClient_FindTxtRecord(t *testing.T) { type expected struct { txtRecord *TXTRecord errorMsg string } testCases := []struct { desc string authFQDN string zoneName string apiResponse string expected }{ { desc: "record found", authFQDN: "_acme-challenge.foo.com.", zoneName: "foo.com", apiResponse: `{ "5769228": { "id": "5769228", "type": "TXT", "host": "_acme-challenge", "record": "txtTXTtxtTXTtxtTXTtxtTXT", "failover": "0", "ttl": "3600", "status": 1 }, "181805209": { "id": "181805209", "type": "TXT", "host": "_github-challenge", "record": "b66b8324b5", "failover": "0", "ttl": "300", "status": 1 } }`, expected: expected{ txtRecord: &TXTRecord{ ID: 5769228, Type: "TXT", Host: "_acme-challenge", Record: "txtTXTtxtTXTtxtTXTtxtTXT", Failover: 0, TTL: 3600, Status: 1, }, }, }, { desc: "no record found", authFQDN: "_acme-challenge.foo.com.", zoneName: "foo.com", apiResponse: `{ "5769228": { "id": "5769228", "type": "TXT", "host": "_other-challenge", "record": "txtTXTtxtTXTtxtTXTtxtTXT", "failover": "0", "ttl": "3600", "status": 1 }, "181805209": { "id": "181805209", "type": "TXT", "host": "_github-challenge", "record": "b66b8324b5", "failover": "0", "ttl": "300", "status": 1 } }`, }, { desc: "zero records", authFQDN: "_acme-challenge.foo.com.", zoneName: "test-zone", apiResponse: `[]`, }, { desc: "invalid json response", authFQDN: "_acme-challenge.foo.com.", zoneName: "test-zone", apiResponse: `[{}]`, expected: expected{ errorMsg: "failed to unmarshall TXT records: json: cannot unmarshal array into Go value of type map[string]internal.TXTRecord: [{}]", }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { server := httptest.NewServer(handlerMock(http.MethodGet, []byte(test.apiResponse))) t.Cleanup(server.Close) client, err := NewClient("myAuthID", "", "myAuthPassword") require.NoError(t, err) client.BaseURL, _ = url.Parse(server.URL) txtRecord, err := client.FindTxtRecord(test.zoneName, test.authFQDN) if test.expected.errorMsg != "" { require.EqualError(t, err, test.expected.errorMsg) } else { require.NoError(t, err) assert.Equal(t, test.expected.txtRecord, txtRecord) } }) } } func TestClient_ListTxtRecord(t *testing.T) { type expected struct { txtRecords []TXTRecord errorMsg string } testCases := []struct { desc string authFQDN string zoneName string apiResponse string expected }{ { desc: "record found", authFQDN: "_acme-challenge.foo.com.", zoneName: "foo.com", apiResponse: `{ "5769228": { "id": "5769228", "type": "TXT", "host": "_acme-challenge", "record": "txtTXTtxtTXTtxtTXTtxtTXT", "failover": "0", "ttl": "3600", "status": 1 }, "181805209": { "id": "181805209", "type": "TXT", "host": "_github-challenge", "record": "b66b8324b5", "failover": "0", "ttl": "300", "status": 1 } }`, expected: expected{ txtRecords: []TXTRecord{ { ID: 5769228, Type: "TXT", Host: "_acme-challenge", Record: "txtTXTtxtTXTtxtTXTtxtTXT", Failover: 0, TTL: 3600, Status: 1, }, }, }, }, { desc: "no record found", authFQDN: "_acme-challenge.foo.com.", zoneName: "foo.com", apiResponse: `{ "5769228": { "id": "5769228", "type": "TXT", "host": "_other-challenge", "record": "txtTXTtxtTXTtxtTXTtxtTXT", "failover": "0", "ttl": "3600", "status": 1 }, "181805209": { "id": "181805209", "type": "TXT", "host": "_github-challenge", "record": "b66b8324b5", "failover": "0", "ttl": "300", "status": 1 } }`, }, { desc: "zero records", authFQDN: "_acme-challenge.foo.com.", zoneName: "test-zone", apiResponse: `[]`, }, { desc: "invalid json response", authFQDN: "_acme-challenge.foo.com.", zoneName: "test-zone", apiResponse: `[{}]`, expected: expected{ errorMsg: "failed to unmarshall TXT records: json: cannot unmarshal array into Go value of type map[string]internal.TXTRecord: [{}]", }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { server := httptest.NewServer(handlerMock(http.MethodGet, []byte(test.apiResponse))) t.Cleanup(server.Close) client, err := NewClient("myAuthID", "", "myAuthPassword") require.NoError(t, err) client.BaseURL, _ = url.Parse(server.URL) txtRecords, err := client.ListTxtRecords(test.zoneName, test.authFQDN) if test.expected.errorMsg != "" { require.EqualError(t, err, test.expected.errorMsg) } else { require.NoError(t, err) assert.Equal(t, test.expected.txtRecords, txtRecords) } }) } } func TestClient_AddTxtRecord(t *testing.T) { type expected struct { query string errorMsg string } testCases := []struct { desc string authID string subAuthID string zoneName string authFQDN string value string ttl int apiResponse string expected }{ { desc: "sub-zone", authID: "myAuthID", zoneName: "bar.com", authFQDN: "_acme-challenge.foo.bar.com.", value: "txtTXTtxtTXTtxtTXTtxtTXT", ttl: 60, apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`, expected: expected{ query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge.foo&record=txtTXTtxtTXTtxtTXTtxtTXT&record-type=TXT&ttl=60`, }, }, { desc: "main zone (authID)", authID: "myAuthID", zoneName: "bar.com", authFQDN: "_acme-challenge.bar.com.", value: "TXTtxtTXTtxtTXTtxtTXTtxt", ttl: 60, apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`, expected: expected{ query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=60`, }, }, { desc: "main zone (subAuthID)", subAuthID: "mySubAuthID", zoneName: "bar.com", authFQDN: "_acme-challenge.bar.com.", value: "TXTtxtTXTtxtTXTtxtTXTtxt", ttl: 60, apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`, expected: expected{ query: `auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&sub-auth-id=mySubAuthID&ttl=60`, }, }, { desc: "invalid status", authID: "myAuthID", zoneName: "bar.com", authFQDN: "_acme-challenge.bar.com.", value: "TXTtxtTXTtxtTXTtxtTXTtxt", ttl: 120, apiResponse: `{"status":"Failed","statusDescription":"Invalid TTL. Choose from the list of the values we support."}`, expected: expected{ query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=300`, errorMsg: "failed to add TXT record: Failed Invalid TTL. Choose from the list of the values we support.", }, }, { desc: "invalid json response", authID: "myAuthID", zoneName: "bar.com", authFQDN: "_acme-challenge.bar.com.", value: "TXTtxtTXTtxtTXTtxtTXTtxt", ttl: 120, apiResponse: `[{}]`, expected: expected{ query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=300`, errorMsg: "failed to unmarshal API response: json: cannot unmarshal array into Go value of type internal.apiResponse: [{}]", }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if test.expected.query != req.URL.RawQuery { msg := fmt.Sprintf("got: %s, want: %s", test.expected.query, req.URL.RawQuery) http.Error(rw, msg, http.StatusBadRequest) return } handlerMock(http.MethodPost, []byte(test.apiResponse))(rw, req) })) t.Cleanup(server.Close) client, err := NewClient(test.authID, test.subAuthID, "myAuthPassword") require.NoError(t, err) client.BaseURL, _ = url.Parse(server.URL) err = client.AddTxtRecord(test.zoneName, test.authFQDN, test.value, test.ttl) if test.expected.errorMsg != "" { require.EqualError(t, err, test.expected.errorMsg) } else { require.NoError(t, err) } }) } } func TestClient_RemoveTxtRecord(t *testing.T) { type expected struct { query string errorMsg string } testCases := []struct { desc string id int zoneName string apiResponse string expected }{ { desc: "record found", id: 5769228, zoneName: "foo.com", apiResponse: `{ "status": "Success", "statusDescription": "The record was deleted successfully." }`, expected: expected{ query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo.com&record-id=5769228`, }, }, { desc: "record not found", id: 5769000, zoneName: "foo.com", apiResponse: `{ "status": "Failed", "statusDescription": "Invalid record-id param." }`, expected: expected{ query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo.com&record-id=5769000`, errorMsg: "failed to remove TXT record: Failed Invalid record-id param.", }, }, { desc: "invalid json response", id: 44, zoneName: "foo-plus.com", apiResponse: `[{}]`, expected: expected{ query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo-plus.com&record-id=44`, errorMsg: "failed to unmarshal API response: json: cannot unmarshal array into Go value of type internal.apiResponse: [{}]", }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if test.expected.query != req.URL.RawQuery { msg := fmt.Sprintf("got: %s, want: %s", test.expected.query, req.URL.RawQuery) http.Error(rw, msg, http.StatusBadRequest) return } handlerMock(http.MethodPost, []byte(test.apiResponse))(rw, req) })) t.Cleanup(server.Close) client, err := NewClient("myAuthID", "", "myAuthPassword") require.NoError(t, err) client.BaseURL, _ = url.Parse(server.URL) err = client.RemoveTxtRecord(test.id, test.zoneName) if test.expected.errorMsg != "" { require.EqualError(t, err, test.expected.errorMsg) } else { require.NoError(t, err) } }) } } func TestClient_GetUpdateStatus(t *testing.T) { type expected struct { progress *SyncProgress errorMsg string } testCases := []struct { desc string authFQDN string zoneName string apiResponse string expected }{ { desc: "50% sync", authFQDN: "_acme-challenge.foo.com.", zoneName: "foo.com", apiResponse: `[ {"server": "ns101.foo.com.", "ip4": "10.11.12.13", "ip6": "2a00:2a00:2a00:9::5", "updated": true }, {"server": "ns102.foo.com.", "ip4": "10.14.16.17", "ip6": "2100:2100:2100:3::1", "updated": false } ]`, expected: expected{progress: &SyncProgress{Updated: 1, Total: 2}}, }, { desc: "100% sync", authFQDN: "_acme-challenge.foo.com.", zoneName: "foo.com", apiResponse: `[ {"server": "ns101.foo.com.", "ip4": "10.11.12.13", "ip6": "2a00:2a00:2a00:9::5", "updated": true }, {"server": "ns102.foo.com.", "ip4": "10.14.16.17", "ip6": "2100:2100:2100:3::1", "updated": true } ]`, expected: expected{progress: &SyncProgress{Complete: true, Updated: 2, Total: 2}}, }, { desc: "record not found", authFQDN: "_acme-challenge.foo.com.", zoneName: "test-zone", apiResponse: `[]`, expected: expected{errorMsg: "no nameservers records returned"}, }, { desc: "invalid json response", authFQDN: "_acme-challenge.foo.com.", zoneName: "test-zone", apiResponse: `[x]`, expected: expected{errorMsg: "failed to unmarshal UpdateRecord: invalid character 'x' looking for beginning of value: [x]"}, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { server := httptest.NewServer(handlerMock(http.MethodGet, []byte(test.apiResponse))) t.Cleanup(server.Close) client, err := NewClient("myAuthID", "", "myAuthPassword") require.NoError(t, err) client.BaseURL, _ = url.Parse(server.URL) syncProgress, err := client.GetUpdateStatus(test.zoneName) if test.expected.errorMsg != "" { require.EqualError(t, err, test.expected.errorMsg) } else { require.NoError(t, err) } assert.Equal(t, test.expected.progress, syncProgress) }) } } lego-4.9.1/providers/dns/cloudns/internal/types.go000066400000000000000000000015161434020463500222360ustar00rootroot00000000000000package internal type apiResponse struct { Status string `json:"status"` StatusDescription string `json:"statusDescription"` } // Zone is a zone. type Zone struct { Name string Type string Zone string Status string // is an integer, but cast as string } // TXTRecord is a TXT record. type TXTRecord struct { ID int `json:"id,string"` Type string `json:"type"` Host string `json:"host"` Record string `json:"record"` Failover int `json:"failover,string"` TTL int `json:"ttl,string"` Status int `json:"status"` } // UpdateRecord is a Server Sync Record. type UpdateRecord struct { Server string `json:"server"` IP4 string `json:"ip4"` IP6 string `json:"ip6"` Updated bool `json:"updated"` } type SyncProgress struct { Complete bool Updated int Total int } lego-4.9.1/providers/dns/cloudxns/000077500000000000000000000000001434020463500171145ustar00rootroot00000000000000lego-4.9.1/providers/dns/cloudxns/cloudxns.go000066400000000000000000000067721434020463500213160ustar00rootroot00000000000000// Package cloudxns implements a DNS provider for solving the DNS-01 challenge using CloudXNS DNS. package cloudxns import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/cloudxns/internal" ) // Environment variables names. const ( envNamespace = "CLOUDXNS_" EnvAPIKey = envNamespace + "API_KEY" EnvSecretKey = envNamespace + "SECRET_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string SecretKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for CloudXNS. // Credentials must be passed in the environment variables: // CLOUDXNS_API_KEY and CLOUDXNS_SECRET_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey, EnvSecretKey) if err != nil { return nil, fmt.Errorf("CloudXNS: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] config.SecretKey = values[EnvSecretKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for CloudXNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("CloudXNS: the configuration of the DNS provider is nil") } client, err := internal.NewClient(config.APIKey, config.SecretKey) if err != nil { return nil, err } client.HTTPClient = config.HTTPClient return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) info, err := d.client.GetDomainInformation(fqdn) if err != nil { return err } return d.client.AddTxtRecord(info, fqdn, value, d.config.TTL) } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) info, err := d.client.GetDomainInformation(fqdn) if err != nil { return err } record, err := d.client.FindTxtRecord(info.ID, fqdn) if err != nil { return err } return d.client.RemoveTxtRecord(record.RecordID, info.ID) } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/cloudxns/cloudxns.toml000066400000000000000000000013771434020463500216600ustar00rootroot00000000000000Name = "CloudXNS" Description = """""" URL = "https://www.cloudxns.net/" Code = "cloudxns" Since = "v0.5.0" Example = ''' CLOUDXNS_API_KEY=xxxx \ CLOUDXNS_SECRET_KEY=yyyy \ lego --email you@example.com --dns cloudxns --domains my.example.org run ''' [Configuration] [Configuration.Credentials] CLOUDXNS_API_KEY = "The API key" CLOUDXNS_SECRET_KEY = "The API secret key" [Configuration.Additional] CLOUDXNS_POLLING_INTERVAL = "Time between DNS propagation check" CLOUDXNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" CLOUDXNS_TTL = "The TTL of the TXT record used for the DNS challenge" CLOUDXNS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.cloudxns.net/Public/Doc/CloudXNS_api2.0_doc_zh-cn.zip" lego-4.9.1/providers/dns/cloudxns/cloudxns_test.go000066400000000000000000000061121434020463500223410ustar00rootroot00000000000000package cloudxns import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIKey, EnvSecretKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", EnvSecretKey: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIKey: "", EnvSecretKey: "", }, expected: "CloudXNS: some credentials information are missing: CLOUDXNS_API_KEY,CLOUDXNS_SECRET_KEY", }, { desc: "missing API key", envVars: map[string]string{ EnvAPIKey: "", EnvSecretKey: "456", }, expected: "CloudXNS: some credentials information are missing: CLOUDXNS_API_KEY", }, { desc: "missing secret key", envVars: map[string]string{ EnvAPIKey: "123", EnvSecretKey: "", }, expected: "CloudXNS: some credentials information are missing: CLOUDXNS_SECRET_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string secretKey string expected string }{ { desc: "success", apiKey: "123", secretKey: "456", }, { desc: "missing credentials", expected: "CloudXNS: credentials missing: apiKey", }, { desc: "missing api key", secretKey: "456", expected: "CloudXNS: credentials missing: apiKey", }, { desc: "missing secret key", apiKey: "123", expected: "CloudXNS: credentials missing: secretKey", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.SecretKey = test.secretKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/cloudxns/internal/000077500000000000000000000000001434020463500207305ustar00rootroot00000000000000lego-4.9.1/providers/dns/cloudxns/internal/client.go000066400000000000000000000121301434020463500225320ustar00rootroot00000000000000package internal import ( "bytes" "crypto/md5" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "strconv" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" ) const defaultBaseURL = "https://www.cloudxns.net/api2/" type apiResponse struct { Code int `json:"code"` Message string `json:"message"` Data json.RawMessage `json:"data,omitempty"` } // Data Domain information. type Data struct { ID string `json:"id"` Domain string `json:"domain"` TTL int `json:"ttl,omitempty"` } // TXTRecord a TXT record. type TXTRecord struct { ID int `json:"domain_id,omitempty"` RecordID string `json:"record_id,omitempty"` Host string `json:"host"` Value string `json:"value"` Type string `json:"type"` LineID int `json:"line_id,string"` TTL int `json:"ttl,string"` } // NewClient creates a CloudXNS client. func NewClient(apiKey, secretKey string) (*Client, error) { if apiKey == "" { return nil, errors.New("CloudXNS: credentials missing: apiKey") } if secretKey == "" { return nil, errors.New("CloudXNS: credentials missing: secretKey") } return &Client{ apiKey: apiKey, secretKey: secretKey, HTTPClient: &http.Client{}, BaseURL: defaultBaseURL, }, nil } // Client CloudXNS client. type Client struct { apiKey string secretKey string HTTPClient *http.Client BaseURL string } // GetDomainInformation Get domain name information for a FQDN. func (c *Client) GetDomainInformation(fqdn string) (*Data, error) { authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return nil, err } result, err := c.doRequest(http.MethodGet, "domain", nil) if err != nil { return nil, err } var domains []Data if len(result) > 0 { err = json.Unmarshal(result, &domains) if err != nil { return nil, fmt.Errorf("CloudXNS: domains unmarshaling error: %w", err) } } for _, data := range domains { if data.Domain == authZone { return &data, nil } } return nil, fmt.Errorf("CloudXNS: zone %s not found for domain %s", authZone, fqdn) } // FindTxtRecord return the TXT record a zone ID and a FQDN. func (c *Client) FindTxtRecord(zoneID, fqdn string) (*TXTRecord, error) { result, err := c.doRequest(http.MethodGet, fmt.Sprintf("record/%s?host_id=0&offset=0&row_num=2000", zoneID), nil) if err != nil { return nil, err } var records []TXTRecord err = json.Unmarshal(result, &records) if err != nil { return nil, fmt.Errorf("CloudXNS: TXT record unmarshaling error: %w", err) } for _, record := range records { if record.Host == dns01.UnFqdn(fqdn) && record.Type == "TXT" { return &record, nil } } return nil, fmt.Errorf("CloudXNS: no existing record found for %q", fqdn) } // AddTxtRecord add a TXT record. func (c *Client) AddTxtRecord(info *Data, fqdn, value string, ttl int) error { id, err := strconv.Atoi(info.ID) if err != nil { return fmt.Errorf("CloudXNS: invalid zone ID: %w", err) } payload := TXTRecord{ ID: id, Host: dns01.UnFqdn(strings.TrimSuffix(fqdn, info.Domain)), Value: value, Type: "TXT", LineID: 1, TTL: ttl, } body, err := json.Marshal(payload) if err != nil { return fmt.Errorf("CloudXNS: record unmarshaling error: %w", err) } _, err = c.doRequest(http.MethodPost, "record", body) return err } // RemoveTxtRecord remove a TXT record. func (c *Client) RemoveTxtRecord(recordID, zoneID string) error { _, err := c.doRequest(http.MethodDelete, fmt.Sprintf("record/%s/%s", recordID, zoneID), nil) return err } func (c *Client) doRequest(method, uri string, body []byte) (json.RawMessage, error) { req, err := c.buildRequest(method, uri, body) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("CloudXNS: %w", err) } defer resp.Body.Close() content, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("CloudXNS: %s", toUnreadableBodyMessage(req, content)) } var r apiResponse err = json.Unmarshal(content, &r) if err != nil { return nil, fmt.Errorf("CloudXNS: response unmashaling error: %w: %s", err, toUnreadableBodyMessage(req, content)) } if r.Code != 1 { return nil, fmt.Errorf("CloudXNS: invalid code (%v), error: %s", r.Code, r.Message) } return r.Data, nil } func (c *Client) buildRequest(method, uri string, body []byte) (*http.Request, error) { url := c.BaseURL + uri req, err := http.NewRequest(method, url, bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("CloudXNS: invalid request: %w", err) } requestDate := time.Now().Format(time.RFC1123Z) req.Header.Set("API-KEY", c.apiKey) req.Header.Set("API-REQUEST-DATE", requestDate) req.Header.Set("API-HMAC", c.hmac(url, requestDate, string(body))) req.Header.Set("API-FORMAT", "json") return req, nil } func (c *Client) hmac(url, date, body string) string { sum := md5.Sum([]byte(c.apiKey + url + body + date + c.secretKey)) return hex.EncodeToString(sum[:]) } func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string { return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody)) } lego-4.9.1/providers/dns/cloudxns/internal/client_test.go000066400000000000000000000141771434020463500236060ustar00rootroot00000000000000package internal import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func handlerMock(method string, response *apiResponse, data interface{}) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if req.Method != method { content, err := json.Marshal(apiResponse{ Code: 999, // random code only for the test Message: fmt.Sprintf("invalid method: got %s want %s", req.Method, method), }) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } http.Error(rw, string(content), http.StatusBadRequest) return } jsonData, err := json.Marshal(data) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } response.Data = jsonData content, err := json.Marshal(response) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } _, err = rw.Write(content) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) } func TestClientGetDomainInformation(t *testing.T) { type result struct { domain *Data error bool } testCases := []struct { desc string fqdn string response *apiResponse data []Data expected result }{ { desc: "domain found", fqdn: "_acme-challenge.foo.com.", response: &apiResponse{ Code: 1, }, data: []Data{ { ID: "1", Domain: "bar.com.", }, { ID: "2", Domain: "foo.com.", }, }, expected: result{domain: &Data{ ID: "2", Domain: "foo.com.", }}, }, { desc: "domains not found", fqdn: "_acme-challenge.huu.com.", response: &apiResponse{ Code: 1, }, data: []Data{ { ID: "5", Domain: "bar.com.", }, { ID: "6", Domain: "foo.com.", }, }, expected: result{error: true}, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { server := httptest.NewServer(handlerMock(http.MethodGet, test.response, test.data)) t.Cleanup(server.Close) client, _ := NewClient("myKey", "mySecret") client.BaseURL = server.URL + "/" domain, err := client.GetDomainInformation(test.fqdn) if test.expected.error { require.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, test.expected.domain, domain) } }) } } func TestClientFindTxtRecord(t *testing.T) { type result struct { txtRecord *TXTRecord error bool } testCases := []struct { desc string fqdn string zoneID string txtRecords []TXTRecord response *apiResponse expected result }{ { desc: "record found", fqdn: "_acme-challenge.foo.com.", zoneID: "test-zone", txtRecords: []TXTRecord{ { ID: 1, RecordID: "Record-A", Host: "_acme-challenge.foo.com", Value: "txtTXTtxtTXTtxtTXTtxtTXT", Type: "TXT", LineID: 6, TTL: 30, }, { ID: 2, RecordID: "Record-B", Host: "_acme-challenge.bar.com", Value: "TXTtxtTXTtxtTXTtxtTXTtxt", Type: "TXT", LineID: 6, TTL: 30, }, }, response: &apiResponse{ Code: 1, }, expected: result{ txtRecord: &TXTRecord{ ID: 1, RecordID: "Record-A", Host: "_acme-challenge.foo.com", Value: "txtTXTtxtTXTtxtTXTtxtTXT", Type: "TXT", LineID: 6, TTL: 30, }, }, }, { desc: "record not found", fqdn: "_acme-challenge.huu.com.", zoneID: "test-zone", txtRecords: []TXTRecord{ { ID: 1, RecordID: "Record-A", Host: "_acme-challenge.foo.com", Value: "txtTXTtxtTXTtxtTXTtxtTXT", Type: "TXT", LineID: 6, TTL: 30, }, { ID: 2, RecordID: "Record-B", Host: "_acme-challenge.bar.com", Value: "TXTtxtTXTtxtTXTtxtTXTtxt", Type: "TXT", LineID: 6, TTL: 30, }, }, response: &apiResponse{ Code: 1, }, expected: result{error: true}, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { server := httptest.NewServer(handlerMock(http.MethodGet, test.response, test.txtRecords)) t.Cleanup(server.Close) client, _ := NewClient("myKey", "mySecret") client.BaseURL = server.URL + "/" txtRecord, err := client.FindTxtRecord(test.zoneID, test.fqdn) if test.expected.error { require.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, test.expected.txtRecord, txtRecord) } }) } } func TestClientAddTxtRecord(t *testing.T) { testCases := []struct { desc string domain *Data fqdn string value string ttl int expected string }{ { desc: "sub-domain", domain: &Data{ ID: "1", Domain: "bar.com.", }, fqdn: "_acme-challenge.foo.bar.com.", value: "txtTXTtxtTXTtxtTXTtxtTXT", ttl: 30, expected: `{"domain_id":1,"host":"_acme-challenge.foo","value":"txtTXTtxtTXTtxtTXTtxtTXT","type":"TXT","line_id":"1","ttl":"30"}`, }, { desc: "main domain", domain: &Data{ ID: "2", Domain: "bar.com.", }, fqdn: "_acme-challenge.bar.com.", value: "TXTtxtTXTtxtTXTtxtTXTtxt", ttl: 30, expected: `{"domain_id":2,"host":"_acme-challenge","value":"TXTtxtTXTtxtTXTtxtTXTtxt","type":"TXT","line_id":"1","ttl":"30"}`, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { response := &apiResponse{ Code: 1, } server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { assert.NotNil(t, req.Body) content, err := io.ReadAll(req.Body) require.NoError(t, err) assert.Equal(t, test.expected, string(content)) handlerMock(http.MethodPost, response, nil).ServeHTTP(rw, req) })) t.Cleanup(server.Close) client, _ := NewClient("myKey", "mySecret") client.BaseURL = server.URL + "/" err := client.AddTxtRecord(test.domain, test.fqdn, test.value, test.ttl) require.NoError(t, err) }) } } lego-4.9.1/providers/dns/conoha/000077500000000000000000000000001434020463500165245ustar00rootroot00000000000000lego-4.9.1/providers/dns/conoha/conoha.go000066400000000000000000000112131434020463500203200ustar00rootroot00000000000000// Package conoha implements a DNS provider for solving the DNS-01 challenge using ConoHa DNS. package conoha import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/conoha/internal" ) // Environment variables names. const ( envNamespace = "CONOHA_" EnvRegion = envNamespace + "REGION" EnvTenantID = envNamespace + "TENANT_ID" EnvAPIUsername = envNamespace + "API_USERNAME" EnvAPIPassword = envNamespace + "API_PASSWORD" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Region string TenantID string Username string Password string TTL int PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ Region: env.GetOrDefaultString(EnvRegion, "tyo1"), TTL: env.GetOrDefaultInt(EnvTTL, 60), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for ConoHa DNS. // Credentials must be passed in the environment variables: // CONOHA_TENANT_ID, CONOHA_API_USERNAME, CONOHA_API_PASSWORD. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvTenantID, EnvAPIUsername, EnvAPIPassword) if err != nil { return nil, fmt.Errorf("conoha: %w", err) } config := NewDefaultConfig() config.TenantID = values[EnvTenantID] config.Username = values[EnvAPIUsername] config.Password = values[EnvAPIPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for ConoHa DNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("conoha: the configuration of the DNS provider is nil") } if config.TenantID == "" || config.Username == "" || config.Password == "" { return nil, errors.New("conoha: some credentials information are missing") } auth := internal.Auth{ TenantID: config.TenantID, PasswordCredentials: internal.PasswordCredentials{ Username: config.Username, Password: config.Password, }, } client, err := internal.NewClient(config.Region, auth, config.HTTPClient) if err != nil { return nil, fmt.Errorf("conoha: failed to create client: %w", err) } return &DNSProvider{config: config, client: client}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return err } id, err := d.client.GetDomainID(authZone) if err != nil { return fmt.Errorf("conoha: failed to get domain ID: %w", err) } record := internal.Record{ Name: fqdn, Type: "TXT", Data: value, TTL: d.config.TTL, } err = d.client.CreateRecord(id, record) if err != nil { return fmt.Errorf("conoha: failed to create record: %w", err) } return nil } // CleanUp clears ConoHa DNS TXT record. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return err } domID, err := d.client.GetDomainID(authZone) if err != nil { return fmt.Errorf("conoha: failed to get domain ID: %w", err) } recID, err := d.client.GetRecordID(domID, fqdn, "TXT", value) if err != nil { return fmt.Errorf("conoha: failed to get record ID: %w", err) } err = d.client.DeleteRecord(domID, recID) if err != nil { return fmt.Errorf("conoha: failed to delete record: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/conoha/conoha.toml000066400000000000000000000015111434020463500206660ustar00rootroot00000000000000Name = "ConoHa" Description = '''''' URL = "https://www.conoha.jp/" Code = "conoha" Since = "v1.2.0" Example = ''' CONOHA_TENANT_ID=487727e3921d44e3bfe7ebb337bf085e \ CONOHA_API_USERNAME=xxxx \ CONOHA_API_PASSWORD=yyyy \ lego --email you@example.com --dns conoha --domains my.example.org run ''' [Configuration] [Configuration.Credentials] CONOHA_TENANT_ID = "Tenant ID" CONOHA_API_USERNAME = "The API username" CONOHA_API_PASSWORD = "The API password" [Configuration.Additional] CONOHA_POLLING_INTERVAL = "Time between DNS propagation check" CONOHA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" CONOHA_TTL = "The TTL of the TXT record used for the DNS challenge" CONOHA_HTTP_TIMEOUT = "API request timeout" CONOHA_REGION = "The region" [Links] API = "https://www.conoha.jp/docs/" lego-4.9.1/providers/dns/conoha/conoha_test.go000066400000000000000000000104131434020463500213600ustar00rootroot00000000000000package conoha import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvTenantID, EnvAPIUsername, EnvAPIPassword). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "complete credentials, but login failed", envVars: map[string]string{ EnvTenantID: "tenant_id", EnvAPIUsername: "api_username", EnvAPIPassword: "api_password", }, expected: `conoha: failed to create client: failed to login: HTTP request failed with status code 401: {"unauthorized":{"message":"Invalid user: api_username","code":401}}`, }, { desc: "missing credentials", envVars: map[string]string{ EnvTenantID: "", EnvAPIUsername: "", EnvAPIPassword: "", }, expected: "conoha: some credentials information are missing: CONOHA_TENANT_ID,CONOHA_API_USERNAME,CONOHA_API_PASSWORD", }, { desc: "missing tenant id", envVars: map[string]string{ EnvTenantID: "", EnvAPIUsername: "api_username", EnvAPIPassword: "api_password", }, expected: "conoha: some credentials information are missing: CONOHA_TENANT_ID", }, { desc: "missing api username", envVars: map[string]string{ EnvTenantID: "tenant_id", EnvAPIUsername: "", EnvAPIPassword: "api_password", }, expected: "conoha: some credentials information are missing: CONOHA_API_USERNAME", }, { desc: "missing api password", envVars: map[string]string{ EnvTenantID: "tenant_id", EnvAPIUsername: "api_username", EnvAPIPassword: "", }, expected: "conoha: some credentials information are missing: CONOHA_API_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string expected string tenant string username string password string }{ { desc: "complete credentials, but login failed", expected: `conoha: failed to create client: failed to login: HTTP request failed with status code 401: {"unauthorized":{"message":"Invalid user: api_username","code":401}}`, tenant: "tenant_id", username: "api_username", password: "api_password", }, { desc: "missing credentials", expected: "conoha: some credentials information are missing", }, { desc: "missing tenant id", expected: "conoha: some credentials information are missing", username: "api_username", password: "api_password", }, { desc: "missing api username", expected: "conoha: some credentials information are missing", tenant: "tenant_id", password: "api_password", }, { desc: "missing api password", expected: "conoha: some credentials information are missing", tenant: "tenant_id", username: "api_username", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.TenantID = test.tenant config.Username = test.username config.Password = test.password p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/conoha/internal/000077500000000000000000000000001434020463500203405ustar00rootroot00000000000000lego-4.9.1/providers/dns/conoha/internal/client.go000066400000000000000000000112701434020463500221460ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" ) const ( identityBaseURL = "https://identity.%s.conoha.io" dnsServiceBaseURL = "https://dns-service.%s.conoha.io" ) // IdentityRequest is an authentication request body. type IdentityRequest struct { Auth Auth `json:"auth"` } // Auth is an authentication information. type Auth struct { TenantID string `json:"tenantId"` PasswordCredentials PasswordCredentials `json:"passwordCredentials"` } // PasswordCredentials is API-user's credentials. type PasswordCredentials struct { Username string `json:"username"` Password string `json:"password"` } // IdentityResponse is an authentication response body. type IdentityResponse struct { Access Access `json:"access"` } // Access is an identity information. type Access struct { Token Token `json:"token"` } // Token is an api access token. type Token struct { ID string `json:"id"` } // DomainListResponse is a response of a domain listing request. type DomainListResponse struct { Domains []Domain `json:"domains"` } // Domain is a hosted domain entry. type Domain struct { ID string `json:"id"` Name string `json:"name"` } // RecordListResponse is a response of record listing request. type RecordListResponse struct { Records []Record `json:"records"` } // Record is a record entry. type Record struct { ID string `json:"id,omitempty"` Name string `json:"name"` Type string `json:"type"` Data string `json:"data"` TTL int `json:"ttl"` } // Client is a ConoHa API client. type Client struct { token string endpoint string httpClient *http.Client } // NewClient returns a client instance logged into the ConoHa service. func NewClient(region string, auth Auth, httpClient *http.Client) (*Client, error) { if httpClient == nil { httpClient = &http.Client{} } c := &Client{httpClient: httpClient} c.endpoint = fmt.Sprintf(identityBaseURL, region) identity, err := c.getIdentity(auth) if err != nil { return nil, fmt.Errorf("failed to login: %w", err) } c.token = identity.Access.Token.ID c.endpoint = fmt.Sprintf(dnsServiceBaseURL, region) return c, nil } func (c *Client) getIdentity(auth Auth) (*IdentityResponse, error) { req := &IdentityRequest{Auth: auth} identity := &IdentityResponse{} err := c.do(http.MethodPost, "/v2.0/tokens", req, identity) if err != nil { return nil, err } return identity, nil } // GetDomainID returns an ID of specified domain. func (c *Client) GetDomainID(domainName string) (string, error) { domainList := &DomainListResponse{} err := c.do(http.MethodGet, "/v1/domains", nil, domainList) if err != nil { return "", err } for _, domain := range domainList.Domains { if domain.Name == domainName { return domain.ID, nil } } return "", fmt.Errorf("no such domain: %s", domainName) } // GetRecordID returns an ID of specified record. func (c *Client) GetRecordID(domainID, recordName, recordType, data string) (string, error) { recordList := &RecordListResponse{} err := c.do(http.MethodGet, fmt.Sprintf("/v1/domains/%s/records", domainID), nil, recordList) if err != nil { return "", err } for _, record := range recordList.Records { if record.Name == recordName && record.Type == recordType && record.Data == data { return record.ID, nil } } return "", errors.New("no such record") } // CreateRecord adds new record. func (c *Client) CreateRecord(domainID string, record Record) error { return c.do(http.MethodPost, fmt.Sprintf("/v1/domains/%s/records", domainID), record, nil) } // DeleteRecord removes specified record. func (c *Client) DeleteRecord(domainID, recordID string) error { return c.do(http.MethodDelete, fmt.Sprintf("/v1/domains/%s/records/%s", domainID, recordID), nil, nil) } func (c *Client) do(method, path string, payload, result interface{}) error { body := bytes.NewReader(nil) if payload != nil { bodyBytes, err := json.Marshal(payload) if err != nil { return err } body = bytes.NewReader(bodyBytes) } req, err := http.NewRequest(method, c.endpoint+path, body) if err != nil { return err } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Auth-Token", c.token) resp, err := c.httpClient.Do(req) if err != nil { return err } if resp.StatusCode != http.StatusOK { respBody, err := io.ReadAll(resp.Body) if err != nil { return err } defer resp.Body.Close() return fmt.Errorf("HTTP request failed with status code %d: %s", resp.StatusCode, string(respBody)) } if result != nil { respBody, err := io.ReadAll(resp.Body) if err != nil { return err } defer resp.Body.Close() return json.Unmarshal(respBody, result) } return nil } lego-4.9.1/providers/dns/conoha/internal/client_test.go000066400000000000000000000114721434020463500232110ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupTest(t *testing.T) (*http.ServeMux, *Client) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := &Client{ token: "secret", endpoint: server.URL, httpClient: server.Client(), } return mux, client } func TestClient_GetDomainID(t *testing.T) { type expected struct { domainID string error bool } testCases := []struct { desc string domainName string handler http.HandlerFunc expected expected }{ { desc: "success", domainName: "domain1.com.", handler: func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, fmt.Sprintf("%s: %s", http.StatusText(http.StatusMethodNotAllowed), req.Method), http.StatusMethodNotAllowed) return } content := ` { "domains":[ { "id": "09494b72-b65b-4297-9efb-187f65a0553e", "name": "domain1.com.", "ttl": 3600, "serial": 1351800668, "email": "nsadmin@example.org", "gslb": 0, "created_at": "2012-11-01T20:11:08.000000", "updated_at": null, "description": "memo" }, { "id": "cf661142-e577-40b5-b3eb-75795cdc0cd7", "name": "domain2.com.", "ttl": 7200, "serial": 1351800670, "email": "nsadmin2@example.org", "gslb": 1, "created_at": "2012-11-01T20:11:08.000000", "updated_at": "2012-12-01T20:11:08.000000", "description": "memomemo" } ] } ` _, err := fmt.Fprint(rw, content) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }, expected: expected{domainID: "09494b72-b65b-4297-9efb-187f65a0553e"}, }, { desc: "non existing domain", domainName: "domain1.com.", handler: func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, fmt.Sprintf("%s: %s", http.StatusText(http.StatusMethodNotAllowed), req.Method), http.StatusMethodNotAllowed) return } _, err := fmt.Fprint(rw, "{}") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }, expected: expected{error: true}, }, { desc: "marshaling error", domainName: "domain1.com.", handler: func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, fmt.Sprintf("%s: %s", http.StatusText(http.StatusMethodNotAllowed), req.Method), http.StatusMethodNotAllowed) return } _, err := fmt.Fprint(rw, "[]") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }, expected: expected{error: true}, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { mux, client := setupTest(t) mux.Handle("/v1/domains", test.handler) domainID, err := client.GetDomainID(test.domainName) if test.expected.error { require.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, test.expected.domainID, domainID) } }) } } func TestClient_CreateRecord(t *testing.T) { testCases := []struct { desc string handler http.HandlerFunc expectError bool }{ { desc: "success", handler: func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, fmt.Sprintf("%s: %s", http.StatusText(http.StatusMethodNotAllowed), req.Method), http.StatusMethodNotAllowed) return } raw, err := io.ReadAll(req.Body) if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } defer req.Body.Close() if string(raw) != `{"name":"lego.com.","type":"TXT","data":"txtTXTtxt","ttl":300}` { http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest) return } }, }, { desc: "bad request", handler: func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, fmt.Sprintf("%s: %s", http.StatusText(http.StatusMethodNotAllowed), req.Method), http.StatusMethodNotAllowed) return } http.Error(rw, "OOPS", http.StatusBadRequest) }, expectError: true, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { mux, client := setupTest(t) mux.Handle("/v1/domains/lego/records", test.handler) domainID := "lego" record := Record{ Name: "lego.com.", Type: "TXT", Data: "txtTXTtxt", TTL: 300, } err := client.CreateRecord(domainID, record) if test.expectError { require.Error(t, err) } else { require.NoError(t, err) } }) } } lego-4.9.1/providers/dns/constellix/000077500000000000000000000000001434020463500174415ustar00rootroot00000000000000lego-4.9.1/providers/dns/constellix/constellix.go000066400000000000000000000164311434020463500221610ustar00rootroot00000000000000// Package constellix implements a DNS provider for solving the DNS-01 challenge using Constellix DNS. package constellix import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/constellix/internal" ) // Environment variables names. const ( envNamespace = "CONSTELLIX_" EnvAPIKey = envNamespace + "API_KEY" EnvSecretKey = envNamespace + "SECRET_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string SecretKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 60), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for Constellix. // Credentials must be passed in the environment variables: // CONSTELLIX_API_KEY and CONSTELLIX_SECRET_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey, EnvSecretKey) if err != nil { return nil, fmt.Errorf("constellix: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] config.SecretKey = values[EnvSecretKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Constellix. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("constellix: the configuration of the DNS provider is nil") } if config.SecretKey == "" || config.APIKey == "" { return nil, errors.New("constellix: incomplete credentials, missing secret key and/or API key") } tr, err := internal.NewTokenTransport(config.APIKey, config.SecretKey) if err != nil { return nil, fmt.Errorf("constellix: %w", err) } client := internal.NewClient(tr.Wrap(config.HTTPClient)) return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("constellix: could not find zone for domain %q and fqdn %q : %w", domain, fqdn, err) } dom, err := d.client.Domains.GetByName(dns01.UnFqdn(authZone)) if err != nil { return fmt.Errorf("constellix: failed to get domain (%s): %w", authZone, err) } recordName := getRecordName(fqdn, authZone) records, err := d.client.TxtRecords.Search(dom.ID, internal.Exact, recordName) if err != nil { return fmt.Errorf("constellix: failed to search TXT records: %w", err) } if len(records) > 1 { return errors.New("constellix: failed to get TXT records") } // TXT record entry already existing if len(records) == 1 { return d.appendRecordValue(dom, records[0].ID, value) } err = d.createRecord(dom, fqdn, recordName, value) if err != nil { return fmt.Errorf("constellix: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("constellix: could not find zone for domain %q and fqdn %q : %w", domain, fqdn, err) } dom, err := d.client.Domains.GetByName(dns01.UnFqdn(authZone)) if err != nil { return fmt.Errorf("constellix: failed to get domain (%s): %w", authZone, err) } recordName := getRecordName(fqdn, authZone) records, err := d.client.TxtRecords.Search(dom.ID, internal.Exact, recordName) if err != nil { return fmt.Errorf("constellix: failed to search TXT records: %w", err) } if len(records) > 1 { return errors.New("constellix: failed to get TXT records") } if len(records) == 0 { return nil } record, err := d.client.TxtRecords.Get(dom.ID, records[0].ID) if err != nil { return fmt.Errorf("constellix: failed to get TXT records: %w", err) } if !containsValue(record, value) { return nil } // only 1 record value, the whole record must be deleted. if len(record.Value) == 1 { _, err = d.client.TxtRecords.Delete(dom.ID, record.ID) if err != nil { return fmt.Errorf("constellix: failed to delete TXT records: %w", err) } return nil } err = d.removeRecordValue(dom, record, value) if err != nil { return fmt.Errorf("constellix: %w", err) } return nil } func (d *DNSProvider) createRecord(dom internal.Domain, fqdn, recordName, value string) error { request := internal.RecordRequest{ Name: recordName, TTL: d.config.TTL, RoundRobin: []internal.RecordValue{ {Value: fmt.Sprintf(`%q`, value)}, }, } _, err := d.client.TxtRecords.Create(dom.ID, request) if err != nil { return fmt.Errorf("failed to create TXT record %s: %w", fqdn, err) } return nil } func (d *DNSProvider) appendRecordValue(dom internal.Domain, recordID int64, value string) error { record, err := d.client.TxtRecords.Get(dom.ID, recordID) if err != nil { return fmt.Errorf("failed to get TXT records: %w", err) } if containsValue(record, value) { return nil } request := internal.RecordRequest{ Name: record.Name, TTL: record.TTL, RoundRobin: append(record.RoundRobin, internal.RecordValue{Value: fmt.Sprintf(`%q`, value)}), } _, err = d.client.TxtRecords.Update(dom.ID, record.ID, request) if err != nil { return fmt.Errorf("failed to update TXT records: %w", err) } return nil } func (d *DNSProvider) removeRecordValue(dom internal.Domain, record *internal.Record, value string) error { request := internal.RecordRequest{ Name: record.Name, TTL: record.TTL, } for _, val := range record.Value { if val.Value != fmt.Sprintf(`%q`, value) { request.RoundRobin = append(request.RoundRobin, val) } } _, err := d.client.TxtRecords.Update(dom.ID, record.ID, request) if err != nil { return fmt.Errorf("failed to update TXT records: %w", err) } return nil } func containsValue(record *internal.Record, value string) bool { if record == nil { return false } for _, val := range record.Value { if val.Value == fmt.Sprintf(`%q`, value) { return true } } return false } func getRecordName(fqdn, authZone string) string { return fqdn[0 : len(fqdn)-len(authZone)-1] } lego-4.9.1/providers/dns/constellix/constellix.toml000066400000000000000000000014561434020463500225300ustar00rootroot00000000000000Name = "Constellix" Description = '''''' URL = "https://constellix.com" Code = "constellix" Since = "v3.4.0" Example = ''' CONSTELLIX_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ CONSTELLIX_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ lego --email you@example.com --dns constellix --domains my.example.org run ''' [Configuration] [Configuration.Credentials] CONSTELLIX_API_KEY = "User API key" CONSTELLIX_SECRET_KEY = "User secret key" [Configuration.Additional] CONSTELLIX_POLLING_INTERVAL = "Time between DNS propagation check" CONSTELLIX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" CONSTELLIX_TTL = "The TTL of the TXT record used for the DNS challenge" CONSTELLIX_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api-docs.constellix.com" lego-4.9.1/providers/dns/constellix/constellix_test.go000066400000000000000000000063061434020463500232200ustar00rootroot00000000000000package constellix import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIKey, EnvSecretKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", EnvSecretKey: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIKey: "", EnvSecretKey: "", }, expected: "constellix: some credentials information are missing: CONSTELLIX_API_KEY,CONSTELLIX_SECRET_KEY", }, { desc: "missing api key", envVars: map[string]string{ EnvAPIKey: "", EnvSecretKey: "api_password", }, expected: "constellix: some credentials information are missing: CONSTELLIX_API_KEY", }, { desc: "missing secret key", envVars: map[string]string{ EnvAPIKey: "api_username", EnvSecretKey: "", }, expected: "constellix: some credentials information are missing: CONSTELLIX_SECRET_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string expected string apiKey string secretKey string }{ { desc: "success", apiKey: "api_key", secretKey: "api_secret", }, { desc: "missing credentials", expected: "constellix: incomplete credentials, missing secret key and/or API key", }, { desc: "missing api key", apiKey: "", secretKey: "api_secret", expected: "constellix: incomplete credentials, missing secret key and/or API key", }, { desc: "missing secret key", apiKey: "api_key", secretKey: "", expected: "constellix: incomplete credentials, missing secret key and/or API key", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.SecretKey = test.secretKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/constellix/internal/000077500000000000000000000000001434020463500212555ustar00rootroot00000000000000lego-4.9.1/providers/dns/constellix/internal/auth.go000066400000000000000000000045231434020463500225510ustar00rootroot00000000000000package internal import ( "crypto/hmac" "crypto/sha1" "encoding/base64" "errors" "fmt" "net/http" "strconv" "time" ) const securityTokenHeader = "x-cns-security-token" // TokenTransport HTTP transport for API authentication. type TokenTransport struct { apiKey string secretKey string // Transport is the underlying HTTP transport to use when making requests. // It will default to http.DefaultTransport if nil. Transport http.RoundTripper } // NewTokenTransport Creates a HTTP transport for API authentication. func NewTokenTransport(apiKey, secretKey string) (*TokenTransport, error) { if apiKey == "" { return nil, errors.New("credentials missing: API key") } if secretKey == "" { return nil, errors.New("credentials missing: secret key") } return &TokenTransport{apiKey: apiKey, secretKey: secretKey}, nil } // RoundTrip executes a single HTTP transaction. func (t *TokenTransport) RoundTrip(req *http.Request) (*http.Response, error) { enrichedReq := &http.Request{} *enrichedReq = *req enrichedReq.Header = make(http.Header, len(req.Header)) for k, s := range req.Header { enrichedReq.Header[k] = append([]string(nil), s...) } if t.apiKey != "" && t.secretKey != "" { securityToken := createCnsSecurityToken(t.apiKey, t.secretKey) enrichedReq.Header.Set(securityTokenHeader, securityToken) } return t.transport().RoundTrip(enrichedReq) } func (t *TokenTransport) transport() http.RoundTripper { if t.Transport != nil { return t.Transport } return http.DefaultTransport } // Client Creates a new HTTP client. func (t *TokenTransport) Client() *http.Client { return &http.Client{Transport: t} } // Wrap Wrap a HTTP client Transport with the TokenTransport. func (t *TokenTransport) Wrap(client *http.Client) *http.Client { backup := client.Transport t.Transport = backup client.Transport = t return client } func createCnsSecurityToken(apiKey, secretKey string) string { timestamp := time.Now().Round(time.Millisecond).UnixNano() / int64(time.Millisecond) hm := encodedHmac(timestamp, secretKey) requestDate := strconv.FormatInt(timestamp, 10) return fmt.Sprintf("%s:%s:%s", apiKey, hm, requestDate) } func encodedHmac(message int64, secret string) string { h := hmac.New(sha1.New, []byte(secret)) _, _ = h.Write([]byte(strconv.FormatInt(message, 10))) return base64.StdEncoding.EncodeToString(h.Sum(nil)) } lego-4.9.1/providers/dns/constellix/internal/auth_test.go000066400000000000000000000016771434020463500236170ustar00rootroot00000000000000package internal import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewTokenTransport_success(t *testing.T) { apiKey := "api" secretKey := "secret" transport, err := NewTokenTransport(apiKey, secretKey) require.NoError(t, err) assert.NotNil(t, transport) } func TestNewTokenTransport_missing_credentials(t *testing.T) { apiKey := "" secretKey := "" transport, err := NewTokenTransport(apiKey, secretKey) require.Error(t, err) assert.Nil(t, transport) } func TestTokenTransport_RoundTrip(t *testing.T) { apiKey := "api" secretKey := "secret" transport, err := NewTokenTransport(apiKey, secretKey) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "http://example.com", nil) resp, err := transport.RoundTrip(req) require.NoError(t, err) assert.Regexp(t, `api:[^:]{28}:\d{13}`, resp.Request.Header.Get(securityTokenHeader)) } lego-4.9.1/providers/dns/constellix/internal/client.go000066400000000000000000000045271434020463500230720ustar00rootroot00000000000000package internal import ( "encoding/json" "fmt" "io" "net/http" "net/url" "path" ) const ( defaultBaseURL = "https://api.dns.constellix.com" defaultVersion = "v1" ) // Client the Constellix client. type Client struct { BaseURL string HTTPClient *http.Client common service // Reuse a single struct instead of allocating one for each service on the heap. // Services used for communicating with the API Domains *DomainService TxtRecords *TxtRecordService } // NewClient Creates a Constellix client. func NewClient(httpClient *http.Client) *Client { if httpClient == nil { httpClient = http.DefaultClient } client := &Client{ BaseURL: defaultBaseURL, HTTPClient: httpClient, } client.common.client = client client.Domains = (*DomainService)(&client.common) client.TxtRecords = (*TxtRecordService)(&client.common) return client } type service struct { client *Client } // do sends an API request and returns the API response. func (c *Client) do(req *http.Request, v interface{}) error { req.Header.Set("Content-Type", "application/json") resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer func() { _ = resp.Body.Close() }() err = checkResponse(resp) if err != nil { return err } raw, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read body: %w", err) } if err = json.Unmarshal(raw, v); err != nil { return fmt.Errorf("unmarshaling %T error: %w: %s", v, err, string(raw)) } return nil } func (c *Client) createEndpoint(fragment ...string) (string, error) { baseURL, err := url.Parse(c.BaseURL) if err != nil { return "", err } endpoint, err := baseURL.Parse(path.Join(fragment...)) if err != nil { return "", err } return endpoint.String(), nil } func checkResponse(resp *http.Response) error { if resp.StatusCode == http.StatusOK { return nil } data, err := io.ReadAll(resp.Body) if err == nil && data != nil { msg := &APIError{StatusCode: resp.StatusCode} if json.Unmarshal(data, msg) != nil { return fmt.Errorf("API error: status code: %d: %v", resp.StatusCode, string(data)) } switch resp.StatusCode { case http.StatusNotFound: return &NotFound{APIError: msg} case http.StatusBadRequest: return &BadRequest{APIError: msg} default: return msg } } return fmt.Errorf("API error, status code: %d", resp.StatusCode) } lego-4.9.1/providers/dns/constellix/internal/domains.go000066400000000000000000000041311434020463500232350ustar00rootroot00000000000000package internal import ( "errors" "fmt" "net/http" querystring "github.com/google/go-querystring/query" ) // DomainService API access to Domain. type DomainService service // GetAll domains. // https://api-docs.constellix.com/?version=latest#484c3f21-d724-4ee4-a6fa-ab22c8eb9e9b func (s *DomainService) GetAll(params *PaginationParameters) ([]Domain, error) { endpoint, err := s.client.createEndpoint(defaultVersion, "domains") if err != nil { return nil, fmt.Errorf("failed to create request endpoint: %w", err) } req, err := http.NewRequest(http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } if params != nil { v, errQ := querystring.Values(params) if errQ != nil { return nil, errQ } req.URL.RawQuery = v.Encode() } var domains []Domain err = s.client.do(req, &domains) if err != nil { return nil, err } return domains, nil } // GetByName Gets domain by name. func (s *DomainService) GetByName(domainName string) (Domain, error) { domains, err := s.Search(Exact, domainName) if err != nil { return Domain{}, err } if len(domains) == 0 { return Domain{}, fmt.Errorf("domain not found: %s", domainName) } if len(domains) > 1 { return Domain{}, fmt.Errorf("multiple domains found: %v", domains) } return domains[0], nil } // Search searches for a domain by name. // https://api-docs.constellix.com/?version=latest#3d7b2679-2209-49f3-b011-b7d24e512008 func (s *DomainService) Search(filter searchFilter, value string) ([]Domain, error) { endpoint, err := s.client.createEndpoint(defaultVersion, "domains", "search") if err != nil { return nil, fmt.Errorf("failed to create request endpoint: %w", err) } req, err := http.NewRequest(http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } query := req.URL.Query() query.Set(string(filter), value) req.URL.RawQuery = query.Encode() var domains []Domain err = s.client.do(req, &domains) if err != nil { var nf *NotFound if !errors.As(err, &nf) { return nil, err } } return domains, nil } lego-4.9.1/providers/dns/constellix/internal/domains_test.go000066400000000000000000000043301434020463500242750ustar00rootroot00000000000000package internal import ( "io" "net/http" "net/http/httptest" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupTest(t *testing.T) (*Client, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient(server.Client()) client.BaseURL = server.URL return client, mux } func TestDomainService_GetAll(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/v1/domains", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } file, err := os.Open("./fixtures/domains-GetAll.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) data, err := client.Domains.GetAll(nil) require.NoError(t, err) expected := []Domain{ {ID: 273301, Name: "aaa.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"}, {ID: 273302, Name: "bbb.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"}, {ID: 273303, Name: "ccc.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"}, {ID: 273304, Name: "ddd.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"}, } assert.Equal(t, expected, data) } func TestDomainService_Search(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/v1/domains/search", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } file, err := os.Open("./fixtures/domains-Search.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) data, err := client.Domains.Search(Exact, "lego.wtf") require.NoError(t, err) expected := []Domain{ {ID: 273302, Name: "lego.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"}, } assert.Equal(t, expected, data) } lego-4.9.1/providers/dns/constellix/internal/fixtures/000077500000000000000000000000001434020463500231265ustar00rootroot00000000000000lego-4.9.1/providers/dns/constellix/internal/fixtures/domains-GetAll.json000066400000000000000000000063331434020463500266260ustar00rootroot00000000000000[ { "id": 273301, "name": "aaa.wtf", "soa": { "primaryNameserver": "ns11.constellix.com.", "email": "dns.constellix.com.", "ttl": 86400, "serial": 2015010110, "refresh": 43200, "retry": 3600, "expire": 1209600, "negCache": 180 }, "createdTs": "2020-02-04T22:42:10Z", "modifiedTs": "2020-02-05T09:23:17Z", "typeId": 1, "domainTags": [], "folder": null, "hasGtdRegions": false, "hasGeoIP": false, "nameserverGroup": 1, "nameservers": [ "ns11.constellix.com.", "ns21.constellix.com.", "ns31.constellix.com.", "ns41.constellix.net.", "ns51.constellix.net.", "ns61.constellix.net." ], "note": "", "version": 9, "status": "ACTIVE", "tags": null, "contactIds": [] }, { "id": 273302, "name": "bbb.wtf", "soa": { "primaryNameserver": "ns11.constellix.com.", "email": "dns.constellix.com.", "ttl": 86400, "serial": 2015010110, "refresh": 43200, "retry": 3600, "expire": 1209600, "negCache": 180 }, "createdTs": "2020-02-04T22:42:10Z", "modifiedTs": "2020-02-05T09:23:17Z", "typeId": 1, "domainTags": [], "folder": null, "hasGtdRegions": false, "hasGeoIP": false, "nameserverGroup": 1, "nameservers": [ "ns11.constellix.com.", "ns21.constellix.com.", "ns31.constellix.com.", "ns41.constellix.net.", "ns51.constellix.net.", "ns61.constellix.net." ], "note": "", "version": 9, "status": "ACTIVE", "tags": null, "contactIds": [] }, { "id": 273303, "name": "ccc.wtf", "soa": { "primaryNameserver": "ns11.constellix.com.", "email": "dns.constellix.com.", "ttl": 86400, "serial": 2015010110, "refresh": 43200, "retry": 3600, "expire": 1209600, "negCache": 180 }, "createdTs": "2020-02-04T22:42:10Z", "modifiedTs": "2020-02-05T09:23:17Z", "typeId": 1, "domainTags": [], "folder": null, "hasGtdRegions": false, "hasGeoIP": false, "nameserverGroup": 1, "nameservers": [ "ns11.constellix.com.", "ns21.constellix.com.", "ns31.constellix.com.", "ns41.constellix.net.", "ns51.constellix.net.", "ns61.constellix.net." ], "note": "", "version": 9, "status": "ACTIVE", "tags": null, "contactIds": [] }, { "id": 273304, "name": "ddd.wtf", "soa": { "primaryNameserver": "ns11.constellix.com.", "email": "dns.constellix.com.", "ttl": 86400, "serial": 2015010110, "refresh": 43200, "retry": 3600, "expire": 1209600, "negCache": 180 }, "createdTs": "2020-02-04T22:42:10Z", "modifiedTs": "2020-02-05T09:23:17Z", "typeId": 1, "domainTags": [], "folder": null, "hasGtdRegions": false, "hasGeoIP": false, "nameserverGroup": 1, "nameservers": [ "ns11.constellix.com.", "ns21.constellix.com.", "ns31.constellix.com.", "ns41.constellix.net.", "ns51.constellix.net.", "ns61.constellix.net." ], "note": "", "version": 9, "status": "ACTIVE", "tags": null, "contactIds": [] } ] lego-4.9.1/providers/dns/constellix/internal/fixtures/domains-Search.json000066400000000000000000000014721434020463500266620ustar00rootroot00000000000000[ { "id": 273302, "name": "lego.wtf", "soa": { "primaryNameserver": "ns11.constellix.com.", "email": "dns.constellix.com.", "ttl": 86400, "serial": 2015010110, "refresh": 43200, "retry": 3600, "expire": 1209600, "negCache": 180 }, "createdTs": "2020-02-04T22:42:10Z", "modifiedTs": "2020-02-05T09:23:17Z", "typeId": 1, "domainTags": [], "folder": null, "hasGtdRegions": false, "hasGeoIP": false, "nameserverGroup": 1, "nameservers": [ "ns11.constellix.com.", "ns21.constellix.com.", "ns31.constellix.com.", "ns41.constellix.net.", "ns51.constellix.net.", "ns61.constellix.net." ], "note": "", "version": 9, "status": "ACTIVE", "tags": null, "contactIds": [] } ] lego-4.9.1/providers/dns/constellix/internal/fixtures/records-Create.json000066400000000000000000000006251434020463500266660ustar00rootroot00000000000000[ { "id": 3557066, "type": "TXT", "recordType": "txt", "name": "test", "recordOption": "roundRobin", "ttl": 300, "gtdRegion": 1, "parentId": 273302, "parent": "domain", "source": "Domain", "modifiedTs": 1580908547865, "value": [ { "value": "\"test\"" } ], "roundRobin": [ { "value": "\"test\"" } ] } ]lego-4.9.1/providers/dns/constellix/internal/fixtures/records-Get.json000066400000000000000000000005441434020463500262020ustar00rootroot00000000000000{ "id": 3557066, "type": "TXT", "recordType": "txt", "name": "test", "recordOption": "roundRobin", "ttl": 300, "gtdRegion": 1, "parentId": 273302, "parent": "domain", "source": "Domain", "modifiedTs": 1580908547863, "value": [ { "value": "\"test\"" } ], "roundRobin": [ { "value": "\"test\"" } ] } lego-4.9.1/providers/dns/constellix/internal/fixtures/records-GetAll.json000066400000000000000000000006251434020463500266330ustar00rootroot00000000000000[ { "id": 3557066, "type": "TXT", "recordType": "txt", "name": "test", "recordOption": "roundRobin", "ttl": 300, "gtdRegion": 1, "parentId": 273302, "parent": "domain", "source": "Domain", "modifiedTs": 1580908547865, "value": [ { "value": "\"test\"" } ], "roundRobin": [ { "value": "\"test\"" } ] } ]lego-4.9.1/providers/dns/constellix/internal/fixtures/records-Search.json000066400000000000000000000001271434020463500266650ustar00rootroot00000000000000[ { "id": 3557066, "name": "test", "recordType": "", "type": "" } ]lego-4.9.1/providers/dns/constellix/internal/model.go000066400000000000000000000054701434020463500227120ustar00rootroot00000000000000package internal import ( "fmt" "strings" ) // Search filters. const ( StartsWith searchFilter = "startswith" Exact searchFilter = "exact" EndsWith searchFilter = "endswith" Contains searchFilter = "contains" ) type searchFilter string // NotFound Not found error. type NotFound struct { *APIError } func (e *NotFound) Unwrap() error { return e.APIError } // BadRequest Bad request error. type BadRequest struct { *APIError } func (e *BadRequest) Unwrap() error { return e.APIError } // APIError is the representation of an API error. type APIError struct { StatusCode int `json:"statusCode"` Errors []string `json:"errors"` } func (a APIError) Error() string { return fmt.Sprintf("%d: %s", a.StatusCode, strings.Join(a.Errors, ": ")) } // SuccessMessage is the representation of a success message. type SuccessMessage struct { Success string `json:"success"` } // RecordRequest is the representation of a request's record. type RecordRequest struct { Name string `json:"name"` TTL int `json:"ttl,omitempty"` RoundRobin []RecordValue `json:"roundRobin,omitempty"` } // RecordValue is the representation of a record's value. type RecordValue struct { Value string `json:"value,omitempty"` DisableFlag bool `json:"disableFlag,omitempty"` // only for the response } // Record is the representation of a record. type Record struct { ID int64 `json:"id"` Type string `json:"type"` RecordType string `json:"recordType"` Name string `json:"name"` RecordOption string `json:"recordOption,omitempty"` NoAnswer bool `json:"noAnswer,omitempty"` Note string `json:"note,omitempty"` TTL int `json:"ttl,omitempty"` GtdRegion int `json:"gtdRegion,omitempty"` ParentID int `json:"parentId,omitempty"` Parent string `json:"parent,omitempty"` Source string `json:"source,omitempty"` ModifiedTS int64 `json:"modifiedTs,omitempty"` Value []RecordValue `json:"value,omitempty"` RoundRobin []RecordValue `json:"roundRobin,omitempty"` } // Domain is the representation of a domain. type Domain struct { ID int64 `json:"id"` Name string `json:"name,omitempty"` TypeID int64 `json:"typeId,omitempty"` Version int64 `json:"version,omitempty"` Status string `json:"status,omitempty"` } // PaginationParameters is pagination parameters. type PaginationParameters struct { // Offset retrieves a subset of records starting with the offset value. Offset int `url:"offset"` // Max retrieves maximum number of dataset. Max int `url:"max"` // Sort on the basis of given property name. Sort string `url:"sort"` // Order Sort order. Possible values are asc / desc. Order string `url:"order"` } lego-4.9.1/providers/dns/constellix/internal/txtrecords.go000066400000000000000000000112511434020463500240050ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "errors" "fmt" "net/http" "strconv" ) // TxtRecordService API access to Record. type TxtRecordService service // Create a TXT record. // https://api-docs.constellix.com/?version=latest#22e24d5b-9ec0-49a7-b2b0-5ff0a28e71be func (s *TxtRecordService) Create(domainID int64, record RecordRequest) ([]Record, error) { body, err := json.Marshal(record) if err != nil { return nil, fmt.Errorf("failed to marshall request body: %w", err) } endpoint, err := s.client.createEndpoint(defaultVersion, "domains", strconv.FormatInt(domainID, 10), "records", "txt") if err != nil { return nil, fmt.Errorf("failed to create request endpoint: %w", err) } req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } var records []Record err = s.client.do(req, &records) if err != nil { return nil, err } return records, nil } // GetAll TXT records. // https://api-docs.constellix.com/?version=latest#e7103c53-2ad8-4bc8-b5b3-4c22c4b571b2 func (s *TxtRecordService) GetAll(domainID int64) ([]Record, error) { endpoint, err := s.client.createEndpoint(defaultVersion, "domains", strconv.FormatInt(domainID, 10), "records", "txt") if err != nil { return nil, fmt.Errorf("failed to create request endpoint: %w", err) } req, err := http.NewRequest(http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } var records []Record err = s.client.do(req, &records) if err != nil { return nil, err } return records, nil } // Get a TXT record. // https://api-docs.constellix.com/?version=latest#e7103c53-2ad8-4bc8-b5b3-4c22c4b571b2 func (s *TxtRecordService) Get(domainID, recordID int64) (*Record, error) { endpoint, err := s.client.createEndpoint(defaultVersion, "domains", strconv.FormatInt(domainID, 10), "records", "txt", strconv.FormatInt(recordID, 10)) if err != nil { return nil, fmt.Errorf("failed to create request endpoint: %w", err) } req, err := http.NewRequest(http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } var records Record err = s.client.do(req, &records) if err != nil { return nil, err } return &records, nil } // Update a TXT record. // https://api-docs.constellix.com/?version=latest#d4e9ab2e-fac0-45a6-b0e4-cf62a2d2e3da func (s *TxtRecordService) Update(domainID, recordID int64, record RecordRequest) (*SuccessMessage, error) { body, err := json.Marshal(record) if err != nil { return nil, fmt.Errorf("failed to marshall request body: %w", err) } endpoint, err := s.client.createEndpoint(defaultVersion, "domains", strconv.FormatInt(domainID, 10), "records", "txt", strconv.FormatInt(recordID, 10)) if err != nil { return nil, fmt.Errorf("failed to create request endpoint: %w", err) } req, err := http.NewRequest(http.MethodPut, endpoint, bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } var msg SuccessMessage err = s.client.do(req, &msg) if err != nil { return nil, err } return &msg, nil } // Delete a TXT record. // https://api-docs.constellix.com/?version=latest#135947f7-d6c8-481a-83c7-4d387b0bdf9e func (s *TxtRecordService) Delete(domainID, recordID int64) (*SuccessMessage, error) { endpoint, err := s.client.createEndpoint(defaultVersion, "domains", strconv.FormatInt(domainID, 10), "records", "txt", strconv.FormatInt(recordID, 10)) if err != nil { return nil, fmt.Errorf("failed to create request endpoint: %w", err) } req, err := http.NewRequest(http.MethodDelete, endpoint, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } var msg *SuccessMessage err = s.client.do(req, &msg) if err != nil { return nil, err } return msg, nil } // Search searches for a TXT record by name. // https://api-docs.constellix.com/?version=latest#81003e4f-bd3f-413f-a18d-6d9d18f10201 func (s *TxtRecordService) Search(domainID int64, filter searchFilter, value string) ([]Record, error) { endpoint, err := s.client.createEndpoint(defaultVersion, "domains", strconv.FormatInt(domainID, 10), "records", "txt", "search") if err != nil { return nil, fmt.Errorf("failed to create request endpoint: %w", err) } req, err := http.NewRequest(http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } query := req.URL.Query() query.Set(string(filter), value) req.URL.RawQuery = query.Encode() var records []Record err = s.client.do(req, &records) if err != nil { var nf *NotFound if !errors.As(err, &nf) { return nil, err } } return records, nil } lego-4.9.1/providers/dns/constellix/internal/txtrecords_test.go000066400000000000000000000125371434020463500250540ustar00rootroot00000000000000package internal import ( "encoding/json" "io" "net/http" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestTxtRecordService_Create(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/v1/domains/12345/records/txt", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } file, err := os.Open("./fixtures/records-Create.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) records, err := client.TxtRecords.Create(12345, RecordRequest{}) require.NoError(t, err) recordsJSON, err := json.Marshal(records) require.NoError(t, err) expectedContent, err := os.ReadFile("./fixtures/records-Create.json") require.NoError(t, err) assert.JSONEq(t, string(expectedContent), string(recordsJSON)) } func TestTxtRecordService_GetAll(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/v1/domains/12345/records/txt", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } file, err := os.Open("./fixtures/records-GetAll.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) records, err := client.TxtRecords.GetAll(12345) require.NoError(t, err) recordsJSON, err := json.Marshal(records) require.NoError(t, err) expectedContent, err := os.ReadFile("./fixtures/records-GetAll.json") require.NoError(t, err) assert.JSONEq(t, string(expectedContent), string(recordsJSON)) } func TestTxtRecordService_Get(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/v1/domains/12345/records/txt/6789", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } file, err := os.Open("./fixtures/records-Get.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) record, err := client.TxtRecords.Get(12345, 6789) require.NoError(t, err) expected := &Record{ ID: 3557066, Type: "TXT", RecordType: "txt", Name: "test", TTL: 300, RecordOption: "roundRobin", GtdRegion: 1, ParentID: 273302, Parent: "domain", Source: "Domain", ModifiedTS: 1580908547863, Value: []RecordValue{{ Value: `"test"`, }}, RoundRobin: []RecordValue{{ Value: `"test"`, }}, } assert.Equal(t, expected, record) } func TestTxtRecordService_Update(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/v1/domains/12345/records/txt/6789", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPut { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } _, err := rw.Write([]byte(`{"success":"Record updated successfully"}`)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) msg, err := client.TxtRecords.Update(12345, 6789, RecordRequest{}) require.NoError(t, err) expected := &SuccessMessage{Success: "Record updated successfully"} assert.Equal(t, expected, msg) } func TestTxtRecordService_Delete(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/v1/domains/12345/records/txt/6789", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodDelete { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } _, err := rw.Write([]byte(`{"success":"Record deleted successfully"}`)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) msg, err := client.TxtRecords.Delete(12345, 6789) require.NoError(t, err) expected := &SuccessMessage{Success: "Record deleted successfully"} assert.Equal(t, expected, msg) } func TestTxtRecordService_Search(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/v1/domains/12345/records/txt/search", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } file, err := os.Open("./fixtures/records-Search.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) records, err := client.TxtRecords.Search(12345, Exact, "test") require.NoError(t, err) recordsJSON, err := json.Marshal(records) require.NoError(t, err) expectedContent, err := os.ReadFile("./fixtures/records-Search.json") require.NoError(t, err) assert.JSONEq(t, string(expectedContent), string(recordsJSON)) } lego-4.9.1/providers/dns/desec/000077500000000000000000000000001434020463500163405ustar00rootroot00000000000000lego-4.9.1/providers/dns/desec/desec.go000066400000000000000000000126401434020463500177550ustar00rootroot00000000000000// Package desec implements a DNS provider for solving the DNS-01 challenge using deSEC DNS. package desec import ( "context" "errors" "fmt" "log" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/nrdcg/desec" ) // Environment variables names. const ( envNamespace = "DESEC_" EnvToken = envNamespace + "TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // https://github.com/desec-io/desec-stack/issues/216 // https://desec.readthedocs.io/_/downloads/en/latest/pdf/ const defaultTTL int = 3600 // Config is used to configure the creation of the DNSProvider. type Config struct { Token string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *desec.Client } // NewDNSProvider returns a DNSProvider instance configured for deSEC. // Credentials must be passed in the environment variable: DESEC_TOKEN. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvToken) if err != nil { return nil, fmt.Errorf("desec: %w", err) } config := NewDefaultConfig() config.Token = values[EnvToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for deSEC. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("desec: the configuration of the DNS provider is nil") } if config.Token == "" { return nil, errors.New("desec: incomplete credentials, missing token") } opts := desec.NewDefaultClientOptions() if config.HTTPClient != nil { opts.HTTPClient = config.HTTPClient } opts.Logger = log.Default() client := desec.New(config.Token, opts) return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { ctx := context.Background() fqdn, value := dns01.GetRecord(domain, keyAuth) quotedValue := fmt.Sprintf(`%q`, value) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("desec: could not find zone for domain %q and fqdn %q : %w", domain, fqdn, err) } recordName := getRecordName(fqdn, authZone) domainName := dns01.UnFqdn(authZone) rrSet, err := d.client.Records.Get(ctx, domainName, recordName, "TXT") if err != nil { var nf *desec.NotFoundError if !errors.As(err, &nf) { return fmt.Errorf("desec: failed to get records: domainName=%s, recordName=%s: %w", domainName, recordName, err) } // Not found case -> create _, err = d.client.Records.Create(ctx, desec.RRSet{ Domain: domainName, SubName: recordName, Type: "TXT", Records: []string{quotedValue}, TTL: d.config.TTL, }) if err != nil { return fmt.Errorf("desec: failed to create records: domainName=%s, recordName=%s: %w", domainName, recordName, err) } return nil } // update records := append(rrSet.Records, quotedValue) _, err = d.client.Records.Update(ctx, domainName, recordName, "TXT", desec.RRSet{Records: records}) if err != nil { return fmt.Errorf("desec: failed to update records: domainName=%s, recordName=%s: %w", domainName, recordName, err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { ctx := context.Background() fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("desec: could not find zone for domain %q and fqdn %q : %w", domain, fqdn, err) } recordName := getRecordName(fqdn, authZone) domainName := dns01.UnFqdn(authZone) rrSet, err := d.client.Records.Get(ctx, domainName, recordName, "TXT") if err != nil { return fmt.Errorf("desec: failed to get records: domainName=%s, recordName=%s: %w", domainName, recordName, err) } records := make([]string, 0) for _, record := range rrSet.Records { if record != fmt.Sprintf(`%q`, value) { records = append(records, record) } } _, err = d.client.Records.Update(ctx, domainName, recordName, "TXT", desec.RRSet{Records: records}) if err != nil { return fmt.Errorf("desec: failed to update records: domainName=%s, recordName=%s: %w", domainName, recordName, err) } return nil } func getRecordName(fqdn, authZone string) string { return fqdn[0 : len(fqdn)-len(authZone)-1] } lego-4.9.1/providers/dns/desec/desec.toml000066400000000000000000000012171434020463500203210ustar00rootroot00000000000000Name = "deSEC.io" Description = '''''' URL = "https://desec.io" Code = "desec" Since = "v3.7.0" Example = ''' DESEC_TOKEN=x-xxxxxxxxxxxxxxxxxxxxxxxxxx \ lego --email you@example.com --dns desec --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DESEC_TOKEN = "Domain token" [Configuration.Additional] DESEC_POLLING_INTERVAL = "Time between DNS propagation check" DESEC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" DESEC_TTL = "The TTL of the TXT record used for the DNS challenge" DESEC_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://desec.readthedocs.io/en/latest/" lego-4.9.1/providers/dns/desec/desec_test.go000066400000000000000000000043001434020463500210060ustar00rootroot00000000000000package desec import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvToken: "123", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvToken: "", }, expected: "desec: some credentials information are missing: DESEC_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string expected string token string }{ { desc: "success", token: "api_key", }, { desc: "missing credentials", expected: "desec: incomplete credentials, missing token", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Token = test.token p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/designate/000077500000000000000000000000001434020463500172205ustar00rootroot00000000000000lego-4.9.1/providers/dns/designate/designate.go000066400000000000000000000174151434020463500215220ustar00rootroot00000000000000// Package designate implements a DNS provider for solving the DNS-01 challenge using the Designate DNSaaS for Openstack. package designate import ( "errors" "fmt" "log" "os" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" "github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets" "github.com/gophercloud/gophercloud/openstack/dns/v2/zones" "github.com/gophercloud/utils/openstack/clientconfig" ) // Environment variables names. const ( envNamespace = "DESIGNATE_" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" envNamespaceClient = "OS_" EnvAuthURL = envNamespaceClient + "AUTH_URL" EnvUsername = envNamespaceClient + "USERNAME" EnvPassword = envNamespaceClient + "PASSWORD" EnvUserID = envNamespaceClient + "USER_ID" EnvAppCredID = envNamespaceClient + "APPLICATION_CREDENTIAL_ID" EnvAppCredName = envNamespaceClient + "APPLICATION_CREDENTIAL_NAME" EnvAppCredSecret = envNamespaceClient + "APPLICATION_CREDENTIAL_SECRET" EnvTenantName = envNamespaceClient + "TENANT_NAME" EnvRegionName = envNamespaceClient + "REGION_NAME" EnvProjectID = envNamespaceClient + "PROJECT_ID" EnvCloud = envNamespaceClient + "CLOUD" ) // Config is used to configure the creation of the DNSProvider. type Config struct { PropagationTimeout time.Duration PollingInterval time.Duration TTL int opts gophercloud.AuthOptions } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 10), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *gophercloud.ServiceClient dnsEntriesMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for Designate. // Credentials must be passed in the environment variables: // OS_AUTH_URL, OS_USERNAME, OS_PASSWORD, OS_REGION_NAME. // Or you can specify OS_CLOUD to read the credentials from the according cloud entry. func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() val, err := env.Get(EnvCloud) if err == nil { opts, erro := clientconfig.AuthOptions(&clientconfig.ClientOpts{ Cloud: val[EnvCloud], }) if erro != nil { return nil, fmt.Errorf("designate: %w", erro) } config.opts = *opts } else { opts, err := openstack.AuthOptionsFromEnv() if err != nil { return nil, fmt.Errorf("designate: %w", err) } config.opts = opts } return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Designate. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("designate: the configuration of the DNS provider is nil") } provider, err := openstack.AuthenticatedClient(config.opts) if err != nil { return nil, fmt.Errorf("designate: failed to authenticate: %w", err) } dnsClient, err := openstack.NewDNSV2(provider, gophercloud.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) if err != nil { return nil, fmt.Errorf("designate: failed to get DNS provider: %w", err) } return &DNSProvider{client: dnsClient, config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("designate: couldn't get zone ID in Present: %w", err) } zoneID, err := d.getZoneID(authZone) if err != nil { return fmt.Errorf("designate: %w", err) } // use mutex to prevent race condition between creating the record and verifying it d.dnsEntriesMu.Lock() defer d.dnsEntriesMu.Unlock() existingRecord, err := d.getRecord(zoneID, fqdn) if err != nil { return fmt.Errorf("designate: %w", err) } if existingRecord != nil { if contains(existingRecord.Records, value) { log.Printf("designate: the record already exists: %s", value) return nil } return d.updateRecord(existingRecord, value) } err = d.createRecord(zoneID, fqdn, value) if err != nil { return fmt.Errorf("designate: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return err } zoneID, err := d.getZoneID(authZone) if err != nil { return fmt.Errorf("designate: couldn't get zone ID in CleanUp: %w", err) } // use mutex to prevent race condition between getting the record and deleting it d.dnsEntriesMu.Lock() defer d.dnsEntriesMu.Unlock() record, err := d.getRecord(zoneID, fqdn) if err != nil { return fmt.Errorf("designate: couldn't get Record ID in CleanUp: %w", err) } if record == nil { // Record is already deleted return nil } err = recordsets.Delete(d.client, zoneID, record.ID).ExtractErr() if err != nil { return fmt.Errorf("designate: error for %s in CleanUp: %w", fqdn, err) } return nil } func contains(values []string, value string) bool { for _, v := range values { if v == value { return true } } return false } func (d *DNSProvider) createRecord(zoneID, fqdn, value string) error { createOpts := recordsets.CreateOpts{ Name: fqdn, Type: "TXT", TTL: d.config.TTL, Description: "ACME verification record", Records: []string{value}, } actual, err := recordsets.Create(d.client, zoneID, createOpts).Extract() if err != nil { return fmt.Errorf("error for %s in Present while creating record: %w", fqdn, err) } if actual.Name != fqdn || actual.TTL != d.config.TTL { return errors.New("the created record doesn't match what we wanted to create") } return nil } func (d *DNSProvider) updateRecord(record *recordsets.RecordSet, value string) error { if contains(record.Records, value) { log.Printf("skip: the record already exists: %s", value) return nil } values := append([]string{value}, record.Records...) updateOpts := recordsets.UpdateOpts{ Description: &record.Description, TTL: &record.TTL, Records: values, } result := recordsets.Update(d.client, record.ZoneID, record.ID, updateOpts) return result.Err } func (d *DNSProvider) getZoneID(wanted string) (string, error) { allPages, err := zones.List(d.client, nil).AllPages() if err != nil { return "", err } allZones, err := zones.ExtractZones(allPages) if err != nil { return "", err } for _, zone := range allZones { if zone.Name == wanted { return zone.ID, nil } } return "", fmt.Errorf("zone id not found for %s", wanted) } func (d *DNSProvider) getRecord(zoneID, wanted string) (*recordsets.RecordSet, error) { allPages, err := recordsets.ListByZone(d.client, zoneID, nil).AllPages() if err != nil { return nil, err } allRecords, err := recordsets.ExtractRecordSets(allPages) if err != nil { return nil, err } for _, record := range allRecords { if record.Name == wanted && record.Type == "TXT" { return &record, nil } } return nil, nil } lego-4.9.1/providers/dns/designate/designate.toml000066400000000000000000000054131434020463500220630ustar00rootroot00000000000000Name = "Designate DNSaaS for Openstack" Description = '''''' URL = "https://docs.openstack.org/designate/latest/" Code = "designate" Since = "v2.2.0" Example = ''' # With a `clouds.yaml` OS_CLOUD=my_openstack \ lego --email you@example.com --dns designate --domains my.example.org run # or OS_AUTH_URL=https://openstack.example.org \ OS_REGION_NAME=RegionOne \ OS_PROJECT_ID=23d4522a987d4ab529f722a007c27846 OS_USERNAME=myuser \ OS_PASSWORD=passw0rd \ lego --email you@example.com --dns designate --domains my.example.org run # or OS_AUTH_URL=https://openstack.example.org \ OS_REGION_NAME=RegionOne \ OS_AUTH_TYPE=v3applicationcredential \ OS_APPLICATION_CREDENTIAL_ID=imn74uq0or7dyzz20dwo1ytls4me8dry \ OS_APPLICATION_CREDENTIAL_SECRET=68FuSPSdQqkFQYH5X1OoriEIJOwyLtQ8QSqXZOc9XxFK1A9tzZT6He2PfPw0OMja \ lego --email you@example.com --dns designate --domains my.example.org run ''' Additional = ''' ## Description There are three main ways of authenticating with Designate: 1. The first one is by using the `OS_CLOUD` environment variable and a `clouds.yaml` file. 2. The second one is using your username and password, via the `OS_USERNAME`, `OS_PASSWORD` and `OS_PROJECT_NAME` environment variables. 3. The third one is by using an application credential, via the `OS_APPLICATION_CREDENTIAL_*` and `OS_USER_ID` environment variables. For the username/password and application methods, the `OS_AUTH_URL` and `OS_REGION_NAME` environment variables are required. For more information, you can read about the different methods of authentication with OpenStack in the Keystone's documentation and the gophercloud documentation: - [Keystone username/password](https://docs.openstack.org/keystone/latest/user/supported_clients.html) - [Keystone application credentials](https://docs.openstack.org/keystone/latest/user/application_credentials.html) ''' [Configuration] [Configuration.Credentials] OS_AUTH_URL = "Identity endpoint URL" OS_USERNAME = "Username" OS_PASSWORD = "Password" OS_USER_ID = "User ID" OS_APPLICATION_CREDENTIAL_ID = "Application credential ID" OS_APPLICATION_CREDENTIAL_NAME = "Application credential name" OS_APPLICATION_CREDENTIAL_SECRET = "Application credential secret" OS_PROJECT_NAME = "Project name" OS_REGION_NAME = "Region name" [Configuration.Additional] OS_PROJECT_ID = "Project ID" OS_TENANT_NAME = "Tenant name (deprecated see OS_PROJECT_NAME and OS_PROJECT_ID)" DESIGNATE_POLLING_INTERVAL = "Time between DNS propagation check" DESIGNATE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" DESIGNATE_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://docs.openstack.org/designate/latest/" GoClient = "https://godoc.org/github.com/gophercloud/gophercloud/openstack/dns/v2" lego-4.9.1/providers/dns/designate/designate_test.go000066400000000000000000000171601434020463500225560ustar00rootroot00000000000000package designate import ( "net/http" "net/http/httptest" "os" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/gophercloud/utils/openstack/clientconfig" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" ) const ( envDomain = envNamespace + "DOMAIN" envOSClientConfigFile = "OS_CLIENT_CONFIG_FILE" ) var envTest = tester.NewEnvTest( EnvCloud, EnvAuthURL, EnvUsername, EnvPassword, EnvUserID, EnvAppCredID, EnvAppCredName, EnvAppCredSecret, EnvTenantName, EnvRegionName, EnvProjectID, envOSClientConfigFile). WithDomain(envDomain) func TestNewDNSProvider_fromEnv(t *testing.T) { serverURL := setupTestProvider(t) testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAuthURL: serverURL + "/v2.0/", EnvUsername: "B", EnvPassword: "C", EnvRegionName: "D", EnvProjectID: "E", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAuthURL: "", EnvUsername: "", EnvPassword: "", EnvRegionName: "", }, expected: "designate: Missing environment variable [OS_AUTH_URL]", }, { desc: "missing auth url", envVars: map[string]string{ EnvAuthURL: "", EnvUsername: "B", EnvPassword: "C", EnvRegionName: "D", }, expected: "designate: Missing environment variable [OS_AUTH_URL]", }, { desc: "missing username", envVars: map[string]string{ EnvAuthURL: serverURL + "/v2.0/", EnvUsername: "", EnvPassword: "C", EnvRegionName: "D", }, expected: "designate: Missing one of the following environment variables [OS_USERID, OS_USERNAME]", }, { desc: "missing password", envVars: map[string]string{ EnvAuthURL: serverURL + "/v2.0/", EnvUsername: "B", EnvPassword: "", EnvRegionName: "D", }, expected: "designate: Missing environment variable [OS_PASSWORD]", }, { desc: "missing application credential secret", envVars: map[string]string{ EnvAuthURL: serverURL + "/v2.0/", EnvRegionName: "D", EnvAppCredID: "F", }, expected: "designate: Missing environment variable [OS_APPLICATION_CREDENTIAL_SECRET]", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProvider_fromCloud(t *testing.T) { serverURL := setupTestProvider(t) testCases := []struct { desc string osCloud string cloud clientconfig.Cloud expected string }{ { desc: "success", osCloud: "good_cloud", cloud: clientconfig.Cloud{ AuthInfo: &clientconfig.AuthInfo{ AuthURL: serverURL + "/v2.0/", Username: "B", Password: "C", ProjectName: "E", ProjectID: "F", }, RegionName: "D", }, }, { desc: "missing auth url", osCloud: "missing_auth_url", cloud: clientconfig.Cloud{ AuthInfo: &clientconfig.AuthInfo{ Username: "B", Password: "C", ProjectName: "E", ProjectID: "F", }, RegionName: "D", }, expected: "designate: Missing input for argument [auth_url]", }, { desc: "missing username", osCloud: "missing_username", cloud: clientconfig.Cloud{ AuthInfo: &clientconfig.AuthInfo{ AuthURL: serverURL + "/v2.0/", Password: "C", ProjectName: "E", ProjectID: "F", }, RegionName: "D", }, expected: "designate: failed to authenticate: Missing input for argument [Username]", }, { desc: "missing password", osCloud: "missing_auth_url", cloud: clientconfig.Cloud{ AuthInfo: &clientconfig.AuthInfo{ AuthURL: serverURL + "/v2.0/", Username: "B", ProjectName: "E", ProjectID: "F", }, RegionName: "D", }, expected: "designate: failed to authenticate: Exactly one of PasswordCredentials and TokenCredentials must be provided", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(map[string]string{ EnvCloud: test.osCloud, envOSClientConfigFile: createCloudsYaml(t, test.osCloud, test.cloud), }) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { serverURL := setupTestProvider(t) testCases := []struct { desc string tenantName string password string userName string authURL string expected string }{ { desc: "success", tenantName: "A", password: "B", userName: "C", authURL: serverURL + "/v2.0/", }, { desc: "wrong auth url", tenantName: "A", password: "B", userName: "C", authURL: serverURL, expected: "designate: failed to authenticate: No supported version available from endpoint " + serverURL + "/", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.opts.TenantName = test.tenantName config.opts.Password = test.password config.opts.Username = test.userName config.opts.IdentityEndpoint = test.authURL p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } // createCloudsYaml creates a temporary cloud file for testing purpose. func createCloudsYaml(t *testing.T, cloudName string, cloud clientconfig.Cloud) string { t.Helper() file, err := os.CreateTemp("", "lego_test") require.NoError(t, err) t.Cleanup(func() { _ = os.RemoveAll(file.Name()) }) clouds := clientconfig.Clouds{ Clouds: map[string]clientconfig.Cloud{ cloudName: cloud, }, } err = yaml.NewEncoder(file).Encode(&clouds) require.NoError(t, err) return file.Name() } func setupTestProvider(t *testing.T) string { t.Helper() mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`{ "access": { "token": { "id": "a", "expires": "9015-06-05T16:24:57.637Z" }, "user": { "name": "a", "roles": [ ], "role_links": [ ] }, "serviceCatalog": [ { "endpoints": [ { "adminURL": "http://23.253.72.207:9696/", "region": "D", "internalURL": "http://23.253.72.207:9696/", "id": "97c526db8d7a4c88bbb8d68db1bdcdb8", "publicURL": "http://23.253.72.207:9696/" } ], "endpoints_links": [ ], "type": "dns", "name": "designate" } ] } }`)) w.WriteHeader(http.StatusOK) }) server := httptest.NewServer(mux) t.Cleanup(server.Close) return server.URL } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/digitalocean/000077500000000000000000000000001434020463500177005ustar00rootroot00000000000000lego-4.9.1/providers/dns/digitalocean/client.go000066400000000000000000000066211434020463500215120ustar00rootroot00000000000000package digitalocean import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "github.com/go-acme/lego/v4/challenge/dns01" ) const defaultBaseURL = "https://api.digitalocean.com" // txtRecordResponse represents a response from DO's API after making a TXT record. type txtRecordResponse struct { DomainRecord record `json:"domain_record"` } type record struct { ID int `json:"id,omitempty"` Type string `json:"type,omitempty"` Name string `json:"name,omitempty"` Data string `json:"data,omitempty"` TTL int `json:"ttl,omitempty"` } type apiError struct { ID string `json:"id"` Message string `json:"message"` } func (d *DNSProvider) removeTxtRecord(domain string, recordID int) error { authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return fmt.Errorf("could not determine zone for domain %q: %w", domain, err) } reqURL := fmt.Sprintf("%s/v2/domains/%s/records/%d", d.config.BaseURL, dns01.UnFqdn(authZone), recordID) req, err := d.newRequest(http.MethodDelete, reqURL, nil) if err != nil { return err } resp, err := d.config.HTTPClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { return readError(req, resp) } return nil } func (d *DNSProvider) addTxtRecord(fqdn, value string) (*txtRecordResponse, error) { authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(fqdn)) if err != nil { return nil, fmt.Errorf("could not determine zone for domain %q: %w", fqdn, err) } reqData := record{Type: "TXT", Name: fqdn, Data: value, TTL: d.config.TTL} body, err := json.Marshal(reqData) if err != nil { return nil, err } reqURL := fmt.Sprintf("%s/v2/domains/%s/records", d.config.BaseURL, dns01.UnFqdn(authZone)) req, err := d.newRequest(http.MethodPost, reqURL, bytes.NewReader(body)) if err != nil { return nil, err } resp, err := d.config.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { return nil, readError(req, resp) } content, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.New(toUnreadableBodyMessage(req, content)) } // Everything looks good; but we'll need the ID later to delete the record respData := &txtRecordResponse{} err = json.Unmarshal(content, respData) if err != nil { return nil, fmt.Errorf("%w: %s", err, toUnreadableBodyMessage(req, content)) } return respData, nil } func (d *DNSProvider) newRequest(method, reqURL string, body io.Reader) (*http.Request, error) { req, err := http.NewRequest(method, reqURL, body) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.config.AuthToken)) return req, nil } func readError(req *http.Request, resp *http.Response) error { content, err := io.ReadAll(resp.Body) if err != nil { return errors.New(toUnreadableBodyMessage(req, content)) } var errInfo apiError err = json.Unmarshal(content, &errInfo) if err != nil { return fmt.Errorf("apiError unmarshaling error: %w: %s", err, toUnreadableBodyMessage(req, content)) } return fmt.Errorf("HTTP %d: %s: %s", resp.StatusCode, errInfo.ID, errInfo.Message) } func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string { return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody)) } lego-4.9.1/providers/dns/digitalocean/digitalocean.go000066400000000000000000000076051434020463500226620ustar00rootroot00000000000000// Package digitalocean implements a DNS provider for solving the DNS-01 challenge using digitalocean DNS. package digitalocean import ( "errors" "fmt" "net/http" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "DO_" EnvAuthToken = envNamespace + "AUTH_TOKEN" EnvAPIUrl = envNamespace + "API_URL" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string AuthToken string TTL int PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ BaseURL: env.GetOrDefaultString(EnvAPIUrl, defaultBaseURL), TTL: env.GetOrDefaultInt(EnvTTL, 30), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 60*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config recordIDs map[string]int recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for Digital // Ocean. Credentials must be passed in the environment variable: // DO_AUTH_TOKEN. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAuthToken) if err != nil { return nil, fmt.Errorf("digitalocean: %w", err) } config := NewDefaultConfig() config.AuthToken = values[EnvAuthToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Digital Ocean. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("digitalocean: the configuration of the DNS provider is nil") } if config.AuthToken == "" { return nil, errors.New("digitalocean: credentials missing") } if config.BaseURL == "" { config.BaseURL = defaultBaseURL } return &DNSProvider{ config: config, recordIDs: make(map[string]int), }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) respData, err := d.addTxtRecord(fqdn, value) if err != nil { return fmt.Errorf("digitalocean: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = respData.DomainRecord.ID d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("digitalocean: %w", err) } // get the record's unique ID from when we created it d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("digitalocean: unknown record ID for '%s'", fqdn) } err = d.removeTxtRecord(authZone, recordID) if err != nil { return fmt.Errorf("digitalocean: %w", err) } // Delete record ID from map d.recordIDsMu.Lock() delete(d.recordIDs, token) d.recordIDsMu.Unlock() return nil } lego-4.9.1/providers/dns/digitalocean/digitalocean.toml000066400000000000000000000013601434020463500232200ustar00rootroot00000000000000Name = "Digital Ocean" Description = '''''' URL = "https://www.digitalocean.com/docs/networking/dns/" Code = "digitalocean" Since = "v0.3.0" Example = ''' DO_AUTH_TOKEN=xxxxxx \ lego --email you@example.com --dns digitalocean --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DO_AUTH_TOKEN = "Authentication token" [Configuration.Additional] DO_API_URL = "The URL of the API" DO_POLLING_INTERVAL = "Time between DNS propagation check" DO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" DO_TTL = "The TTL of the TXT record used for the DNS challenge" DO_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developers.digitalocean.com/documentation/v2/#domain-records" lego-4.9.1/providers/dns/digitalocean/digitalocean_test.go000066400000000000000000000101641434020463500237130ustar00rootroot00000000000000package digitalocean import ( "fmt" "io" "net/http" "net/http/httptest" "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var envTest = tester.NewEnvTest(EnvAuthToken) func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) config := NewDefaultConfig() config.AuthToken = "asdf1234" config.BaseURL = server.URL config.HTTPClient = server.Client() provider, err := NewDNSProviderConfig(config) require.NoError(t, err) return provider, mux } func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAuthToken: "123", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAuthToken: "", }, expected: "digitalocean: some credentials information are missing: DO_AUTH_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string authToken string expected string }{ { desc: "success", authToken: "123", }, { desc: "missing credentials", expected: "digitalocean: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.AuthToken = test.authToken p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } }) } } func TestDNSProvider_Present(t *testing.T) { provider, mux := setupTest(t) mux.HandleFunc("/v2/domains/example.com/records", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method, "method") assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type") assert.Equal(t, "Bearer asdf1234", r.Header.Get("Authorization"), "Authorization") reqBody, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } expectedReqBody := `{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":30}` assert.Equal(t, expectedReqBody, string(reqBody)) w.WriteHeader(http.StatusCreated) _, err = fmt.Fprintf(w, `{ "domain_record": { "id": 1234567, "type": "TXT", "name": "_acme-challenge", "data": "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI", "priority": null, "port": null, "weight": null } }`) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) err := provider.Present("example.com", "", "foobar") require.NoError(t, err) } func TestDNSProvider_CleanUp(t *testing.T) { provider, mux := setupTest(t) mux.HandleFunc("/v2/domains/example.com/records/1234567", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodDelete, r.Method, "method") assert.Equal(t, "/v2/domains/example.com/records/1234567", r.URL.Path, "Path") // NOTE: Even though the body is empty, DigitalOcean API docs still show setting this Content-Type... assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type") assert.Equal(t, "Bearer asdf1234", r.Header.Get("Authorization"), "Authorization") w.WriteHeader(http.StatusNoContent) }) provider.recordIDsMu.Lock() provider.recordIDs["token"] = 1234567 provider.recordIDsMu.Unlock() err := provider.CleanUp("example.com", "token", "") require.NoError(t, err, "fail to remove TXT record") } lego-4.9.1/providers/dns/dns_providers.go000066400000000000000000000266731434020463500205030ustar00rootroot00000000000000package dns import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/providers/dns/acmedns" "github.com/go-acme/lego/v4/providers/dns/alidns" "github.com/go-acme/lego/v4/providers/dns/allinkl" "github.com/go-acme/lego/v4/providers/dns/arvancloud" "github.com/go-acme/lego/v4/providers/dns/auroradns" "github.com/go-acme/lego/v4/providers/dns/autodns" "github.com/go-acme/lego/v4/providers/dns/azure" "github.com/go-acme/lego/v4/providers/dns/bindman" "github.com/go-acme/lego/v4/providers/dns/bluecat" "github.com/go-acme/lego/v4/providers/dns/checkdomain" "github.com/go-acme/lego/v4/providers/dns/civo" "github.com/go-acme/lego/v4/providers/dns/clouddns" "github.com/go-acme/lego/v4/providers/dns/cloudflare" "github.com/go-acme/lego/v4/providers/dns/cloudns" "github.com/go-acme/lego/v4/providers/dns/cloudxns" "github.com/go-acme/lego/v4/providers/dns/conoha" "github.com/go-acme/lego/v4/providers/dns/constellix" "github.com/go-acme/lego/v4/providers/dns/desec" "github.com/go-acme/lego/v4/providers/dns/designate" "github.com/go-acme/lego/v4/providers/dns/digitalocean" "github.com/go-acme/lego/v4/providers/dns/dnsimple" "github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy" "github.com/go-acme/lego/v4/providers/dns/dnspod" "github.com/go-acme/lego/v4/providers/dns/dode" "github.com/go-acme/lego/v4/providers/dns/domeneshop" "github.com/go-acme/lego/v4/providers/dns/dreamhost" "github.com/go-acme/lego/v4/providers/dns/duckdns" "github.com/go-acme/lego/v4/providers/dns/dyn" "github.com/go-acme/lego/v4/providers/dns/dynu" "github.com/go-acme/lego/v4/providers/dns/easydns" "github.com/go-acme/lego/v4/providers/dns/edgedns" "github.com/go-acme/lego/v4/providers/dns/epik" "github.com/go-acme/lego/v4/providers/dns/exec" "github.com/go-acme/lego/v4/providers/dns/exoscale" "github.com/go-acme/lego/v4/providers/dns/freemyip" "github.com/go-acme/lego/v4/providers/dns/gandi" "github.com/go-acme/lego/v4/providers/dns/gandiv5" "github.com/go-acme/lego/v4/providers/dns/gcloud" "github.com/go-acme/lego/v4/providers/dns/gcore" "github.com/go-acme/lego/v4/providers/dns/glesys" "github.com/go-acme/lego/v4/providers/dns/godaddy" "github.com/go-acme/lego/v4/providers/dns/hetzner" "github.com/go-acme/lego/v4/providers/dns/hostingde" "github.com/go-acme/lego/v4/providers/dns/hosttech" "github.com/go-acme/lego/v4/providers/dns/httpreq" "github.com/go-acme/lego/v4/providers/dns/hurricane" "github.com/go-acme/lego/v4/providers/dns/hyperone" "github.com/go-acme/lego/v4/providers/dns/ibmcloud" "github.com/go-acme/lego/v4/providers/dns/iij" "github.com/go-acme/lego/v4/providers/dns/iijdpf" "github.com/go-acme/lego/v4/providers/dns/infoblox" "github.com/go-acme/lego/v4/providers/dns/infomaniak" "github.com/go-acme/lego/v4/providers/dns/internetbs" "github.com/go-acme/lego/v4/providers/dns/inwx" "github.com/go-acme/lego/v4/providers/dns/ionos" "github.com/go-acme/lego/v4/providers/dns/iwantmyname" "github.com/go-acme/lego/v4/providers/dns/joker" "github.com/go-acme/lego/v4/providers/dns/lightsail" "github.com/go-acme/lego/v4/providers/dns/linode" "github.com/go-acme/lego/v4/providers/dns/liquidweb" "github.com/go-acme/lego/v4/providers/dns/loopia" "github.com/go-acme/lego/v4/providers/dns/luadns" "github.com/go-acme/lego/v4/providers/dns/mydnsjp" "github.com/go-acme/lego/v4/providers/dns/mythicbeasts" "github.com/go-acme/lego/v4/providers/dns/namecheap" "github.com/go-acme/lego/v4/providers/dns/namedotcom" "github.com/go-acme/lego/v4/providers/dns/namesilo" "github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech" "github.com/go-acme/lego/v4/providers/dns/netcup" "github.com/go-acme/lego/v4/providers/dns/netlify" "github.com/go-acme/lego/v4/providers/dns/nicmanager" "github.com/go-acme/lego/v4/providers/dns/nifcloud" "github.com/go-acme/lego/v4/providers/dns/njalla" "github.com/go-acme/lego/v4/providers/dns/ns1" "github.com/go-acme/lego/v4/providers/dns/oraclecloud" "github.com/go-acme/lego/v4/providers/dns/otc" "github.com/go-acme/lego/v4/providers/dns/ovh" "github.com/go-acme/lego/v4/providers/dns/pdns" "github.com/go-acme/lego/v4/providers/dns/porkbun" "github.com/go-acme/lego/v4/providers/dns/rackspace" "github.com/go-acme/lego/v4/providers/dns/regru" "github.com/go-acme/lego/v4/providers/dns/rfc2136" "github.com/go-acme/lego/v4/providers/dns/rimuhosting" "github.com/go-acme/lego/v4/providers/dns/route53" "github.com/go-acme/lego/v4/providers/dns/safedns" "github.com/go-acme/lego/v4/providers/dns/sakuracloud" "github.com/go-acme/lego/v4/providers/dns/scaleway" "github.com/go-acme/lego/v4/providers/dns/selectel" "github.com/go-acme/lego/v4/providers/dns/servercow" "github.com/go-acme/lego/v4/providers/dns/simply" "github.com/go-acme/lego/v4/providers/dns/sonic" "github.com/go-acme/lego/v4/providers/dns/stackpath" "github.com/go-acme/lego/v4/providers/dns/tencentcloud" "github.com/go-acme/lego/v4/providers/dns/transip" "github.com/go-acme/lego/v4/providers/dns/variomedia" "github.com/go-acme/lego/v4/providers/dns/vegadns" "github.com/go-acme/lego/v4/providers/dns/vercel" "github.com/go-acme/lego/v4/providers/dns/versio" "github.com/go-acme/lego/v4/providers/dns/vinyldns" "github.com/go-acme/lego/v4/providers/dns/vkcloud" "github.com/go-acme/lego/v4/providers/dns/vscale" "github.com/go-acme/lego/v4/providers/dns/vultr" "github.com/go-acme/lego/v4/providers/dns/wedos" "github.com/go-acme/lego/v4/providers/dns/yandex" "github.com/go-acme/lego/v4/providers/dns/yandexcloud" "github.com/go-acme/lego/v4/providers/dns/zoneee" "github.com/go-acme/lego/v4/providers/dns/zonomi" ) // NewDNSChallengeProviderByName Factory for DNS providers. func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { switch name { case "acme-dns": return acmedns.NewDNSProvider() case "alidns": return alidns.NewDNSProvider() case "allinkl": return allinkl.NewDNSProvider() case "arvancloud": return arvancloud.NewDNSProvider() case "azure": return azure.NewDNSProvider() case "auroradns": return auroradns.NewDNSProvider() case "autodns": return autodns.NewDNSProvider() case "bindman": return bindman.NewDNSProvider() case "bluecat": return bluecat.NewDNSProvider() case "checkdomain": return checkdomain.NewDNSProvider() case "civo": return civo.NewDNSProvider() case "clouddns": return clouddns.NewDNSProvider() case "cloudflare": return cloudflare.NewDNSProvider() case "cloudns": return cloudns.NewDNSProvider() case "cloudxns": return cloudxns.NewDNSProvider() case "conoha": return conoha.NewDNSProvider() case "constellix": return constellix.NewDNSProvider() case "desec": return desec.NewDNSProvider() case "designate": return designate.NewDNSProvider() case "digitalocean": return digitalocean.NewDNSProvider() case "dnsimple": return dnsimple.NewDNSProvider() case "dnsmadeeasy": return dnsmadeeasy.NewDNSProvider() case "dnspod": return dnspod.NewDNSProvider() case "dode": return dode.NewDNSProvider() case "domeneshop", "domainnameshop": return domeneshop.NewDNSProvider() case "dreamhost": return dreamhost.NewDNSProvider() case "duckdns": return duckdns.NewDNSProvider() case "dyn": return dyn.NewDNSProvider() case "dynu": return dynu.NewDNSProvider() case "easydns": return easydns.NewDNSProvider() case "edgedns", "fastdns": // "fastdns" is for compatibility with v3, must be dropped in v5 return edgedns.NewDNSProvider() case "epik": return epik.NewDNSProvider() case "exec": return exec.NewDNSProvider() case "exoscale": return exoscale.NewDNSProvider() case "freemyip": return freemyip.NewDNSProvider() case "gandi": return gandi.NewDNSProvider() case "gandiv5": return gandiv5.NewDNSProvider() case "gcloud": return gcloud.NewDNSProvider() case "gcore": return gcore.NewDNSProvider() case "glesys": return glesys.NewDNSProvider() case "godaddy": return godaddy.NewDNSProvider() case "hetzner": return hetzner.NewDNSProvider() case "hostingde": return hostingde.NewDNSProvider() case "hosttech": return hosttech.NewDNSProvider() case "httpreq": return httpreq.NewDNSProvider() case "hurricane": return hurricane.NewDNSProvider() case "hyperone": return hyperone.NewDNSProvider() case "ibmcloud": return ibmcloud.NewDNSProvider() case "iij": return iij.NewDNSProvider() case "iijdpf": return iijdpf.NewDNSProvider() case "infoblox": return infoblox.NewDNSProvider() case "infomaniak": return infomaniak.NewDNSProvider() case "internetbs": return internetbs.NewDNSProvider() case "inwx": return inwx.NewDNSProvider() case "ionos": return ionos.NewDNSProvider() case "iwantmyname": return iwantmyname.NewDNSProvider() case "joker": return joker.NewDNSProvider() case "lightsail": return lightsail.NewDNSProvider() case "linode", "linodev4": // "linodev4" is for compatibility with v3, must be dropped in v5 return linode.NewDNSProvider() case "liquidweb": return liquidweb.NewDNSProvider() case "luadns": return luadns.NewDNSProvider() case "loopia": return loopia.NewDNSProvider() case "manual": return dns01.NewDNSProviderManual() case "mydnsjp": return mydnsjp.NewDNSProvider() case "mythicbeasts": return mythicbeasts.NewDNSProvider() case "namecheap": return namecheap.NewDNSProvider() case "namedotcom": return namedotcom.NewDNSProvider() case "namesilo": return namesilo.NewDNSProvider() case "nearlyfreespeech": return nearlyfreespeech.NewDNSProvider() case "netcup": return netcup.NewDNSProvider() case "netlify": return netlify.NewDNSProvider() case "nicmanager": return nicmanager.NewDNSProvider() case "nifcloud": return nifcloud.NewDNSProvider() case "njalla": return njalla.NewDNSProvider() case "ns1": return ns1.NewDNSProvider() case "oraclecloud": return oraclecloud.NewDNSProvider() case "otc": return otc.NewDNSProvider() case "ovh": return ovh.NewDNSProvider() case "pdns": return pdns.NewDNSProvider() case "porkbun": return porkbun.NewDNSProvider() case "rackspace": return rackspace.NewDNSProvider() case "regru": return regru.NewDNSProvider() case "rfc2136": return rfc2136.NewDNSProvider() case "rimuhosting": return rimuhosting.NewDNSProvider() case "route53": return route53.NewDNSProvider() case "safedns": return safedns.NewDNSProvider() case "sakuracloud": return sakuracloud.NewDNSProvider() case "scaleway": return scaleway.NewDNSProvider() case "selectel": return selectel.NewDNSProvider() case "servercow": return servercow.NewDNSProvider() case "simply": return simply.NewDNSProvider() case "sonic": return sonic.NewDNSProvider() case "stackpath": return stackpath.NewDNSProvider() case "tencentcloud": return tencentcloud.NewDNSProvider() case "transip": return transip.NewDNSProvider() case "variomedia": return variomedia.NewDNSProvider() case "vegadns": return vegadns.NewDNSProvider() case "vercel": return vercel.NewDNSProvider() case "versio": return versio.NewDNSProvider() case "vinyldns": return vinyldns.NewDNSProvider() case "vkcloud": return vkcloud.NewDNSProvider() case "vultr": return vultr.NewDNSProvider() case "vscale": return vscale.NewDNSProvider() case "wedos": return wedos.NewDNSProvider() case "yandex": return yandex.NewDNSProvider() case "yandexcloud": return yandexcloud.NewDNSProvider() case "zoneee": return zoneee.NewDNSProvider() case "zonomi": return zonomi.NewDNSProvider() default: return nil, fmt.Errorf("unrecognized DNS provider: %s", name) } } lego-4.9.1/providers/dns/dns_providers_test.go000066400000000000000000000017071434020463500215310ustar00rootroot00000000000000package dns import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/go-acme/lego/v4/providers/dns/exec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var envTest = tester.NewEnvTest("EXEC_PATH") func TestKnownDNSProviderSuccess(t *testing.T) { defer envTest.RestoreEnv() envTest.Apply(map[string]string{ "EXEC_PATH": "abc", }) provider, err := NewDNSChallengeProviderByName("exec") require.NoError(t, err) assert.NotNil(t, provider) assert.IsType(t, &exec.DNSProvider{}, provider, "The loaded DNS provider doesn't have the expected type.") } func TestKnownDNSProviderError(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() provider, err := NewDNSChallengeProviderByName("exec") assert.Error(t, err) assert.Nil(t, provider) } func TestUnknownDNSProvider(t *testing.T) { provider, err := NewDNSChallengeProviderByName("foobar") assert.Error(t, err) assert.Nil(t, provider) } lego-4.9.1/providers/dns/dnsimple/000077500000000000000000000000001434020463500170705ustar00rootroot00000000000000lego-4.9.1/providers/dns/dnsimple/dnsimple.go000066400000000000000000000143151434020463500212360ustar00rootroot00000000000000// Package dnsimple implements a DNS provider for solving the DNS-01 challenge using dnsimple DNS. package dnsimple import ( "context" "errors" "fmt" "strconv" "strings" "time" "github.com/dnsimple/dnsimple-go/dnsimple" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "golang.org/x/oauth2" ) // Environment variables names. const ( envNamespace = "DNSIMPLE_" EnvOAuthToken = envNamespace + "OAUTH_TOKEN" EnvBaseURL = envNamespace + "BASE_URL" EnvDebug = envNamespace + "DEBUG" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Debug bool AccessToken string BaseURL string PropagationTimeout time.Duration PollingInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), Debug: env.GetOrDefaultBool(EnvDebug, false), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *dnsimple.Client } // NewDNSProvider returns a DNSProvider instance configured for dnsimple. // Credentials must be passed in the environment variable: DNSIMPLE_OAUTH_TOKEN. // // See: https://developer.dnsimple.com/v2/#authentication func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.AccessToken = env.GetOrFile(EnvOAuthToken) config.BaseURL = env.GetOrFile(EnvBaseURL) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for DNSimple. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("dnsimple: the configuration of the DNS provider is nil") } if config.AccessToken == "" { return nil, errors.New("dnsimple: OAuth token is missing") } ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: config.AccessToken}) client := dnsimple.NewClient(oauth2.NewClient(context.Background(), ts)) client.SetUserAgent("go-acme/lego") if config.BaseURL != "" { client.BaseURL = config.BaseURL } client.Debug = config.Debug return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zoneName, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("dnsimple: %w", err) } accountID, err := d.getAccountID() if err != nil { return fmt.Errorf("dnsimple: %w", err) } recordAttributes := newTxtRecord(zoneName, fqdn, value, d.config.TTL) _, err = d.client.Zones.CreateRecord(context.Background(), accountID, zoneName, recordAttributes) if err != nil { return fmt.Errorf("dnsimple: API call failed: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) records, err := d.findTxtRecords(fqdn) if err != nil { return fmt.Errorf("dnsimple: %w", err) } accountID, err := d.getAccountID() if err != nil { return fmt.Errorf("dnsimple: %w", err) } var lastErr error for _, rec := range records { _, err := d.client.Zones.DeleteRecord(context.Background(), accountID, rec.ZoneID, rec.ID) if err != nil { lastErr = fmt.Errorf("dnsimple: %w", err) } } return lastErr } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func (d *DNSProvider) getHostedZone(domain string) (string, error) { authZone, err := dns01.FindZoneByFqdn(domain) if err != nil { return "", err } accountID, err := d.getAccountID() if err != nil { return "", err } zoneName := dns01.UnFqdn(authZone) zones, err := d.client.Zones.ListZones(context.Background(), accountID, &dnsimple.ZoneListOptions{NameLike: &zoneName}) if err != nil { return "", fmt.Errorf("API call failed: %w", err) } var hostedZone dnsimple.Zone for _, zone := range zones.Data { if zone.Name == zoneName { hostedZone = zone } } if hostedZone.ID == 0 { return "", fmt.Errorf("zone %s not found in DNSimple for domain %s", authZone, domain) } return hostedZone.Name, nil } func (d *DNSProvider) findTxtRecords(fqdn string) ([]dnsimple.ZoneRecord, error) { zoneName, err := d.getHostedZone(fqdn) if err != nil { return nil, err } accountID, err := d.getAccountID() if err != nil { return nil, err } recordName := extractRecordName(fqdn, zoneName) result, err := d.client.Zones.ListRecords(context.Background(), accountID, zoneName, &dnsimple.ZoneRecordListOptions{Name: &recordName, Type: dnsimple.String("TXT"), ListOptions: dnsimple.ListOptions{}}) if err != nil { return nil, fmt.Errorf("API call has failed: %w", err) } return result.Data, nil } func newTxtRecord(zoneName, fqdn, value string, ttl int) dnsimple.ZoneRecordAttributes { name := extractRecordName(fqdn, zoneName) return dnsimple.ZoneRecordAttributes{ Type: "TXT", Name: &name, Content: value, TTL: ttl, } } func extractRecordName(fqdn, zone string) string { name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+zone); idx != -1 { return name[:idx] } return name } func (d *DNSProvider) getAccountID() (string, error) { whoamiResponse, err := d.client.Identity.Whoami(context.Background()) if err != nil { return "", err } if whoamiResponse.Data.Account == nil { return "", errors.New("user tokens are not supported, please use an account token") } return strconv.FormatInt(whoamiResponse.Data.Account.ID, 10), nil } lego-4.9.1/providers/dns/dnsimple/dnsimple.toml000066400000000000000000000027611434020463500216060ustar00rootroot00000000000000Name = "DNSimple" Description = '''''' URL = "https://dnsimple.com/" Code = "dnsimple" Since = "v0.3.0" Example = ''' DNSIMPLE_OAUTH_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz \ lego --email you@example.com --dns dnsimple --domains my.example.org run ''' Additional = ''' ## Description `DNSIMPLE_BASE_URL` is optional and must be set to production (https://api.dnsimple.com). if `DNSIMPLE_BASE_URL` is not defined or empty, the production URL is used by default. While you can manage DNS records in the [DNSimple Sandbox environment](https://developer.dnsimple.com/sandbox/), DNS records will not resolve and you will not be able to satisfy the ACME DNS challenge. To authenticate you need to provide a valid API token. HTTP Basic Authentication is intentionally not supported. ### API tokens You can [generate a new API token](https://support.dnsimple.com/articles/api-access-token/) from your account page. Only Account API tokens are supported, if you try to use an User API token you will receive an error message. ''' [Configuration] [Configuration.Credentials] DNSIMPLE_OAUTH_TOKEN = "OAuth token" [Configuration.Additional] DNSIMPLE_BASE_URL = "API endpoint URL" DNSIMPLE_POLLING_INTERVAL = "Time between DNS propagation check" DNSIMPLE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" DNSIMPLE_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://developer.dnsimple.com/v2/" GoClient = "https://github.com/dnsimple/dnsimple-go" lego-4.9.1/providers/dns/dnsimple/dnsimple_test.go000066400000000000000000000061631434020463500222770ustar00rootroot00000000000000package dnsimple import ( "os" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const sandboxURL = "https://api.sandbox.fake.com" const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvOAuthToken, EnvBaseURL). WithDomain(envDomain). WithLiveTestRequirements(EnvOAuthToken, envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvOAuthToken: "my_token", }, }, { desc: "success: base url", envVars: map[string]string{ EnvOAuthToken: "my_token", EnvBaseURL: "https://api.dnsimple.test", }, }, { desc: "missing oauth token", envVars: map[string]string{ EnvOAuthToken: "", }, expected: "dnsimple: OAuth token is missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) baseURL := os.Getenv(EnvBaseURL) if baseURL != "" { assert.Equal(t, baseURL, p.client.BaseURL) } } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string accessToken string baseURL string expected string }{ { desc: "success", accessToken: "my_token", baseURL: "", }, { desc: "success: base url", accessToken: "my_token", baseURL: "https://api.dnsimple.test", }, { desc: "missing oauth token", expected: "dnsimple: OAuth token is missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.AccessToken = test.accessToken config.BaseURL = test.baseURL p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) if test.baseURL != "" { assert.Equal(t, test.baseURL, p.client.BaseURL) } } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() if os.Getenv(EnvBaseURL) == "" { os.Setenv(EnvBaseURL, sandboxURL) } provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() if os.Getenv(EnvBaseURL) == "" { os.Setenv(EnvBaseURL, sandboxURL) } provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/dnsmadeeasy/000077500000000000000000000000001434020463500175525ustar00rootroot00000000000000lego-4.9.1/providers/dns/dnsmadeeasy/dnsmadeeasy.go000066400000000000000000000123151434020463500224000ustar00rootroot00000000000000// Package dnsmadeeasy implements a DNS provider for solving the DNS-01 challenge using DNS Made Easy. package dnsmadeeasy import ( "crypto/tls" "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy/internal" ) // Environment variables names. const ( envNamespace = "DNSMADEEASY_" EnvAPIKey = envNamespace + "API_KEY" EnvAPISecret = envNamespace + "API_SECRET" EnvSandbox = envNamespace + "SANDBOX" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string APIKey string APISecret string Sandbox bool HTTPClient *http.Client PropagationTimeout time.Duration PollingInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for DNSMadeEasy DNS. // Credentials must be passed in the environment variables: // DNSMADEEASY_API_KEY and DNSMADEEASY_API_SECRET. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey, EnvAPISecret) if err != nil { return nil, fmt.Errorf("dnsmadeeasy: %w", err) } config := NewDefaultConfig() config.Sandbox = env.GetOrDefaultBool(EnvSandbox, false) config.APIKey = values[EnvAPIKey] config.APISecret = values[EnvAPISecret] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for DNS Made Easy. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("dnsmadeeasy: the configuration of the DNS provider is nil") } var baseURL string if config.Sandbox { baseURL = "https://api.sandbox.dnsmadeeasy.com/V2.0" } else { if len(config.BaseURL) > 0 { baseURL = config.BaseURL } else { baseURL = "https://api.dnsmadeeasy.com/V2.0" } } client, err := internal.NewClient(config.APIKey, config.APISecret) if err != nil { return nil, fmt.Errorf("dnsmadeeasy: %w", err) } client.HTTPClient = config.HTTPClient client.BaseURL = baseURL return &DNSProvider{ client: client, config: config, }, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domainName, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domainName, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("dnsmadeeasy: unable to find zone for %s: %w", fqdn, err) } // fetch the domain details domain, err := d.client.GetDomain(authZone) if err != nil { return fmt.Errorf("dnsmadeeasy: unable to get domain for zone %s: %w", authZone, err) } // create the TXT record name := strings.Replace(fqdn, "."+authZone, "", 1) record := &internal.Record{Type: "TXT", Name: name, Value: value, TTL: d.config.TTL} err = d.client.CreateRecord(domain, record) if err != nil { return fmt.Errorf("dnsmadeeasy: unable to create record for %s: %w", name, err) } return nil } // CleanUp removes the TXT records matching the specified parameters. func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domainName, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("dnsmadeeasy: unable to find zone for %s: %w", fqdn, err) } // fetch the domain details domain, err := d.client.GetDomain(authZone) if err != nil { return fmt.Errorf("dnsmadeeasy: unable to get domain for zone %s: %w", authZone, err) } // find matching records name := strings.Replace(fqdn, "."+authZone, "", 1) records, err := d.client.GetRecords(domain, name, "TXT") if err != nil { return fmt.Errorf("dnsmadeeasy: unable to get records for domain %s: %w", domain.Name, err) } // delete records var lastError error for _, record := range *records { err = d.client.DeleteRecord(record) if err != nil { lastError = fmt.Errorf("dnsmadeeasy: unable to delete record [id=%d, name=%s]: %w", record.ID, record.Name, err) } } return lastError } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/dnsmadeeasy/dnsmadeeasy.toml000066400000000000000000000014771434020463500227550ustar00rootroot00000000000000Name = "DNS Made Easy" Description = '''''' URL = "https://dnsmadeeasy.com/" Code = "dnsmadeeasy" Since = "v0.4.0" Example = ''' DNSMADEEASY_API_KEY=xxxxxx \ DNSMADEEASY_API_SECRET=yyyyy \ lego --email you@example.com --dns dnsmadeeasy --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DNSMADEEASY_API_KEY = "The API key" DNSMADEEASY_API_SECRET = "The API Secret key" [Configuration.Additional] DNSMADEEASY_SANDBOX = "Activate the sandbox (boolean)" DNSMADEEASY_POLLING_INTERVAL = "Time between DNS propagation check" DNSMADEEASY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" DNSMADEEASY_TTL = "The TTL of the TXT record used for the DNS challenge" DNSMADEEASY_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api-docs.dnsmadeeasy.com/" lego-4.9.1/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go000066400000000000000000000060021434020463500234330ustar00rootroot00000000000000package dnsmadeeasy import ( "os" "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIKey, EnvAPISecret). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { os.Setenv(EnvSandbox, "true") testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", EnvAPISecret: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIKey: "", EnvAPISecret: "", }, expected: "dnsmadeeasy: some credentials information are missing: DNSMADEEASY_API_KEY,DNSMADEEASY_API_SECRET", }, { desc: "missing access key", envVars: map[string]string{ EnvAPIKey: "", EnvAPISecret: "456", }, expected: "dnsmadeeasy: some credentials information are missing: DNSMADEEASY_API_KEY", }, { desc: "missing secret key", envVars: map[string]string{ EnvAPIKey: "123", EnvAPISecret: "", }, expected: "dnsmadeeasy: some credentials information are missing: DNSMADEEASY_API_SECRET", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { os.Setenv(EnvSandbox, "true") testCases := []struct { desc string apiKey string apiSecret string expected string }{ { desc: "success", apiKey: "123", apiSecret: "456", }, { desc: "missing credentials", expected: "dnsmadeeasy: credentials missing: API key", }, { desc: "missing api key", apiSecret: "456", expected: "dnsmadeeasy: credentials missing: API key", }, { desc: "missing secret key", apiKey: "123", expected: "dnsmadeeasy: credentials missing: API secret", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.APISecret = test.apiSecret p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresentAndCleanup(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } os.Setenv(EnvSandbox, "true") envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/dnsmadeeasy/internal/000077500000000000000000000000001434020463500213665ustar00rootroot00000000000000lego-4.9.1/providers/dns/dnsmadeeasy/internal/client.go000066400000000000000000000100101434020463500231630ustar00rootroot00000000000000package internal import ( "bytes" "crypto/hmac" "crypto/sha1" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "time" ) // Domain holds the DNSMadeEasy API representation of a Domain. type Domain struct { ID int `json:"id"` Name string `json:"name"` } // Record holds the DNSMadeEasy API representation of a Domain Record. type Record struct { ID int `json:"id"` Type string `json:"type"` Name string `json:"name"` Value string `json:"value"` TTL int `json:"ttl"` SourceID int `json:"sourceId"` } type recordsResponse struct { Records *[]Record `json:"data"` } // Client DNSMadeEasy client. type Client struct { apiKey string apiSecret string BaseURL string HTTPClient *http.Client } // NewClient creates a DNSMadeEasy client. func NewClient(apiKey, apiSecret string) (*Client, error) { if apiKey == "" { return nil, errors.New("credentials missing: API key") } if apiSecret == "" { return nil, errors.New("credentials missing: API secret") } return &Client{ apiKey: apiKey, apiSecret: apiSecret, HTTPClient: &http.Client{}, }, nil } // GetDomain gets a domain. func (c *Client) GetDomain(authZone string) (*Domain, error) { domainName := authZone[0 : len(authZone)-1] resource := fmt.Sprintf("%s%s", "/dns/managed/name?domainname=", domainName) resp, err := c.sendRequest(http.MethodGet, resource, nil) if err != nil { return nil, err } defer resp.Body.Close() domain := &Domain{} err = json.NewDecoder(resp.Body).Decode(&domain) if err != nil { return nil, err } return domain, nil } // GetRecords gets all TXT records. func (c *Client) GetRecords(domain *Domain, recordName, recordType string) (*[]Record, error) { resource := fmt.Sprintf("%s/%d/%s%s%s%s", "/dns/managed", domain.ID, "records?recordName=", recordName, "&type=", recordType) resp, err := c.sendRequest(http.MethodGet, resource, nil) if err != nil { return nil, err } defer resp.Body.Close() records := &recordsResponse{} err = json.NewDecoder(resp.Body).Decode(&records) if err != nil { return nil, err } return records.Records, nil } // CreateRecord creates a TXT records. func (c *Client) CreateRecord(domain *Domain, record *Record) error { url := fmt.Sprintf("%s/%d/%s", "/dns/managed", domain.ID, "records") resp, err := c.sendRequest(http.MethodPost, url, record) if err != nil { return err } defer resp.Body.Close() return nil } // DeleteRecord deletes a TXT records. func (c *Client) DeleteRecord(record Record) error { resource := fmt.Sprintf("%s/%d/%s/%d", "/dns/managed", record.SourceID, "records", record.ID) resp, err := c.sendRequest(http.MethodDelete, resource, nil) if err != nil { return err } defer resp.Body.Close() return nil } func (c *Client) sendRequest(method, resource string, payload interface{}) (*http.Response, error) { url := fmt.Sprintf("%s%s", c.BaseURL, resource) body, err := json.Marshal(payload) if err != nil { return nil, err } timestamp := time.Now().UTC().Format(time.RFC1123) signature, err := computeHMAC(timestamp, c.apiSecret) if err != nil { return nil, err } req, err := http.NewRequest(method, url, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("x-dnsme-apiKey", c.apiKey) req.Header.Set("x-dnsme-requestDate", timestamp) req.Header.Set("x-dnsme-hmac", signature) req.Header.Set("accept", "application/json") req.Header.Set("content-type", "application/json") resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } if resp.StatusCode > 299 { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("request failed with HTTP status code %d", resp.StatusCode) } return nil, fmt.Errorf("request failed with HTTP status code %d: %s", resp.StatusCode, string(body)) } return resp, nil } func computeHMAC(message, secret string) (string, error) { key := []byte(secret) h := hmac.New(sha1.New, key) _, err := h.Write([]byte(message)) if err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } lego-4.9.1/providers/dns/dnspod/000077500000000000000000000000001434020463500165445ustar00rootroot00000000000000lego-4.9.1/providers/dns/dnspod/dnspod.go000066400000000000000000000121241434020463500203620ustar00rootroot00000000000000// Package dnspod implements a DNS provider for solving the DNS-01 challenge using dnspod DNS. package dnspod import ( "errors" "fmt" "net/http" "strconv" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/nrdcg/dnspod-go" ) // Environment variables names. const ( envNamespace = "DNSPOD_" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { LoginToken string TTL int PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *dnspod.Client } // NewDNSProvider returns a DNSProvider instance configured for dnspod. // Credentials must be passed in the environment variables: DNSPOD_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("dnspod: %w", err) } config := NewDefaultConfig() config.LoginToken = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for dnspod. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("dnspod: the configuration of the DNS provider is nil") } if config.LoginToken == "" { return nil, errors.New("dnspod: credentials missing") } params := dnspod.CommonParams{LoginToken: config.LoginToken, Format: "json"} client := dnspod.NewClient(params) client.HTTPClient = config.HTTPClient return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zoneID, zoneName, err := d.getHostedZone(fqdn) if err != nil { return err } recordAttributes := d.newTxtRecord(zoneName, fqdn, value, d.config.TTL) _, _, err = d.client.Records.Create(zoneID, *recordAttributes) if err != nil { return fmt.Errorf("API call failed: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) zoneID, zoneName, err := d.getHostedZone(fqdn) if err != nil { return err } records, err := d.findTxtRecords(fqdn, zoneID, zoneName) if err != nil { return err } for _, rec := range records { _, err := d.client.Records.Delete(zoneID, rec.ID) if err != nil { return err } } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func (d *DNSProvider) getHostedZone(domain string) (string, string, error) { zones, _, err := d.client.Domains.List() if err != nil { return "", "", fmt.Errorf("API call failed: %w", err) } authZone, err := dns01.FindZoneByFqdn(domain) if err != nil { return "", "", err } var hostedZone dnspod.Domain for _, zone := range zones { if zone.Name == dns01.UnFqdn(authZone) { hostedZone = zone } } if hostedZone.ID == "" || hostedZone.ID == "0" { return "", "", fmt.Errorf("zone %s not found in dnspod for domain %s", authZone, domain) } return hostedZone.ID.String(), hostedZone.Name, nil } func (d *DNSProvider) newTxtRecord(zone, fqdn, value string, ttl int) *dnspod.Record { name := extractRecordName(fqdn, zone) return &dnspod.Record{ Type: "TXT", Name: name, Value: value, Line: "默认", TTL: strconv.Itoa(ttl), } } func (d *DNSProvider) findTxtRecords(fqdn, zoneID, zoneName string) ([]dnspod.Record, error) { recordName := extractRecordName(fqdn, zoneName) var records []dnspod.Record result, _, err := d.client.Records.List(zoneID, recordName) if err != nil { return records, fmt.Errorf("API call has failed: %w", err) } for _, record := range result { if record.Name == recordName { records = append(records, record) } } return records, nil } func extractRecordName(fqdn, zone string) string { name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+zone); idx != -1 { return name[:idx] } return name } lego-4.9.1/providers/dns/dnspod/dnspod.toml000066400000000000000000000013511434020463500207300ustar00rootroot00000000000000Name = "DNSPod (deprecated)" Description = ''' Use the Tencent Cloud provider instead. ''' URL = "https://www.dnspod.com/" Code = "dnspod" Since = "v0.4.0" Example = ''' DNSPOD_API_KEY=xxxxxx \ lego --email you@example.com --dns dnspod --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DNSPOD_API_KEY = "The user token" [Configuration.Additional] DNSPOD_POLLING_INTERVAL = "Time between DNS propagation check" DNSPOD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" DNSPOD_TTL = "The TTL of the TXT record used for the DNS challenge" DNSPOD_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://docs.dnspod.com/api/" GoClient = "https://github.com/nrdcg/dnspod-go" lego-4.9.1/providers/dns/dnspod/dnspod_test.go000066400000000000000000000044131434020463500214230ustar00rootroot00000000000000package dnspod import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAPIKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", }, }, { desc: "missing api key", envVars: map[string]string{ EnvAPIKey: "", }, expected: "dnspod: some credentials information are missing: DNSPOD_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string loginToken string expected string }{ { desc: "success", loginToken: "123", }, { desc: "missing credentials", expected: "dnspod: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.LoginToken = test.loginToken p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/dode/000077500000000000000000000000001434020463500161705ustar00rootroot00000000000000lego-4.9.1/providers/dns/dode/client.go000066400000000000000000000023731434020463500200020ustar00rootroot00000000000000package dode import ( "encoding/json" "fmt" "io" "net/url" "github.com/go-acme/lego/v4/challenge/dns01" ) type apiResponse struct { Domain string Success bool } // updateTxtRecord Update the domains TXT record // To update the TXT record we just need to make one simple get request. func (d *DNSProvider) updateTxtRecord(fqdn, token, txt string, clear bool) error { u, _ := url.Parse("https://www.do.de/api/letsencrypt") query := u.Query() query.Set("token", token) query.Set("domain", dns01.UnFqdn(fqdn)) // api call differs per set/delete if clear { query.Set("action", "delete") } else { query.Set("value", txt) } u.RawQuery = query.Encode() response, err := d.config.HTTPClient.Get(u.String()) if err != nil { return err } defer response.Body.Close() bodyBytes, err := io.ReadAll(response.Body) if err != nil { return err } var r apiResponse err = json.Unmarshal(bodyBytes, &r) if err != nil { return fmt.Errorf("request to change TXT record for do.de returned the following invalid json (%s); used url [%s]", string(bodyBytes), u) } body := string(bodyBytes) if !r.Success { return fmt.Errorf("request to change TXT record for do.de returned the following error result (%s); used url [%s]", body, u) } return nil } lego-4.9.1/providers/dns/dode/dode.go000066400000000000000000000061521434020463500174360ustar00rootroot00000000000000// Package dode implements a DNS provider for solving the DNS-01 challenge using do.de. package dode import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "DODE_" EnvToken = envNamespace + "TOKEN" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Token string PropagationTimeout time.Duration PollingInterval time.Duration SequenceInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config } // NewDNSProvider returns a new DNS provider using // environment variable DODE_TOKEN for adding and removing the DNS record. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvToken) if err != nil { return nil, fmt.Errorf("do.de: %w", err) } config := NewDefaultConfig() config.Token = values[EnvToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for do.de. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("do.de: the configuration of the DNS provider is nil") } if config.Token == "" { return nil, errors.New("do.de: credentials missing") } return &DNSProvider{config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, txtRecord := dns01.GetRecord(domain, keyAuth) return d.updateTxtRecord(fqdn, d.config.Token, txtRecord, false) } // CleanUp clears TXT record. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) return d.updateTxtRecord(fqdn, d.config.Token, "", true) } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Sequential All DNS challenges for this provider will be resolved sequentially. // Returns the interval between each iteration. func (d *DNSProvider) Sequential() time.Duration { return d.config.SequenceInterval } lego-4.9.1/providers/dns/dode/dode.toml000066400000000000000000000013101434020463500177730ustar00rootroot00000000000000Name = "Domain Offensive (do.de)" Description = '''''' URL = "https://www.do.de/" Code = "dode" Since = "v2.4.0" Example = ''' DODE_TOKEN=xxxxxx \ lego --email you@example.com --dns dode --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DODE_TOKEN = "API token" [Configuration.Additional] DODE_POLLING_INTERVAL = "Time between DNS propagation check" DODE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" DODE_TTL = "The TTL of the TXT record used for the DNS challenge" DODE_HTTP_TIMEOUT = "API request timeout" DODE_SEQUENCE_INTERVAL = "Time between sequential requests" [Links] API = "https://www.do.de/wiki/LetsEncrypt_-_Entwickler" lego-4.9.1/providers/dns/dode/dode_test.go000066400000000000000000000042461434020463500204770ustar00rootroot00000000000000package dode import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvToken). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvToken: "123", }, }, { desc: "missing api key", envVars: map[string]string{ EnvToken: "", }, expected: "do.de: some credentials information are missing: DODE_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string token string expected string }{ { desc: "success", token: "123", }, { desc: "missing credentials", expected: "do.de: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Token = test.token p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/domeneshop/000077500000000000000000000000001434020463500174165ustar00rootroot00000000000000lego-4.9.1/providers/dns/domeneshop/domeneshop.go000066400000000000000000000103271434020463500221110ustar00rootroot00000000000000// Package domeneshop implements a DNS provider for solving the DNS-01 challenge using domeneshop DNS. package domeneshop import ( "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/domeneshop/internal" ) // Environment variables names. const ( envNamespace = "DOMENESHOP_" EnvAPIToken = envNamespace + "API_TOKEN" EnvAPISecret = envNamespace + "API_SECRET" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIToken string APISecret string PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 20*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for domeneshop. // Credentials must be passed in the environment variables: // DOMENESHOP_API_TOKEN, DOMENESHOP_API_SECRET. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIToken, EnvAPISecret) if err != nil { return nil, fmt.Errorf("domeneshop: %w", err) } config := NewDefaultConfig() config.APIToken = values[EnvAPIToken] config.APISecret = values[EnvAPISecret] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Domeneshop. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("domeneshop: the configuration of the DNS provider is nil") } if config.APIToken == "" || config.APISecret == "" { return nil, errors.New("domeneshop: credentials missing") } client := internal.NewClient(config.APIToken, config.APISecret) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, _, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, host, err := d.splitDomain(fqdn) if err != nil { return fmt.Errorf("domeneshop: %w", err) } domainInstance, err := d.client.GetDomainByName(zone) if err != nil { return fmt.Errorf("domeneshop: %w", err) } err = d.client.CreateTXTRecord(domainInstance, host, value) if err != nil { return fmt.Errorf("domeneshop: failed to create record: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, host, err := d.splitDomain(fqdn) if err != nil { return fmt.Errorf("domeneshop: %w", err) } domainInstance, err := d.client.GetDomainByName(zone) if err != nil { return fmt.Errorf("domeneshop: %w", err) } if err := d.client.DeleteTXTRecord(domainInstance, host, value); err != nil { return fmt.Errorf("domeneshop: failed to create record: %w", err) } return nil } // splitDomain splits the hostname from the authoritative zone, and returns both parts (non-fqdn). func (d *DNSProvider) splitDomain(fqdn string) (string, string, error) { zone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", "", err } host := dns01.UnFqdn(strings.TrimSuffix(fqdn, zone)) zone = dns01.UnFqdn(zone) return zone, host, nil } lego-4.9.1/providers/dns/domeneshop/domeneshop.toml000066400000000000000000000015421434020463500224560ustar00rootroot00000000000000Name = "Domeneshop" Description = '''''' URL = "https://domene.shop" Code = "domeneshop" Since = "v4.3.0" Example = ''' DOMENESHOP_API_TOKEN= \ DOMENESHOP_API_SECRET= \ lego --email example@example.com --dns domeneshop --domains example.com run ''' Additional = ''' ### API credentials Visit the following page for information on how to create API credentials with Domeneshop: https://api.domeneshop.no/docs/#section/Authentication ''' [Configuration] [Configuration.Credentials] DOMENESHOP_API_TOKEN = "API token" DOMENESHOP_API_SECRET = "API secret" [Configuration.Additional] DOMENESHOP_POLLING_INTERVAL = "Time between DNS propagation check" DOMENESHOP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" DOMENESHOP_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.domeneshop.no/docs" lego-4.9.1/providers/dns/domeneshop/domeneshop_test.go000066400000000000000000000060531434020463500231510ustar00rootroot00000000000000package domeneshop import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIToken, EnvAPISecret). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIToken: "A", EnvAPISecret: "B", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIToken: "", EnvAPISecret: "", }, expected: "domeneshop: some credentials information are missing: DOMENESHOP_API_TOKEN,DOMENESHOP_API_SECRET", }, { desc: "missing api token", envVars: map[string]string{ EnvAPIToken: "", EnvAPISecret: "A", }, expected: "domeneshop: some credentials information are missing: DOMENESHOP_API_TOKEN", }, { desc: "missing api secret", envVars: map[string]string{ EnvAPIToken: "A", EnvAPISecret: "", }, expected: "domeneshop: some credentials information are missing: DOMENESHOP_API_SECRET", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiSecret string apiToken string expected string }{ { desc: "success", apiToken: "A", apiSecret: "B", }, { desc: "missing credentials", expected: "domeneshop: credentials missing", }, { desc: "missing api token", apiToken: "", apiSecret: "B", expected: "domeneshop: credentials missing", }, { desc: "missing api secret", apiToken: "A", apiSecret: "", expected: "domeneshop: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIToken = test.apiToken config.APISecret = test.apiSecret p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/domeneshop/internal/000077500000000000000000000000001434020463500212325ustar00rootroot00000000000000lego-4.9.1/providers/dns/domeneshop/internal/client.go000066400000000000000000000072051434020463500230430ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "fmt" "io" "net/http" "time" ) const defaultBaseURL string = "https://api.domeneshop.no/v0" // Client implements a very simple wrapper around the Domeneshop API. // For now it will only deal with adding and removing TXT records, as required by ACME providers. // https://api.domeneshop.no/docs/ type Client struct { HTTPClient *http.Client baseURL string apiToken string apiSecret string } // NewClient returns an instance of the Domeneshop API wrapper. func NewClient(apiToken, apiSecret string) *Client { return &Client{ HTTPClient: &http.Client{Timeout: 5 * time.Second}, baseURL: defaultBaseURL, apiToken: apiToken, apiSecret: apiSecret, } } // GetDomainByName fetches the domain list and returns the Domain object for the matching domain. // https://api.domeneshop.no/docs/#operation/getDomains func (c *Client) GetDomainByName(domain string) (*Domain, error) { var domains []Domain err := c.doRequest(http.MethodGet, "domains", nil, &domains) if err != nil { return nil, err } for _, d := range domains { if !d.Services.DNS { // Domains without DNS service cannot have DNS record added. continue } if d.Name == domain { return &d, nil } } return nil, fmt.Errorf("failed to find matching domain name: %s", domain) } // CreateTXTRecord creates a TXT record with the provided host (subdomain) and data. // https://api.domeneshop.no/docs/#tag/dns/paths/~1domains~1{domainId}~1dns/post func (c *Client) CreateTXTRecord(domain *Domain, host string, data string) error { jsonRecord, err := json.Marshal(DNSRecord{ Data: data, Host: host, TTL: 300, Type: "TXT", }) if err != nil { return err } return c.doRequest(http.MethodPost, fmt.Sprintf("domains/%d/dns", domain.ID), jsonRecord, nil) } // DeleteTXTRecord deletes the DNS record matching the provided host and data. // https://api.domeneshop.no/docs/#tag/dns/paths/~1domains~1{domainId}~1dns~1{recordId}/delete func (c *Client) DeleteTXTRecord(domain *Domain, host string, data string) error { record, err := c.getDNSRecordByHostData(*domain, host, data) if err != nil { return err } return c.doRequest(http.MethodDelete, fmt.Sprintf("domains/%d/dns/%d", domain.ID, record.ID), nil, nil) } // getDNSRecordByHostData finds the first matching DNS record with the provided host and data. // https://api.domeneshop.no/docs/#operation/getDnsRecords func (c *Client) getDNSRecordByHostData(domain Domain, host string, data string) (*DNSRecord, error) { var records []DNSRecord err := c.doRequest(http.MethodGet, fmt.Sprintf("domains/%d/dns", domain.ID), nil, &records) if err != nil { return nil, err } for _, r := range records { if r.Host == host && r.Data == data { return &r, nil } } return nil, fmt.Errorf("failed to find record with host %s for domain %s", host, domain.Name) } // doRequest makes a request against the API with an optional body, // and makes sure that the required Authorization header is set using `setBasicAuth`. func (c *Client) doRequest(method string, endpoint string, reqBody []byte, v interface{}) error { req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", c.baseURL, endpoint), bytes.NewBuffer(reqBody)) if err != nil { return err } req.SetBasicAuth(c.apiToken, c.apiSecret) resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode >= http.StatusBadRequest { respBody, err := io.ReadAll(resp.Body) if err != nil { return err } return fmt.Errorf("API returned %s: %s", resp.Status, respBody) } if v != nil { return json.NewDecoder(resp.Body).Decode(&v) } return nil } lego-4.9.1/providers/dns/domeneshop/internal/client_test.go000066400000000000000000000104111434020463500240730ustar00rootroot00000000000000package internal import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setup(t *testing.T) (*Client, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient("token", "secret") client.baseURL = server.URL return client, mux } func TestClient_CreateTXTRecord(t *testing.T) { client, mux := setup(t) mux.HandleFunc("/domains/1/dns", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } auth := req.Header.Get("Authorization") if auth != "Basic dG9rZW46c2VjcmV0" { http.Error(rw, "invalid method: "+req.Method, http.StatusUnauthorized) return } _, _ = rw.Write([]byte(`{"id": 1}`)) }) err := client.CreateTXTRecord(&Domain{ID: 1}, "example", "txtTXTtxt") require.NoError(t, err) } func TestClient_DeleteTXTRecord(t *testing.T) { client, mux := setup(t) mux.HandleFunc("/domains/1/dns", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } auth := req.Header.Get("Authorization") if auth != "Basic dG9rZW46c2VjcmV0" { http.Error(rw, "invalid method: "+req.Method, http.StatusUnauthorized) return } _, _ = rw.Write([]byte(`[ { "id": 1, "host": "example.com", "ttl": 3600, "type": "TXT", "data": "txtTXTtxt" } ]`)) }) mux.HandleFunc("/domains/1/dns/1", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodDelete { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } auth := req.Header.Get("Authorization") if auth != "Basic dG9rZW46c2VjcmV0" { http.Error(rw, "invalid method: "+req.Method, http.StatusUnauthorized) return } }) err := client.DeleteTXTRecord(&Domain{ID: 1}, "example.com", "txtTXTtxt") require.NoError(t, err) } func TestClient_getDNSRecordByHostData(t *testing.T) { client, mux := setup(t) mux.HandleFunc("/domains/1/dns", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } auth := req.Header.Get("Authorization") if auth != "Basic dG9rZW46c2VjcmV0" { http.Error(rw, "invalid method: "+req.Method, http.StatusUnauthorized) return } _, _ = rw.Write([]byte(`[ { "id": 1, "host": "example.com", "ttl": 3600, "type": "TXT", "data": "txtTXTtxt" } ]`)) }) record, err := client.getDNSRecordByHostData(Domain{ID: 1}, "example.com", "txtTXTtxt") require.NoError(t, err) expected := &DNSRecord{ ID: 1, Type: "TXT", Host: "example.com", Data: "txtTXTtxt", TTL: 3600, } assert.Equal(t, expected, record) } func TestClient_GetDomainByName(t *testing.T) { client, mux := setup(t) mux.HandleFunc("/domains", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } auth := req.Header.Get("Authorization") if auth != "Basic dG9rZW46c2VjcmV0" { http.Error(rw, "invalid method: "+req.Method, http.StatusUnauthorized) return } _, _ = rw.Write([]byte(`[ { "id": 1, "domain": "example.com", "expiry_date": "2019-08-24", "registered_date": "2019-08-24", "renew": true, "registrant": "Ola Nordmann", "status": "active", "nameservers": [ "ns1.hyp.net", "ns2.hyp.net", "ns3.hyp.net" ], "services": { "registrar": true, "dns": true, "email": true, "webhotel": "none" } } ]`)) }) domain, err := client.GetDomainByName("example.com") require.NoError(t, err) expected := &Domain{ Name: "example.com", ID: 1, ExpiryDate: "2019-08-24", Nameservers: []string{"ns1.hyp.net", "ns2.hyp.net", "ns3.hyp.net"}, RegisteredDate: "2019-08-24", Registrant: "Ola Nordmann", Renew: true, Services: Service{ DNS: true, Email: true, Registrar: true, Webhotel: "none", }, Status: "active", } assert.Equal(t, expected, domain) } lego-4.9.1/providers/dns/domeneshop/internal/types.go000066400000000000000000000014361434020463500227310ustar00rootroot00000000000000package internal // Domain JSON data structure. type Domain struct { Name string `json:"domain"` ID int `json:"id"` ExpiryDate string `json:"expiry_date"` Nameservers []string `json:"nameservers"` RegisteredDate string `json:"registered_date"` Registrant string `json:"registrant"` Renew bool `json:"renew"` Services Service `json:"services"` Status string } type Service struct { DNS bool `json:"dns"` Email bool `json:"email"` Registrar bool `json:"registrar"` Webhotel string `json:"webhotel"` } // DNSRecord JSON data structure. type DNSRecord struct { Data string `json:"data"` Host string `json:"host"` ID int `json:"id"` TTL int `json:"ttl"` Type string `json:"type"` } lego-4.9.1/providers/dns/dreamhost/000077500000000000000000000000001434020463500172435ustar00rootroot00000000000000lego-4.9.1/providers/dns/dreamhost/client.go000066400000000000000000000031471434020463500210550ustar00rootroot00000000000000package dreamhost import ( "encoding/json" "fmt" "io" "net/http" "net/url" "github.com/go-acme/lego/v4/log" ) const ( defaultBaseURL = "https://api.dreamhost.com" cmdAddRecord = "dns-add_record" cmdRemoveRecord = "dns-remove_record" ) type apiResponse struct { Data string `json:"data"` Result string `json:"result"` } func (d *DNSProvider) buildQuery(action, domain, txt string) (*url.URL, error) { u, err := url.Parse(d.config.BaseURL) if err != nil { return nil, err } query := u.Query() query.Set("key", d.config.APIKey) query.Set("cmd", action) query.Set("format", "json") query.Set("record", domain) query.Set("type", "TXT") query.Set("value", txt) query.Set("comment", url.QueryEscape("Managed By lego")) u.RawQuery = query.Encode() return u, nil } // updateTxtRecord will either add or remove a TXT record. // action is either cmdAddRecord or cmdRemoveRecord. func (d *DNSProvider) updateTxtRecord(u fmt.Stringer) error { resp, err := d.config.HTTPClient.Get(u.String()) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("request failed with HTTP status code %d", resp.StatusCode) } raw, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read body: %w", err) } var response apiResponse err = json.Unmarshal(raw, &response) if err != nil { return fmt.Errorf("unable to decode API server response: %w: %s", err, string(raw)) } if response.Result == "error" { return fmt.Errorf("add TXT record failed: %s", response.Data) } log.Infof("dreamhost: %s", response.Data) return nil } lego-4.9.1/providers/dns/dreamhost/client_test.go000066400000000000000000000024731434020463500221150ustar00rootroot00000000000000package dreamhost import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDNSProvider_buildQuery(t *testing.T) { testCases := []struct { desc string apiKey string baseURL string action string domain string txt string expected string }{ { desc: "success", apiKey: fakeAPIKey, action: cmdAddRecord, domain: "domain", txt: "TXTtxtTXT", expected: "https://api.dreamhost.com?cmd=dns-add_record&comment=Managed%2BBy%2Blego&format=json&key=asdf1234&record=domain&type=TXT&value=TXTtxtTXT", }, { desc: "Invalid base URL", apiKey: fakeAPIKey, baseURL: ":", action: cmdAddRecord, domain: "domain", txt: "TXTtxtTXT", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() config := NewDefaultConfig() config.APIKey = test.apiKey if test.baseURL != "" { config.BaseURL = test.baseURL } provider, err := NewDNSProviderConfig(config) require.NoError(t, err) require.NotNil(t, provider) u, err := provider.buildQuery(test.action, test.domain, test.txt) if test.expected == "" { require.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, test.expected, u.String()) } }) } } lego-4.9.1/providers/dns/dreamhost/dreamhost.go000066400000000000000000000066541434020463500215730ustar00rootroot00000000000000// Package dreamhost implements a DNS provider for solving the DNS-01 challenge using DreamHost. // See https://help.dreamhost.com/hc/en-us/articles/217560167-API_overview // and https://help.dreamhost.com/hc/en-us/articles/217555707-DNS-API-commands for the API spec. package dreamhost import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "DREAMHOST_" EnvAPIKey = envNamespace + "API_KEY" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string APIKey string PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ BaseURL: defaultBaseURL, PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 60*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 1*time.Minute), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config } // NewDNSProvider returns a new DNS provider using // environment variable DREAMHOST_API_KEY for adding and removing the DNS record. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("dreamhost: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for DreamHost. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("dreamhost: the configuration of the DNS provider is nil") } if config.APIKey == "" { return nil, errors.New("dreamhost: credentials missing") } if config.BaseURL == "" { config.BaseURL = defaultBaseURL } return &DNSProvider{config: config}, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) record := dns01.UnFqdn(fqdn) u, err := d.buildQuery(cmdAddRecord, record, value) if err != nil { return fmt.Errorf("dreamhost: %w", err) } err = d.updateTxtRecord(u) if err != nil { return fmt.Errorf("dreamhost: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) record := dns01.UnFqdn(fqdn) u, err := d.buildQuery(cmdRemoveRecord, record, value) if err != nil { return fmt.Errorf("dreamhost: %w", err) } err = d.updateTxtRecord(u) if err != nil { return fmt.Errorf("dreamhost: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/dreamhost/dreamhost.toml000066400000000000000000000013101434020463500221210ustar00rootroot00000000000000Name = "DreamHost" Description = '''''' URL = "https://www.dreamhost.com" Code = "dreamhost" Since = "v1.1.0" Example = ''' DREAMHOST_API_KEY="YOURAPIKEY" \ lego --email you@example.com --dns dreamhost --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DREAMHOST_API_KEY = "The API key" [Configuration.Additional] DREAMHOST_POLLING_INTERVAL = "Time between DNS propagation check" DREAMHOST_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" DREAMHOST_TTL = "The TTL of the TXT record used for the DNS challenge" DREAMHOST_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://help.dreamhost.com/hc/en-us/articles/217560167-API_overview" lego-4.9.1/providers/dns/dreamhost/dreamhost_test.go000066400000000000000000000113461434020463500226240ustar00rootroot00000000000000package dreamhost import ( "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAPIKey). WithDomain(envDomain) const ( fakeAPIKey = "asdf1234" fakeChallengeToken = "foobar" fakeKeyAuth = "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI" ) func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) config := NewDefaultConfig() config.APIKey = fakeAPIKey config.BaseURL = server.URL config.HTTPClient = server.Client() provider, err := NewDNSProviderConfig(config) require.NoError(t, err) return provider, mux } func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", }, }, { desc: "missing API key", envVars: map[string]string{ EnvAPIKey: "", }, expected: "dreamhost: some credentials information are missing: DREAMHOST_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { assert.NoError(t, err) assert.NotNil(t, p) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string expected string }{ { desc: "success", apiKey: "123", }, { desc: "missing credentials", expected: "dreamhost: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) if test.expected == "" { assert.NoError(t, err) assert.NotNil(t, p) } else { require.EqualError(t, err, test.expected) } }) } } func TestDNSProvider_Present(t *testing.T) { provider, mux := setupTest(t) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method, "method") q := r.URL.Query() assert.Equal(t, q.Get("key"), fakeAPIKey) assert.Equal(t, q.Get("cmd"), "dns-add_record") assert.Equal(t, q.Get("format"), "json") assert.Equal(t, q.Get("record"), "_acme-challenge.example.com") assert.Equal(t, q.Get("value"), fakeKeyAuth) assert.Equal(t, q.Get("comment"), "Managed+By+lego") _, err := fmt.Fprintf(w, `{"data":"record_added","result":"success"}`) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) err := provider.Present("example.com", "", fakeChallengeToken) require.NoError(t, err) } func TestDNSProvider_PresentFailed(t *testing.T) { provider, mux := setupTest(t) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method, "method") _, err := fmt.Fprintf(w, `{"data":"record_already_exists_remove_first","result":"error"}`) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) err := provider.Present("example.com", "", fakeChallengeToken) require.EqualError(t, err, "dreamhost: add TXT record failed: record_already_exists_remove_first") } func TestDNSProvider_Cleanup(t *testing.T) { provider, mux := setupTest(t) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method, "method") q := r.URL.Query() assert.Equal(t, q.Get("key"), fakeAPIKey, "key mismatch") assert.Equal(t, q.Get("cmd"), "dns-remove_record", "cmd mismatch") assert.Equal(t, q.Get("format"), "json") assert.Equal(t, q.Get("record"), "_acme-challenge.example.com") assert.Equal(t, q.Get("value"), fakeKeyAuth, "value mismatch") assert.Equal(t, q.Get("comment"), "Managed+By+lego") _, err := fmt.Fprintf(w, `{"data":"record_removed","result":"success"}`) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) err := provider.CleanUp("example.com", "", fakeChallengeToken) require.NoError(t, err, "failed to remove TXT record") } func TestLivePresentAndCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/duckdns/000077500000000000000000000000001434020463500167105ustar00rootroot00000000000000lego-4.9.1/providers/dns/duckdns/client.go000066400000000000000000000033441434020463500205210ustar00rootroot00000000000000package duckdns import ( "fmt" "io" "net/url" "strconv" "strings" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/miekg/dns" ) // updateTxtRecord Update the domains TXT record // To update the TXT record we just need to make one simple get request. // In DuckDNS you only have one TXT record shared with the domain and all sub domains. func (d *DNSProvider) updateTxtRecord(domain, token, txt string, clear bool) error { u, _ := url.Parse("https://www.duckdns.org/update") mainDomain := getMainDomain(domain) if mainDomain == "" { return fmt.Errorf("unable to find the main domain for: %s", domain) } query := u.Query() query.Set("domains", mainDomain) query.Set("token", token) query.Set("clear", strconv.FormatBool(clear)) query.Set("txt", txt) u.RawQuery = query.Encode() response, err := d.config.HTTPClient.Get(u.String()) if err != nil { return err } defer response.Body.Close() bodyBytes, err := io.ReadAll(response.Body) if err != nil { return err } body := string(bodyBytes) if body != "OK" { return fmt.Errorf("request to change TXT record for DuckDNS returned the following result (%s) this does not match expectation (OK) used url [%s]", body, u) } return nil } // DuckDNS only lets you write to your subdomain. // It must be in format subdomain.duckdns.org, // not in format subsubdomain.subdomain.duckdns.org. // So strip off everything that is not top 3 levels. func getMainDomain(domain string) string { domain = dns01.UnFqdn(domain) split := dns.Split(domain) if strings.HasSuffix(strings.ToLower(domain), "duckdns.org") { if len(split) < 3 { return "" } firstSubDomainIndex := split[len(split)-3] return domain[firstSubDomainIndex:] } return domain[split[len(split)-1]:] } lego-4.9.1/providers/dns/duckdns/duckdns.go000066400000000000000000000063621434020463500207010ustar00rootroot00000000000000// Package duckdns implements a DNS provider for solving the DNS-01 challenge using DuckDNS. // See http://www.duckdns.org/spec.jsp for more info on updating TXT records. package duckdns import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "DUCKDNS_" EnvToken = envNamespace + "TOKEN" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Token string PropagationTimeout time.Duration PollingInterval time.Duration SequenceInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config } // NewDNSProvider returns a new DNS provider using // environment variable DUCKDNS_TOKEN for adding and removing the DNS record. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvToken) if err != nil { return nil, fmt.Errorf("duckdns: %w", err) } config := NewDefaultConfig() config.Token = values[EnvToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for DuckDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("duckdns: the configuration of the DNS provider is nil") } if config.Token == "" { return nil, errors.New("duckdns: credentials missing") } return &DNSProvider{config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, txtRecord := dns01.GetRecord(domain, keyAuth) return d.updateTxtRecord(dns01.UnFqdn(fqdn), d.config.Token, txtRecord, false) } // CleanUp clears DuckDNS TXT record. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) return d.updateTxtRecord(dns01.UnFqdn(fqdn), d.config.Token, "", true) } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Sequential All DNS challenges for this provider will be resolved sequentially. // Returns the interval between each iteration. func (d *DNSProvider) Sequential() time.Duration { return d.config.SequenceInterval } lego-4.9.1/providers/dns/duckdns/duckdns.toml000066400000000000000000000013161434020463500212410ustar00rootroot00000000000000Name = "Duck DNS" Description = '''''' URL = "https://www.duckdns.org/" Code = "duckdns" Since = "v0.5.0" Example = ''' DUCKDNS_TOKEN=xxxxxx \ lego --email you@example.com --dns duckdns --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DUCKDNS_TOKEN = "Account token" [Configuration.Additional] DUCKDNS_POLLING_INTERVAL = "Time between DNS propagation check" DUCKDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" DUCKDNS_TTL = "The TTL of the TXT record used for the DNS challenge" DUCKDNS_HTTP_TIMEOUT = "API request timeout" DUCKDNS_SEQUENCE_INTERVAL = "Time between sequential requests" [Links] API = "https://www.duckdns.org/spec.jsp" lego-4.9.1/providers/dns/duckdns/duckdns_test.go000066400000000000000000000066531434020463500217430ustar00rootroot00000000000000package duckdns import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvToken). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvToken: "123", }, }, { desc: "missing api key", envVars: map[string]string{ EnvToken: "", }, expected: "duckdns: some credentials information are missing: DUCKDNS_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string token string expected string }{ { desc: "success", token: "123", }, { desc: "missing credentials", expected: "duckdns: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Token = test.token p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func Test_getMainDomain(t *testing.T) { testCases := []struct { desc string domain string expected string }{ { desc: "empty", domain: "", expected: "", }, { desc: "missing sub domain", domain: "duckdns.org", expected: "", }, { desc: "explicit domain: sub domain", domain: "_acme-challenge.sub.duckdns.org", expected: "sub.duckdns.org", }, { desc: "explicit domain: subsub domain", domain: "_acme-challenge.my.sub.duckdns.org", expected: "sub.duckdns.org", }, { desc: "explicit domain: subsubsub domain", domain: "_acme-challenge.my.sub.sub.duckdns.org", expected: "sub.duckdns.org", }, { desc: "only subname: sub domain", domain: "_acme-challenge.sub", expected: "sub", }, { desc: "only subname: subsub domain", domain: "_acme-challenge.my.sub", expected: "sub", }, { desc: "only subname: subsubsub domain", domain: "_acme-challenge.my.sub.sub", expected: "sub", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() wDomain := getMainDomain(test.domain) assert.Equal(t, test.expected, wDomain) }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/dyn/000077500000000000000000000000001434020463500160475ustar00rootroot00000000000000lego-4.9.1/providers/dns/dyn/client.go000066400000000000000000000067461434020463500176710ustar00rootroot00000000000000package dyn import ( "bytes" "encoding/json" "errors" "fmt" "net/http" ) const defaultBaseURL = "https://api.dynect.net/REST" type dynResponse struct { // One of 'success', 'failure', or 'incomplete' Status string `json:"status"` // The structure containing the actual results of the request Data json.RawMessage `json:"data"` // The ID of the job that was created in response to a request. JobID int `json:"job_id"` // A list of zero or more messages Messages json.RawMessage `json:"msgs"` } type credentials struct { Customer string `json:"customer_name"` User string `json:"user_name"` Pass string `json:"password"` } type session struct { Token string `json:"token"` Version string `json:"version"` } type publish struct { Publish bool `json:"publish"` Notes string `json:"notes"` } // Starts a new Dyn API Session. Authenticates using customerName, userName, // password and receives a token to be used in for subsequent requests. func (d *DNSProvider) login() error { payload := &credentials{Customer: d.config.CustomerName, User: d.config.UserName, Pass: d.config.Password} dynRes, err := d.sendRequest(http.MethodPost, "Session", payload) if err != nil { return err } var s session err = json.Unmarshal(dynRes.Data, &s) if err != nil { return err } d.token = s.Token return nil } // Destroys Dyn Session. func (d *DNSProvider) logout() error { if d.token == "" { // nothing to do return nil } url := fmt.Sprintf("%s/Session", defaultBaseURL) req, err := http.NewRequest(http.MethodDelete, url, nil) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Auth-Token", d.token) resp, err := d.config.HTTPClient.Do(req) if err != nil { return err } resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("API request failed to delete session with HTTP status code %d", resp.StatusCode) } d.token = "" return nil } func (d *DNSProvider) publish(zone, notes string) error { pub := &publish{Publish: true, Notes: notes} resource := fmt.Sprintf("Zone/%s/", zone) _, err := d.sendRequest(http.MethodPut, resource, pub) return err } func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*dynResponse, error) { url := fmt.Sprintf("%s/%s", defaultBaseURL, resource) body, err := json.Marshal(payload) if err != nil { return nil, err } req, err := http.NewRequest(method, url, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") if len(d.token) > 0 { req.Header.Set("Auth-Token", d.token) } resp, err := d.config.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= http.StatusInternalServerError { return nil, fmt.Errorf("API request failed with HTTP status code %d", resp.StatusCode) } var dynRes dynResponse err = json.NewDecoder(resp.Body).Decode(&dynRes) if err != nil { return nil, err } if resp.StatusCode >= http.StatusBadRequest { return nil, fmt.Errorf("API request failed with HTTP status code %d: %s", resp.StatusCode, dynRes.Messages) } else if resp.StatusCode == http.StatusTemporaryRedirect { // TODO add support for HTTP 307 response and long running jobs return nil, errors.New("API request returned HTTP 307. This is currently unsupported") } if dynRes.Status == "failure" { // TODO add better error handling return nil, fmt.Errorf("API request failed: %s", dynRes.Messages) } return &dynRes, nil } lego-4.9.1/providers/dns/dyn/dyn.go000066400000000000000000000113401434020463500171670ustar00rootroot00000000000000// Package dyn implements a DNS provider for solving the DNS-01 challenge using Dyn Managed DNS. package dyn import ( "errors" "fmt" "net/http" "strconv" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "DYN_" EnvCustomerName = envNamespace + "CUSTOMER_NAME" EnvUserName = envNamespace + "USER_NAME" EnvPassword = envNamespace + "PASSWORD" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { CustomerName string UserName string Password string HTTPClient *http.Client PropagationTimeout time.Duration PollingInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config token string } // NewDNSProvider returns a DNSProvider instance configured for Dyn DNS. // Credentials must be passed in the environment variables: // DYN_CUSTOMER_NAME, DYN_USER_NAME and DYN_PASSWORD. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvCustomerName, EnvUserName, EnvPassword) if err != nil { return nil, fmt.Errorf("dyn: %w", err) } config := NewDefaultConfig() config.CustomerName = values[EnvCustomerName] config.UserName = values[EnvUserName] config.Password = values[EnvPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Dyn DNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("dyn: the configuration of the DNS provider is nil") } if config.CustomerName == "" || config.UserName == "" || config.Password == "" { return nil, errors.New("dyn: credentials missing") } return &DNSProvider{config: config}, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("dyn: %w", err) } err = d.login() if err != nil { return fmt.Errorf("dyn: %w", err) } data := map[string]interface{}{ "rdata": map[string]string{ "txtdata": value, }, "ttl": strconv.Itoa(d.config.TTL), } resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn) _, err = d.sendRequest(http.MethodPost, resource, data) if err != nil { return fmt.Errorf("dyn: %w", err) } err = d.publish(authZone, "Added TXT record for ACME dns-01 challenge using lego client") if err != nil { return fmt.Errorf("dyn: %w", err) } return d.logout() } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("dyn: %w", err) } err = d.login() if err != nil { return fmt.Errorf("dyn: %w", err) } resource := fmt.Sprintf("TXTRecord/%s/%s/", authZone, fqdn) url := fmt.Sprintf("%s/%s", defaultBaseURL, resource) req, err := http.NewRequest(http.MethodDelete, url, nil) if err != nil { return fmt.Errorf("dyn: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Auth-Token", d.token) resp, err := d.config.HTTPClient.Do(req) if err != nil { return fmt.Errorf("dyn: %w", err) } resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("dyn: API request failed to delete TXT record HTTP status code %d", resp.StatusCode) } err = d.publish(authZone, "Removed TXT record for ACME dns-01 challenge using lego client") if err != nil { return fmt.Errorf("dyn: %w", err) } return d.logout() } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/dyn/dyn.toml000066400000000000000000000013201434020463500175320ustar00rootroot00000000000000Name = "Dyn" Description = '''''' URL = "https://dyn.com/" Code = "dyn" Since = "v0.3.0" Example = ''' DYN_CUSTOMER_NAME=xxxxxx \ DYN_USER_NAME=yyyyy \ DYN_PASSWORD=zzzz \ lego --email you@example.com --dns dyn --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DYN_CUSTOMER_NAME = "Customer name" DYN_USER_NAME = "User name" DYN_PASSWORD = "Password" [Configuration.Additional] DYN_POLLING_INTERVAL = "Time between DNS propagation check" DYN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" DYN_TTL = "The TTL of the TXT record used for the DNS challenge" DYN_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://help.dyn.com/rest/" lego-4.9.1/providers/dns/dyn/dyn_test.go000066400000000000000000000072621434020463500202360ustar00rootroot00000000000000package dyn import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvCustomerName, EnvUserName, EnvPassword). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvCustomerName: "A", EnvUserName: "B", EnvPassword: "C", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvCustomerName: "", EnvUserName: "", EnvPassword: "", }, expected: "dyn: some credentials information are missing: DYN_CUSTOMER_NAME,DYN_USER_NAME,DYN_PASSWORD", }, { desc: "missing customer name", envVars: map[string]string{ EnvCustomerName: "", EnvUserName: "B", EnvPassword: "C", }, expected: "dyn: some credentials information are missing: DYN_CUSTOMER_NAME", }, { desc: "missing password", envVars: map[string]string{ EnvCustomerName: "A", EnvUserName: "", EnvPassword: "C", }, expected: "dyn: some credentials information are missing: DYN_USER_NAME", }, { desc: "missing username", envVars: map[string]string{ EnvCustomerName: "A", EnvUserName: "B", EnvPassword: "", }, expected: "dyn: some credentials information are missing: DYN_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string customerName string password string userName string expected string }{ { desc: "success", customerName: "A", password: "B", userName: "C", }, { desc: "missing credentials", expected: "dyn: credentials missing", }, { desc: "missing customer name", customerName: "", password: "B", userName: "C", expected: "dyn: credentials missing", }, { desc: "missing password", customerName: "A", password: "", userName: "C", expected: "dyn: credentials missing", }, { desc: "missing username", customerName: "A", password: "B", userName: "", expected: "dyn: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.CustomerName = test.customerName config.Password = test.password config.UserName = test.userName p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/dynu/000077500000000000000000000000001434020463500162345ustar00rootroot00000000000000lego-4.9.1/providers/dns/dynu/dynu.go000066400000000000000000000112011434020463500175350ustar00rootroot00000000000000// Package dynu implements a DNS provider for solving the DNS-01 challenge using Dynu DNS. package dynu import ( "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/dynu/internal" "github.com/miekg/dns" ) // Environment variables names. const ( envNamespace = "DYNU_" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 300), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 3*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for Dynu. // Credentials must be passed in the environment variables. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("dynu: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Dynu. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("dynu: the configuration of the DNS provider is nil") } if config.APIKey == "" { return nil, errors.New("dynu: incomplete credentials, missing API key") } tr, err := internal.NewTokenTransport(config.APIKey) if err != nil { return nil, fmt.Errorf("dynu: %w", err) } client := internal.NewClient() client.HTTPClient = tr.Wrap(config.HTTPClient) return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) // TODO(ldez) replace domain by FQDN to follow CNAME. rootDomain, err := d.client.GetRootDomain(domain) if err != nil { return fmt.Errorf("dynu: could not find root domain for %s: %w", domain, err) } records, err := d.client.GetRecords(dns01.UnFqdn(fqdn), "TXT") if err != nil { return fmt.Errorf("dynu: failed to get records for %s: %w", domain, err) } for _, record := range records { // the record already exist if record.Hostname == dns01.UnFqdn(fqdn) && record.TextData == value { return nil } } record := internal.DNSRecord{ Type: "TXT", DomainName: rootDomain.DomainName, Hostname: dns01.UnFqdn(fqdn), NodeName: dns01.UnFqdn(strings.TrimSuffix(fqdn, dns.Fqdn(domain))), TextData: value, State: true, TTL: d.config.TTL, } err = d.client.AddNewRecord(rootDomain.ID, record) if err != nil { return fmt.Errorf("dynu: failed to add record to %s: %w", domain, err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) // TODO(ldez) replace domain by FQDN to follow CNAME. rootDomain, err := d.client.GetRootDomain(domain) if err != nil { return fmt.Errorf("dynu: could not find root domain for %s: %w", domain, err) } records, err := d.client.GetRecords(dns01.UnFqdn(fqdn), "TXT") if err != nil { return fmt.Errorf("dynu: failed to get records for %s: %w", domain, err) } for _, record := range records { if record.Hostname == dns01.UnFqdn(fqdn) && record.TextData == value { err = d.client.DeleteRecord(rootDomain.ID, record.ID) if err != nil { return fmt.Errorf("dynu: failed to remove TXT record for %s: %w", domain, err) } } } return nil } lego-4.9.1/providers/dns/dynu/dynu.toml000066400000000000000000000012161434020463500201100ustar00rootroot00000000000000Name = "Dynu" Description = '''''' URL = "https://www.dynu.com/" Code = "dynu" Since = "v3.5.0" Example = ''' DYNU_API_KEY=1234567890abcdefghijklmnopqrstuvwxyz \ lego --email you@example.com --dns dynu --domains my.example.org run ''' [Configuration] [Configuration.Credentials] DYNU_API_KEY = "API key" [Configuration.Additional] DYNU_POLLING_INTERVAL = "Time between DNS propagation check" DYNU_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" DYNU_TTL = "The TTL of the TXT record used for the DNS challenge" DYNU_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.dynu.com/en-US/Support/API" lego-4.9.1/providers/dns/dynu/dynu_test.go000066400000000000000000000043241434020463500206040ustar00rootroot00000000000000package dynu import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", }, }, { desc: "missing api key", envVars: map[string]string{ EnvAPIKey: "", }, expected: "dynu: some credentials information are missing: DYNU_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string expected string apiKey string }{ { desc: "success", apiKey: "api_key", }, { desc: "missing api key", apiKey: "", expected: "dynu: incomplete credentials, missing API key", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/dynu/internal/000077500000000000000000000000001434020463500200505ustar00rootroot00000000000000lego-4.9.1/providers/dns/dynu/internal/auth.go000066400000000000000000000030071434020463500213400ustar00rootroot00000000000000package internal import ( "errors" "net/http" ) const apiKeyHeader = "Api-Key" // TokenTransport HTTP transport for API authentication. type TokenTransport struct { apiKey string // Transport is the underlying HTTP transport to use when making requests. // It will default to http.DefaultTransport if nil. Transport http.RoundTripper } // NewTokenTransport Creates a HTTP transport for API authentication. func NewTokenTransport(apiKey string) (*TokenTransport, error) { if apiKey == "" { return nil, errors.New("credentials missing: API key") } return &TokenTransport{apiKey: apiKey}, nil } // RoundTrip executes a single HTTP transaction. func (t *TokenTransport) RoundTrip(req *http.Request) (*http.Response, error) { enrichedReq := &http.Request{} *enrichedReq = *req enrichedReq.Header = make(http.Header, len(req.Header)) for k, s := range req.Header { enrichedReq.Header[k] = append([]string(nil), s...) } if t.apiKey != "" { enrichedReq.Header.Set(apiKeyHeader, t.apiKey) } return t.transport().RoundTrip(enrichedReq) } func (t *TokenTransport) transport() http.RoundTripper { if t.Transport != nil { return t.Transport } return http.DefaultTransport } // Client Creates a new HTTP client. func (t *TokenTransport) Client() *http.Client { return &http.Client{Transport: t} } // Wrap Wrap a HTTP client Transport with the TokenTransport. func (t *TokenTransport) Wrap(client *http.Client) *http.Client { backup := client.Transport t.Transport = backup client.Transport = t return client } lego-4.9.1/providers/dns/dynu/internal/auth_test.go000066400000000000000000000015071434020463500224020ustar00rootroot00000000000000package internal import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewTokenTransport_success(t *testing.T) { apiKey := "api" transport, err := NewTokenTransport(apiKey) require.NoError(t, err) assert.NotNil(t, transport) } func TestNewTokenTransport_missing_credentials(t *testing.T) { apiKey := "" transport, err := NewTokenTransport(apiKey) require.Error(t, err) assert.Nil(t, transport) } func TestTokenTransport_RoundTrip(t *testing.T) { apiKey := "api" transport, err := NewTokenTransport(apiKey) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "http://example.com", nil) resp, err := transport.RoundTrip(req) require.NoError(t, err) assert.Equal(t, "api", resp.Request.Header.Get(apiKeyHeader)) } lego-4.9.1/providers/dns/dynu/internal/client.go000066400000000000000000000076071434020463500216670ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "path" "strconv" "time" "github.com/cenkalti/backoff/v4" "github.com/go-acme/lego/v4/log" ) const defaultBaseURL = "https://api.dynu.com/v2" type Client struct { HTTPClient *http.Client BaseURL string } func NewClient() *Client { return &Client{ HTTPClient: http.DefaultClient, BaseURL: defaultBaseURL, } } // GetRecords Get DNS records based on a hostname and resource record type. func (c Client) GetRecords(hostname, recordType string) ([]DNSRecord, error) { endpoint, err := c.createEndpoint("dns", "record", hostname) if err != nil { return nil, err } query := endpoint.Query() query.Set("recordType", recordType) endpoint.RawQuery = query.Encode() apiResp := RecordsResponse{} err = c.doRetry(http.MethodGet, endpoint.String(), nil, &apiResp) if err != nil { return nil, err } if apiResp.StatusCode/100 != 2 { return nil, fmt.Errorf("API error: %w", apiResp.APIException) } return apiResp.DNSRecords, nil } // AddNewRecord Add a new DNS record for DNS service. func (c Client) AddNewRecord(domainID int64, record DNSRecord) error { endpoint, err := c.createEndpoint("dns", strconv.FormatInt(domainID, 10), "record") if err != nil { return err } reqBody, err := json.Marshal(record) if err != nil { return err } apiResp := RecordResponse{} err = c.doRetry(http.MethodPost, endpoint.String(), reqBody, &apiResp) if err != nil { return err } if apiResp.StatusCode/100 != 2 { return fmt.Errorf("API error: %w", apiResp.APIException) } return nil } // DeleteRecord Remove a DNS record from DNS service. func (c Client) DeleteRecord(domainID, recordID int64) error { endpoint, err := c.createEndpoint("dns", strconv.FormatInt(domainID, 10), "record", strconv.FormatInt(recordID, 10)) if err != nil { return err } apiResp := APIException{} err = c.doRetry(http.MethodDelete, endpoint.String(), nil, &apiResp) if err != nil { return err } if apiResp.StatusCode/100 != 2 { return fmt.Errorf("API error: %w", apiResp) } return nil } // GetRootDomain Get the root domain name based on a hostname. func (c Client) GetRootDomain(hostname string) (*DNSHostname, error) { endpoint, err := c.createEndpoint("dns", "getroot", hostname) if err != nil { return nil, err } apiResp := DNSHostname{} err = c.doRetry(http.MethodGet, endpoint.String(), nil, &apiResp) if err != nil { return nil, err } if apiResp.StatusCode/100 != 2 { return nil, fmt.Errorf("API error: %w", apiResp.APIException) } return &apiResp, nil } // doRetry the API is really unstable so we need to retry on EOF. func (c Client) doRetry(method, uri string, body []byte, data interface{}) error { var resp *http.Response operation := func() error { var reqBody io.Reader if len(body) > 0 { reqBody = bytes.NewReader(body) } req, err := http.NewRequest(method, uri, reqBody) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") resp, err = c.HTTPClient.Do(req) if errors.Is(err, io.EOF) { return err } if err != nil { return backoff.Permanent(fmt.Errorf("client error: %w", err)) } return nil } notify := func(err error, duration time.Duration) { log.Printf("client retries because of %v", err) } bo := backoff.NewExponentialBackOff() bo.InitialInterval = 1 * time.Second err := backoff.RetryNotify(operation, bo, notify) if err != nil { return err } defer func() { _ = resp.Body.Close() }() all, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response body: %w", err) } return json.Unmarshal(all, data) } func (c Client) createEndpoint(fragments ...string) (*url.URL, error) { baseURL, err := url.Parse(c.BaseURL) if err != nil { return nil, err } return baseURL.Parse(path.Join(baseURL.Path, path.Join(fragments...))) } lego-4.9.1/providers/dns/dynu/internal/client_test.go000066400000000000000000000152411434020463500227170ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { if req.Method != method { http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) return } open, err := os.Open(file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = open.Close() }() rw.WriteHeader(status) _, err = io.Copy(rw, open) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient() client.HTTPClient = server.Client() client.BaseURL = server.URL return client } func TestGetRootDomain(t *testing.T) { type expected struct { domain *DNSHostname error string } testCases := []struct { desc string pattern string status int file string expected expected }{ { desc: "success", pattern: "/dns/getroot/test.lego.freeddns.org", status: http.StatusOK, file: "./fixtures/get_root_domain.json", expected: expected{ domain: &DNSHostname{ APIException: &APIException{ StatusCode: 200, }, ID: 9007481, DomainName: "lego.freeddns.org", Hostname: "test.lego.freeddns.org", Node: "test", }, }, }, { desc: "invalid", pattern: "/dns/getroot/test.lego.freeddns.org", status: http.StatusNotImplemented, file: "./fixtures/get_root_domain_invalid.json", expected: expected{ error: "API error: 501: Argument Exception: Invalid.", }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() client := setupTest(t, http.MethodGet, test.pattern, test.status, test.file) domain, err := client.GetRootDomain("test.lego.freeddns.org") if test.expected.error != "" { assert.EqualError(t, err, test.expected.error) return } require.NoError(t, err) assert.NotNil(t, domain) assert.Equal(t, test.expected.domain, domain) }) } } func TestGetRecords(t *testing.T) { type expected struct { records []DNSRecord error string } testCases := []struct { desc string pattern string status int file string expected expected }{ { desc: "success", pattern: "/dns/record/_acme-challenge.lego.freeddns.org", status: http.StatusOK, file: "./fixtures/get_records.json", expected: expected{ records: []DNSRecord{ { ID: 6041417, Type: "TXT", DomainID: 9007481, DomainName: "lego.freeddns.org", NodeName: "_acme-challenge", Hostname: "_acme-challenge.lego.freeddns.org", State: true, Content: `_acme-challenge.lego.freeddns.org. 300 IN TXT "txt_txt_txt_txt_txt_txt_txt"`, TextData: "txt_txt_txt_txt_txt_txt_txt", TTL: 300, }, { ID: 6041422, Type: "TXT", DomainID: 9007481, DomainName: "lego.freeddns.org", NodeName: "_acme-challenge", Hostname: "_acme-challenge.lego.freeddns.org", State: true, Content: `_acme-challenge.lego.freeddns.org. 300 IN TXT "txt_txt_txt_txt_txt_txt_txt_2"`, TextData: "txt_txt_txt_txt_txt_txt_txt_2", TTL: 300, }, }, }, }, { desc: "empty", pattern: "/dns/record/_acme-challenge.lego.freeddns.org", status: http.StatusOK, file: "./fixtures/get_records_empty.json", expected: expected{ records: []DNSRecord{}, }, }, { desc: "invalid", pattern: "/dns/record/_acme-challenge.lego.freeddns.org", status: http.StatusNotImplemented, file: "./fixtures/get_records_invalid.json", expected: expected{ error: "API error: 501: Argument Exception: Invalid.", }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() client := setupTest(t, http.MethodGet, test.pattern, test.status, test.file) records, err := client.GetRecords("_acme-challenge.lego.freeddns.org", "TXT") if test.expected.error != "" { assert.EqualError(t, err, test.expected.error) return } require.NoError(t, err) assert.NotNil(t, records) assert.Equal(t, test.expected.records, records) }) } } func TestAddNewRecord(t *testing.T) { type expected struct { error string } testCases := []struct { desc string pattern string status int file string expected expected }{ { desc: "success", pattern: "/dns/9007481/record", status: http.StatusOK, file: "./fixtures/add_new_record.json", }, { desc: "invalid", pattern: "/dns/9007481/record", status: http.StatusNotImplemented, file: "./fixtures/add_new_record_invalid.json", expected: expected{ error: "API error: 501: Argument Exception: Invalid.", }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() client := setupTest(t, http.MethodPost, test.pattern, test.status, test.file) record := DNSRecord{ Type: "TXT", DomainName: "lego.freeddns.org", Hostname: "_acme-challenge.lego.freeddns.org", NodeName: "_acme-challenge", TextData: "txt_txt_txt_txt_txt_txt_txt_2", State: true, TTL: 300, } err := client.AddNewRecord(9007481, record) if test.expected.error != "" { assert.EqualError(t, err, test.expected.error) return } require.NoError(t, err) }) } } func TestDeleteRecord(t *testing.T) { type expected struct { error string } testCases := []struct { desc string pattern string status int file string expected expected }{ { desc: "success", pattern: "/", status: http.StatusOK, file: "./fixtures/delete_record.json", }, { desc: "invalid", pattern: "/", status: http.StatusNotImplemented, file: "./fixtures/delete_record_invalid.json", expected: expected{ error: "API error: 501: Argument Exception: Invalid.", }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() client := setupTest(t, http.MethodDelete, test.pattern, test.status, test.file) err := client.DeleteRecord(9007481, 6041418) if test.expected.error != "" { assert.EqualError(t, err, test.expected.error) return } require.NoError(t, err) }) } } lego-4.9.1/providers/dns/dynu/internal/fixtures/000077500000000000000000000000001434020463500217215ustar00rootroot00000000000000lego-4.9.1/providers/dns/dynu/internal/fixtures/add_new_record.json000066400000000000000000000006431434020463500255560ustar00rootroot00000000000000{ "statusCode": 200, "id": 6041417, "domainId": 9007481, "domainName": "lego.freeddns.org", "nodeName": "_acme-challenge", "hostname": "_acme-challenge.lego.freeddns.org", "recordType": "TXT", "ttl": 300, "state": true, "content": "_acme-challenge.lego.freeddns.org. 300 IN TXT \"txt_txt_txt_txt_txt_txt_txt\"", "updatedOn": "2020-03-10T04:00:36.923", "textData": "txt_txt_txt_txt_txt_txt_txt" }lego-4.9.1/providers/dns/dynu/internal/fixtures/add_new_record_invalid.json000066400000000000000000000001201434020463500272520ustar00rootroot00000000000000{ "statusCode": 501, "type": "Argument Exception", "message": "Invalid." }lego-4.9.1/providers/dns/dynu/internal/fixtures/delete_record.json000066400000000000000000000000271434020463500254130ustar00rootroot00000000000000{ "statusCode": 200 }lego-4.9.1/providers/dns/dynu/internal/fixtures/delete_record_invalid.json000066400000000000000000000001201434020463500271130ustar00rootroot00000000000000{ "statusCode": 501, "type": "Argument Exception", "message": "Invalid." }lego-4.9.1/providers/dns/dynu/internal/fixtures/get_records.json000066400000000000000000000016711434020463500251210ustar00rootroot00000000000000{ "statusCode": 200, "dnsRecords": [ { "id": 6041417, "domainId": 9007481, "domainName": "lego.freeddns.org", "nodeName": "_acme-challenge", "hostname": "_acme-challenge.lego.freeddns.org", "recordType": "TXT", "ttl": 300, "state": true, "content": "_acme-challenge.lego.freeddns.org. 300 IN TXT \"txt_txt_txt_txt_txt_txt_txt\"", "updatedOn": "2020-03-10T04:00:36.923", "textData": "txt_txt_txt_txt_txt_txt_txt" }, { "id": 6041422, "domainId": 9007481, "domainName": "lego.freeddns.org", "nodeName": "_acme-challenge", "hostname": "_acme-challenge.lego.freeddns.org", "recordType": "TXT", "ttl": 300, "state": true, "content": "_acme-challenge.lego.freeddns.org. 300 IN TXT \"txt_txt_txt_txt_txt_txt_txt_2\"", "updatedOn": "2020-03-10T04:03:17.563", "textData": "txt_txt_txt_txt_txt_txt_txt_2" } ] }lego-4.9.1/providers/dns/dynu/internal/fixtures/get_records_empty.json000066400000000000000000000000531434020463500263300ustar00rootroot00000000000000{ "statusCode": 200, "dnsRecords": [] }lego-4.9.1/providers/dns/dynu/internal/fixtures/get_records_invalid.json000066400000000000000000000001201434020463500266130ustar00rootroot00000000000000{ "statusCode": 501, "type": "Argument Exception", "message": "Invalid." }lego-4.9.1/providers/dns/dynu/internal/fixtures/get_root_domain.json000066400000000000000000000002071434020463500257640ustar00rootroot00000000000000{ "statusCode": 200, "id": 9007481, "domainName": "lego.freeddns.org", "hostname": "test.lego.freeddns.org", "node": "test" }lego-4.9.1/providers/dns/dynu/internal/fixtures/get_root_domain_invalid.json000066400000000000000000000001201434020463500274640ustar00rootroot00000000000000{ "statusCode": 501, "type": "Argument Exception", "message": "Invalid." }lego-4.9.1/providers/dns/dynu/internal/model.go000066400000000000000000000031131434020463500214750ustar00rootroot00000000000000package internal import "fmt" // APIException defines model for apiException. type APIException struct { Message string `json:"message,omitempty"` StatusCode int32 `json:"statusCode,omitempty"` Type string `json:"type,omitempty"` } func (a APIException) Error() string { return fmt.Sprintf("%d: %s: %s", a.StatusCode, a.Type, a.Message) } // APIResponse defines model for apiResponse. type APIResponse struct { Exception *APIException `json:"exception,omitempty"` StatusCode int32 `json:"statusCode,omitempty"` } // DNSRecord defines model for dnsRecords. type DNSRecord struct { ID int64 `json:"id,omitempty"` Type string `json:"recordType,omitempty"` DomainID int64 `json:"domainId,omitempty"` DomainName string `json:"domainName,omitempty"` NodeName string `json:"nodeName,omitempty"` Hostname string `json:"hostname,omitempty"` State bool `json:"state,omitempty"` Content string `json:"content,omitempty"` TextData string `json:"textData,omitempty"` TTL int `json:"ttl,omitempty"` } // DNSHostname defines model for DNS.hostname. type DNSHostname struct { *APIException ID int64 `json:"id,omitempty"` DomainName string `json:"domainName,omitempty"` Hostname string `json:"hostname,omitempty"` Node string `json:"node,omitempty"` } // RecordsResponse defines model for recordsResponse. type RecordsResponse struct { *APIException DNSRecords []DNSRecord `json:"dnsRecords,omitempty"` } // RecordResponse defines model for recordResponse. type RecordResponse struct { *APIException DNSRecord } lego-4.9.1/providers/dns/easydns/000077500000000000000000000000001434020463500167235ustar00rootroot00000000000000lego-4.9.1/providers/dns/easydns/client.go000066400000000000000000000044321434020463500205330ustar00rootroot00000000000000package easydns import ( "bytes" "encoding/json" "fmt" "io" "net/http" "path" ) const defaultEndpoint = "https://rest.easydns.net" type zoneRecord struct { ID string `json:"id,omitempty"` Domain string `json:"domain"` Host string `json:"host"` TTL string `json:"ttl"` Prio string `json:"prio"` Type string `json:"type"` Rdata string `json:"rdata"` LastMod string `json:"last_mod,omitempty"` Revoked int `json:"revoked,omitempty"` NewHost string `json:"new_host,omitempty"` } type addRecordResponse struct { Msg string `json:"msg"` Tm int `json:"tm"` Data zoneRecord `json:"data"` Status int `json:"status"` } func (d *DNSProvider) addRecord(domain string, record interface{}) (string, error) { pathAdd := path.Join("/zones/records/add", domain, "TXT") response := &addRecordResponse{} err := d.doRequest(http.MethodPut, pathAdd, record, response) if err != nil { return "", err } recordID := response.Data.ID return recordID, nil } func (d *DNSProvider) deleteRecord(domain, recordID string) error { pathDelete := path.Join("/zones/records", domain, recordID) return d.doRequest(http.MethodDelete, pathDelete, nil, nil) } func (d *DNSProvider) doRequest(method, resource string, requestMsg, responseMsg interface{}) error { reqBody := &bytes.Buffer{} if requestMsg != nil { err := json.NewEncoder(reqBody).Encode(requestMsg) if err != nil { return err } } endpoint, err := d.config.Endpoint.Parse(resource + "?format=json") if err != nil { return err } request, err := http.NewRequest(method, endpoint.String(), reqBody) if err != nil { return err } request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.SetBasicAuth(d.config.Token, d.config.Key) response, err := d.config.HTTPClient.Do(request) if err != nil { return err } defer response.Body.Close() if response.StatusCode >= http.StatusBadRequest { body, err := io.ReadAll(response.Body) if err != nil { return fmt.Errorf("%d: failed to read response body: %w", response.StatusCode, err) } return fmt.Errorf("%d: request failed: %v", response.StatusCode, string(body)) } if responseMsg != nil { return json.NewDecoder(response.Body).Decode(responseMsg) } return nil } lego-4.9.1/providers/dns/easydns/easydns.go000066400000000000000000000113751434020463500207270ustar00rootroot00000000000000// Package easydns implements a DNS provider for solving the DNS-01 challenge using EasyDNS API. package easydns import ( "errors" "fmt" "net/http" "net/url" "strconv" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/miekg/dns" ) // Environment variables names. const ( envNamespace = "EASYDNS_" EnvEndpoint = envNamespace + "ENDPOINT" EnvToken = envNamespace + "TOKEN" EnvKey = envNamespace + "KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Endpoint *url.URL Token string Key string TTL int HTTPClient *http.Client PropagationTimeout time.Duration PollingInterval time.Duration SequenceInterval time.Duration } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config recordIDs map[string]string recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance. func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() endpoint, err := url.Parse(env.GetOrDefaultString(EnvEndpoint, defaultEndpoint)) if err != nil { return nil, fmt.Errorf("easydns: %w", err) } config.Endpoint = endpoint values, err := env.Get(EnvToken, EnvKey) if err != nil { return nil, fmt.Errorf("easydns: %w", err) } config.Token = values[EnvToken] config.Key = values[EnvKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for EasyDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("easydns: the configuration of the DNS provider is nil") } if config.Token == "" { return nil, errors.New("easydns: the API token is missing") } if config.Key == "" { return nil, errors.New("easydns: the API key is missing") } return &DNSProvider{config: config, recordIDs: map[string]string{}}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) apiHost, apiDomain := splitFqdn(fqdn) record := &zoneRecord{ Domain: apiDomain, Host: apiHost, Type: "TXT", Rdata: value, TTL: strconv.Itoa(d.config.TTL), Prio: "0", } recordID, err := d.addRecord(apiDomain, record) if err != nil { return fmt.Errorf("easydns: error adding zone record: %w", err) } key := getMapKey(fqdn, value) d.recordIDsMu.Lock() d.recordIDs[key] = recordID d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, challenge := dns01.GetRecord(domain, keyAuth) key := getMapKey(fqdn, challenge) recordID, exists := d.recordIDs[key] if !exists { return nil } _, apiDomain := splitFqdn(fqdn) err := d.deleteRecord(apiDomain, recordID) d.recordIDsMu.Lock() defer delete(d.recordIDs, key) d.recordIDsMu.Unlock() if err != nil { return fmt.Errorf("easydns: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Sequential All DNS challenges for this provider will be resolved sequentially. // Returns the interval between each iteration. func (d *DNSProvider) Sequential() time.Duration { return d.config.SequenceInterval } func splitFqdn(fqdn string) (host, domain string) { parts := dns.SplitDomainName(fqdn) length := len(parts) host = strings.Join(parts[0:length-2], ".") domain = strings.Join(parts[length-2:length], ".") return } func getMapKey(fqdn, value string) string { return fqdn + "|" + value } lego-4.9.1/providers/dns/easydns/easydns.toml000066400000000000000000000016701434020463500212720ustar00rootroot00000000000000Name = "EasyDNS" Description = '''''' URL = "https://easydns.com/" Code = "easydns" Since = "v2.6.0" Example = ''' EASYDNS_TOKEN= \ EASYDNS_KEY= \ lego --email you@example.com --dns easydns --domains my.example.org run ''' Additional = ''' To test with the sandbox environment set ```EASYDNS_ENDPOINT=https://sandbox.rest.easydns.net``` ''' [Configuration] [Configuration.Credentials] EASYDNS_TOKEN = "API Token" EASYDNS_KEY = "API Key" [Configuration.Additional] EASYDNS_ENDPOINT = "The endpoint URL of the API Server" EASYDNS_POLLING_INTERVAL = "Time between DNS propagation check" EASYDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" EASYDNS_SEQUENCE_INTERVAL = "Time between sequential requests" EASYDNS_TTL = "The TTL of the TXT record used for the DNS challenge" EASYDNS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://docs.sandbox.rest.easydns.net" lego-4.9.1/providers/dns/easydns/easydns_test.go000066400000000000000000000171301434020463500217610ustar00rootroot00000000000000package easydns import ( "fmt" "io" "net/http" "net/http/httptest" "net/url" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvEndpoint, EnvToken, EnvKey). WithDomain(envDomain) func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) endpoint, err := url.Parse(server.URL) require.NoError(t, err) config := NewDefaultConfig() config.Token = "TOKEN" config.Key = "SECRET" config.Endpoint = endpoint config.HTTPClient = server.Client() provider, err := NewDNSProviderConfig(config) require.NoError(t, err) return provider, mux } func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvToken: "TOKEN", EnvKey: "SECRET", }, }, { desc: "missing token", envVars: map[string]string{ EnvKey: "SECRET", }, expected: "easydns: some credentials information are missing: EASYDNS_TOKEN", }, { desc: "missing key", envVars: map[string]string{ EnvToken: "TOKEN", }, expected: "easydns: some credentials information are missing: EASYDNS_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string config *Config expected string }{ { desc: "success", config: &Config{ Token: "TOKEN", Key: "KEY", }, }, { desc: "nil config", config: nil, expected: "easydns: the configuration of the DNS provider is nil", }, { desc: "missing token", config: &Config{ Key: "KEY", }, expected: "easydns: the API token is missing", }, { desc: "missing key", config: &Config{ Token: "TOKEN", }, expected: "easydns: the API key is missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { p, err := NewDNSProviderConfig(test.config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestDNSProvider_Present(t *testing.T) { provider, mux := setupTest(t) mux.HandleFunc("/zones/records/add/example.com/TXT", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPut, r.Method, "method") assert.Equal(t, "format=json", r.URL.RawQuery, "query") assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type") assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get("Authorization"), "Authorization") reqBody, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } expectedReqBody := `{"domain":"example.com","host":"_acme-challenge","ttl":"120","prio":"0","type":"TXT","rdata":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"} ` assert.Equal(t, expectedReqBody, string(reqBody)) w.WriteHeader(http.StatusCreated) _, err = fmt.Fprintf(w, `{ "msg": "OK", "tm": 1554681934, "data": { "host": "_acme-challenge", "geozone_id": 0, "ttl": "120", "prio": "0", "rdata": "pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM", "revoked": 0, "id": "123456789", "new_host": "_acme-challenge.example.com" }, "status": 201 }`) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) err := provider.Present("example.com", "token", "keyAuth") require.NoError(t, err) require.Contains(t, provider.recordIDs, "_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM") } func TestDNSProvider_Cleanup_WhenRecordIdNotSet_NoOp(t *testing.T) { provider, _ := setupTest(t) err := provider.CleanUp("example.com", "token", "keyAuth") require.NoError(t, err) } func TestDNSProvider_Cleanup_WhenRecordIdSet_DeletesTxtRecord(t *testing.T) { provider, mux := setupTest(t) mux.HandleFunc("/zones/records/example.com/123456", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodDelete, r.Method, "method") assert.Equal(t, "format=json", r.URL.RawQuery, "query") assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get("Authorization"), "Authorization") w.WriteHeader(http.StatusOK) _, err := fmt.Fprintf(w, `{ "msg": "OK", "data": { "domain": "example.com", "id": "123456" }, "status": 200 }`) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) provider.recordIDs["_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"] = "123456" err := provider.CleanUp("example.com", "token", "keyAuth") require.NoError(t, err) } func TestDNSProvider_Cleanup_WhenHttpError_ReturnsError(t *testing.T) { provider, mux := setupTest(t) errorMessage := `{ "error": { "code": 406, "message": "Provided id is invalid or you do not have permission to access it." } }` mux.HandleFunc("/zones/records/example.com/123456", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodDelete, r.Method, "method") assert.Equal(t, "format=json", r.URL.RawQuery, "query") assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get("Authorization"), "Authorization") w.WriteHeader(http.StatusNotAcceptable) _, err := fmt.Fprint(w, errorMessage) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) provider.recordIDs["_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"] = "123456" err := provider.CleanUp("example.com", "token", "keyAuth") expectedError := fmt.Sprintf("easydns: 406: request failed: %v", errorMessage) require.EqualError(t, err, expectedError) } func TestSplitFqdn(t *testing.T) { testCases := []struct { desc string fqdn string expectedHost string expectedDomain string }{ { desc: "domain only", fqdn: "domain.com.", expectedHost: "", expectedDomain: "domain.com", }, { desc: "single-part host", fqdn: "_acme-challenge.domain.com.", expectedHost: "_acme-challenge", expectedDomain: "domain.com", }, { desc: "multi-part host", fqdn: "_acme-challenge.sub.domain.com.", expectedHost: "_acme-challenge.sub", expectedDomain: "domain.com", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { actualHost, actualDomain := splitFqdn(test.fqdn) require.Equal(t, test.expectedHost, actualHost) require.Equal(t, test.expectedDomain, actualDomain) }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/edgedns/000077500000000000000000000000001434020463500166665ustar00rootroot00000000000000lego-4.9.1/providers/dns/edgedns/edgedns.go000066400000000000000000000145041434020463500206320ustar00rootroot00000000000000// Package edgedns replaces fastdns, implementing a DNS provider for solving the DNS-01 challenge using Akamai EdgeDNS. package edgedns import ( "errors" "fmt" "strings" "time" configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2" "github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "AKAMAI_" EnvEdgeRc = envNamespace + "EDGERC" EnvEdgeRcSection = envNamespace + "EDGERC_SECTION" EnvHost = envNamespace + "HOST" EnvClientToken = envNamespace + "CLIENT_TOKEN" EnvClientSecret = envNamespace + "CLIENT_SECRET" EnvAccessToken = envNamespace + "ACCESS_TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) const ( defaultPropagationTimeout = 3 * time.Minute defaultPollInterval = 15 * time.Second ) const maxBody = 131072 // Config is used to configure the creation of the DNSProvider. type Config struct { edgegrid.Config PropagationTimeout time.Duration PollingInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollInterval), Config: edgegrid.Config{MaxBody: maxBody}, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config } // NewDNSProvider returns a DNSProvider instance configured for Akamai EdgeDNS: // Akamai credentials are automatically detected in the following locations and prioritized in the following order: // // 1. Section-specific environment variables `AKAMAI_{SECTION}_HOST`, `AKAMAI_{SECTION}_ACCESS_TOKEN`, `AKAMAI_{SECTION}_CLIENT_TOKEN`, `AKAMAI_{SECTION}_CLIENT_SECRET` where `{SECTION}` is specified using `AKAMAI_EDGERC_SECTION` // 2. If `AKAMAI_EDGERC_SECTION` is not defined or is set to `default`: Environment variables `AKAMAI_HOST`, `AKAMAI_ACCESS_TOKEN`, `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET` // 3. .edgerc file located at `AKAMAI_EDGERC` (defaults to `~/.edgerc`, sections can be specified using `AKAMAI_EDGERC_SECTION`) // 4. Default environment variables: `AKAMAI_HOST`, `AKAMAI_ACCESS_TOKEN`, `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET` // // See also: https://developer.akamai.com/api/getting-started func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() rcPath := env.GetOrDefaultString(EnvEdgeRc, "") rcSection := env.GetOrDefaultString(EnvEdgeRcSection, "") conf, err := edgegrid.Init(rcPath, rcSection) if err != nil { return nil, fmt.Errorf("edgedns: %w", err) } conf.MaxBody = maxBody config.Config = conf return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for EdgeDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("edgedns: the configuration of the DNS provider is nil") } configdns.Init(config.Config) return &DNSProvider{config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := findZone(fqdn) if err != nil { return fmt.Errorf("edgedns: %w", err) } record, err := configdns.GetRecord(zone, fqdn, "TXT") if err != nil && !isNotFound(err) { return fmt.Errorf("edgedns: %w", err) } if err == nil && record == nil { return fmt.Errorf("edgedns: unknown error") } if record != nil { log.Infof("TXT record already exists. Updating target") if containsValue(record.Target, value) { // have a record and have entry already return nil } record.Target = append(record.Target, `"`+value+`"`) record.TTL = d.config.TTL err = record.Update(zone) if err != nil { return fmt.Errorf("edgedns: %w", err) } return nil } record = &configdns.RecordBody{ Name: fqdn, RecordType: "TXT", TTL: d.config.TTL, Target: []string{`"` + value + `"`}, } err = record.Save(zone) if err != nil { return fmt.Errorf("edgedns: %w", err) } return nil } // CleanUp removes the record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := findZone(fqdn) if err != nil { return fmt.Errorf("edgedns: %w", err) } existingRec, err := configdns.GetRecord(zone, fqdn, "TXT") if err != nil { if isNotFound(err) { return nil } return fmt.Errorf("edgedns: %w", err) } if existingRec == nil { return fmt.Errorf("edgedns: unknown failure") } if len(existingRec.Target) == 0 { return fmt.Errorf("edgedns: TXT record is invalid") } if !containsValue(existingRec.Target, value) { return nil } var newRData []string for _, val := range existingRec.Target { val = strings.Trim(val, `"`) if val == value { continue } newRData = append(newRData, val) } if len(newRData) > 0 { existingRec.Target = newRData err = existingRec.Update(zone) if err != nil { return fmt.Errorf("edgedns: %w", err) } return nil } err = existingRec.Delete(zone) if err != nil { return fmt.Errorf("edgedns: %w", err) } return nil } func findZone(domain string) (string, error) { zone, err := dns01.FindZoneByFqdn(domain) if err != nil { return "", err } return dns01.UnFqdn(zone), nil } func containsValue(values []string, value string) bool { for _, val := range values { if strings.Trim(val, `"`) == value { return true } } return false } func isNotFound(err error) bool { if err == nil { return false } var e configdns.ConfigDNSError return errors.As(err, &e) && e.NotFound() } lego-4.9.1/providers/dns/edgedns/edgedns.toml000066400000000000000000000054271434020463500212040ustar00rootroot00000000000000Name = "Akamai EdgeDNS" Description = ''' Akamai edgedns supersedes FastDNS; implementing a DNS provider for solving the DNS-01 challenge using Akamai EdgeDNS ''' URL = "https://www.akamai.com/us/en/products/security/edge-dns.jsp" Code = "edgedns" Since = "v3.9.0" Example = ''' AKAMAI_CLIENT_SECRET=abcdefghijklmnopqrstuvwxyz1234567890ABCDEFG= \ AKAMAI_CLIENT_TOKEN=akab-mnbvcxzlkjhgfdsapoiuytrewq1234567 \ AKAMAI_HOST=akab-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.luna.akamaiapis.net \ AKAMAI_ACCESS_TOKEN=akab-1234567890qwerty-asdfghjklzxcvtnu \ lego --email you@example.com --dns edgedns --domains my.example.org run ''' Additional = ''' Akamai credentials are automatically detected in the following locations and prioritized in the following order: 1. Section-specific environment variables (where `{SECTION}` is specified using `AKAMAI_EDGERC_SECTION`): - `AKAMAI_{SECTION}_HOST` - `AKAMAI_{SECTION}_ACCESS_TOKEN` - `AKAMAI_{SECTION}_CLIENT_TOKEN` - `AKAMAI_{SECTION}_CLIENT_SECRET` 2. If `AKAMAI_EDGERC_SECTION` is not defined or is set to `default`, environment variables: - `AKAMAI_HOST` - `AKAMAI_ACCESS_TOKEN` - `AKAMAI_CLIENT_TOKEN` - `AKAMAI_CLIENT_SECRET` 3. `.edgerc` file located at `AKAMAI_EDGERC` - defaults to `~/.edgerc`, sections can be specified using `AKAMAI_EDGERC_SECTION` 4. Default environment variables: - `AKAMAI_HOST` - `AKAMAI_ACCESS_TOKEN` - `AKAMAI_CLIENT_TOKEN` - `AKAMAI_CLIENT_SECRET` See also: - [Setting up Akamai credentials](https://developer.akamai.com/api/getting-started) - [.edgerc Format](https://developer.akamai.com/legacy/introduction/Conf_Client.html#edgercformat) - [API Client Authentication](https://developer.akamai.com/legacy/introduction/Client_Auth.html) - [Config from Env](https://github.com/akamai/AkamaiOPEN-edgegrid-golang/blob/master/edgegrid/config.go#L118) ''' [Configuration] [Configuration.Credentials] AKAMAI_HOST = "API host, managed by the Akamai EdgeGrid client" AKAMAI_CLIENT_TOKEN = "Client token, managed by the Akamai EdgeGrid client" AKAMAI_CLIENT_SECRET = "Client secret, managed by the Akamai EdgeGrid client" AKAMAI_ACCESS_TOKEN = "Access token, managed by the Akamai EdgeGrid client" AKAMAI_EDGERC = "Path to the .edgerc file, managed by the Akamai EdgeGrid client" AKAMAI_EDGERC_SECTION = "Configuration section, managed by the Akamai EdgeGrid client" [Configuration.Additional] AKAMAI_POLLING_INTERVAL = "Time between DNS propagation check. Default: 15 seconds" AKAMAI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation. Default: 3 minutes" AKAMAI_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://developer.akamai.com/api/cloud_security/edge_dns_zone_management/v2.html" GoClient = "https://github.com/akamai/AkamaiOPEN-edgegrid-golang" lego-4.9.1/providers/dns/edgedns/edgedns_integration_test.go000066400000000000000000000034451434020463500242760ustar00rootroot00000000000000package edgedns import ( "fmt" "testing" "time" configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) // Present Twice to handle create / update err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveTTL(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) domain := envTest.GetDomain() err = provider.Present(domain, "foo", "bar") require.NoError(t, err) defer func() { e := provider.CleanUp(domain, "foo", "bar") if e != nil { t.Log(e) } }() fqdn := "_acme-challenge." + domain + "." zone, err := findZone(fqdn) require.NoError(t, err) resourceRecordSets, err := configdns.GetRecordList(zone, fqdn, "TXT") require.NoError(t, err) for i, rrset := range resourceRecordSets.Recordsets { if rrset.Name != fqdn { continue } t.Run(fmt.Sprintf("testing record set %d", i), func(t *testing.T) { assert.Equal(t, rrset.Name, fqdn) assert.Equal(t, rrset.Type, "TXT") assert.Equal(t, rrset.TTL, dns01.DefaultTTL) }) } } lego-4.9.1/providers/dns/edgedns/edgedns_test.go000066400000000000000000000135311434020463500216700ustar00rootroot00000000000000package edgedns import ( "os" "testing" "time" configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2" "github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const ( envDomain = envNamespace + "TEST_DOMAIN" envTestHost = envNamespace + "TEST_HOST" envTestClientToken = envNamespace + "TEST_CLIENT_TOKEN" envTestClientSecret = envNamespace + "TEST_CLIENT_SECRET" envTestAccessToken = envNamespace + "TEST_ACCESS_TOKEN" ) var envTest = tester.NewEnvTest( EnvHost, EnvClientToken, EnvClientSecret, EnvAccessToken, EnvEdgeRc, EnvEdgeRcSection, envTestHost, envTestClientToken, envTestClientSecret, envTestAccessToken). WithDomain(envDomain). WithLiveTestRequirements(EnvHost, EnvClientToken, EnvClientSecret, EnvAccessToken, envDomain) func TestNewDNSProvider_FromEnv(t *testing.T) { testCases := []struct { desc string envVars map[string]string expectedConfig *edgegrid.Config expectedErr string }{ { desc: "success", envVars: map[string]string{ EnvHost: "akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", EnvClientToken: "akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", EnvClientSecret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", EnvAccessToken: "akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", }, expectedConfig: &edgegrid.Config{ Host: "akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", ClientToken: "akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", ClientSecret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", AccessToken: "akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", MaxBody: maxBody, }, }, { desc: "with section", envVars: map[string]string{ EnvEdgeRcSection: "test", envTestHost: "akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", envTestClientToken: "akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", envTestClientSecret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", envTestAccessToken: "akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", }, expectedConfig: &edgegrid.Config{ Host: "akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net", ClientToken: "akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", ClientSecret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", AccessToken: "akac-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx", MaxBody: maxBody, }, }, { desc: "missing credentials", expectedErr: "edgedns: Unable to create instance using environment or .edgerc file", }, { desc: "missing host", envVars: map[string]string{ EnvHost: "", EnvClientToken: "B", EnvClientSecret: "C", EnvAccessToken: "D", }, expectedErr: "edgedns: Unable to create instance using environment or .edgerc file", }, { desc: "missing client token", envVars: map[string]string{ EnvHost: "A", EnvClientToken: "", EnvClientSecret: "C", EnvAccessToken: "D", }, expectedErr: "edgedns: Fatal missing required environment variables: [AKAMAI_CLIENT_TOKEN]", }, { desc: "missing client secret", envVars: map[string]string{ EnvHost: "A", EnvClientToken: "B", EnvClientSecret: "", EnvAccessToken: "D", }, expectedErr: "edgedns: Fatal missing required environment variables: [AKAMAI_CLIENT_SECRET]", }, { desc: "missing access token", envVars: map[string]string{ EnvHost: "A", EnvClientToken: "B", EnvClientSecret: "C", EnvAccessToken: "", }, expectedErr: "edgedns: Fatal missing required environment variables: [AKAMAI_ACCESS_TOKEN]", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() if test.envVars == nil { test.envVars = map[string]string{} } test.envVars[EnvEdgeRc] = "/dev/null" envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expectedErr != "" { require.EqualError(t, err, test.expectedErr) return } require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) if test.expectedConfig != nil { require.Equal(t, *test.expectedConfig, configdns.Config) } }) } } func TestDNSProvider_findZone(t *testing.T) { testCases := []struct { desc string domain string expected string }{ { desc: "Extract root record name", domain: "bar.com.", expected: "bar.com", }, { desc: "Extract sub record name", domain: "foo.bar.com.", expected: "bar.com", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() zone, err := findZone(test.domain) require.NoError(t, err) require.Equal(t, test.expected, zone) }) } } func TestNewDefaultConfig(t *testing.T) { defer envTest.RestoreEnv() testCases := []struct { desc string envVars map[string]string expected *Config }{ { desc: "default configuration", expected: &Config{ TTL: dns01.DefaultTTL, PropagationTimeout: 3 * time.Minute, PollingInterval: 15 * time.Second, Config: edgegrid.Config{ MaxBody: maxBody, }, }, }, { desc: "custom values", envVars: map[string]string{ EnvTTL: "99", EnvPropagationTimeout: "60", EnvPollingInterval: "60", }, expected: &Config{ TTL: 99, PropagationTimeout: 60 * time.Second, PollingInterval: 60 * time.Second, Config: edgegrid.Config{ MaxBody: maxBody, }, }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { envTest.ClearEnv() for key, value := range test.envVars { os.Setenv(key, value) } config := NewDefaultConfig() require.Equal(t, test.expected, config) }) } } lego-4.9.1/providers/dns/epik/000077500000000000000000000000001434020463500162055ustar00rootroot00000000000000lego-4.9.1/providers/dns/epik/epik.go000066400000000000000000000077421434020463500174760ustar00rootroot00000000000000// Package epik implements a DNS provider for solving the DNS-01 challenge using Epik. package epik import ( "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/epik/internal" ) // Environment variables names. const ( envNamespace = "EPIK_" EnvSignature = envNamespace + "SIGNATURE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Signature string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 3600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for Epik. // Credentials must be passed in the environment variable: EPIK_SIGNATURE. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvSignature) if err != nil { return nil, fmt.Errorf("epik: %w", err) } config := NewDefaultConfig() config.Signature = values[EnvSignature] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Epik. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("epik: the configuration of the DNS provider is nil") } if config.Signature == "" { return nil, errors.New("epik: missing credentials") } client := internal.NewClient(config.Signature) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) // find authZone authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("epik: %w", err) } record := internal.RecordRequest{ Host: dns01.UnFqdn(strings.TrimSuffix(fqdn, authZone)), Type: "TXT", Data: value, TTL: d.config.TTL, } _, err = d.client.CreateHostRecord(dns01.UnFqdn(authZone), record) if err != nil { return fmt.Errorf("epik: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) // find authZone authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("epik: %w", err) } dom := dns01.UnFqdn(authZone) host := dns01.UnFqdn(strings.TrimSuffix(fqdn, authZone)) records, err := d.client.GetDNSRecords(dom) if err != nil { return fmt.Errorf("epik: %w", err) } for _, record := range records { if strings.EqualFold(record.Type, "TXT") && record.Data == value && record.Name == host { _, err = d.client.RemoveHostRecord(dom, record.ID) if err != nil { return fmt.Errorf("epik: %w", err) } } } return nil } lego-4.9.1/providers/dns/epik/epik.toml000066400000000000000000000013031434020463500200270ustar00rootroot00000000000000Name = "Epik" Description = '''''' URL = "https://www.epik.com/" Code = "epik" Since = "v4.5.0" Example = ''' EPIK_SIGNATURE=xxxxxxxxxxxxxxxxxxxxxxxxxx \ lego --email you@example.com --dns epik --domains my.example.org run ''' [Configuration] [Configuration.Credentials] EPIK_SIGNATURE = "Epik API signature (https://registrar.epik.com/account/api-settings/)" [Configuration.Additional] EPIK_POLLING_INTERVAL = "Time between DNS propagation check" EPIK_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" EPIK_TTL = "The TTL of the TXT record used for the DNS challenge" EPIK_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://docs.userapi.epik.com/v2/#/" lego-4.9.1/providers/dns/epik/epik_test.go000066400000000000000000000043171434020463500205300ustar00rootroot00000000000000package epik import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvSignature).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvSignature: "secret", }, }, { desc: "missing credentials", envVars: map[string]string{}, expected: "epik: some credentials information are missing: EPIK_SIGNATURE", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string signature string expected string }{ { desc: "success", signature: "A", }, { desc: "missing credentials", expected: "epik: missing credentials", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Signature = test.signature p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/epik/internal/000077500000000000000000000000001434020463500200215ustar00rootroot00000000000000lego-4.9.1/providers/dns/epik/internal/client.go000066400000000000000000000072461434020463500216370ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "path" "time" ) const defaultBaseURL = "https://usersapiv2.epik.com/v2" type Client struct { HTTPClient *http.Client baseURL *url.URL signature string } func NewClient(signature string) *Client { baseURL, _ := url.Parse(defaultBaseURL) return &Client{ HTTPClient: &http.Client{Timeout: 5 * time.Second}, baseURL: baseURL, signature: signature, } } // GetDNSRecords gets DNS records for a domain. // https://docs.userapi.epik.com/v2/#/DNS%20Host%20Records/getDnsRecord func (c Client) GetDNSRecords(domain string) ([]Record, error) { resp, err := c.do(http.MethodGet, domain, url.Values{}, nil) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() all, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read request body (%d): %w", resp.StatusCode, err) } err = checkError(resp.StatusCode, all) if err != nil { return nil, err } var data GetDNSRecordResponse err = json.Unmarshal(all, &data) if err != nil { return nil, fmt.Errorf("failed to unmarshal request body (%d): %s", resp.StatusCode, string(all)) } return data.Data.Records, nil } // CreateHostRecord creates a record for a domain. // https://docs.userapi.epik.com/v2/#/DNS%20Host%20Records/createHostRecord func (c Client) CreateHostRecord(domain string, record RecordRequest) (*Data, error) { payload := CreateHostRecords{Payload: record} body, err := json.Marshal(payload) if err != nil { return nil, err } resp, err := c.do(http.MethodPost, domain, url.Values{}, bytes.NewReader(body)) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() all, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read request body (%d): %w", resp.StatusCode, err) } err = checkError(resp.StatusCode, all) if err != nil { return nil, err } var data Data err = json.Unmarshal(all, &data) if err != nil { return nil, fmt.Errorf("%d: %s", resp.StatusCode, string(all)) } return &data, nil } // RemoveHostRecord removes a record for a domain. // https://docs.userapi.epik.com/v2/#/DNS%20Host%20Records/removeHostRecord func (c Client) RemoveHostRecord(domain string, recordID string) (*Data, error) { params := url.Values{} params.Set("ID", recordID) resp, err := c.do(http.MethodDelete, domain, params, nil) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() all, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read request body (%d): %w", resp.StatusCode, err) } err = checkError(resp.StatusCode, all) if err != nil { return nil, err } var data Data err = json.Unmarshal(all, &data) if err != nil { return nil, fmt.Errorf("%d: %s", resp.StatusCode, string(all)) } return &data, nil } func (c *Client) do(method, domain string, params url.Values, body io.Reader) (*http.Response, error) { endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "domains", domain, "records")) if err != nil { return nil, fmt.Errorf("create endpoint: %w", err) } params.Set("SIGNATURE", c.signature) endpoint.RawQuery = params.Encode() req, err := http.NewRequest(method, endpoint.String(), body) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") if body != nil { req.Header.Set("Content-Type", "application/json") } return c.HTTPClient.Do(req) } func checkError(statusCode int, all []byte) error { if statusCode == http.StatusOK { return nil } var apiErr APIError err := json.Unmarshal(all, &apiErr) if err != nil { return fmt.Errorf("%d: %s", statusCode, string(all)) } return &apiErr } lego-4.9.1/providers/dns/epik/internal/client_test.go000066400000000000000000000103001434020463500226570ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupTest(t *testing.T) (*http.ServeMux, *Client) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient("secret") client.HTTPClient = server.Client() client.baseURL, _ = url.Parse(server.URL) return mux, client } func TestClient_GetDNSRecords(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodGet, http.StatusOK, "getDnsRecord.json")) records, err := client.GetDNSRecords("example.com") require.NoError(t, err) expected := []Record{ { ID: "abc123", Name: "www", Type: "CAA", Data: "1 issue letsencrypt.org", AUX: 0, TTL: 300, }, { ID: "abc123", Name: "www", Type: "A", Data: "192.64.147.249", AUX: 0, TTL: 300, }, { ID: "abc123", Name: "*", Type: "A", Data: "192.64.147.249", AUX: 0, TTL: 300, }, { ID: "abc123", Type: "CAA", Data: "0 issue trust-provider.com", AUX: 0, TTL: 300, }, { ID: "abc123", Type: "CAA", Data: "1 issue letsencrypt.org", AUX: 0, TTL: 300, }, { ID: "abc123", Type: "A", Data: "192.64.147.249", AUX: 0, TTL: 300, }, } assert.Equal(t, expected, records) } func TestClient_GetDNSRecords_error(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodGet, http.StatusUnauthorized, "error.json")) _, err := client.GetDNSRecords("example.com") assert.Error(t, err) } func TestClient_CreateHostRecord(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodPost, http.StatusOK, "createHostRecord.json")) record := RecordRequest{ Host: "www2", Type: "A", Data: "192.64.147.249", Aux: 0, TTL: 300, } data, err := client.CreateHostRecord("example.com", record) require.NoError(t, err) expected := &Data{ Code: 1000, Message: "Command completed successfully.", } assert.Equal(t, expected, data) } func TestClient_CreateHostRecord_error(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodPost, http.StatusUnauthorized, "error.json")) record := RecordRequest{ Host: "www2", Type: "A", Data: "192.64.147.249", Aux: 0, TTL: 300, } _, err := client.CreateHostRecord("example.com", record) assert.Error(t, err) } func TestClient_RemoveHostRecord(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodDelete, http.StatusOK, "removeHostRecord.json")) data, err := client.RemoveHostRecord("example.com", "abc123") require.NoError(t, err) expected := &Data{ Code: 1000, Message: "Command completed successfully.", } assert.Equal(t, expected, data) } func TestClient_RemoveHostRecord_error(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodDelete, http.StatusUnauthorized, "error.json")) _, err := client.RemoveHostRecord("example.com", "abc123") assert.Error(t, err) } func testHandler(method string, statusCode int, filename string) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { if req.Method != method { http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed) return } auth := req.URL.Query().Get("SIGNATURE") if auth != "secret" { http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) return } rw.WriteHeader(statusCode) if statusCode == http.StatusNoContent { return } file, err := os.Open(filepath.Join("fixtures", filename)) if err != nil { http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) return } } } lego-4.9.1/providers/dns/epik/internal/fixtures/000077500000000000000000000000001434020463500216725ustar00rootroot00000000000000lego-4.9.1/providers/dns/epik/internal/fixtures/createHostRecord.json000066400000000000000000000001321434020463500260210ustar00rootroot00000000000000{ "code": 1000, "message": "Command completed successfully.", "description": null } lego-4.9.1/providers/dns/epik/internal/fixtures/error.json000066400000000000000000000002431434020463500237150ustar00rootroot00000000000000{ "errors": [ { "code": 1, "message": "Unauthorized", "description": "Unauthorized: Signature was not provided or was invalid" } ] } lego-4.9.1/providers/dns/epik/internal/fixtures/getDnsRecord.json000066400000000000000000000020311434020463500251440ustar00rootroot00000000000000{ "data": { "name": "MYDOMAIN.ORG", "code": 1000, "records": [ { "id": "abc123", "name": "www", "type": "CAA", "data": "1 issue letsencrypt.org", "aux": 0, "ttl": 300 }, { "id": "abc123", "name": "www", "type": "A", "data": "192.64.147.249", "aux": 0, "ttl": 300 }, { "id": "abc123", "name": "*", "type": "A", "data": "192.64.147.249", "aux": 0, "ttl": 300 }, { "id": "abc123", "name": "", "type": "CAA", "data": "0 issue trust-provider.com", "aux": 0, "ttl": 300 }, { "id": "abc123", "name": "", "type": "CAA", "data": "1 issue letsencrypt.org", "aux": 0, "ttl": 300 }, { "id": "abc123", "name": "", "type": "A", "data": "192.64.147.249", "aux": 0, "ttl": 300 } ] } } lego-4.9.1/providers/dns/epik/internal/fixtures/removeHostRecord.json000066400000000000000000000001321434020463500260530ustar00rootroot00000000000000{ "code": 1000, "message": "Command completed successfully.", "description": null } lego-4.9.1/providers/dns/epik/internal/types.go000066400000000000000000000023711434020463500215170ustar00rootroot00000000000000package internal import ( "fmt" "strings" ) type RecordRequest struct { Host string `json:"HOST,omitempty"` Type string `json:"TYPE,omitempty"` Data string `json:"DATA,omitempty"` Aux int `json:"AUX,omitempty"` TTL int `json:"TTL,omitempty"` } type SetHostRecords struct { Payload []RecordRequest `json:"set_host_records_payload"` } type CreateHostRecords struct { Payload RecordRequest `json:"create_host_records_payload"` } type Data struct { Code int `json:"code,omitempty"` Message string `json:"message,omitempty"` Description string `json:"description,omitempty"` } type APIError struct { Errors []Data `json:"errors"` } func (a APIError) Error() string { var parts []string for _, data := range a.Errors { parts = append(parts, fmt.Sprintf("code: %d, message: %s, description: %s", data.Code, data.Message, data.Description)) } return strings.Join(parts, ", ") } type Record struct { ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` Data string `json:"data"` AUX int `json:"aux"` TTL int `json:"ttl"` } type GetDNSRecordResponse struct { Data struct { Name string `json:"name"` Code int `json:"code"` Records []Record `json:"records"` } `json:"data"` } lego-4.9.1/providers/dns/exec/000077500000000000000000000000001434020463500162015ustar00rootroot00000000000000lego-4.9.1/providers/dns/exec/exec.go000066400000000000000000000070401434020463500174550ustar00rootroot00000000000000// Package exec implements a DNS provider which runs a program for adding/removing the DNS record. package exec import ( "errors" "fmt" "os" "os/exec" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "EXEC_" EnvPath = envNamespace + "PATH" EnvMode = envNamespace + "MODE" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) // Config Provider configuration. type Config struct { Program string Mode string PropagationTimeout time.Duration PollingInterval time.Duration SequenceInterval time.Duration } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config } // NewDNSProvider returns a new DNS provider which runs the program in the // environment variable EXEC_PATH for adding and removing the DNS record. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvPath) if err != nil { return nil, fmt.Errorf("exec: %w", err) } config := NewDefaultConfig() config.Program = values[EnvPath] config.Mode = os.Getenv(EnvMode) return NewDNSProviderConfig(config) } // NewDNSProviderConfig returns a new DNS provider which runs the given configuration // for adding and removing the DNS record. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("the configuration is nil") } return &DNSProvider{config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { var args []string if d.config.Mode == "RAW" { args = []string{"present", "--", domain, token, keyAuth} } else { fqdn, value := dns01.GetRecord(domain, keyAuth) args = []string{"present", fqdn, value} } cmd := exec.Command(d.config.Program, args...) output, err := cmd.CombinedOutput() if len(output) > 0 { log.Println(string(output)) } return err } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { var args []string if d.config.Mode == "RAW" { args = []string{"cleanup", "--", domain, token, keyAuth} } else { fqdn, value := dns01.GetRecord(domain, keyAuth) args = []string{"cleanup", fqdn, value} } cmd := exec.Command(d.config.Program, args...) output, err := cmd.CombinedOutput() if len(output) > 0 { log.Println(string(output)) } return err } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Sequential All DNS challenges for this provider will be resolved sequentially. // Returns the interval between each iteration. func (d *DNSProvider) Sequential() time.Duration { return d.config.SequenceInterval } lego-4.9.1/providers/dns/exec/exec.toml000066400000000000000000000075021434020463500200260ustar00rootroot00000000000000Name = "External program" Description = "Solving the DNS-01 challenge using an external program." URL = "/dns/exec" Code = "exec" Since = "v0.5.0" Example = ''' EXEC_PATH=/the/path/to/myscript.sh \ lego --email you@example.com --dns exec --domains my.example.org run ''' Additional = ''' ## Base Configuration | Environment Variable Name | Description | |---------------------------|---------------------------------------| | `EXEC_MODE` | `RAW`, none | | `EXEC_PATH` | The path of the the external program. | ## Additional Configuration | Environment Variable Name | Description | |----------------------------|-------------------------------------------| | `EXEC_POLLING_INTERVAL` | Time between DNS propagation check. | | `EXEC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation. | | `EXEC_SEQUENCE_INTERVAL` | Time between sequential requests. | ## Description The file name of the external program is specified in the environment variable `EXEC_PATH`. When it is run by lego, three command-line parameters are passed to it: The action ("present" or "cleanup"), the fully-qualified domain name and the value for the record. For example, requesting a certificate for the domain 'my.example.org' can be achieved by calling lego as follows: ```bash EXEC_PATH=./update-dns.sh \ lego --email you@example.com \ --dns exec \ --domains my.example.org run ``` It will then call the program './update-dns.sh' with like this: ```bash ./update-dns.sh "present" "_acme-challenge.my.example.org." "MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI" ``` The program then needs to make sure the record is inserted. When it returns an error via a non-zero exit code, lego aborts. When the record is to be removed again, the program is called with the first command-line parameter set to `cleanup` instead of `present`. If you want to use the raw domain, token, and keyAuth values with your program, you can set `EXEC_MODE=RAW`: ```bash EXEC_MODE=RAW \ EXEC_PATH=./update-dns.sh \ lego --email you@example.com \ --dns exec \ --domains my.example.org run ``` It will then call the program `./update-dns.sh` like this: ```bash ./update-dns.sh "present" "my.example.org." "--" "some-token" "KxAy-J3NwUmg9ZQuM-gP_Mq1nStaYSaP9tYQs5_-YsE.ksT-qywTd8058G-SHHWA3RAN72Pr0yWtPYmmY5UBpQ8" ``` ## Commands {{% notice note %}} The `--` is because the token MAY start with a `-`, and the called program may try and interpret a `-` as indicating a flag. In the case of urfave, which is commonly used, you can use the `--` delimiter to specify the start of positional arguments, and handle such a string safely. {{% /notice %}} ### Present | Mode | Command | |---------|----------------------------------------------------| | default | `myprogram present -- ` | | `RAW` | `myprogram present -- ` | ### Cleanup | Mode | Command | |---------|----------------------------------------------------| | default | `myprogram cleanup -- ` | | `RAW` | `myprogram cleanup -- ` | ### Timeout The command have to display propagation timeout and polling interval into Stdout. The values must be formatted as JSON, and times are in seconds. Example: `{"timeout": 30, "interval": 5}` If an error occurs or if the command is not provided: the default display propagation timeout and polling interval are used. | Mode | Command | |---------|----------------------------------------------------| | default | `myprogram timeout` | | `RAW` | `myprogram timeout` | ''' lego-4.9.1/providers/dns/exec/exec_test.go000066400000000000000000000057721434020463500205260ustar00rootroot00000000000000package exec import ( "fmt" "os" "strings" "testing" "github.com/go-acme/lego/v4/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) func TestDNSProvider_Present(t *testing.T) { backupLogger := log.Logger defer func() { log.Logger = backupLogger }() logRecorder := &LogRecorder{} log.Logger = logRecorder type expected struct { args string error bool } testCases := []struct { desc string config *Config expected expected }{ { desc: "Standard mode", config: &Config{ Program: "echo", Mode: "", }, expected: expected{ args: "present _acme-challenge.domain. pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM", }, }, { desc: "program error", config: &Config{ Program: "ogellego", Mode: "", }, expected: expected{error: true}, }, { desc: "Raw mode", config: &Config{ Program: "echo", Mode: "RAW", }, expected: expected{ args: "present -- domain token keyAuth", }, }, } var message string logRecorder.On("Println", mock.Anything).Run(func(args mock.Arguments) { message = args.String(0) fmt.Fprintln(os.Stdout, "XXX", message) }) for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { message = "" provider, err := NewDNSProviderConfig(test.config) require.NoError(t, err) err = provider.Present("domain", "token", "keyAuth") if test.expected.error { require.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, test.expected.args, strings.TrimSpace(message)) } }) } } func TestDNSProvider_CleanUp(t *testing.T) { backupLogger := log.Logger defer func() { log.Logger = backupLogger }() logRecorder := &LogRecorder{} log.Logger = logRecorder type expected struct { args string error bool } testCases := []struct { desc string config *Config expected expected }{ { desc: "Standard mode", config: &Config{ Program: "echo", Mode: "", }, expected: expected{ args: "cleanup _acme-challenge.domain. pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM", }, }, { desc: "program error", config: &Config{ Program: "ogellego", Mode: "", }, expected: expected{error: true}, }, { desc: "Raw mode", config: &Config{ Program: "echo", Mode: "RAW", }, expected: expected{ args: "cleanup -- domain token keyAuth", }, }, } var message string logRecorder.On("Println", mock.Anything).Run(func(args mock.Arguments) { message = args.String(0) fmt.Fprintln(os.Stdout, "XXX", message) }) for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { message = "" provider, err := NewDNSProviderConfig(test.config) require.NoError(t, err) err = provider.CleanUp("domain", "token", "keyAuth") if test.expected.error { require.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, test.expected.args, strings.TrimSpace(message)) } }) } } lego-4.9.1/providers/dns/exec/log_mock_test.go000066400000000000000000000011041434020463500213550ustar00rootroot00000000000000package exec import "github.com/stretchr/testify/mock" type LogRecorder struct { mock.Mock } func (*LogRecorder) Fatal(args ...interface{}) { panic("implement me") } func (*LogRecorder) Fatalln(args ...interface{}) { panic("implement me") } func (*LogRecorder) Fatalf(format string, args ...interface{}) { panic("implement me") } func (*LogRecorder) Print(args ...interface{}) { panic("implement me") } func (l *LogRecorder) Println(args ...interface{}) { l.Called(args...) } func (*LogRecorder) Printf(format string, args ...interface{}) { panic("implement me") } lego-4.9.1/providers/dns/exoscale/000077500000000000000000000000001434020463500170605ustar00rootroot00000000000000lego-4.9.1/providers/dns/exoscale/exoscale.go000066400000000000000000000160741434020463500212220ustar00rootroot00000000000000// Package exoscale implements a DNS provider for solving the DNS-01 challenge using Exoscale DNS. package exoscale import ( "context" "errors" "fmt" "time" egoscale "github.com/exoscale/egoscale/v2" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Default Exoscale API endpoint. const defaultBaseURL = "https://api.exoscale.com/v2" // Default Exosacle API zone. // Each data center location hosts the API and API zone determines which one to connect to. const defaultAPIZone = "ch-gva-2" // Environment variables names. const ( envNamespace = "EXOSCALE_" EnvAPISecret = envNamespace + "API_SECRET" EnvAPIKey = envNamespace + "API_KEY" EnvEndpoint = envNamespace + "ENDPOINT" EnvAPIZone = envNamespace + "API_ZONE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string APISecret string Endpoint string HTTPTimeout time.Duration PropagationTimeout time.Duration PollingInterval time.Duration TTL int64 } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: int64(env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL)), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *egoscale.Client apiZone string } // NewDNSProvider Credentials must be passed in the environment variables: // EXOSCALE_API_KEY, EXOSCALE_API_SECRET, EXOSCALE_ENDPOINT. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey, EnvAPISecret) if err != nil { return nil, fmt.Errorf("exoscale: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] config.APISecret = values[EnvAPISecret] config.Endpoint = env.GetOrFile(EnvEndpoint) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Exoscale. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("exoscale: the configuration of the DNS provider is nil") } if config.APIKey == "" || config.APISecret == "" { return nil, errors.New("exoscale: credentials missing") } if config.Endpoint == "" { config.Endpoint = defaultBaseURL } client, err := egoscale.NewClient( config.APIKey, config.APISecret, egoscale.ClientOptWithAPIEndpoint(config.Endpoint), egoscale.ClientOptWithTimeout(config.HTTPTimeout), ) if err != nil { return nil, fmt.Errorf("exoscale: initializing client: %w", err) } return &DNSProvider{ client: client, config: config, apiZone: env.GetOrDefaultString(EnvAPIZone, defaultAPIZone), }, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { ctx := context.Background() fqdn, value := dns01.GetRecord(domain, keyAuth) zoneName, recordName, err := d.findZoneAndRecordName(fqdn) if err != nil { return err } zone, err := d.findExistingZone(zoneName) if err != nil { return fmt.Errorf("exoscale: %w", err) } if zone == nil { return fmt.Errorf("exoscale: zone %q not found", zoneName) } recordID, err := d.findExistingRecordID(*zone.ID, recordName) if err != nil { return fmt.Errorf("exoscale: %w", err) } recordType := "TXT" if recordID == "" { record := egoscale.DNSDomainRecord{ Name: &recordName, TTL: &d.config.TTL, Content: &value, Type: &recordType, } _, err = d.client.CreateDNSDomainRecord(ctx, d.apiZone, *zone.ID, &record) if err != nil { return fmt.Errorf("exoscale: error while creating DNS record: %w", err) } return nil } record := egoscale.DNSDomainRecord{ ID: &recordID, Name: &recordName, TTL: &d.config.TTL, Content: &value, Type: &recordType, } err = d.client.UpdateDNSDomainRecord(ctx, d.apiZone, *zone.ID, &record) if err != nil { return fmt.Errorf("exoscale: error while updating DNS record: %w", err) } return nil } // CleanUp removes the record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { ctx := context.Background() fqdn, _ := dns01.GetRecord(domain, keyAuth) zoneName, recordName, err := d.findZoneAndRecordName(fqdn) if err != nil { return err } zone, err := d.findExistingZone(zoneName) if err != nil { return fmt.Errorf("exoscale: %w", err) } if zone == nil { return fmt.Errorf("exoscale: zone %q not found", zoneName) } recordID, err := d.findExistingRecordID(*zone.ID, recordName) if err != nil { return err } if recordID != "" { err = d.client.DeleteDNSDomainRecord(ctx, d.apiZone, *zone.ID, &egoscale.DNSDomainRecord{ID: &recordID}) if err != nil { return fmt.Errorf("exoscale: error while deleting DNS record: %w", err) } } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // findExistingZone Query Exoscale to find an existing zone for this name. // Returns nil result if no zone could be found. func (d *DNSProvider) findExistingZone(zoneName string) (*egoscale.DNSDomain, error) { ctx := context.Background() zones, err := d.client.ListDNSDomains(ctx, d.apiZone) if err != nil { return nil, fmt.Errorf("error while retrieving DNS zones: %w", err) } for _, zone := range zones { if zone.UnicodeName != nil && *zone.UnicodeName == zoneName { return &zone, nil } } return nil, nil } // findExistingRecordID Query Exoscale to find an existing record for this name. // Returns empty result if no record could be found. func (d *DNSProvider) findExistingRecordID(zoneID, recordName string) (string, error) { ctx := context.Background() records, err := d.client.ListDNSDomainRecords(ctx, d.apiZone, zoneID) if err != nil { return "", fmt.Errorf("error while retrieving DNS records: %w", err) } recordType := "TXT" for _, record := range records { if record.Name != nil && *record.Name == recordName && record.Type != nil && *record.Type == recordType { return *record.ID, nil } } return "", nil } // findZoneAndRecordName Extract DNS zone and DNS entry name. func (d *DNSProvider) findZoneAndRecordName(fqdn string) (string, string, error) { zone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", "", err } zone = dns01.UnFqdn(zone) name := dns01.UnFqdn(fqdn) name = name[:len(name)-len("."+zone)] return zone, name, nil } lego-4.9.1/providers/dns/exoscale/exoscale.toml000066400000000000000000000015701434020463500215630ustar00rootroot00000000000000Name = "Exoscale" Description = '''''' URL = "https://www.exoscale.com/" Code = "exoscale" Since = "v0.4.0" Example = ''' EXOSCALE_API_KEY=abcdefghijklmnopqrstuvwx \ EXOSCALE_API_SECRET=xxxxxxx \ lego --email you@example.com --dns exoscale --domains my.example.org run ''' [Configuration] [Configuration.Credentials] EXOSCALE_API_KEY = "API key" EXOSCALE_API_SECRET = "API secret" [Configuration.Additional] EXOSCALE_ENDPOINT = "API endpoint URL" EXOSCALE_API_ZONE = "API zone" EXOSCALE_POLLING_INTERVAL = "Time between DNS propagation check" EXOSCALE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" EXOSCALE_TTL = "The TTL of the TXT record used for the DNS challenge" EXOSCALE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://openapi-v2.exoscale.com/#endpoint-dns" GoClient = "https://github.com/exoscale/egoscale" lego-4.9.1/providers/dns/exoscale/exoscale_test.go000066400000000000000000000103711434020463500222530ustar00rootroot00000000000000package exoscale import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPISecret, EnvAPIKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", EnvAPISecret: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIKey: "", EnvAPISecret: "", }, expected: "exoscale: some credentials information are missing: EXOSCALE_API_KEY,EXOSCALE_API_SECRET", }, { desc: "missing access key", envVars: map[string]string{ EnvAPIKey: "", EnvAPISecret: "456", }, expected: "exoscale: some credentials information are missing: EXOSCALE_API_KEY", }, { desc: "missing secret key", envVars: map[string]string{ EnvAPIKey: "123", EnvAPISecret: "", }, expected: "exoscale: some credentials information are missing: EXOSCALE_API_SECRET", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string apiSecret string expected string }{ { desc: "success", apiKey: "123", apiSecret: "456", }, { desc: "missing credentials", expected: "exoscale: credentials missing", }, { desc: "missing api key", apiSecret: "456", expected: "exoscale: credentials missing", }, { desc: "missing secret key", apiKey: "123", expected: "exoscale: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.APISecret = test.apiSecret p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestDNSProvider_FindZoneAndRecordName(t *testing.T) { config := NewDefaultConfig() config.APIKey = "example@example.com" config.APISecret = "123" provider, err := NewDNSProviderConfig(config) require.NoError(t, err) type expected struct { zone string recordName string } testCases := []struct { desc string fqdn string expected expected }{ { desc: "Extract root record name", fqdn: "_acme-challenge.bar.com.", expected: expected{ zone: "bar.com", recordName: "_acme-challenge", }, }, { desc: "Extract sub record name", fqdn: "_acme-challenge.foo.bar.com.", expected: expected{ zone: "bar.com", recordName: "_acme-challenge.foo", }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() zone, recordName, err := provider.findZoneAndRecordName(test.fqdn) require.NoError(t, err) assert.Equal(t, test.expected.zone, zone) assert.Equal(t, test.expected.recordName, recordName) }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) // Present Twice to handle create / update err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/freemyip/000077500000000000000000000000001434020463500170755ustar00rootroot00000000000000lego-4.9.1/providers/dns/freemyip/freemyip.go000066400000000000000000000076001434020463500212470ustar00rootroot00000000000000// Package freemyip implements a DNS provider for solving the DNS-01 challenge using freemyip.com. package freemyip import ( "context" "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/nrdcg/freemyip" ) // Environment variables names. const ( envNamespace = "FREEMYIP_" EnvToken = envNamespace + "TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Token string PropagationTimeout time.Duration PollingInterval time.Duration SequenceInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 3600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *freemyip.Client } // NewDNSProvider returns a DNSProvider instance configured for freemyip.com. // Credentials must be passed in the environment variable: FREEMYIP_TOKEN. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvToken) if err != nil { return nil, fmt.Errorf("freemyip: %w", err) } config := NewDefaultConfig() config.Token = values[EnvToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for freemyip.com. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("freemyip: the configuration of the DNS provider is nil") } if config.Token == "" { return nil, errors.New("freemyip: missing credentials") } client := freemyip.New(config.Token, true) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Sequential All DNS challenges for this provider will be resolved sequentially. // Returns the interval between each iteration. func (d *DNSProvider) Sequential() time.Duration { return d.config.SequenceInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) subDomain := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), freemyip.RootDomain)) _, err := d.client.EditTXTRecord(context.Background(), subDomain, value) if err != nil { return fmt.Errorf("freemyip: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) subDomain := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), freemyip.RootDomain)) _, err := d.client.DeleteTXTRecord(context.Background(), subDomain) if err != nil { return fmt.Errorf("freemyip: %w", err) } return nil } lego-4.9.1/providers/dns/freemyip/freemyip.toml000066400000000000000000000013211434020463500216070ustar00rootroot00000000000000Name = "freemyip.com" Description = '''''' URL = "https://freemyip.com/" Code = "freemyip" Since = "v4.5.0" Example = ''' FREEMYIP_TOKEN=xxxxxx \ lego --email you@example.com --dns freemyip --domains my.example.org run ''' [Configuration] [Configuration.Credentials] FREEMYIP_TOKEN = "Account token" [Configuration.Additional] FREEMYIP_POLLING_INTERVAL = "Time between DNS propagation check" FREEMYIP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" FREEMYIP_TTL = "The TTL of the TXT record used for the DNS challenge" FREEMYIP_HTTP_TIMEOUT = "API request timeout" FREEMYIP_SEQUENCE_INTERVAL = "Time between sequential requests" [Links] API = "https://freemyip.com/help" lego-4.9.1/providers/dns/freemyip/freemyip_test.go000066400000000000000000000042641434020463500223110ustar00rootroot00000000000000package freemyip import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvToken). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvToken: "123", }, }, { desc: "missing api key", envVars: map[string]string{ EnvToken: "", }, expected: "freemyip: some credentials information are missing: FREEMYIP_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string token string expected string }{ { desc: "success", token: "123", }, { desc: "missing credentials", expected: "freemyip: missing credentials", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Token = test.token p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/gandi/000077500000000000000000000000001434020463500163375ustar00rootroot00000000000000lego-4.9.1/providers/dns/gandi/client.go000066400000000000000000000154721434020463500201550ustar00rootroot00000000000000package gandi import ( "bytes" "encoding/xml" "errors" "fmt" "io" ) // types for XML-RPC method calls and parameters type param interface { param() } type paramString struct { XMLName xml.Name `xml:"param"` Value string `xml:"value>string"` } type paramInt struct { XMLName xml.Name `xml:"param"` Value int `xml:"value>int"` } type structMember interface { structMember() } type structMemberString struct { Name string `xml:"name"` Value string `xml:"value>string"` } type structMemberInt struct { Name string `xml:"name"` Value int `xml:"value>int"` } type paramStruct struct { XMLName xml.Name `xml:"param"` StructMembers []structMember `xml:"value>struct>member"` } func (p paramString) param() {} func (p paramInt) param() {} func (m structMemberString) structMember() {} func (m structMemberInt) structMember() {} func (p paramStruct) param() {} type methodCall struct { XMLName xml.Name `xml:"methodCall"` MethodName string `xml:"methodName"` Params []param `xml:"params"` } // types for XML-RPC responses type response interface { faultCode() int faultString() string } type responseFault struct { FaultCode int `xml:"fault>value>struct>member>value>int"` FaultString string `xml:"fault>value>struct>member>value>string"` } func (r responseFault) faultCode() int { return r.FaultCode } func (r responseFault) faultString() string { return r.FaultString } type responseStruct struct { responseFault StructMembers []struct { Name string `xml:"name"` ValueInt int `xml:"value>int"` } `xml:"params>param>value>struct>member"` } type responseInt struct { responseFault Value int `xml:"params>param>value>int"` } type responseBool struct { responseFault Value bool `xml:"params>param>value>boolean"` } type rpcError struct { faultCode int faultString string } func (e rpcError) Error() string { return fmt.Sprintf("Gandi DNS: RPC Error: (%d) %s", e.faultCode, e.faultString) } // rpcCall makes an XML-RPC call to Gandi's RPC endpoint by // marshaling the data given in the call argument to XML and sending // that via HTTP Post to Gandi. // The response is then unmarshalled into the resp argument. func (d *DNSProvider) rpcCall(call *methodCall, resp response) error { // marshal b, err := xml.MarshalIndent(call, "", " ") if err != nil { return fmt.Errorf("marshal error: %w", err) } // post b = append([]byte(``+"\n"), b...) respBody, err := d.httpPost(d.config.BaseURL, "text/xml", bytes.NewReader(b)) if err != nil { return err } // unmarshal err = xml.Unmarshal(respBody, resp) if err != nil { return fmt.Errorf("unmarshal error: %w", err) } if resp.faultCode() != 0 { return rpcError{ faultCode: resp.faultCode(), faultString: resp.faultString(), } } return nil } // functions to perform API actions func (d *DNSProvider) getZoneID(domain string) (int, error) { resp := &responseStruct{} err := d.rpcCall(&methodCall{ MethodName: "domain.info", Params: []param{ paramString{Value: d.config.APIKey}, paramString{Value: domain}, }, }, resp) if err != nil { return 0, err } var zoneID int for _, member := range resp.StructMembers { if member.Name == "zone_id" { zoneID = member.ValueInt } } if zoneID == 0 { return 0, fmt.Errorf("could not determine zone_id for %s", domain) } return zoneID, nil } func (d *DNSProvider) cloneZone(zoneID int, name string) (int, error) { resp := &responseStruct{} err := d.rpcCall(&methodCall{ MethodName: "domain.zone.clone", Params: []param{ paramString{Value: d.config.APIKey}, paramInt{Value: zoneID}, paramInt{Value: 0}, paramStruct{ StructMembers: []structMember{ structMemberString{ Name: "name", Value: name, }, }, }, }, }, resp) if err != nil { return 0, err } var newZoneID int for _, member := range resp.StructMembers { if member.Name == "id" { newZoneID = member.ValueInt } } if newZoneID == 0 { return 0, errors.New("could not determine cloned zone_id") } return newZoneID, nil } func (d *DNSProvider) newZoneVersion(zoneID int) (int, error) { resp := &responseInt{} err := d.rpcCall(&methodCall{ MethodName: "domain.zone.version.new", Params: []param{ paramString{Value: d.config.APIKey}, paramInt{Value: zoneID}, }, }, resp) if err != nil { return 0, err } if resp.Value == 0 { return 0, errors.New("could not create new zone version") } return resp.Value, nil } func (d *DNSProvider) addTXTRecord(zoneID, version int, name, value string, ttl int) error { resp := &responseStruct{} err := d.rpcCall(&methodCall{ MethodName: "domain.zone.record.add", Params: []param{ paramString{Value: d.config.APIKey}, paramInt{Value: zoneID}, paramInt{Value: version}, paramStruct{ StructMembers: []structMember{ structMemberString{ Name: "type", Value: "TXT", }, structMemberString{ Name: "name", Value: name, }, structMemberString{ Name: "value", Value: value, }, structMemberInt{ Name: "ttl", Value: ttl, }, }, }, }, }, resp) return err } func (d *DNSProvider) setZoneVersion(zoneID, version int) error { resp := &responseBool{} err := d.rpcCall(&methodCall{ MethodName: "domain.zone.version.set", Params: []param{ paramString{Value: d.config.APIKey}, paramInt{Value: zoneID}, paramInt{Value: version}, }, }, resp) if err != nil { return err } if !resp.Value { return errors.New("could not set zone version") } return nil } func (d *DNSProvider) setZone(domain string, zoneID int) error { resp := &responseStruct{} err := d.rpcCall(&methodCall{ MethodName: "domain.zone.set", Params: []param{ paramString{Value: d.config.APIKey}, paramString{Value: domain}, paramInt{Value: zoneID}, }, }, resp) if err != nil { return err } var respZoneID int for _, member := range resp.StructMembers { if member.Name == "zone_id" { respZoneID = member.ValueInt } } if respZoneID != zoneID { return fmt.Errorf("could not set new zone_id for %s", domain) } return nil } func (d *DNSProvider) deleteZone(zoneID int) error { resp := &responseBool{} err := d.rpcCall(&methodCall{ MethodName: "domain.zone.delete", Params: []param{ paramString{Value: d.config.APIKey}, paramInt{Value: zoneID}, }, }, resp) if err != nil { return err } if !resp.Value { return errors.New("could not delete zone_id") } return nil } func (d *DNSProvider) httpPost(url, bodyType string, body io.Reader) ([]byte, error) { resp, err := d.config.HTTPClient.Post(url, bodyType, body) if err != nil { return nil, fmt.Errorf("HTTP Post Error: %w", err) } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("HTTP Post Error: %w", err) } return b, nil } lego-4.9.1/providers/dns/gandi/gandi.go000066400000000000000000000151161434020463500177540ustar00rootroot00000000000000// Package gandi implements a DNS provider for solving the DNS-01 challenge using Gandi DNS. package gandi import ( "errors" "fmt" "net/http" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Gandi API reference: http://doc.rpc.gandi.net/index.html // Gandi API domain examples: http://doc.rpc.gandi.net/domain/faq.html const ( // defaultBaseURL Gandi XML-RPC endpoint used by Present and CleanUp. defaultBaseURL = "https://rpc.gandi.net/xmlrpc/" minTTL = 300 ) // Environment variables names. const ( envNamespace = "GANDI_" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 40*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 60*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second), }, } } // inProgressInfo contains information about an in-progress challenge. type inProgressInfo struct { zoneID int // zoneID of gandi zone to restore in CleanUp newZoneID int // zoneID of temporary gandi zone containing TXT record authZone string // the domain name registered at gandi with trailing "." } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { inProgressFQDNs map[string]inProgressInfo inProgressAuthZones map[string]struct{} inProgressMu sync.Mutex config *Config // findZoneByFqdn determines the DNS zone of an fqdn. It is overridden during tests. findZoneByFqdn func(fqdn string) (string, error) } // NewDNSProvider returns a DNSProvider instance configured for Gandi. // Credentials must be passed in the environment variable: GANDI_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("gandi: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Gandi. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("gandi: the configuration of the DNS provider is nil") } if config.APIKey == "" { return nil, errors.New("gandi: no API Key given") } if config.BaseURL == "" { config.BaseURL = defaultBaseURL } return &DNSProvider{ config: config, inProgressFQDNs: make(map[string]inProgressInfo), inProgressAuthZones: make(map[string]struct{}), findZoneByFqdn: dns01.FindZoneByFqdn, }, nil } // Present creates a TXT record using the specified parameters. It // does this by creating and activating a new temporary Gandi DNS // zone. This new zone contains the TXT record. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) if d.config.TTL < minTTL { d.config.TTL = minTTL // 300 is gandi minimum value for ttl } // find authZone and Gandi zone_id for fqdn authZone, err := d.findZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("gandi: findZoneByFqdn failure: %w", err) } zoneID, err := d.getZoneID(authZone) if err != nil { return fmt.Errorf("gandi: %w", err) } // determine name of TXT record if !strings.HasSuffix( strings.ToLower(fqdn), strings.ToLower("."+authZone)) { return fmt.Errorf("gandi: unexpected authZone %s for fqdn %s", authZone, fqdn) } name := fqdn[:len(fqdn)-len("."+authZone)] // acquire lock and check there is not a challenge already in // progress for this value of authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() if _, ok := d.inProgressAuthZones[authZone]; ok { return fmt.Errorf("gandi: challenge already in progress for authZone %s", authZone) } // perform API actions to create and activate new gandi zone // containing the required TXT record newZoneName := fmt.Sprintf("%s [ACME Challenge %s]", dns01.UnFqdn(authZone), time.Now().Format(time.RFC822Z)) newZoneID, err := d.cloneZone(zoneID, newZoneName) if err != nil { return err } newZoneVersion, err := d.newZoneVersion(newZoneID) if err != nil { return fmt.Errorf("gandi: %w", err) } err = d.addTXTRecord(newZoneID, newZoneVersion, name, value, d.config.TTL) if err != nil { return fmt.Errorf("gandi: %w", err) } err = d.setZoneVersion(newZoneID, newZoneVersion) if err != nil { return fmt.Errorf("gandi: %w", err) } err = d.setZone(authZone, newZoneID) if err != nil { return fmt.Errorf("gandi: %w", err) } // save data necessary for CleanUp d.inProgressFQDNs[fqdn] = inProgressInfo{ zoneID: zoneID, newZoneID: newZoneID, authZone: authZone, } d.inProgressAuthZones[authZone] = struct{}{} return nil } // CleanUp removes the TXT record matching the specified // parameters. It does this by restoring the old Gandi DNS zone and // removing the temporary one created by Present. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) // acquire lock and retrieve zoneID, newZoneID and authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() if _, ok := d.inProgressFQDNs[fqdn]; !ok { // if there is no cleanup information then just return return nil } zoneID := d.inProgressFQDNs[fqdn].zoneID newZoneID := d.inProgressFQDNs[fqdn].newZoneID authZone := d.inProgressFQDNs[fqdn].authZone delete(d.inProgressFQDNs, fqdn) delete(d.inProgressAuthZones, authZone) // perform API actions to restore old gandi zone for authZone err := d.setZone(authZone, zoneID) if err != nil { return fmt.Errorf("gandi: %w", err) } return d.deleteZone(newZoneID) } // Timeout returns the values (40*time.Minute, 60*time.Second) which // are used by the acme package as timeout and check interval values // when checking for DNS record propagation with Gandi. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/gandi/gandi.toml000066400000000000000000000012111434020463500203110ustar00rootroot00000000000000Name = "Gandi" Description = """""" URL = "https://www.gandi.net" Code = "gandi" Since = "v0.3.0" Example = ''' GANDI_API_KEY=abcdefghijklmnopqrstuvwx \ lego --email you@example.com --dns gandi --domains my.example.org run ''' [Configuration] [Configuration.Credentials] GANDI_API_KEY = "API key" [Configuration.Additional] GANDI_POLLING_INTERVAL = "Time between DNS propagation check" GANDI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" GANDI_TTL = "The TTL of the TXT record used for the DNS challenge" GANDI_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://doc.rpc.gandi.net/index.html" lego-4.9.1/providers/dns/gandi/gandi_mock_test.go000066400000000000000000000421761434020463500220320ustar00rootroot00000000000000package gandi // CleanUp Request->Response 1 (setZone). const cleanupSetZoneRequestMock = ` domain.zone.set 123412341234123412341234 example.com. 1234567 ` // CleanUp Request->Response 1 (setZone). const cleanupSetZoneResponseMock = ` date_updated 20160216T16:24:38 date_delete 20170331T16:04:06 is_premium 0 date_hold_begin 20170215T02:04:06 date_registry_end 20170215T02:04:06 authinfo_expiration_date 20161211T21:31:20 contacts owner handle LEGO-GANDI id 111111 admin handle LEGO-GANDI id 111111 bill handle LEGO-GANDI id 111111 tech handle LEGO-GANDI id 111111 reseller nameservers a.dns.gandi.net b.dns.gandi.net c.dns.gandi.net date_restore_end 20170501T02:04:06 id 2222222 authinfo ABCDABCDAB status clientTransferProhibited serverTransferProhibited tags date_hold_end 20170401T02:04:06 services gandidns gandimail date_pending_delete_end 20170506T02:04:06 zone_id 1234567 date_renew_begin 20120101T00:00:00 fqdn example.com autorenew date_registry_creation 20150215T02:04:06 tld org date_created 20150215T03:04:06 ` // CleanUp Request->Response 2 (deleteZone). const cleanupDeleteZoneRequestMock = ` domain.zone.delete 123412341234123412341234 7654321 ` // CleanUp Request->Response 2 (deleteZone). const cleanupDeleteZoneResponseMock = ` 1 ` // Present Request->Response 1 (getZoneID). const presentGetZoneIDRequestMock = ` domain.info 123412341234123412341234 example.com. ` // Present Request->Response 1 (getZoneID). const presentGetZoneIDResponseMock = ` date_updated 20160216T16:14:23 date_delete 20170331T16:04:06 is_premium 0 date_hold_begin 20170215T02:04:06 date_registry_end 20170215T02:04:06 authinfo_expiration_date 20161211T21:31:20 contacts owner handle LEGO-GANDI id 111111 admin handle LEGO-GANDI id 111111 bill handle LEGO-GANDI id 111111 tech handle LEGO-GANDI id 111111 reseller nameservers a.dns.gandi.net b.dns.gandi.net c.dns.gandi.net date_restore_end 20170501T02:04:06 id 2222222 authinfo ABCDABCDAB status clientTransferProhibited serverTransferProhibited tags date_hold_end 20170401T02:04:06 services gandidns gandimail date_pending_delete_end 20170506T02:04:06 zone_id 1234567 date_renew_begin 20120101T00:00:00 fqdn example.com autorenew date_registry_creation 20150215T02:04:06 tld org date_created 20150215T03:04:06 ` // Present Request->Response 2 (cloneZone). const presentCloneZoneRequestMock = ` domain.zone.clone 123412341234123412341234 1234567 0 name example.com [ACME Challenge 01 Jan 16 00:00 +0000] ` // Present Request->Response 2 (cloneZone). const presentCloneZoneResponseMock = ` name example.com [ACME Challenge 01 Jan 16 00:00 +0000] versions 1 date_updated 20160216T16:24:29 id 7654321 owner LEGO-GANDI version 1 domains 0 public 0 ` // Present Request->Response 3 (newZoneVersion). const presentNewZoneVersionRequestMock = ` domain.zone.version.new 123412341234123412341234 7654321 ` // Present Request->Response 3 (newZoneVersion). const presentNewZoneVersionResponseMock = ` 2 ` // Present Request->Response 4 (addTXTRecord). const presentAddTXTRecordRequestMock = ` domain.zone.record.add 123412341234123412341234 7654321 2 type TXT name _acme-challenge.abc.def value ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ ttl 300 ` // Present Request->Response 4 (addTXTRecord). const presentAddTXTRecordResponseMock = ` name _acme-challenge.abc.def type TXT id 333333333 value "ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ" ttl 300 ` // Present Request->Response 5 (setZoneVersion). const presentSetZoneVersionRequestMock = ` domain.zone.version.set 123412341234123412341234 7654321 2 ` // Present Request->Response 5 (setZoneVersion). const presentSetZoneVersionResponseMock = ` 1 ` // Present Request->Response 6 (setZone). const presentSetZoneRequestMock = ` domain.zone.set 123412341234123412341234 example.com. 7654321 ` // Present Request->Response 6 (setZone). const presentSetZoneResponseMock = ` date_updated 20160216T16:14:23 date_delete 20170331T16:04:06 is_premium 0 date_hold_begin 20170215T02:04:06 date_registry_end 20170215T02:04:06 authinfo_expiration_date 20161211T21:31:20 contacts owner handle LEGO-GANDI id 111111 admin handle LEGO-GANDI id 111111 bill handle LEGO-GANDI id 111111 tech handle LEGO-GANDI id 111111 reseller nameservers a.dns.gandi.net b.dns.gandi.net c.dns.gandi.net date_restore_end 20170501T02:04:06 id 2222222 authinfo ABCDABCDAB status clientTransferProhibited serverTransferProhibited tags date_hold_end 20170401T02:04:06 services gandidns gandimail date_pending_delete_end 20170506T02:04:06 zone_id 7654321 date_renew_begin 20120101T00:00:00 fqdn example.com autorenew date_registry_creation 20150215T02:04:06 tld org date_created 20150215T03:04:06 ` lego-4.9.1/providers/dns/gandi/gandi_test.go000066400000000000000000000107101434020463500210060ustar00rootroot00000000000000package gandi import ( "io" "net/http" "net/http/httptest" "regexp" "strings" "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) var envTest = tester.NewEnvTest(EnvAPIKey) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", }, }, { desc: "missing api key", envVars: map[string]string{ EnvAPIKey: "", }, expected: "gandi: some credentials information are missing: GANDI_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.inProgressFQDNs) require.NotNil(t, p.inProgressAuthZones) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string expected string }{ { desc: "success", apiKey: "123", }, { desc: "missing credentials", expected: "gandi: no API Key given", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.inProgressFQDNs) require.NotNil(t, p.inProgressAuthZones) } else { require.EqualError(t, err, test.expected) } }) } } // TestDNSProvider runs Present and CleanUp against a fake Gandi RPC // Server, whose responses are predetermined for particular requests. func TestDNSProvider(t *testing.T) { // serverResponses is the XML-RPC Request->Response map used by the // fake RPC server. It was generated by recording a real RPC session // which resulted in the successful issue of a cert, and then // anonymizing the RPC data. serverResponses := map[string]string{ // Present Request->Response 1 (getZoneID) presentGetZoneIDRequestMock: presentGetZoneIDResponseMock, // Present Request->Response 2 (cloneZone) presentCloneZoneRequestMock: presentCloneZoneResponseMock, // Present Request->Response 3 (newZoneVersion) presentNewZoneVersionRequestMock: presentNewZoneVersionResponseMock, // Present Request->Response 4 (addTXTRecord) presentAddTXTRecordRequestMock: presentAddTXTRecordResponseMock, // Present Request->Response 5 (setZoneVersion) presentSetZoneVersionRequestMock: presentSetZoneVersionResponseMock, // Present Request->Response 6 (setZone) presentSetZoneRequestMock: presentSetZoneResponseMock, // CleanUp Request->Response 1 (setZone) cleanupSetZoneRequestMock: cleanupSetZoneResponseMock, // CleanUp Request->Response 2 (deleteZone) cleanupDeleteZoneRequestMock: cleanupDeleteZoneResponseMock, } fakeKeyAuth := "XXXX" regexpDate := regexp.MustCompile(`\[ACME Challenge [^\]:]*:[^\]]*\]`) // start fake RPC server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, "text/xml", r.Header.Get("Content-Type"), "invalid content type") req, errS := io.ReadAll(r.Body) require.NoError(t, errS) req = regexpDate.ReplaceAllLiteral(req, []byte(`[ACME Challenge 01 Jan 16 00:00 +0000]`)) resp, ok := serverResponses[string(req)] require.True(t, ok, "Server response for request not found") _, errS = io.Copy(w, strings.NewReader(resp)) require.NoError(t, errS) })) t.Cleanup(server.Close) // define function to override findZoneByFqdn with fakeFindZoneByFqdn := func(fqdn string) (string, error) { return "example.com.", nil } config := NewDefaultConfig() config.BaseURL = server.URL + "/" config.APIKey = "123412341234123412341234" provider, err := NewDNSProviderConfig(config) require.NoError(t, err) // override findZoneByFqdn function savedFindZoneByFqdn := provider.findZoneByFqdn t.Cleanup(func() { provider.findZoneByFqdn = savedFindZoneByFqdn }) provider.findZoneByFqdn = fakeFindZoneByFqdn // run Present err = provider.Present("abc.def.example.com", "", fakeKeyAuth) require.NoError(t, err) // run CleanUp err = provider.CleanUp("abc.def.example.com", "", fakeKeyAuth) require.NoError(t, err) } lego-4.9.1/providers/dns/gandiv5/000077500000000000000000000000001434020463500166125ustar00rootroot00000000000000lego-4.9.1/providers/dns/gandiv5/client.go000066400000000000000000000107401434020463500204210ustar00rootroot00000000000000package gandiv5 import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "github.com/go-acme/lego/v4/log" ) const apiKeyHeader = "X-Api-Key" // types for JSON responses with only a message. type apiResponse struct { Message string `json:"message"` UUID string `json:"uuid,omitempty"` } // Record TXT record representation. type Record struct { RRSetTTL int `json:"rrset_ttl"` RRSetValues []string `json:"rrset_values"` RRSetName string `json:"rrset_name,omitempty"` RRSetType string `json:"rrset_type,omitempty"` } func (d *DNSProvider) addTXTRecord(domain, name, value string, ttl int) error { // Get exiting values for the TXT records // Needed to create challenges for both wildcard and base name domains txtRecord, err := d.getTXTRecord(domain, name) if err != nil { return err } values := []string{value} if len(txtRecord.RRSetValues) > 0 { values = append(values, txtRecord.RRSetValues...) } target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name) newRecord := &Record{RRSetTTL: ttl, RRSetValues: values} req, err := d.newRequest(http.MethodPut, target, newRecord) if err != nil { return err } message := apiResponse{} err = d.do(req, &message) if err != nil { return fmt.Errorf("unable to create TXT record for domain %s and name %s: %w", domain, name, err) } if len(message.Message) > 0 { log.Infof("API response: %s", message.Message) } return nil } func (d *DNSProvider) getTXTRecord(domain, name string) (*Record, error) { target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name) // Get exiting values for the TXT records // Needed to create challenges for both wildcard and base name domains req, err := d.newRequest(http.MethodGet, target, nil) if err != nil { return nil, err } txtRecord := &Record{} err = d.do(req, txtRecord) if err != nil { return nil, fmt.Errorf("unable to get TXT records for domain %s and name %s: %w", domain, name, err) } return txtRecord, nil } func (d *DNSProvider) deleteTXTRecord(domain, name string) error { target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name) req, err := d.newRequest(http.MethodDelete, target, nil) if err != nil { return err } message := apiResponse{} err = d.do(req, &message) if err != nil { return fmt.Errorf("unable to delete TXT record for domain %s and name %s: %w", domain, name, err) } if len(message.Message) > 0 { log.Infof("API response: %s", message.Message) } return nil } func (d *DNSProvider) newRequest(method, resource string, body interface{}) (*http.Request, error) { u := fmt.Sprintf("%s/%s", d.config.BaseURL, resource) if body == nil { req, err := http.NewRequest(method, u, nil) if err != nil { return nil, err } return req, nil } reqBody, err := json.Marshal(body) if err != nil { return nil, err } req, err := http.NewRequest(method, u, bytes.NewBuffer(reqBody)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") return req, nil } func (d *DNSProvider) do(req *http.Request, v interface{}) error { if len(d.config.APIKey) > 0 { req.Header.Set(apiKeyHeader, d.config.APIKey) } resp, err := d.config.HTTPClient.Do(req) if err != nil { return err } err = checkResponse(resp) if err != nil { return err } if v == nil { return nil } raw, err := readBody(resp) if err != nil { return fmt.Errorf("failed to read body: %w", err) } if len(raw) > 0 { err = json.Unmarshal(raw, v) if err != nil { return fmt.Errorf("unmarshaling error: %w: %s", err, string(raw)) } } return nil } func checkResponse(resp *http.Response) error { if resp.StatusCode == http.StatusNotFound && resp.Request.Method == http.MethodGet { return nil } if resp.StatusCode >= http.StatusBadRequest { data, err := readBody(resp) if err != nil { return fmt.Errorf("%d [%s] request failed: %w", resp.StatusCode, http.StatusText(resp.StatusCode), err) } message := &apiResponse{} err = json.Unmarshal(data, message) if err != nil { return fmt.Errorf("%d [%s] request failed: %w: %s", resp.StatusCode, http.StatusText(resp.StatusCode), err, data) } return fmt.Errorf("%d [%s] request failed: %s", resp.StatusCode, http.StatusText(resp.StatusCode), message.Message) } return nil } func readBody(resp *http.Response) ([]byte, error) { if resp.Body == nil { return nil, errors.New("response body is nil") } defer resp.Body.Close() rawBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return rawBody, nil } lego-4.9.1/providers/dns/gandiv5/gandiv5.go000066400000000000000000000117471434020463500205100ustar00rootroot00000000000000// Package gandiv5 implements a DNS provider for solving the DNS-01 challenge using Gandi LiveDNS api. package gandiv5 import ( "errors" "fmt" "net/http" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Gandi API reference: http://doc.livedns.gandi.net/ const ( // defaultBaseURL endpoint is the Gandi API endpoint used by Present and CleanUp. defaultBaseURL = "https://dns.api.gandi.net/api/v5" minTTL = 300 ) // Environment variables names. const ( envNamespace = "GANDIV5_" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // inProgressInfo contains information about an in-progress challenge. type inProgressInfo struct { fieldName string authZone string } // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 20*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 20*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config inProgressFQDNs map[string]inProgressInfo inProgressMu sync.Mutex // findZoneByFqdn determines the DNS zone of an fqdn. It is overridden during tests. findZoneByFqdn func(fqdn string) (string, error) } // NewDNSProvider returns a DNSProvider instance configured for Gandi. // Credentials must be passed in the environment variable: GANDIV5_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("gandi: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Gandi. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("gandiv5: the configuration of the DNS provider is nil") } if config.APIKey == "" { return nil, errors.New("gandiv5: no API Key given") } if config.BaseURL == "" { config.BaseURL = defaultBaseURL } if config.TTL < minTTL { return nil, fmt.Errorf("gandiv5: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } return &DNSProvider{ config: config, inProgressFQDNs: make(map[string]inProgressInfo), findZoneByFqdn: dns01.FindZoneByFqdn, }, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) // find authZone authZone, err := d.findZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("gandiv5: findZoneByFqdn failure: %w", err) } // determine name of TXT record if !strings.HasSuffix( strings.ToLower(fqdn), strings.ToLower("."+authZone)) { return fmt.Errorf("gandiv5: unexpected authZone %s for fqdn %s", authZone, fqdn) } name := fqdn[:len(fqdn)-len("."+authZone)] // acquire lock and check there is not a challenge already in // progress for this value of authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() // add TXT record into authZone err = d.addTXTRecord(dns01.UnFqdn(authZone), name, value, d.config.TTL) if err != nil { return err } // save data necessary for CleanUp d.inProgressFQDNs[fqdn] = inProgressInfo{ authZone: authZone, fieldName: name, } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) // acquire lock and retrieve authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() if _, ok := d.inProgressFQDNs[fqdn]; !ok { // if there is no cleanup information then just return return nil } fieldName := d.inProgressFQDNs[fqdn].fieldName authZone := d.inProgressFQDNs[fqdn].authZone delete(d.inProgressFQDNs, fqdn) // delete TXT record from authZone err := d.deleteTXTRecord(dns01.UnFqdn(authZone), fieldName) if err != nil { return fmt.Errorf("gandiv5: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/gandiv5/gandiv5.toml000066400000000000000000000012461434020463500210470ustar00rootroot00000000000000Name = "Gandi Live DNS (v5)" Description = '''''' URL = "https://www.gandi.net" Code = "gandiv5" Since = "v0.5.0" Example = ''' GANDIV5_API_KEY=abcdefghijklmnopqrstuvwx \ lego --email you@example.com --dns gandiv5 --domains my.example.org run ''' [Configuration] [Configuration.Credentials] GANDIV5_API_KEY = "API key" [Configuration.Additional] GANDIV5_POLLING_INTERVAL = "Time between DNS propagation check" GANDIV5_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" GANDIV5_TTL = "The TTL of the TXT record used for the DNS challenge" GANDIV5_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.gandi.net/docs/livedns/" lego-4.9.1/providers/dns/gandiv5/gandiv5_test.go000066400000000000000000000111471434020463500215410ustar00rootroot00000000000000package gandiv5 import ( "fmt" "io" "net/http" "net/http/httptest" "regexp" "testing" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) var envTest = tester.NewEnvTest(EnvAPIKey) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", }, }, { desc: "missing api key", envVars: map[string]string{ EnvAPIKey: "", }, expected: "gandi: some credentials information are missing: GANDIV5_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.inProgressFQDNs) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string expected string }{ { desc: "success", apiKey: "123", }, { desc: "missing credentials", expected: "gandiv5: no API Key given", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.inProgressFQDNs) } else { require.EqualError(t, err, test.expected) } }) } } // TestDNSProvider runs Present and CleanUp against a fake Gandi RPC // Server, whose responses are predetermined for particular requests. func TestDNSProvider(t *testing.T) { // serverResponses is the JSON Request->Response map used by the // fake JSON server. serverResponses := map[string]map[string]string{ http.MethodGet: { ``: `{"rrset_ttl":300,"rrset_values":[],"rrset_name":"_acme-challenge.abc.def","rrset_type":"TXT"}`, }, http.MethodPut: { `{"rrset_ttl":300,"rrset_values":["TOKEN"]}`: `{"message": "Zone Record Created"}`, }, http.MethodDelete: { ``: ``, }, } fakeKeyAuth := "XXXX" regexpToken := regexp.MustCompile(`"rrset_values":\[".+"\]`) // start fake RPC server mux := http.NewServeMux() mux.HandleFunc("/domains/example.com/records/_acme-challenge.abc.def/TXT", func(rw http.ResponseWriter, req *http.Request) { log.Infof("request: %s %s", req.Method, req.URL) if req.Header.Get(apiKeyHeader) == "" { http.Error(rw, `{"message": "missing API key"}`, http.StatusUnauthorized) return } if req.Method == http.MethodPost && req.Header.Get("Content-Type") != "application/json" { http.Error(rw, `{"message": "invalid content type"}`, http.StatusBadRequest) return } body, errS := io.ReadAll(req.Body) if errS != nil { http.Error(rw, fmt.Sprintf(`{"message": "read body error: %v"}`, errS), http.StatusInternalServerError) return } body = regexpToken.ReplaceAllLiteral(body, []byte(`"rrset_values":["TOKEN"]`)) responses, ok := serverResponses[req.Method] if !ok { http.Error(rw, fmt.Sprintf(`{"message": "Server response for request not found: %#q"}`, string(body)), http.StatusInternalServerError) return } resp := responses[string(body)] _, errS = rw.Write([]byte(resp)) if errS != nil { http.Error(rw, fmt.Sprintf(`{"message": "failed to write response: %v"}`, errS), http.StatusInternalServerError) return } }) mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { log.Infof("request: %s %s", req.Method, req.URL) http.Error(rw, fmt.Sprintf(`{"message": "URL doesn't match: %s"}`, req.URL), http.StatusNotFound) }) server := httptest.NewServer(mux) t.Cleanup(server.Close) // define function to override findZoneByFqdn with fakeFindZoneByFqdn := func(fqdn string) (string, error) { return "example.com.", nil } config := NewDefaultConfig() config.APIKey = "123412341234123412341234" config.BaseURL = server.URL provider, err := NewDNSProviderConfig(config) require.NoError(t, err) // override findZoneByFqdn function savedFindZoneByFqdn := provider.findZoneByFqdn defer func() { provider.findZoneByFqdn = savedFindZoneByFqdn }() provider.findZoneByFqdn = fakeFindZoneByFqdn // run Present err = provider.Present("abc.def.example.com", "", fakeKeyAuth) require.NoError(t, err) // run CleanUp err = provider.CleanUp("abc.def.example.com", "", fakeKeyAuth) require.NoError(t, err) } lego-4.9.1/providers/dns/gcloud/000077500000000000000000000000001434020463500165325ustar00rootroot00000000000000lego-4.9.1/providers/dns/gcloud/fixtures/000077500000000000000000000000001434020463500204035ustar00rootroot00000000000000lego-4.9.1/providers/dns/gcloud/fixtures/gce_account_service_file.json000066400000000000000000000003511434020463500262660ustar00rootroot00000000000000{ "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" }lego-4.9.1/providers/dns/gcloud/gcloud.toml000066400000000000000000000020021434020463500206760ustar00rootroot00000000000000Name = "Google Cloud" Description = '''''' URL = "https://cloud.google.com" Code = "gcloud" Since = "v0.3.0" Example = '''''' [Configuration] [Configuration.Credentials] GCE_PROJECT = "Project name (by default, the project name is auto-detected by using the metadata service)" 'Application Default Credentials' = "[Documentation](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application)" GCE_SERVICE_ACCOUNT_FILE = "Account file path" GCE_SERVICE_ACCOUNT = "Account" [Configuration.Additional] GCE_ALLOW_PRIVATE_ZONE = "Allows requested domain to be in private DNS zone, works only with a private ACME server (by default: false)" GCE_POLLING_INTERVAL = "Time between DNS propagation check" GCE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" GCE_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://cloud.google.com/dns/api/v1/" GoClient = "https://github.com/googleapis/google-api-go-client" lego-4.9.1/providers/dns/gcloud/googlecloud.go000066400000000000000000000244511434020463500213720ustar00rootroot00000000000000// Package gcloud implements a DNS provider for solving the DNS-01 challenge using Google Cloud DNS. package gcloud import ( "encoding/json" "errors" "fmt" "net/http" "os" "strconv" "time" "cloud.google.com/go/compute/metadata" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/platform/wait" "golang.org/x/net/context" "golang.org/x/oauth2/google" "google.golang.org/api/dns/v1" "google.golang.org/api/googleapi" "google.golang.org/api/option" ) const ( changeStatusDone = "done" ) // Environment variables names. const ( envNamespace = "GCE_" EnvServiceAccount = envNamespace + "SERVICE_ACCOUNT" EnvProject = envNamespace + "PROJECT" EnvAllowPrivateZone = envNamespace + "ALLOW_PRIVATE_ZONE" EnvDebug = envNamespace + "DEBUG" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Debug bool Project string AllowPrivateZone bool PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ Debug: env.GetOrDefaultBool(EnvDebug, false), AllowPrivateZone: env.GetOrDefaultBool(EnvAllowPrivateZone, false), TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 180*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *dns.Service } // NewDNSProvider returns a DNSProvider instance configured for Google Cloud DNS. // By default, the project name is auto-detected by using the metadata service, // it can be overridden using the GCE_PROJECT environment variable. // A Service Account can be passed in the environment variable: GCE_SERVICE_ACCOUNT // or by specifying the keyfile location: GCE_SERVICE_ACCOUNT_FILE. func NewDNSProvider() (*DNSProvider, error) { // Use a service account file if specified via environment variable. if saKey := env.GetOrFile(EnvServiceAccount); len(saKey) > 0 { return NewDNSProviderServiceAccountKey([]byte(saKey)) } // Use default credentials. project := env.GetOrDefaultString(EnvProject, autodetectProjectID()) return NewDNSProviderCredentials(project) } // NewDNSProviderCredentials uses the supplied credentials // to return a DNSProvider instance configured for Google Cloud DNS. func NewDNSProviderCredentials(project string) (*DNSProvider, error) { if project == "" { return nil, errors.New("googlecloud: project name missing") } client, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope) if err != nil { return nil, fmt.Errorf("googlecloud: unable to get Google Cloud client: %w", err) } config := NewDefaultConfig() config.Project = project config.HTTPClient = client return NewDNSProviderConfig(config) } // NewDNSProviderServiceAccountKey uses the supplied service account JSON // to return a DNSProvider instance configured for Google Cloud DNS. func NewDNSProviderServiceAccountKey(saKey []byte) (*DNSProvider, error) { if len(saKey) == 0 { return nil, errors.New("googlecloud: Service Account is missing") } // If GCE_PROJECT is non-empty it overrides the project in the service // account file. project := env.GetOrDefaultString(EnvProject, "") if project == "" { // read project id from service account file var datJSON struct { ProjectID string `json:"project_id"` } err := json.Unmarshal(saKey, &datJSON) if err != nil || datJSON.ProjectID == "" { return nil, errors.New("googlecloud: project ID not found in Google Cloud Service Account file") } project = datJSON.ProjectID } conf, err := google.JWTConfigFromJSON(saKey, dns.NdevClouddnsReadwriteScope) if err != nil { return nil, fmt.Errorf("googlecloud: unable to acquire config: %w", err) } client := conf.Client(context.Background()) config := NewDefaultConfig() config.Project = project config.HTTPClient = client return NewDNSProviderConfig(config) } // NewDNSProviderServiceAccount uses the supplied service account JSON file // to return a DNSProvider instance configured for Google Cloud DNS. func NewDNSProviderServiceAccount(saFile string) (*DNSProvider, error) { if saFile == "" { return nil, errors.New("googlecloud: Service Account file missing") } saKey, err := os.ReadFile(saFile) if err != nil { return nil, fmt.Errorf("googlecloud: unable to read Service Account file: %w", err) } return NewDNSProviderServiceAccountKey(saKey) } // NewDNSProviderConfig return a DNSProvider instance configured for Google Cloud DNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("googlecloud: the configuration of the DNS provider is nil") } if config.HTTPClient == nil { return nil, errors.New("googlecloud: unable to create Google Cloud DNS service: client is nil") } svc, err := dns.NewService(context.Background(), option.WithHTTPClient(config.HTTPClient)) if err != nil { return nil, fmt.Errorf("googlecloud: unable to create Google Cloud DNS service: %w", err) } return &DNSProvider{config: config, client: svc}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("googlecloud: %w", err) } // Look for existing records. existingRrSet, err := d.findTxtRecords(zone, fqdn) if err != nil { return fmt.Errorf("googlecloud: %w", err) } for _, rrSet := range existingRrSet { var rrd []string for _, rr := range rrSet.Rrdatas { data := mustUnquote(rr) rrd = append(rrd, data) if data == value { log.Printf("skip: the record already exists: %s", value) return nil } } rrSet.Rrdatas = rrd } // Attempt to delete the existing records before adding the new one. if len(existingRrSet) > 0 { if err = d.applyChanges(zone, &dns.Change{Deletions: existingRrSet}); err != nil { return fmt.Errorf("googlecloud: %w", err) } } rec := &dns.ResourceRecordSet{ Name: fqdn, Rrdatas: []string{value}, Ttl: int64(d.config.TTL), Type: "TXT", } // Append existing TXT record data to the new TXT record data for _, rrSet := range existingRrSet { for _, rr := range rrSet.Rrdatas { if rr != value { rec.Rrdatas = append(rec.Rrdatas, rr) } } } change := &dns.Change{ Additions: []*dns.ResourceRecordSet{rec}, } if err = d.applyChanges(zone, change); err != nil { return fmt.Errorf("googlecloud: %w", err) } return nil } func (d *DNSProvider) applyChanges(zone string, change *dns.Change) error { if d.config.Debug { data, _ := json.Marshal(change) log.Printf("change (Create): %s", string(data)) } chg, err := d.client.Changes.Create(d.config.Project, zone, change).Do() if err != nil { var v *googleapi.Error if errors.As(err, &v) && v.Code == http.StatusNotFound { return nil } data, _ := json.Marshal(change) return fmt.Errorf("failed to perform changes [zone %s, change %s]: %w", zone, string(data), err) } if chg.Status == changeStatusDone { return nil } chgID := chg.Id // wait for change to be acknowledged return wait.For("apply change", 30*time.Second, 3*time.Second, func() (bool, error) { if d.config.Debug { data, _ := json.Marshal(change) log.Printf("change (Get): %s", string(data)) } chg, err = d.client.Changes.Get(d.config.Project, zone, chgID).Do() if err != nil { data, _ := json.Marshal(change) return false, fmt.Errorf("failed to get changes [zone %s, change %s]: %w", zone, string(data), err) } if chg.Status == changeStatusDone { return true, nil } return false, fmt.Errorf("status: %s", chg.Status) }) } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("googlecloud: %w", err) } records, err := d.findTxtRecords(zone, fqdn) if err != nil { return fmt.Errorf("googlecloud: %w", err) } if len(records) == 0 { return nil } _, err = d.client.Changes.Create(d.config.Project, zone, &dns.Change{Deletions: records}).Do() if err != nil { return fmt.Errorf("googlecloud: %w", err) } return nil } // Timeout customizes the timeout values used by the ACME package for checking // DNS record validity. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // getHostedZone returns the managed-zone. func (d *DNSProvider) getHostedZone(domain string) (string, error) { authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return "", err } zones, err := d.client.ManagedZones. List(d.config.Project). DnsName(authZone). Do() if err != nil { return "", fmt.Errorf("API call failed: %w", err) } if len(zones.ManagedZones) == 0 { return "", fmt.Errorf("no matching domain found for domain %s", authZone) } for _, z := range zones.ManagedZones { if z.Visibility == "public" || z.Visibility == "" || (z.Visibility == "private" && d.config.AllowPrivateZone) { return z.Name, nil } } if d.config.AllowPrivateZone { return "", fmt.Errorf("no public or private zone found for domain %s", authZone) } return "", fmt.Errorf("no public zone found for domain %s", authZone) } func (d *DNSProvider) findTxtRecords(zone, fqdn string) ([]*dns.ResourceRecordSet, error) { recs, err := d.client.ResourceRecordSets.List(d.config.Project, zone).Name(fqdn).Type("TXT").Do() if err != nil { return nil, err } return recs.Rrsets, nil } func mustUnquote(raw string) string { clean, err := strconv.Unquote(raw) if err != nil { return raw } return clean } func autodetectProjectID() string { if pid, err := metadata.ProjectID(); err == nil { return pid } return "" } lego-4.9.1/providers/dns/gcloud/googlecloud_test.go000066400000000000000000000264221434020463500224310ustar00rootroot00000000000000package gcloud import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "sort" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" "golang.org/x/net/context" "golang.org/x/oauth2/google" "google.golang.org/api/dns/v1" ) const ( envDomain = envNamespace + "DOMAIN" envServiceAccountFile = envNamespace + "SERVICE_ACCOUNT_FILE" envMetadataHost = envNamespace + "METADATA_HOST" envGoogleApplicationCredentials = "GOOGLE_APPLICATION_CREDENTIALS" ) var envTest = tester.NewEnvTest( EnvProject, envServiceAccountFile, envGoogleApplicationCredentials, envMetadataHost, EnvServiceAccount). WithDomain(envDomain). WithLiveTestExtra(func() bool { _, err := google.DefaultClient(context.Background(), dns.NdevClouddnsReadwriteScope) return err == nil }) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "invalid credentials", envVars: map[string]string{ EnvProject: "123", envServiceAccountFile: "", // as Travis run on GCE, we have to alter env envGoogleApplicationCredentials: "not-a-secret-file", envMetadataHost: "http://lego.wtf", // defined here to avoid the client cache. }, // the error message varies according to the OS used. expected: "googlecloud: unable to get Google Cloud client: google: error getting credentials using GOOGLE_APPLICATION_CREDENTIALS environment variable: ", }, { desc: "missing project", envVars: map[string]string{ EnvProject: "", envServiceAccountFile: "", // as Travis run on GCE, we have to alter env envMetadataHost: "http://lego.wtf", }, expected: "googlecloud: project name missing", }, { desc: "success key file", envVars: map[string]string{ EnvProject: "", envServiceAccountFile: "fixtures/gce_account_service_file.json", }, }, { desc: "success key", envVars: map[string]string{ EnvProject: "", EnvServiceAccount: `{"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"}`, }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.Error(t, err) require.Contains(t, err.Error(), test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string project string expected string }{ { desc: "invalid project", project: "123", expected: "googlecloud: unable to create Google Cloud DNS service: client is nil", }, { desc: "missing project", expected: "googlecloud: unable to create Google Cloud DNS service: client is nil", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() config := NewDefaultConfig() config.Project = test.project p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestPresentNoExistingRR(t *testing.T) { mux := http.NewServeMux() // getHostedZone: /manhattan/managedZones?alt=json&dnsName=lego.wtf. mux.HandleFunc("/manhattan/managedZones", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } mzlrs := &dns.ManagedZonesListResponse{ ManagedZones: []*dns.ManagedZone{ {Name: "test", Visibility: "public"}, }, } err := json.NewEncoder(w).Encode(mzlrs) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) // findTxtRecords: /manhattan/managedZones/test/rrsets?alt=json&name=_acme-challenge.lego.wtf.&type=TXT mux.HandleFunc("/manhattan/managedZones/test/rrsets", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } rrslr := &dns.ResourceRecordSetsListResponse{ Rrsets: []*dns.ResourceRecordSet{}, } err := json.NewEncoder(w).Encode(rrslr) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) // applyChanges [Create]: /manhattan/managedZones/test/changes?alt=json mux.HandleFunc("/manhattan/managedZones/test/changes", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } var chgReq dns.Change if err := json.NewDecoder(r.Body).Decode(&chgReq); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } chgResp := chgReq chgResp.Status = changeStatusDone if err := json.NewEncoder(w).Encode(chgResp); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) server := httptest.NewServer(mux) t.Cleanup(server.Close) config := NewDefaultConfig() config.HTTPClient = &http.Client{} config.Project = "manhattan" p, err := NewDNSProviderConfig(config) require.NoError(t, err) p.client.BasePath = server.URL domain := "lego.wtf" err = p.Present(domain, "", "") require.NoError(t, err) } func TestPresentWithExistingRR(t *testing.T) { mux := http.NewServeMux() // getHostedZone: /manhattan/managedZones?alt=json&dnsName=lego.wtf. mux.HandleFunc("/manhattan/managedZones", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } mzlrs := &dns.ManagedZonesListResponse{ ManagedZones: []*dns.ManagedZone{ {Name: "test", Visibility: "public"}, }, } err := json.NewEncoder(w).Encode(mzlrs) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) // findTxtRecords: /manhattan/managedZones/test/rrsets?alt=json&name=_acme-challenge.lego.wtf.&type=TXT mux.HandleFunc("/manhattan/managedZones/test/rrsets", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } rrslr := &dns.ResourceRecordSetsListResponse{ Rrsets: []*dns.ResourceRecordSet{{ Name: "_acme-challenge.lego.wtf.", Rrdatas: []string{`"X7DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"huji"`}, Ttl: 120, Type: "TXT", }}, } err := json.NewEncoder(w).Encode(rrslr) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) // applyChanges [Create]: /manhattan/managedZones/test/changes?alt=json mux.HandleFunc("/manhattan/managedZones/test/changes", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } var chgReq dns.Change if err := json.NewDecoder(r.Body).Decode(&chgReq); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if len(chgReq.Additions) > 0 { sort.Strings(chgReq.Additions[0].Rrdatas) } var prevVal string for _, addition := range chgReq.Additions { for _, value := range addition.Rrdatas { if prevVal == value { http.Error(w, fmt.Sprintf("The resource %s already exists", value), http.StatusConflict) return } prevVal = value } } chgResp := chgReq chgResp.Status = changeStatusDone if err := json.NewEncoder(w).Encode(chgResp); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) server := httptest.NewServer(mux) t.Cleanup(server.Close) config := NewDefaultConfig() config.HTTPClient = &http.Client{} config.Project = "manhattan" p, err := NewDNSProviderConfig(config) require.NoError(t, err) p.client.BasePath = server.URL domain := "lego.wtf" err = p.Present(domain, "", "") require.NoError(t, err) } func TestPresentSkipExistingRR(t *testing.T) { mux := http.NewServeMux() // getHostedZone: /manhattan/managedZones?alt=json&dnsName=lego.wtf. mux.HandleFunc("/manhattan/managedZones", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } mzlrs := &dns.ManagedZonesListResponse{ ManagedZones: []*dns.ManagedZone{ {Name: "test", Visibility: "public"}, }, } err := json.NewEncoder(w).Encode(mzlrs) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) // findTxtRecords: /manhattan/managedZones/test/rrsets?alt=json&name=_acme-challenge.lego.wtf.&type=TXT mux.HandleFunc("/manhattan/managedZones/test/rrsets", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } rrslr := &dns.ResourceRecordSetsListResponse{ Rrsets: []*dns.ResourceRecordSet{{ Name: "_acme-challenge.lego.wtf.", Rrdatas: []string{`"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"X7DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"huji"`}, Ttl: 120, Type: "TXT", }}, } err := json.NewEncoder(w).Encode(rrslr) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) server := httptest.NewServer(mux) t.Cleanup(server.Close) config := NewDefaultConfig() config.HTTPClient = &http.Client{} config.Project = "manhattan" p, err := NewDNSProviderConfig(config) require.NoError(t, err) p.client.BasePath = server.URL domain := "lego.wtf" err = p.Present(domain, "", "") require.NoError(t, err) } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProviderCredentials(envTest.GetValue(EnvProject)) require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLivePresentMultiple(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProviderCredentials(envTest.GetValue(EnvProject)) require.NoError(t, err) // Check that we're able to create multiple entries err = provider.Present(envTest.GetDomain(), "1", "123d==") require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "2", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProviderCredentials(envTest.GetValue(EnvProject)) require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/gcore/000077500000000000000000000000001434020463500163545ustar00rootroot00000000000000lego-4.9.1/providers/dns/gcore/gcore.go000066400000000000000000000102211434020463500177760ustar00rootroot00000000000000package gcore import ( "context" "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/gcore/internal" ) const ( defaultPropagationTimeout = 360 * time.Second defaultPollingInterval = 20 * time.Second ) // Environment variables names. const ( envNamespace = "GCORE_" EnvPermanentAPIToken = envNamespace + "PERMANENT_API_TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config for DNSProvider. type Config struct { APIToken string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, } } // DNSProvider an implementation of challenge.Provider contract. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns an instance of DNSProvider configured for G-Core Labs DNS API. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvPermanentAPIToken) if err != nil { return nil, fmt.Errorf("gcore: %w", err) } config := NewDefaultConfig() config.APIToken = values[EnvPermanentAPIToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for G-Core Labs DNS API. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("gcore: the configuration of the DNS provider is nil") } if config.APIToken == "" { return nil, errors.New("gcore: incomplete credentials provided") } client := internal.NewClient(config.APIToken) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, }, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, _, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) ctx := context.Background() zone, err := d.guessZone(ctx, fqdn) if err != nil { return fmt.Errorf("gcore: %w", err) } err = d.client.AddRRSet(ctx, zone, dns01.UnFqdn(fqdn), value, d.config.TTL) if err != nil { return fmt.Errorf("gcore: add txt record: %w", err) } return nil } // CleanUp removes the record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) ctx := context.Background() zone, err := d.guessZone(ctx, fqdn) if err != nil { return fmt.Errorf("gcore: %w", err) } err = d.client.DeleteRRSet(ctx, zone, dns01.UnFqdn(fqdn)) if err != nil { return fmt.Errorf("gcore: remove txt record: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS // propagation. Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func (d *DNSProvider) guessZone(ctx context.Context, fqdn string) (string, error) { var lastErr error for _, zone := range extractAllZones(fqdn) { dnsZone, err := d.client.GetZone(ctx, zone) if err == nil { return dnsZone.Name, nil } lastErr = err } return "", fmt.Errorf("zone %q not found: %w", fqdn, lastErr) } func extractAllZones(fqdn string) []string { parts := strings.Split(dns01.UnFqdn(fqdn), ".") if len(parts) < 3 { return nil } var zones []string for i := 1; i < len(parts)-1; i++ { zones = append(zones, strings.Join(parts[i:], ".")) } return zones } lego-4.9.1/providers/dns/gcore/gcore.toml000066400000000000000000000013531434020463500203520ustar00rootroot00000000000000Name = "G-Core Labs" Description = '''''' URL = "https://gcorelabs.com/dns/" Code = "gcore" Since = "v4.5.0" Example = ''' GCORE_PERMANENT_API_TOKEN=xxxxx \ lego --email you@example.com --dns gcore --domains my.example.org run ''' [Configuration] [Configuration.Credentials] GCORE_PERMANENT_API_TOKEN = "Permanent API tokene (https://gcorelabs.com/blog/permanent-api-token-explained/)" [Configuration.Additional] GCORE_POLLING_INTERVAL = "Time between DNS propagation check" GCORE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" GCORE_TTL = "The TTL of the TXT record used for the DNS challenge" GCORE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://dnsapi.gcorelabs.com/docs#tag/zonesV2" lego-4.9.1/providers/dns/gcore/gcore_test.go000066400000000000000000000054661434020463500210540ustar00rootroot00000000000000package gcore import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var envTest = tester.NewEnvTest(EnvPermanentAPIToken).WithDomain(envNamespace + "DOMAIN") func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvPermanentAPIToken: "A", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvPermanentAPIToken: "", }, expected: "gcore: some credentials information are missing: GCORE_PERMANENT_API_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiToken string expected string }{ { desc: "success", apiToken: "A", }, { desc: "missing credentials", expected: "gcore: incomplete credentials provided", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIToken = test.apiToken p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func Test_extractAllZones(t *testing.T) { testCases := []struct { desc string fqdn string expected []string }{ { desc: "success", fqdn: "_acme-challenge.my.test.domain.com.", expected: []string{"my.test.domain.com", "test.domain.com", "domain.com"}, }, { desc: "empty", fqdn: "_acme-challenge.com.", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() got := extractAllZones(test.fqdn) assert.Equal(t, test.expected, got) }) } } lego-4.9.1/providers/dns/gcore/internal/000077500000000000000000000000001434020463500201705ustar00rootroot00000000000000lego-4.9.1/providers/dns/gcore/internal/client.go000066400000000000000000000100661434020463500220000ustar00rootroot00000000000000package internal import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "path" "strings" "time" ) const ( defaultBaseURL = "https://api.gcorelabs.com/dns" tokenHeader = "APIKey" txtRecordType = "TXT" ) // Client for DNS API. type Client struct { HTTPClient *http.Client baseURL *url.URL token string } // NewClient constructor of Client. func NewClient(token string) *Client { baseURL, _ := url.Parse(defaultBaseURL) return &Client{ token: token, baseURL: baseURL, HTTPClient: &http.Client{Timeout: 10 * time.Second}, } } // GetZone gets zone information. // https://dnsapi.gcorelabs.com/docs#operation/Zone func (c *Client) GetZone(ctx context.Context, name string) (Zone, error) { zone := Zone{} uri := path.Join("/v2/zones", name) err := c.do(ctx, http.MethodGet, uri, nil, &zone) if err != nil { return Zone{}, fmt.Errorf("get zone %s: %w", name, err) } return zone, nil } // GetRRSet gets RRSet item. // https://dnsapi.gcorelabs.com/docs#operation/RRSet func (c *Client) GetRRSet(ctx context.Context, zone, name string) (RRSet, error) { var result RRSet uri := path.Join("/v2/zones", zone, name, txtRecordType) err := c.do(ctx, http.MethodGet, uri, nil, &result) if err != nil { return RRSet{}, fmt.Errorf("get txt records %s -> %s: %w", zone, name, err) } return result, nil } // DeleteRRSet removes RRSet record. // https://dnsapi.gcorelabs.com/docs#operation/DeleteRRSet func (c *Client) DeleteRRSet(ctx context.Context, zone, name string) error { uri := path.Join("/v2/zones", zone, name, txtRecordType) err := c.do(ctx, http.MethodDelete, uri, nil, nil) if err != nil { // Support DELETE idempotence https://developer.mozilla.org/en-US/docs/Glossary/Idempotent statusErr := new(APIError) if errors.As(err, statusErr) && statusErr.StatusCode == http.StatusNotFound { return nil } return fmt.Errorf("delete record request: %w", err) } return nil } // AddRRSet adds TXT record (create or update). func (c *Client) AddRRSet(ctx context.Context, zone, recordName, value string, ttl int) error { record := RRSet{TTL: ttl, Records: []Records{{Content: []string{value}}}} txt, err := c.GetRRSet(ctx, zone, recordName) if err == nil && len(txt.Records) > 0 { record.Records = append(record.Records, txt.Records...) return c.updateRRSet(ctx, zone, recordName, record) } return c.createRRSet(ctx, zone, recordName, record) } // https://dnsapi.gcorelabs.com/docs#operation/CreateRRSet func (c *Client) createRRSet(ctx context.Context, zone, name string, record RRSet) error { uri := path.Join("/v2/zones", zone, name, txtRecordType) return c.do(ctx, http.MethodPost, uri, record, nil) } // https://dnsapi.gcorelabs.com/docs#operation/UpdateRRSet func (c *Client) updateRRSet(ctx context.Context, zone, name string, record RRSet) error { uri := path.Join("/v2/zones", zone, name, txtRecordType) return c.do(ctx, http.MethodPut, uri, record, nil) } func (c *Client) do(ctx context.Context, method, uri string, bodyParams interface{}, dest interface{}) error { var bs []byte if bodyParams != nil { var err error bs, err = json.Marshal(bodyParams) if err != nil { return fmt.Errorf("encode bodyParams: %w", err) } } endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, uri)) if err != nil { return fmt.Errorf("failed to parse endpoint: %w", err) } req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), strings.NewReader(string(bs))) if err != nil { return fmt.Errorf("new request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("%s %s", tokenHeader, c.token)) resp, err := c.HTTPClient.Do(req) if err != nil { return fmt.Errorf("send request: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode/100 != 2 { all, _ := io.ReadAll(resp.Body) e := APIError{ StatusCode: resp.StatusCode, } err := json.Unmarshal(all, &e) if err != nil { e.Message = string(all) } return e } if dest == nil { return nil } return json.NewDecoder(resp.Body).Decode(dest) } lego-4.9.1/providers/dns/gcore/internal/client_test.go000066400000000000000000000143631434020463500230430ustar00rootroot00000000000000package internal import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "reflect" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( testToken = "test" testRecordContent = "acme" testRecordContent2 = "foo" testTTL = 10 ) func setupTest(t *testing.T) (*http.ServeMux, *Client) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient(testToken) client.baseURL, _ = url.Parse(server.URL) return mux, client } func TestClient_GetZone(t *testing.T) { mux, client := setupTest(t) expected := Zone{Name: "example.com"} mux.Handle("/v2/zones/example.com", validationHandler{ method: http.MethodGet, next: handleJSONResponse(expected), }) zone, err := client.GetZone(context.Background(), "example.com") require.NoError(t, err) assert.Equal(t, expected, zone) } func TestClient_GetZone_error(t *testing.T) { mux, client := setupTest(t) mux.Handle("/v2/zones/example.com", validationHandler{ method: http.MethodGet, next: handleAPIError(), }) _, err := client.GetZone(context.Background(), "example.com") require.Error(t, err) } func TestClient_GetRRSet(t *testing.T) { mux, client := setupTest(t) expected := RRSet{ TTL: testTTL, Records: []Records{ {Content: []string{testRecordContent}}, }, } mux.Handle("/v2/zones/example.com/foo.example.com/TXT", validationHandler{ method: http.MethodGet, next: handleJSONResponse(expected), }) rrSet, err := client.GetRRSet(context.Background(), "example.com", "foo.example.com") require.NoError(t, err) assert.Equal(t, expected, rrSet) } func TestClient_GetRRSet_error(t *testing.T) { mux, client := setupTest(t) mux.Handle("/v2/zones/example.com/foo.example.com/TXT", validationHandler{ method: http.MethodGet, next: handleAPIError(), }) _, err := client.GetRRSet(context.Background(), "example.com", "foo.example.com") require.Error(t, err) } func TestClient_DeleteRRSet(t *testing.T) { mux, client := setupTest(t) mux.Handle("/v2/zones/test.example.com/my.test.example.com/"+txtRecordType, validationHandler{method: http.MethodDelete}) err := client.DeleteRRSet(context.Background(), "test.example.com", "my.test.example.com.") require.NoError(t, err) } func TestClient_DeleteRRSet_error(t *testing.T) { mux, client := setupTest(t) mux.Handle("/v2/zones/test.example.com/my.test.example.com/"+txtRecordType, validationHandler{ method: http.MethodDelete, next: handleAPIError(), }) err := client.DeleteRRSet(context.Background(), "test.example.com", "my.test.example.com.") require.NoError(t, err) } func TestClient_AddRRSet(t *testing.T) { testCases := []struct { desc string zone string recordName string value string handledDomain string handlers map[string]http.Handler wantErr bool }{ { desc: "success add", zone: "test.example.com", recordName: "my.test.example.com", value: testRecordContent, handlers: map[string]http.Handler{ // createRRSet "/v2/zones/test.example.com/my.test.example.com/" + txtRecordType: validationHandler{ method: http.MethodPost, next: handleAddRRSet([]Records{{Content: []string{testRecordContent}}}), }, }, }, { desc: "success update", zone: "test.example.com", recordName: "my.test.example.com", value: testRecordContent, handlers: map[string]http.Handler{ "/v2/zones/test.example.com/my.test.example.com/" + txtRecordType: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodGet: // GetRRSet data := RRSet{ TTL: testTTL, Records: []Records{{Content: []string{testRecordContent2}}}, } handleJSONResponse(data).ServeHTTP(rw, req) case http.MethodPut: // updateRRSet expected := []Records{ {Content: []string{testRecordContent}}, {Content: []string{testRecordContent2}}, } handleAddRRSet(expected).ServeHTTP(rw, req) default: http.Error(rw, "wrong method", http.StatusMethodNotAllowed) } }), }, }, { desc: "not in the zone", zone: "test.example.com", recordName: "notfound.example.com", value: testRecordContent, wantErr: true, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { mux, cl := setupTest(t) for pattern, handler := range test.handlers { mux.Handle(pattern, handler) } err := cl.AddRRSet(context.Background(), test.zone, test.recordName, test.value, testTTL) if test.wantErr { require.Error(t, err) return } require.NoError(t, err) }) } } type validationHandler struct { method string next http.Handler } func (v validationHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { if req.Header.Get("Authorization") != fmt.Sprintf("%s %s", tokenHeader, testToken) { rw.WriteHeader(http.StatusForbidden) _ = json.NewEncoder(rw).Encode(APIError{Message: "token up for parsing was not passed through the context"}) return } if req.Method != v.method { http.Error(rw, "wrong method", http.StatusMethodNotAllowed) return } if v.next != nil { v.next.ServeHTTP(rw, req) } } func handleAPIError() http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusInternalServerError) _ = json.NewEncoder(rw).Encode(APIError{Message: "oops"}) } } func handleJSONResponse(data interface{}) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { err := json.NewEncoder(rw).Encode(data) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } } } func handleAddRRSet(expected []Records) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { body := RRSet{} err := json.NewDecoder(req.Body).Decode(&body) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } if body.TTL != testTTL { http.Error(rw, "wrong ttl", http.StatusInternalServerError) return } if !reflect.DeepEqual(body.Records, expected) { http.Error(rw, "wrong resource records", http.StatusInternalServerError) return } } } lego-4.9.1/providers/dns/gcore/internal/types.go000066400000000000000000000006611434020463500216660ustar00rootroot00000000000000package internal import "fmt" type Zone struct { Name string `json:"name"` } type RRSet struct { TTL int `json:"ttl"` Records []Records `json:"resource_records"` } type Records struct { Content []string `json:"content"` } type APIError struct { StatusCode int `json:"-"` Message string `json:"error,omitempty"` } func (a APIError) Error() string { return fmt.Sprintf("%d: %s", a.StatusCode, a.Message) } lego-4.9.1/providers/dns/glesys/000077500000000000000000000000001434020463500165635ustar00rootroot00000000000000lego-4.9.1/providers/dns/glesys/client.go000066400000000000000000000043521434020463500203740ustar00rootroot00000000000000package glesys import ( "bytes" "encoding/json" "fmt" "net/http" "github.com/go-acme/lego/v4/log" ) // types for JSON method calls, parameters, and responses type addRecordRequest struct { DomainName string `json:"domainname"` Host string `json:"host"` Type string `json:"type"` Data string `json:"data"` TTL int `json:"ttl,omitempty"` } type deleteRecordRequest struct { RecordID int `json:"recordid"` } type responseStruct struct { Response struct { Status struct { Code int `json:"code"` } `json:"status"` Record deleteRecordRequest `json:"record"` } `json:"response"` } func (d *DNSProvider) addTXTRecord(fqdn, domain, name, value string, ttl int) (int, error) { response, err := d.sendRequest(http.MethodPost, "addrecord", addRecordRequest{ DomainName: domain, Host: name, Type: "TXT", Data: value, TTL: ttl, }) if response != nil && response.Response.Status.Code == http.StatusOK { log.Infof("[%s]: Successfully created record id %d", fqdn, response.Response.Record.RecordID) return response.Response.Record.RecordID, nil } return 0, err } func (d *DNSProvider) deleteTXTRecord(fqdn string, recordid int) error { response, err := d.sendRequest(http.MethodPost, "deleterecord", deleteRecordRequest{ RecordID: recordid, }) if response != nil && response.Response.Status.Code == 200 { log.Infof("[%s]: Successfully deleted record id %d", fqdn, recordid) } return err } func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*responseStruct, error) { url := fmt.Sprintf("%s/%s", defaultBaseURL, resource) body, err := json.Marshal(payload) if err != nil { return nil, err } req, err := http.NewRequest(method, url, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.SetBasicAuth(d.config.APIUser, d.config.APIKey) resp, err := d.config.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { return nil, fmt.Errorf("request failed with HTTP status code %d", resp.StatusCode) } var response responseStruct err = json.NewDecoder(resp.Body).Decode(&response) return &response, err } lego-4.9.1/providers/dns/glesys/glesys.go000066400000000000000000000111651434020463500204240ustar00rootroot00000000000000// Package glesys implements a DNS provider for solving the DNS-01 challenge using GleSYS api. package glesys import ( "errors" "fmt" "net/http" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) const ( // defaultBaseURL is the GleSYS API endpoint used by Present and CleanUp. defaultBaseURL = "https://api.glesys.com/domain" minTTL = 60 ) // Environment variables names. const ( envNamespace = "GLESYS_" EnvAPIUser = envNamespace + "API_USER" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIUser string APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 20*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 20*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config activeRecords map[string]int inProgressMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for GleSYS. // Credentials must be passed in the environment variables: // GLESYS_API_USER and GLESYS_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIUser, EnvAPIKey) if err != nil { return nil, fmt.Errorf("glesys: %w", err) } config := NewDefaultConfig() config.APIUser = values[EnvAPIUser] config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for GleSYS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("glesys: the configuration of the DNS provider is nil") } if config.APIUser == "" || config.APIKey == "" { return nil, errors.New("glesys: incomplete credentials provided") } if config.TTL < minTTL { return nil, fmt.Errorf("glesys: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } return &DNSProvider{ config: config, activeRecords: make(map[string]int), }, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) // find authZone authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("glesys: findZoneByFqdn failure: %w", err) } // determine name of TXT record if !strings.HasSuffix( strings.ToLower(fqdn), strings.ToLower("."+authZone)) { return fmt.Errorf("glesys: unexpected authZone %s for fqdn %s", authZone, fqdn) } name := fqdn[:len(fqdn)-len("."+authZone)] // acquire lock and check there is not a challenge already in // progress for this value of authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() // add TXT record into authZone // TODO(ldez) replace domain by FQDN to follow CNAME. recordID, err := d.addTXTRecord(domain, dns01.UnFqdn(authZone), name, value, d.config.TTL) if err != nil { return err } // save data necessary for CleanUp d.activeRecords[fqdn] = recordID return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) // acquire lock and retrieve authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() if _, ok := d.activeRecords[fqdn]; !ok { // if there is no cleanup information then just return return nil } recordID := d.activeRecords[fqdn] delete(d.activeRecords, fqdn) // delete TXT record from authZone // TODO(ldez) replace domain by FQDN to follow CNAME. return d.deleteTXTRecord(domain, recordID) } // Timeout returns the values (20*time.Minute, 20*time.Second) which // are used by the acme package as timeout and check interval values // when checking for DNS record propagation with GleSYS. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/glesys/glesys.toml000066400000000000000000000013061434020463500207660ustar00rootroot00000000000000Name = "Glesys" Description = '''''' URL = "https://glesys.com/" Code = "glesys" Since = "v0.5.0" Example = ''' GLESYS_API_USER=xxxxx \ GLESYS_API_KEY=yyyyy \ lego --email you@example.com --dns glesys --domains my.example.org run ''' [Configuration] [Configuration.Credentials] GLESYS_API_USER = "API user" GLESYS_API_KEY = "API key" [Configuration.Additional] GLESYS_POLLING_INTERVAL = "Time between DNS propagation check" GLESYS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" GLESYS_TTL = "The TTL of the TXT record used for the DNS challenge" GLESYS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://github.com/GleSYS/API/wiki/API-Documentation" lego-4.9.1/providers/dns/glesys/glesys_test.go000066400000000000000000000060141434020463500214600ustar00rootroot00000000000000package glesys import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIUser, EnvAPIKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIUser: "A", EnvAPIKey: "B", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIUser: "", EnvAPIKey: "", }, expected: "glesys: some credentials information are missing: GLESYS_API_USER,GLESYS_API_KEY", }, { desc: "missing api user", envVars: map[string]string{ EnvAPIUser: "", EnvAPIKey: "B", }, expected: "glesys: some credentials information are missing: GLESYS_API_USER", }, { desc: "missing api key", envVars: map[string]string{ EnvAPIUser: "A", EnvAPIKey: "", }, expected: "glesys: some credentials information are missing: GLESYS_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.activeRecords) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiUser string apiKey string expected string }{ { desc: "success", apiUser: "A", apiKey: "B", }, { desc: "missing credentials", expected: "glesys: incomplete credentials provided", }, { desc: "missing api user", apiUser: "", apiKey: "B", expected: "glesys: incomplete credentials provided", }, { desc: "missing api key", apiUser: "A", apiKey: "", expected: "glesys: incomplete credentials provided", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.APIUser = test.apiUser p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.activeRecords) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/godaddy/000077500000000000000000000000001434020463500166705ustar00rootroot00000000000000lego-4.9.1/providers/dns/godaddy/godaddy.go000066400000000000000000000130061434020463500206320ustar00rootroot00000000000000// Package godaddy implements a DNS provider for solving the DNS-01 challenge using godaddy DNS. package godaddy import ( "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/godaddy/internal" ) const minTTL = 600 // Environment variables names. const ( envNamespace = "GODADDY_" EnvAPIKey = envNamespace + "API_KEY" EnvAPISecret = envNamespace + "API_SECRET" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string APISecret string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for godaddy. // Credentials must be passed in the environment variables: // GODADDY_API_KEY and GODADDY_API_SECRET. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey, EnvAPISecret) if err != nil { return nil, fmt.Errorf("godaddy: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] config.APISecret = values[EnvAPISecret] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for godaddy. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("godaddy: the configuration of the DNS provider is nil") } if config.APIKey == "" || config.APISecret == "" { return nil, errors.New("godaddy: credentials missing") } if config.TTL < minTTL { return nil, fmt.Errorf("godaddy: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } client := internal.NewClient(config.APIKey, config.APISecret) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS // propagation. Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) domainZone, err := getZone(fqdn) if err != nil { return fmt.Errorf("godaddy: failed to get zone: %w", err) } recordName := extractRecordName(fqdn, domainZone) records, err := d.client.GetRecords(domainZone, "TXT", recordName) if err != nil { return fmt.Errorf("godaddy: failed to get TXT records: %w", err) } var newRecords []internal.DNSRecord for _, record := range records { if record.Data != "" { newRecords = append(newRecords, record) } } record := internal.DNSRecord{ Type: "TXT", Name: recordName, Data: value, TTL: d.config.TTL, } newRecords = append(newRecords, record) err = d.client.UpdateTxtRecords(newRecords, domainZone, recordName) if err != nil { return fmt.Errorf("godaddy: failed to add TXT record: %w", err) } return nil } // CleanUp removes the record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) domainZone, err := getZone(fqdn) if err != nil { return fmt.Errorf("godaddy: failed to get zone: %w", err) } recordName := extractRecordName(fqdn, domainZone) records, err := d.client.GetRecords(domainZone, "TXT", recordName) if err != nil { return fmt.Errorf("godaddy: failed to get TXT records: %w", err) } if len(records) == 0 { return nil } allTxtRecords, err := d.client.GetRecords(domainZone, "TXT", "") if err != nil { return fmt.Errorf("godaddy: failed to get all TXT records: %w", err) } var recordsKeep []internal.DNSRecord for _, record := range allTxtRecords { if record.Data != value && record.Data != "" { recordsKeep = append(recordsKeep, record) } } // GoDaddy API don't provide a way to delete a record, an "empty" record must be added. if len(recordsKeep) == 0 { emptyRecord := internal.DNSRecord{Name: "empty", Data: ""} recordsKeep = append(recordsKeep, emptyRecord) } err = d.client.UpdateTxtRecords(recordsKeep, domainZone, "") if err != nil { return fmt.Errorf("godaddy: failed to remove TXT record: %w", err) } return nil } func extractRecordName(fqdn, zone string) string { name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+zone); idx != -1 { return name[:idx] } return name } func getZone(fqdn string) (string, error) { authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", err } return dns01.UnFqdn(authZone), nil } lego-4.9.1/providers/dns/godaddy/godaddy.toml000066400000000000000000000013341434020463500212010ustar00rootroot00000000000000Name = "Go Daddy" Description = '''''' URL = "https://godaddy.com" Code = "godaddy" Since = "v0.5.0" Example = ''' GODADDY_API_KEY=xxxxxxxx \ GODADDY_API_SECRET=yyyyyyyy \ lego --email you@example.com --dns godaddy --domains my.example.org run ''' [Configuration] [Configuration.Credentials] GODADDY_API_KEY = "API key" GODADDY_API_SECRET = "API secret" [Configuration.Additional] GODADDY_POLLING_INTERVAL = "Time between DNS propagation check" GODADDY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" GODADDY_TTL = "The TTL of the TXT record used for the DNS challenge" GODADDY_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developer.godaddy.com/doc/endpoint/domains" lego-4.9.1/providers/dns/godaddy/godaddy_test.go000066400000000000000000000057011434020463500216740ustar00rootroot00000000000000package godaddy import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIKey, EnvAPISecret). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", EnvAPISecret: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIKey: "", EnvAPISecret: "", }, expected: "godaddy: some credentials information are missing: GODADDY_API_KEY,GODADDY_API_SECRET", }, { desc: "missing access key", envVars: map[string]string{ EnvAPIKey: "", EnvAPISecret: "456", }, expected: "godaddy: some credentials information are missing: GODADDY_API_KEY", }, { desc: "missing secret key", envVars: map[string]string{ EnvAPIKey: "123", EnvAPISecret: "", }, expected: "godaddy: some credentials information are missing: GODADDY_API_SECRET", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string apiSecret string expected string }{ { desc: "success", apiKey: "123", apiSecret: "456", }, { desc: "missing credentials", expected: "godaddy: credentials missing", }, { desc: "missing api key", apiSecret: "456", expected: "godaddy: credentials missing", }, { desc: "missing secret key", apiKey: "123", expected: "godaddy: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.APISecret = test.apiSecret p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/godaddy/internal/000077500000000000000000000000001434020463500205045ustar00rootroot00000000000000lego-4.9.1/providers/dns/godaddy/internal/client.go000066400000000000000000000046221434020463500223150ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "path" "time" ) // DefaultBaseURL represents the API endpoint to call. const DefaultBaseURL = "https://api.godaddy.com" type Client struct { HTTPClient *http.Client baseURL *url.URL apiKey string apiSecret string } func NewClient(apiKey string, apiSecret string) *Client { baseURL, _ := url.Parse(DefaultBaseURL) return &Client{ HTTPClient: &http.Client{Timeout: 5 * time.Second}, baseURL: baseURL, apiKey: apiKey, apiSecret: apiSecret, } } func (d *Client) GetRecords(domainZone, rType, recordName string) ([]DNSRecord, error) { resource := path.Clean(fmt.Sprintf("/v1/domains/%s/records/%s/%s", domainZone, rType, recordName)) resp, err := d.makeRequest(http.MethodGet, resource, nil) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("could not get records: Domain: %s; Record: %s, Status: %v; Body: %s", domainZone, recordName, resp.StatusCode, string(bodyBytes)) } var records []DNSRecord err = json.NewDecoder(resp.Body).Decode(&records) if err != nil { return nil, err } return records, nil } func (d *Client) UpdateTxtRecords(records []DNSRecord, domainZone, recordName string) error { body, err := json.Marshal(records) if err != nil { return err } resource := path.Clean(fmt.Sprintf("/v1/domains/%s/records/TXT/%s", domainZone, recordName)) var resp *http.Response resp, err = d.makeRequest(http.MethodPut, resource, bytes.NewReader(body)) if err != nil { return err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("could not create record %v; Status: %v; Body: %s", string(body), resp.StatusCode, string(bodyBytes)) } return nil } func (d *Client) makeRequest(method, uri string, body io.Reader) (*http.Response, error) { endpoint, err := d.baseURL.Parse(path.Join(d.baseURL.Path, uri)) if err != nil { return nil, err } req, err := http.NewRequest(method, endpoint.String(), body) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("sso-key %s:%s", d.apiKey, d.apiSecret)) return d.HTTPClient.Do(req) } lego-4.9.1/providers/dns/godaddy/internal/client_test.go000066400000000000000000000112051434020463500233470ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupTest(t *testing.T) (*http.ServeMux, *Client) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient("key", "secret") client.HTTPClient = server.Client() client.baseURL, _ = url.Parse(server.URL) return mux, client } func TestClient_GetRecords(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/v1/domains/example.com/records/TXT/", testHandler(http.MethodGet, http.StatusOK, "getrecords.json")) records, err := client.GetRecords("example.com", "TXT", "") require.NoError(t, err) expected := []DNSRecord{ {Name: "_acme-challenge", Type: "TXT", Data: " ", TTL: 600}, {Name: "_acme-challenge.example", Type: "TXT", Data: "6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU", TTL: 600}, {Name: "_acme-challenge.example", Type: "TXT", Data: "8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek", TTL: 600}, {Name: "_acme-challenge.lego", Type: "TXT", Data: " ", TTL: 600}, {Name: "_acme-challenge.lego", Type: "TXT", Data: "0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A", TTL: 600}, {Name: "_acme-challenge.lego", Type: "TXT", Data: "acme", TTL: 600}, } assert.Equal(t, expected, records) } func TestClient_GetRecords_errors(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/v1/domains/example.com/records/TXT/", testHandler(http.MethodGet, http.StatusUnprocessableEntity, "errors.json")) records, err := client.GetRecords("example.com", "TXT", "") require.Error(t, err) assert.Nil(t, records) } func TestClient_UpdateTxtRecords(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/v1/domains/example.com/records/TXT/lego", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPut { http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed) return } auth := req.Header.Get("Authorization") if auth != "sso-key key:secret" { http.Error(rw, fmt.Sprintf("invalid API key or secret: %s", auth), http.StatusUnauthorized) return } }) records := []DNSRecord{ {Name: "_acme-challenge", Type: "TXT", Data: " ", TTL: 600}, {Name: "_acme-challenge.example", Type: "TXT", Data: "6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU", TTL: 600}, {Name: "_acme-challenge.example", Type: "TXT", Data: "8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek", TTL: 600}, {Name: "_acme-challenge.lego", Type: "TXT", Data: " ", TTL: 600}, {Name: "_acme-challenge.lego", Type: "TXT", Data: "0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A", TTL: 600}, {Name: "_acme-challenge.lego", Type: "TXT", Data: "acme", TTL: 600}, } err := client.UpdateTxtRecords(records, "example.com", "lego") require.NoError(t, err) } func TestClient_UpdateTxtRecords_errors(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/v1/domains/example.com/records/TXT/lego", testHandler(http.MethodPut, http.StatusUnprocessableEntity, "errors.json")) records := []DNSRecord{ {Name: "_acme-challenge", Type: "TXT", Data: " ", TTL: 600}, {Name: "_acme-challenge.example", Type: "TXT", Data: "6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU", TTL: 600}, {Name: "_acme-challenge.example", Type: "TXT", Data: "8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek", TTL: 600}, {Name: "_acme-challenge.lego", Type: "TXT", Data: " ", TTL: 600}, {Name: "_acme-challenge.lego", Type: "TXT", Data: "0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A", TTL: 600}, {Name: "_acme-challenge.lego", Type: "TXT", Data: "acme", TTL: 600}, } err := client.UpdateTxtRecords(records, "example.com", "lego") require.Error(t, err) } func testHandler(method string, statusCode int, filename string) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { if req.Method != method { http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed) return } auth := req.Header.Get("Authorization") if auth != "sso-key key:secret" { http.Error(rw, fmt.Sprintf("invalid API key or secret: %s", auth), http.StatusUnauthorized) return } rw.WriteHeader(statusCode) if statusCode == http.StatusNoContent { return } file, err := os.Open(filepath.Join("fixtures", filename)) if err != nil { http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) return } } } lego-4.9.1/providers/dns/godaddy/internal/fixtures/000077500000000000000000000000001434020463500223555ustar00rootroot00000000000000lego-4.9.1/providers/dns/godaddy/internal/fixtures/errors.json000066400000000000000000000001521434020463500245620ustar00rootroot00000000000000{ "code": "INVALID_BODY", "message": "Request body doesn't fulfill schema, see details in `fields`" } lego-4.9.1/providers/dns/godaddy/internal/fixtures/getrecords.json000066400000000000000000000012551434020463500254140ustar00rootroot00000000000000[ { "name":"_acme-challenge", "type":"TXT", "data":" ", "ttl":600 }, { "name":"_acme-challenge.example", "type":"TXT", "data":"6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU", "ttl":600 }, { "name":"_acme-challenge.example", "type":"TXT", "data":"8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek", "ttl":600 }, { "name":"_acme-challenge.lego", "type":"TXT", "data":" ", "ttl":600 }, { "name":"_acme-challenge.lego", "type":"TXT", "data":"0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A", "ttl":600 }, { "name":"_acme-challenge.lego", "type":"TXT", "data":"acme", "ttl":600 } ] lego-4.9.1/providers/dns/godaddy/internal/types.go000066400000000000000000000006531434020463500222030ustar00rootroot00000000000000package internal // DNSRecord a DNS record. type DNSRecord struct { Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` Data string `json:"data"` TTL int `json:"ttl,omitempty"` Priority int `json:"priority,omitempty"` Port int `json:"port,omitempty"` Protocol string `json:"protocol,omitempty"` Service string `json:"service,omitempty"` Weight int `json:"weight,omitempty"` } lego-4.9.1/providers/dns/hetzner/000077500000000000000000000000001434020463500167345ustar00rootroot00000000000000lego-4.9.1/providers/dns/hetzner/hetzner.go000066400000000000000000000111221434020463500207370ustar00rootroot00000000000000// Package hetzner implements a DNS provider for solving the DNS-01 challenge using Hetzner DNS. package hetzner import ( "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/hetzner/internal" ) const minTTL = 60 // Environment variables names. const ( envNamespace = "HETZNER_" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for hetzner. // Credentials must be passed in the environment variable: HETZNER_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("hetzner: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for hetzner. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("hetzner: the configuration of the DNS provider is nil") } if config.APIKey == "" { return nil, errors.New("hetzner: credentials missing") } if config.TTL < minTTL { return nil, fmt.Errorf("hetzner: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } client := internal.NewClient(config.APIKey) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS // propagation. Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := getZone(fqdn) if err != nil { return fmt.Errorf("hetzner: failed to find zone: fqdn=%s: %w", fqdn, err) } zoneID, err := d.client.GetZoneID(zone) if err != nil { return fmt.Errorf("hetzner: %w", err) } record := internal.DNSRecord{ Type: "TXT", Name: extractRecordName(fqdn, zone), Value: value, TTL: d.config.TTL, ZoneID: zoneID, } if err := d.client.CreateRecord(record); err != nil { return fmt.Errorf("hetzner: failed to add TXT record: fqdn=%s, zoneID=%s: %w", fqdn, zoneID, err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := getZone(fqdn) if err != nil { return fmt.Errorf("hetzner: failed to find zone: fqdn=%s: %w", fqdn, err) } zoneID, err := d.client.GetZoneID(zone) if err != nil { return fmt.Errorf("hetzner: %w", err) } recordName := extractRecordName(fqdn, zone) record, err := d.client.GetTxtRecord(recordName, value, zoneID) if err != nil { return fmt.Errorf("hetzner: %w", err) } if err := d.client.DeleteRecord(record.ID); err != nil { return fmt.Errorf("hetzner: failed to delate TXT record: id=%s, name=%s: %w", record.ID, record.Name, err) } return nil } func extractRecordName(fqdn, zone string) string { name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+zone); idx != -1 { return name[:idx] } return name } func getZone(fqdn string) (string, error) { authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", err } return dns01.UnFqdn(authZone), nil } lego-4.9.1/providers/dns/hetzner/hetzner.toml000066400000000000000000000012411434020463500213060ustar00rootroot00000000000000Name = "Hetzner" Description = '''''' URL = "https://hetzner.com" Code = "hetzner" Since = "v3.7.0" Example = ''' HETZNER_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ lego --email you@example.com --dns hetzner --domains my.example.org run ''' [Configuration] [Configuration.Credentials] HETZNER_API_KEY = "API key" [Configuration.Additional] HETZNER_POLLING_INTERVAL = "Time between DNS propagation check" HETZNER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" HETZNER_TTL = "The TTL of the TXT record used for the DNS challenge" HETZNER_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://dns.hetzner.com/api-docs" lego-4.9.1/providers/dns/hetzner/hetzner_test.go000066400000000000000000000045721434020463500220110ustar00rootroot00000000000000package hetzner import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIKey: "", }, expected: "hetzner: some credentials information are missing: HETZNER_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string ttl int expected string }{ { desc: "success", ttl: minTTL, apiKey: "123", }, { desc: "missing credentials", ttl: minTTL, expected: "hetzner: credentials missing", }, { desc: "invalid TTL", apiKey: "123", ttl: 10, expected: "hetzner: invalid TTL, TTL (10) must be greater than 60", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.TTL = test.ttl p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/hetzner/internal/000077500000000000000000000000001434020463500205505ustar00rootroot00000000000000lego-4.9.1/providers/dns/hetzner/internal/client.go000066400000000000000000000111521434020463500223550ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "path" ) // defaultBaseURL represents the API endpoint to call. const defaultBaseURL = "https://dns.hetzner.com" const authHeader = "Auth-API-Token" // Client the Hetzner client. type Client struct { HTTPClient *http.Client BaseURL string apiKey string } // NewClient Creates a new Hetzner client. func NewClient(apiKey string) *Client { return &Client{ HTTPClient: http.DefaultClient, BaseURL: defaultBaseURL, apiKey: apiKey, } } // GetTxtRecord gets a TXT record. func (c *Client) GetTxtRecord(name, value, zoneID string) (*DNSRecord, error) { records, err := c.getRecords(zoneID) if err != nil { return nil, err } for _, record := range records.Records { if record.Type == "TXT" && record.Name == name && record.Value == value { return &record, nil } } return nil, fmt.Errorf("could not find record: zone ID: %s; Record: %s", zoneID, name) } // https://dns.hetzner.com/api-docs#operation/GetRecords func (c *Client) getRecords(zoneID string) (*DNSRecords, error) { endpoint, err := c.createEndpoint("api", "v1", "records") if err != nil { return nil, fmt.Errorf("failed to create endpoint: %w", err) } query := endpoint.Query() query.Set("zone_id", zoneID) endpoint.RawQuery = query.Encode() resp, err := c.do(http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("could not get records: zone ID: %s; Status: %s; Body: %s", zoneID, resp.Status, string(bodyBytes)) } records := &DNSRecords{} err = json.NewDecoder(resp.Body).Decode(records) if err != nil { return nil, fmt.Errorf("failed to decode response body: %w", err) } return records, nil } // CreateRecord creates a DNS record. // https://dns.hetzner.com/api-docs#operation/CreateRecord func (c *Client) CreateRecord(record DNSRecord) error { body, err := json.Marshal(record) if err != nil { return err } endpoint, err := c.createEndpoint("api", "v1", "records") if err != nil { return fmt.Errorf("failed to create endpoint: %w", err) } resp, err := c.do(http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { return err } if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return fmt.Errorf("could not create record %s; Status: %s; Body: %s", string(body), resp.Status, string(bodyBytes)) } return nil } // DeleteRecord deletes a DNS record. // https://dns.hetzner.com/api-docs#operation/DeleteRecord func (c *Client) DeleteRecord(recordID string) error { endpoint, err := c.createEndpoint("api", "v1", "records", recordID) if err != nil { return fmt.Errorf("failed to create endpoint: %w", err) } resp, err := c.do(http.MethodDelete, endpoint, nil) if err != nil { return err } if resp.StatusCode != http.StatusOK { return fmt.Errorf("could not delete record: %s; Status: %s", resp.Status, recordID) } return nil } // GetZoneID gets the zone ID for a domain. func (c *Client) GetZoneID(domain string) (string, error) { zones, err := c.getZones() if err != nil { return "", err } for _, zone := range zones.Zones { if zone.Name == domain { return zone.ID, nil } } return "", fmt.Errorf("could not get zone for domain %s not found", domain) } // https://dns.hetzner.com/api-docs#operation/GetZones func (c *Client) getZones() (*Zones, error) { endpoint, err := c.createEndpoint("api", "v1", "zones") if err != nil { return nil, fmt.Errorf("failed to create endpoint: %w", err) } resp, err := c.do(http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("could not get zones: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("could not get zones: %s", resp.Status) } zones := &Zones{} err = json.NewDecoder(resp.Body).Decode(zones) if err != nil { return nil, fmt.Errorf("failed to decode response body: %w", err) } return zones, nil } func (c *Client) do(method string, endpoint fmt.Stringer, body io.Reader) (*http.Response, error) { req, err := http.NewRequest(method, endpoint.String(), body) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.Header.Set(authHeader, c.apiKey) return c.HTTPClient.Do(req) } func (c *Client) createEndpoint(parts ...string) (*url.URL, error) { baseURL, err := url.Parse(c.BaseURL) if err != nil { return nil, err } endpoint, err := baseURL.Parse(path.Join(parts...)) if err != nil { return nil, err } return endpoint, nil } lego-4.9.1/providers/dns/hetzner/internal/client_test.go000066400000000000000000000102531434020463500234150ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_GetTxtRecord(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) const zoneID = "zoneA" const apiKey = "myKeyA" mux.HandleFunc("/api/v1/records", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } auth := req.Header.Get(authHeader) if auth != apiKey { http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) return } zID := req.URL.Query().Get("zone_id") if zID != zoneID { http.Error(rw, fmt.Sprintf("invalid zone ID: %s", zID), http.StatusBadRequest) return } file, err := os.Open("./fixtures/get_txt_record.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient(apiKey) client.BaseURL = server.URL record, err := client.GetTxtRecord("test1", "txttxttxt", zoneID) require.NoError(t, err) fmt.Println(record) } func TestClient_CreateRecord(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) const zoneID = "zoneA" const apiKey = "myKeyB" mux.HandleFunc("/api/v1/records", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } auth := req.Header.Get(authHeader) if auth != apiKey { http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) return } file, err := os.Open("./fixtures/create_txt_record.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient(apiKey) client.BaseURL = server.URL record := DNSRecord{ Name: "test", Type: "TXT", Value: "txttxttxt", TTL: 600, ZoneID: zoneID, } err := client.CreateRecord(record) require.NoError(t, err) } func TestClient_DeleteRecord(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) const apiKey = "myKeyC" mux.HandleFunc("/api/v1/records/recordID", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodDelete { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } auth := req.Header.Get(authHeader) if auth != apiKey { http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) return } }) client := NewClient(apiKey) client.BaseURL = server.URL err := client.DeleteRecord("recordID") require.NoError(t, err) } func TestClient_GetZoneID(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) const apiKey = "myKeyD" mux.HandleFunc("/api/v1/zones", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } auth := req.Header.Get(authHeader) if auth != apiKey { http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) return } file, err := os.Open("./fixtures/get_zone_id.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient(apiKey) client.BaseURL = server.URL zoneID, err := client.GetZoneID("example.com") require.NoError(t, err) assert.Equal(t, "zoneA", zoneID) } lego-4.9.1/providers/dns/hetzner/internal/fixtures/000077500000000000000000000000001434020463500224215ustar00rootroot00000000000000lego-4.9.1/providers/dns/hetzner/internal/fixtures/create_txt_record.json000066400000000000000000000003341434020463500270140ustar00rootroot00000000000000{ "record": { "type": "A", "id": "string", "created": "2020-05-08T10:49:18Z", "modified": "2020-05-08T10:49:18Z", "zone_id": "string", "name": "string", "value": "string", "ttl": 0 } }lego-4.9.1/providers/dns/hetzner/internal/fixtures/get_txt_record.json000066400000000000000000000011371434020463500263320ustar00rootroot00000000000000{ "records": [ { "type": "A", "id": "1a", "created": "2020-05-08T10:49:18Z", "modified": "2020-05-08T10:49:18Z", "zone_id": "zoneA", "name": "test", "value": "10.10.10.10", "ttl": 600 }, { "type": "TXT", "id": "1b", "created": "2020-05-08T10:49:19Z", "modified": "2020-05-08T10:49:19Z", "zone_id": "zoneA", "name": "test1", "value": "txttxttxt", "ttl": 600 } ], "meta": { "pagination": { "page": 1, "per_page": 20, "last_page": 1, "total_entries": 2 } } }lego-4.9.1/providers/dns/hetzner/internal/fixtures/get_zone_id.json000066400000000000000000000026051434020463500256050ustar00rootroot00000000000000{ "zones": [ { "id": "zoneA", "created": "2020-05-08T10:49:18Z", "modified": "2020-05-08T10:49:18Z", "legacy_dns_host": "string", "legacy_ns": [ "string" ], "name": "example.com", "ns": [ "string" ], "owner": "string", "paused": true, "permission": "string", "project": "string", "registrar": "string", "status": "verified", "ttl": 0, "verified": "2020-05-08T10:49:18Z", "records_count": 0, "is_secondary_dns": true, "txt_verification": { "name": "string", "token": "string" } }, { "id": "zoneB", "created": "2020-05-08T10:49:18Z", "modified": "2020-05-08T10:49:18Z", "legacy_dns_host": "string", "legacy_ns": [ "string" ], "name": "example.org", "ns": [ "string" ], "owner": "string", "paused": true, "permission": "string", "project": "string", "registrar": "string", "status": "verified", "ttl": 0, "verified": "2020-05-08T10:49:18Z", "records_count": 0, "is_secondary_dns": true, "txt_verification": { "name": "string", "token": "string" } } ], "meta": { "pagination": { "page": 1, "per_page": 1, "last_page": 1, "total_entries": 0 } } }lego-4.9.1/providers/dns/hetzner/internal/model.go000066400000000000000000000011651434020463500222020ustar00rootroot00000000000000package internal // DNSRecord a DNS record. type DNSRecord struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` Value string `json:"value"` Priority int `json:"priority,omitempty"` TTL int `json:"ttl,omitempty"` ZoneID string `json:"zone_id,omitempty"` } // DNSRecords a set of DNS record. type DNSRecords struct { Records []DNSRecord `json:"records"` } // Zone a DNS zone. type Zone struct { ID string `json:"id"` Name string `json:"name"` } // Zones a set of DNS zones. type Zones struct { Zones []Zone `json:"zones"` } lego-4.9.1/providers/dns/hostingde/000077500000000000000000000000001434020463500172415ustar00rootroot00000000000000lego-4.9.1/providers/dns/hostingde/client.go000066400000000000000000000060571434020463500210560ustar00rootroot00000000000000package hostingde import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "time" "github.com/cenkalti/backoff/v4" ) const defaultBaseURL = "https://secure.hosting.de/api/dns/v1/json" // https://www.hosting.de/api/?json#list-zoneconfigs func (d *DNSProvider) listZoneConfigs(findRequest ZoneConfigsFindRequest) (*ZoneConfigsFindResponse, error) { uri := defaultBaseURL + "/zoneConfigsFind" findResponse := &ZoneConfigsFindResponse{} rawResp, err := d.post(uri, findRequest, findResponse) if err != nil { return nil, err } if len(findResponse.Response.Data) == 0 { return nil, fmt.Errorf("%w: %s", err, toUnreadableBodyMessage(uri, rawResp)) } if findResponse.Status != "success" && findResponse.Status != "pending" { return findResponse, errors.New(toUnreadableBodyMessage(uri, rawResp)) } return findResponse, nil } // https://www.hosting.de/api/?json#updating-zones func (d *DNSProvider) updateZone(updateRequest ZoneUpdateRequest) (*ZoneUpdateResponse, error) { uri := defaultBaseURL + "/zoneUpdate" // but we'll need the ID later to delete the record updateResponse := &ZoneUpdateResponse{} rawResp, err := d.post(uri, updateRequest, updateResponse) if err != nil { return nil, err } if updateResponse.Status != "success" && updateResponse.Status != "pending" { return nil, errors.New(toUnreadableBodyMessage(uri, rawResp)) } return updateResponse, nil } func (d *DNSProvider) getZone(findRequest ZoneConfigsFindRequest) (*ZoneConfig, error) { var zoneConfig *ZoneConfig operation := func() error { findResponse, err := d.listZoneConfigs(findRequest) if err != nil { return backoff.Permanent(err) } if findResponse.Response.Data[0].Status != "active" { return fmt.Errorf("unexpected status: %q", findResponse.Response.Data[0].Status) } zoneConfig = &findResponse.Response.Data[0] return nil } bo := backoff.NewExponentialBackOff() bo.InitialInterval = 3 * time.Second bo.MaxInterval = 10 * bo.InitialInterval bo.MaxElapsedTime = 100 * bo.InitialInterval // retry in case the zone was edited recently and is not yet active err := backoff.Retry(operation, bo) if err != nil { return nil, err } return zoneConfig, nil } func (d *DNSProvider) post(uri string, request, response interface{}) ([]byte, error) { body, err := json.Marshal(request) if err != nil { return nil, err } req, err := http.NewRequest(http.MethodPost, uri, bytes.NewReader(body)) if err != nil { return nil, err } resp, err := d.config.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("error querying API: %w", err) } defer resp.Body.Close() content, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.New(toUnreadableBodyMessage(uri, content)) } err = json.Unmarshal(content, response) if err != nil { return nil, fmt.Errorf("%w: %s", err, toUnreadableBodyMessage(uri, content)) } return content, nil } func toUnreadableBodyMessage(uri string, rawBody []byte) string { return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", uri, string(rawBody)) } lego-4.9.1/providers/dns/hostingde/hostingde.go000066400000000000000000000126311434020463500215570ustar00rootroot00000000000000// Package hostingde implements a DNS provider for solving the DNS-01 challenge using hosting.de. package hostingde import ( "errors" "fmt" "net/http" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "HOSTINGDE_" EnvAPIKey = envNamespace + "API_KEY" EnvZoneName = envNamespace + "ZONE_NAME" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string ZoneName string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config recordIDs map[string]string recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for hosting.de. // Credentials must be passed in the environment variables: // HOSTINGDE_ZONE_NAME and HOSTINGDE_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("hostingde: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] config.ZoneName = env.GetOrFile(EnvZoneName) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for hosting.de. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("hostingde: the configuration of the DNS provider is nil") } if config.APIKey == "" { return nil, errors.New("hostingde: API key missing") } return &DNSProvider{ config: config, recordIDs: make(map[string]string), }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zoneName, err := d.getZoneName(fqdn) if err != nil { return fmt.Errorf("hostingde: could not determine zone for domain %q: %w", domain, err) } // get the ZoneConfig for that domain zonesFind := ZoneConfigsFindRequest{ Filter: Filter{Field: "zoneName", Value: zoneName}, Limit: 1, Page: 1, } zonesFind.AuthToken = d.config.APIKey zoneConfig, err := d.getZone(zonesFind) if err != nil { return fmt.Errorf("hostingde: %w", err) } zoneConfig.Name = zoneName rec := []DNSRecord{{ Type: "TXT", Name: dns01.UnFqdn(fqdn), Content: value, TTL: d.config.TTL, }} req := ZoneUpdateRequest{ ZoneConfig: *zoneConfig, RecordsToAdd: rec, } req.AuthToken = d.config.APIKey resp, err := d.updateZone(req) if err != nil { return fmt.Errorf("hostingde: %w", err) } for _, record := range resp.Response.Records { if record.Name == dns01.UnFqdn(fqdn) && record.Content == fmt.Sprintf(`%q`, value) { d.recordIDsMu.Lock() d.recordIDs[fqdn] = record.ID d.recordIDsMu.Unlock() } } if d.recordIDs[fqdn] == "" { return fmt.Errorf("hostingde: error getting ID of just created record, for domain %s", domain) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zoneName, err := d.getZoneName(fqdn) if err != nil { return fmt.Errorf("hostingde: could not determine zone for domain %q: %w", domain, err) } rec := []DNSRecord{{ Type: "TXT", Name: dns01.UnFqdn(fqdn), Content: `"` + value + `"`, }} // get the ZoneConfig for that domain zonesFind := ZoneConfigsFindRequest{ Filter: Filter{Field: "zoneName", Value: zoneName}, Limit: 1, Page: 1, } zonesFind.AuthToken = d.config.APIKey zoneConfig, err := d.getZone(zonesFind) if err != nil { return fmt.Errorf("hostingde: %w", err) } zoneConfig.Name = zoneName req := ZoneUpdateRequest{ ZoneConfig: *zoneConfig, RecordsToDelete: rec, } req.AuthToken = d.config.APIKey // Delete record ID from map d.recordIDsMu.Lock() delete(d.recordIDs, fqdn) d.recordIDsMu.Unlock() _, err = d.updateZone(req) if err != nil { return fmt.Errorf("hostingde: %w", err) } return nil } func (d *DNSProvider) getZoneName(fqdn string) (string, error) { if d.config.ZoneName != "" { return d.config.ZoneName, nil } zoneName, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", err } if zoneName == "" { return "", errors.New("empty zone name") } return dns01.UnFqdn(zoneName), nil } lego-4.9.1/providers/dns/hostingde/hostingde.toml000066400000000000000000000013211434020463500221170ustar00rootroot00000000000000Name = "Hosting.de" Description = '''''' URL = "https://www.hosting.de/" Code = "hostingde" Since = "v1.1.0" Example = ''' HOSTINGDE_API_KEY=xxxxxxxx \ lego --email you@example.com --dns hostingde --domains my.example.org run ''' [Configuration] [Configuration.Credentials] HOSTINGDE_API_KEY = "API key" [Configuration.Additional] HOSTINGDE_ZONE_NAME = "Zone name in ACE format" HOSTINGDE_POLLING_INTERVAL = "Time between DNS propagation check" HOSTINGDE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" HOSTINGDE_TTL = "The TTL of the TXT record used for the DNS challenge" HOSTINGDE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.hosting.de/api/#dns" lego-4.9.1/providers/dns/hostingde/hostingde_test.go000066400000000000000000000053361434020463500226220ustar00rootroot00000000000000package hostingde import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIKey, EnvZoneName). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", EnvZoneName: "example.org", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIKey: "", EnvZoneName: "", }, expected: "hostingde: some credentials information are missing: HOSTINGDE_API_KEY", }, { desc: "missing access key", envVars: map[string]string{ EnvAPIKey: "", EnvZoneName: "456", }, expected: "hostingde: some credentials information are missing: HOSTINGDE_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string zoneName string expected string }{ { desc: "success", apiKey: "123", zoneName: "example.org", }, { desc: "missing credentials", expected: "hostingde: API key missing", }, { desc: "missing api key", zoneName: "456", expected: "hostingde: API key missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.ZoneName = test.zoneName p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/hostingde/model.go000066400000000000000000000113751434020463500206770ustar00rootroot00000000000000package hostingde import "encoding/json" // APIError represents an error in an API response. // https://www.hosting.de/api/?json#warnings-and-errors type APIError struct { Code int `json:"code"` ContextObject string `json:"contextObject"` ContextPath string `json:"contextPath"` Details []string `json:"details"` Text string `json:"text"` Value string `json:"value"` } // Filter is used to filter FindRequests to the API. // https://www.hosting.de/api/?json#filter-object type Filter struct { Field string `json:"field"` Value string `json:"value"` } // Sort is used to sort FindRequests from the API. // https://www.hosting.de/api/?json#filtering-and-sorting type Sort struct { Field string `json:"zoneName"` Order string `json:"order"` } // Metadata represents the metadata in an API response. // https://www.hosting.de/api/?json#metadata-object type Metadata struct { ClientTransactionID string `json:"clientTransactionId"` ServerTransactionID string `json:"serverTransactionId"` } // ZoneConfig The ZoneConfig object defines a zone. // https://www.hosting.de/api/?json#the-zoneconfig-object type ZoneConfig struct { ID string `json:"id"` AccountID string `json:"accountId"` Status string `json:"status"` Name string `json:"name"` NameUnicode string `json:"nameUnicode"` MasterIP string `json:"masterIp"` Type string `json:"type"` EMailAddress string `json:"emailAddress"` ZoneTransferWhitelist []string `json:"zoneTransferWhitelist"` LastChangeDate string `json:"lastChangeDate"` DNSServerGroupID string `json:"dnsServerGroupId"` DNSSecMode string `json:"dnsSecMode"` SOAValues *SOAValues `json:"soaValues,omitempty"` TemplateValues json.RawMessage `json:"templateValues,omitempty"` } // SOAValues The SOA values object contains the time (seconds) used in a zone’s SOA record. // https://www.hosting.de/api/?json#the-soa-values-object type SOAValues struct { Refresh int `json:"refresh"` Retry int `json:"retry"` Expire int `json:"expire"` TTL int `json:"ttl"` NegativeTTL int `json:"negativeTtl"` } // DNSRecord The DNS Record object is part of a zone. It is used to manage DNS resource records. // https://www.hosting.de/api/?json#the-record-object type DNSRecord struct { ID string `json:"id,omitempty"` ZoneID string `json:"zoneId,omitempty"` RecordTemplateID string `json:"recordTemplateId,omitempty"` Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` Content string `json:"content,omitempty"` TTL int `json:"ttl,omitempty"` Priority int `json:"priority,omitempty"` LastChangeDate string `json:"lastChangeDate,omitempty"` } // Zone The Zone Object. // https://www.hosting.de/api/?json#the-zone-object type Zone struct { Records []DNSRecord `json:"records"` ZoneConfig ZoneConfig `json:"zoneConfig"` } // ZoneUpdateRequest represents a API ZoneUpdate request. // https://www.hosting.de/api/?json#updating-zones type ZoneUpdateRequest struct { BaseRequest ZoneConfig `json:"zoneConfig"` RecordsToAdd []DNSRecord `json:"recordsToAdd"` RecordsToDelete []DNSRecord `json:"recordsToDelete"` } // ZoneUpdateResponse represents a response from the API. // https://www.hosting.de/api/?json#updating-zones type ZoneUpdateResponse struct { BaseResponse Response Zone `json:"response"` } // ZoneConfigsFindRequest represents a API ZonesFind request. // https://www.hosting.de/api/?json#list-zoneconfigs type ZoneConfigsFindRequest struct { BaseRequest Filter Filter `json:"filter"` Limit int `json:"limit"` Page int `json:"page"` Sort *Sort `json:"sort,omitempty"` } // ZoneConfigsFindResponse represents the API response for ZoneConfigsFind. // https://www.hosting.de/api/?json#list-zoneconfigs type ZoneConfigsFindResponse struct { BaseResponse Response struct { Limit int `json:"limit"` Page int `json:"page"` TotalEntries int `json:"totalEntries"` TotalPages int `json:"totalPages"` Type string `json:"type"` Data []ZoneConfig `json:"data"` } `json:"response"` } // BaseResponse Common response struct. // https://www.hosting.de/api/?json#responses type BaseResponse struct { Errors []APIError `json:"errors"` Metadata Metadata `json:"metadata"` Warnings []string `json:"warnings"` Status string `json:"status"` } // BaseRequest Common request struct. type BaseRequest struct { AuthToken string `json:"authToken"` } lego-4.9.1/providers/dns/hosttech/000077500000000000000000000000001434020463500170765ustar00rootroot00000000000000lego-4.9.1/providers/dns/hosttech/hosttech.go000066400000000000000000000110171434020463500212460ustar00rootroot00000000000000// Package hosttech implements a DNS provider for solving the DNS-01 challenge using hosttech. package hosttech import ( "errors" "fmt" "net/http" "strconv" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/hosttech/internal" ) // Environment variables names. const ( envNamespace = "HOSTTECH_" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 3600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client recordIDs map[string]int recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for hosttech. // Credentials must be passed in the environment variable: HOSTTECH_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("hosttech: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for hosttech. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("hosttech: the configuration of the DNS provider is nil") } if config.APIKey == "" { return nil, errors.New("hosttech: missing credentials") } client := internal.NewClient(config.APIKey) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, recordIDs: map[string]int{}, }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("hosttech: could not determine zone for domain %q: %w", domain, err) } zone, err := d.client.GetZone(dns01.UnFqdn(authZone)) if err != nil { return fmt.Errorf("hosttech: could not find zone for domain %q (%s): %w", domain, authZone, err) } record := internal.Record{ Type: "TXT", Name: dns01.UnFqdn(strings.TrimSuffix(fqdn, authZone)), Text: value, TTL: d.config.TTL, } newRecord, err := d.client.AddRecord(strconv.Itoa(zone.ID), record) if err != nil { return fmt.Errorf("hosttech: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = newRecord.ID d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("hosttech: could not determine zone for domain %q: %w", domain, err) } zone, err := d.client.GetZone(dns01.UnFqdn(authZone)) if err != nil { return fmt.Errorf("hosttech: could not find zone for domain %q (%s): %w", domain, authZone, err) } // gets the record's unique ID from when we created it d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("hosttech: unknown record ID for '%s' '%s'", fqdn, token) } err = d.client.DeleteRecord(strconv.Itoa(zone.ID), strconv.Itoa(recordID)) if err != nil { return fmt.Errorf("hosttech: %w", err) } return nil } lego-4.9.1/providers/dns/hosttech/hosttech.toml000066400000000000000000000013331434020463500216140ustar00rootroot00000000000000Name = "Hosttech" Description = '''''' URL = "https://www.hosttech.eu/" Code = "hosttech" Since = "v4.5.0" Example = ''' HOSTTECH_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ lego --email you@example.com --dns hosttech --domains my.example.org run ''' [Configuration] [Configuration.Credentials] HOSTTECH_API_KEY = "API login" HOSTTECH_PASSWORD = "API password" [Configuration.Additional] HOSTTECH_POLLING_INTERVAL = "Time between DNS propagation check" HOSTTECH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" HOSTTECH_TTL = "The TTL of the TXT record used for the DNS challenge" HOSTTECH_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.ns1.hosttech.eu/api/documentation" lego-4.9.1/providers/dns/hosttech/hosttech_test.go000066400000000000000000000043111434020463500223040ustar00rootroot00000000000000package hosttech import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "secret", }, }, { desc: "missing API key", expected: "hosttech: some credentials information are missing: HOSTTECH_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string expected string }{ { desc: "success", apiKey: "secret", }, { desc: "missing API key", expected: "hosttech: missing credentials", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/hosttech/internal/000077500000000000000000000000001434020463500207125ustar00rootroot00000000000000lego-4.9.1/providers/dns/hosttech/internal/client.go000066400000000000000000000126411434020463500225230ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "path" "strconv" "time" ) const defaultBaseURL = "https://api.ns1.hosttech.eu/api" // Client a Hosttech client. type Client struct { HTTPClient *http.Client baseURL *url.URL apiKey string } // NewClient creates a new Client. func NewClient(apiKey string) *Client { baseURL, _ := url.Parse(defaultBaseURL) return &Client{ HTTPClient: &http.Client{Timeout: 10 * time.Second}, baseURL: baseURL, apiKey: apiKey, } } // GetZones Get a list of all zones. // https://api.ns1.hosttech.eu/api/documentation/#/Zones/get_api_user_v1_zones func (c Client) GetZones(query string, limit, offset int) ([]Zone, error) { endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "user", "v1", "zones")) if err != nil { return nil, fmt.Errorf("parse URL: %w", err) } values := endpoint.Query() values.Set("query", query) if limit > 0 { values.Set("limit", strconv.Itoa(limit)) } if offset > 0 { values.Set("offset", strconv.Itoa(offset)) } endpoint.RawQuery = values.Encode() req, err := http.NewRequest(http.MethodGet, endpoint.String(), nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } raw, err := c.do(req) if err != nil { return nil, err } var r []Zone err = json.Unmarshal(raw, &r) if err != nil { return nil, fmt.Errorf("unmarshal response data: %s: %w", string(raw), err) } return r, nil } // GetZone Get a single zone. // https://api.ns1.hosttech.eu/api/documentation/#/Zones/get_api_user_v1_zones__zoneId_ func (c Client) GetZone(zoneID string) (*Zone, error) { endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "user", "v1", "zones", zoneID)) if err != nil { return nil, fmt.Errorf("parse URL: %w", err) } req, err := http.NewRequest(http.MethodGet, endpoint.String(), nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } raw, err := c.do(req) if err != nil { return nil, err } var r Zone err = json.Unmarshal(raw, &r) if err != nil { return nil, fmt.Errorf("unmarshal response data: %s: %w", string(raw), err) } return &r, nil } // GetRecords Returns a list of all records for the given zone. // https://api.ns1.hosttech.eu/api/documentation/#/Records/get_api_user_v1_zones__zoneId__records func (c Client) GetRecords(zoneID, recordType string) ([]Record, error) { endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "user", "v1", "zones", zoneID, "records")) if err != nil { return nil, fmt.Errorf("parse URL: %w", err) } values := endpoint.Query() if recordType != "" { values.Set("type", recordType) } endpoint.RawQuery = values.Encode() req, err := http.NewRequest(http.MethodGet, endpoint.String(), nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } raw, err := c.do(req) if err != nil { return nil, err } var r []Record err = json.Unmarshal(raw, &r) if err != nil { return nil, fmt.Errorf("unmarshal response data: %s: %w", string(raw), err) } return r, nil } // AddRecord Adds a new record to the zone and returns the newly created record. // https://api.ns1.hosttech.eu/api/documentation/#/Records/post_api_user_v1_zones__zoneId__records func (c Client) AddRecord(zoneID string, record Record) (*Record, error) { endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "user", "v1", "zones", zoneID, "records")) if err != nil { return nil, fmt.Errorf("parse URL: %w", err) } body, err := json.Marshal(record) if err != nil { return nil, fmt.Errorf("marshal request data: %w", err) } req, err := http.NewRequest(http.MethodPost, endpoint.String(), bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("create request: %w", err) } raw, err := c.do(req) if err != nil { return nil, err } var r Record err = json.Unmarshal(raw, &r) if err != nil { return nil, fmt.Errorf("unmarshal response data: %s: %w", string(raw), err) } return &r, nil } // DeleteRecord Deletes a single record for the given id. // https://api.ns1.hosttech.eu/api/documentation/#/Records/delete_api_user_v1_zones__zoneId__records__recordId_ func (c Client) DeleteRecord(zoneID, recordID string) error { endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "user", "v1", "zones", zoneID, "records", recordID)) if err != nil { return fmt.Errorf("parse URL: %w", err) } req, err := http.NewRequest(http.MethodDelete, endpoint.String(), nil) if err != nil { return fmt.Errorf("create request: %w", err) } _, err = c.do(req) return err } func (c Client) do(req *http.Request) (json.RawMessage, error) { req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey)) resp, errD := c.HTTPClient.Do(req) if errD != nil { return nil, fmt.Errorf("send request: %w", errD) } defer func() { _ = resp.Body.Close() }() switch resp.StatusCode { case http.StatusOK, http.StatusCreated: all, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response: %w", err) } var r apiResponse err = json.Unmarshal(all, &r) if err != nil { return nil, fmt.Errorf("unmarshal response: %w", err) } return r.Data, nil case http.StatusNoContent: return nil, nil default: data, _ := io.ReadAll(resp.Body) e := APIError{StatusCode: resp.StatusCode} err := json.Unmarshal(data, &e) if err != nil { e.Message = string(data) } return nil, e } } lego-4.9.1/providers/dns/hosttech/internal/client_test.go000066400000000000000000000135051434020463500235620ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const testAPIKey = "secret" func TestClient_GetZones(t *testing.T) { client := setupTest(t, "/user/v1/zones", testHandler(http.MethodGet, http.StatusOK, "zones.json")) zones, err := client.GetZones("", 100, 0) require.NoError(t, err) expected := []Zone{ { ID: 10, Name: "user1.ch", Email: "test@hosttech.ch", TTL: 10800, Nameserver: "ns1.hosttech.ch", Dnssec: false, DnssecEmail: "test@hosttech.ch", }, } assert.Equal(t, expected, zones) } func TestClient_GetZones_error(t *testing.T) { client := setupTest(t, "/user/v1/zones", testHandler(http.MethodGet, http.StatusUnauthorized, "error.json")) _, err := client.GetZones("", 100, 0) require.Error(t, err) } func TestClient_GetZone(t *testing.T) { client := setupTest(t, "/user/v1/zones/123", testHandler(http.MethodGet, http.StatusOK, "zone.json")) zone, err := client.GetZone("123") require.NoError(t, err) expected := &Zone{ ID: 10, Name: "user1.ch", Email: "test@hosttech.ch", TTL: 10800, Nameserver: "ns1.hosttech.ch", Dnssec: false, DnssecEmail: "test@hosttech.ch", } assert.Equal(t, expected, zone) } func TestClient_GetZone_error(t *testing.T) { client := setupTest(t, "/user/v1/zones/123", testHandler(http.MethodGet, http.StatusUnauthorized, "error.json")) _, err := client.GetZone("123") require.Error(t, err) } func TestClient_GetRecords(t *testing.T) { client := setupTest(t, "/user/v1/zones/123/records", testHandler(http.MethodGet, http.StatusOK, "records.json")) records, err := client.GetRecords("123", "TXT") require.NoError(t, err) expected := []Record{ { ID: 10, Type: "A", Name: "www", TTL: 3600, Comment: "my first record", }, { ID: 11, Type: "AAAA", Name: "www", TTL: 3600, Comment: "my first record", }, { ID: 12, Type: "CAA", TTL: 3600, Comment: "my first record", }, { ID: 13, Type: "CNAME", Name: "www", TTL: 3600, Comment: "my first record", }, { ID: 14, Type: "MX", Name: "mail.example.com", TTL: 3600, Comment: "my first record", }, { ID: 14, Type: "NS", Name: "ns1.example.com", TTL: 3600, Comment: "my first record", }, { ID: 15, Type: "PTR", Name: "smtp.example.com", TTL: 3600, Comment: "my first record", }, { ID: 16, Type: "SRV", TTL: 3600, Comment: "my first record", }, { ID: 17, Type: "TXT", Text: "v=spf1 ip4:1.2.3.4/32 -all", TTL: 3600, Comment: "my first record", }, { ID: 17, Type: "TLSA", Text: "0 0 1 d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971", TTL: 3600, Comment: "my first record", }, } assert.Equal(t, expected, records) } func TestClient_GetRecords_error(t *testing.T) { client := setupTest(t, "/user/v1/zones/123/records", testHandler(http.MethodGet, http.StatusUnauthorized, "error.json")) _, err := client.GetRecords("123", "TXT") require.Error(t, err) } func TestClient_AddRecord(t *testing.T) { client := setupTest(t, "/user/v1/zones/123/records", testHandler(http.MethodPost, http.StatusCreated, "record.json")) record := Record{ Type: "TXT", Name: "lego", Text: "content", TTL: 3600, Comment: "example", } newRecord, err := client.AddRecord("123", record) require.NoError(t, err) expected := &Record{ ID: 10, Type: "TXT", Name: "lego", Text: "content", TTL: 3600, Comment: "example", } assert.Equal(t, expected, newRecord) } func TestClient_AddRecord_error(t *testing.T) { client := setupTest(t, "/user/v1/zones/123/records", testHandler(http.MethodPost, http.StatusUnauthorized, "error-details.json")) record := Record{ Type: "TXT", Name: "lego", Text: "content", TTL: 3600, Comment: "example", } _, err := client.AddRecord("123", record) require.Error(t, err) } func TestClient_DeleteRecord(t *testing.T) { client := setupTest(t, "/user/v1/zones/123/records/6", testHandler(http.MethodDelete, http.StatusUnauthorized, "error.json")) err := client.DeleteRecord("123", "6") require.Error(t, err) } func TestClient_DeleteRecord_error(t *testing.T) { client := setupTest(t, "/user/v1/zones/123/records/6", testHandler(http.MethodDelete, http.StatusNoContent, "")) err := client.DeleteRecord("123", "6") require.NoError(t, err) } func setupTest(t *testing.T, path string, handler http.Handler) *Client { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.Handle(path, handler) client := NewClient(testAPIKey) client.baseURL, _ = url.Parse(server.URL) return client } func testHandler(method string, statusCode int, filename string) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { if req.Method != method { http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed) return } if req.Header.Get("Authorization") != "Bearer "+testAPIKey { http.Error(rw, `{"message":"Unauthenticated"}`, http.StatusUnauthorized) return } rw.WriteHeader(statusCode) if statusCode == http.StatusNoContent { return } file, err := os.Open(filepath.Join("fixtures", filename)) if err != nil { http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) return } } } lego-4.9.1/providers/dns/hosttech/internal/fixtures/000077500000000000000000000000001434020463500225635ustar00rootroot00000000000000lego-4.9.1/providers/dns/hosttech/internal/fixtures/error-details.json000066400000000000000000000001641434020463500262330ustar00rootroot00000000000000{ "message": "The given data was invalid.", "errors": { "type": [ "Darf nicht leer sein." ] } } lego-4.9.1/providers/dns/hosttech/internal/fixtures/error.json000066400000000000000000000000441434020463500246050ustar00rootroot00000000000000{ "message": "Unauthenticated." } lego-4.9.1/providers/dns/hosttech/internal/fixtures/record.json000066400000000000000000000002121434020463500247270ustar00rootroot00000000000000{ "data": { "id": 10, "type": "TXT", "name": "lego", "Text": "content", "ttl": 3600, "comment": "example" } } lego-4.9.1/providers/dns/hosttech/internal/fixtures/records.json000066400000000000000000000034221434020463500251200ustar00rootroot00000000000000{ "data": [ { "id": 10, "type": "A", "name": "www", "ipv4": "1.2.3.4", "ttl": 3600, "comment": "my first record" }, { "id": 11, "type": "AAAA", "name": "www", "ipv6": "2001:db8:1234::1", "ttl": 3600, "comment": "my first record" }, { "id": 12, "type": "CAA", "name": "", "flag": "0", "tag": "issue", "value": "letsencrypt.org", "ttl": 3600, "comment": "my first record" }, { "id": 13, "type": "CNAME", "name": "www", "cname": "site.example.com", "ttl": 3600, "comment": "my first record" }, { "id": 14, "type": "MX", "ownername": "", "name": "mail.example.com", "pref": 10, "ttl": 3600, "comment": "my first record" }, { "id": 14, "type": "NS", "ownername": "sub", "name": "ns1.example.com", "ttl": 3600, "comment": "my first record" }, { "id": 15, "type": "PTR", "origin": "4.3.2.1", "name": "smtp.example.com", "ttl": 3600, "comment": "my first record" }, { "id": 16, "type": "SRV", "service": "_autodiscover._tcp", "priority": 0, "weight": 0, "port": 443, "target": "exchange.example.com", "ttl": 3600, "comment": "my first record" }, { "id": 17, "type": "TXT", "name": "", "text": "v=spf1 ip4:1.2.3.4/32 -all", "ttl": 3600, "comment": "my first record" }, { "id": 17, "type": "TLSA", "name": "", "text": "0 0 1 d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971", "ttl": 3600, "comment": "my first record" } ] } lego-4.9.1/providers/dns/hosttech/internal/fixtures/zone.json000066400000000000000000000005371434020463500244360ustar00rootroot00000000000000{ "data": { "id": 10, "name": "user1.ch", "email": "test@hosttech.ch", "ttl": 10800, "nameserver": "ns1.hosttech.ch", "dnssec": false, "dnssec_email": "test@hosttech.ch", "ds_records": "[]", "records": "[{'id': 10, 'type': 'A', 'name': 'www', 'ipv4': '1.2.3.4', 'ttl': 3600, 'comment': 'my first record'}]" } } lego-4.9.1/providers/dns/hosttech/internal/fixtures/zones.json000066400000000000000000000003501434020463500246120ustar00rootroot00000000000000{ "data": [ { "id": 10, "name": "user1.ch", "email": "test@hosttech.ch", "ttl": 10800, "nameserver": "ns1.hosttech.ch", "dnssec": false, "dnssec_email": "test@hosttech.ch" } ] } lego-4.9.1/providers/dns/hosttech/internal/types.go000066400000000000000000000021471434020463500224110ustar00rootroot00000000000000package internal import ( "encoding/json" "fmt" ) type apiResponse struct { Data json.RawMessage `json:"data"` } type APIError struct { Message string `json:"message,omitempty"` Errors map[string]interface{} `json:"errors,omitempty"` StatusCode int `json:"-"` } func (a APIError) Error() string { msg := fmt.Sprintf("%d: %s", a.StatusCode, a.Message) for k, v := range a.Errors { msg += fmt.Sprintf(" %s: %v", k, v) } return msg } type Zone struct { ID int `json:"id"` Name string `json:"name,omitempty"` Email string `json:"email,omitempty"` TTL int `json:"ttl,omitempty"` Nameserver string `json:"nameserver,omitempty"` Dnssec bool `json:"dnssec,omitempty"` DnssecEmail string `json:"dnssec_email,omitempty"` } type Record struct { ID int `json:"id,omitempty"` Type string `json:"type,omitempty"` Name string `json:"name,omitempty"` Zone string `json:"zone,omitempty"` Text string `json:"text,omitempty"` TTL int `json:"ttl,omitempty"` Comment string `json:"comment,omitempty"` } lego-4.9.1/providers/dns/httpreq/000077500000000000000000000000001434020463500167445ustar00rootroot00000000000000lego-4.9.1/providers/dns/httpreq/httpreq.go000066400000000000000000000117331434020463500207670ustar00rootroot00000000000000// Package httpreq implements a DNS provider for solving the DNS-01 challenge through a HTTP server. package httpreq import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "path" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "HTTPREQ_" EnvEndpoint = envNamespace + "ENDPOINT" EnvMode = envNamespace + "MODE" EnvUsername = envNamespace + "USERNAME" EnvPassword = envNamespace + "PASSWORD" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) type message struct { FQDN string `json:"fqdn"` Value string `json:"value"` } type messageRaw struct { Domain string `json:"domain"` Token string `json:"token"` KeyAuth string `json:"keyAuth"` } // Config is used to configure the creation of the DNSProvider. type Config struct { Endpoint *url.URL Mode string Username string Password string PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config } // NewDNSProvider returns a DNSProvider instance. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvEndpoint) if err != nil { return nil, fmt.Errorf("httpreq: %w", err) } endpoint, err := url.Parse(values[EnvEndpoint]) if err != nil { return nil, fmt.Errorf("httpreq: %w", err) } config := NewDefaultConfig() config.Mode = env.GetOrFile(EnvMode) config.Username = env.GetOrFile(EnvUsername) config.Password = env.GetOrFile(EnvPassword) config.Endpoint = endpoint return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("httpreq: the configuration of the DNS provider is nil") } if config.Endpoint == nil { return nil, errors.New("httpreq: the endpoint is missing") } return &DNSProvider{config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { if d.config.Mode == "RAW" { msg := &messageRaw{ Domain: domain, Token: token, KeyAuth: keyAuth, } err := d.doPost("/present", msg) if err != nil { return fmt.Errorf("httpreq: %w", err) } return nil } fqdn, value := dns01.GetRecord(domain, keyAuth) msg := &message{ FQDN: fqdn, Value: value, } err := d.doPost("/present", msg) if err != nil { return fmt.Errorf("httpreq: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if d.config.Mode == "RAW" { msg := &messageRaw{ Domain: domain, Token: token, KeyAuth: keyAuth, } err := d.doPost("/cleanup", msg) if err != nil { return fmt.Errorf("httpreq: %w", err) } return nil } fqdn, value := dns01.GetRecord(domain, keyAuth) msg := &message{ FQDN: fqdn, Value: value, } err := d.doPost("/cleanup", msg) if err != nil { return fmt.Errorf("httpreq: %w", err) } return nil } func (d *DNSProvider) doPost(uri string, msg interface{}) error { reqBody := &bytes.Buffer{} err := json.NewEncoder(reqBody).Encode(msg) if err != nil { return err } newURI := path.Join(d.config.Endpoint.EscapedPath(), uri) endpoint, err := d.config.Endpoint.Parse(newURI) if err != nil { return err } req, err := http.NewRequest(http.MethodPost, endpoint.String(), reqBody) if err != nil { return err } req.Header.Set("Content-Type", "application/json") if len(d.config.Username) > 0 && len(d.config.Password) > 0 { req.SetBasicAuth(d.config.Username, d.config.Password) } resp, err := d.config.HTTPClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("%d: failed to read response body: %w", resp.StatusCode, err) } return fmt.Errorf("%d: request failed: %v", resp.StatusCode, string(body)) } return nil } lego-4.9.1/providers/dns/httpreq/httpreq.toml000066400000000000000000000024761434020463500213410ustar00rootroot00000000000000Name = "HTTP request" Description = '''''' URL = "/lego/dns/httpreq/" Code = "httpreq" Since = "v2.0.0" Example = ''' HTTPREQ_ENDPOINT=http://my.server.com:9090 \ lego --email you@example.com --dns httpreq --domains my.example.org run ''' Additional = ''' ## Description The server must provide: - `POST` `/present` - `POST` `/cleanup` The URL of the server must be define by `HTTPREQ_ENDPOINT`. ### Mode There are 2 modes (`HTTPREQ_MODE`): - default mode: ```json { "fqdn": "_acme-challenge.domain.", "value": "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM" } ``` - `RAW` ```json { "domain": "domain", "token": "token", "keyAuth": "key" } ``` ### Authentication Basic authentication (optional) can be set with some environment variables: - `HTTPREQ_USERNAME` and `HTTPREQ_PASSWORD` - both values must be set, otherwise basic authentication is not defined. ''' [Configuration] [Configuration.Credentials] HTTPREQ_MODE = "`RAW`, none" HTTPREQ_ENDPOINT = "The URL of the server" [Configuration.Additional] HTTPREQ_USERNAME = "Basic authentication username" HTTPREQ_PASSWORD = "Basic authentication password" HTTPREQ_POLLING_INTERVAL = "Time between DNS propagation check" HTTPREQ_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" HTTPREQ_HTTP_TIMEOUT = "API request timeout" lego-4.9.1/providers/dns/httpreq/httpreq_test.go000066400000000000000000000151651434020463500220310ustar00rootroot00000000000000package httpreq import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "path" "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) var envTest = tester.NewEnvTest(EnvEndpoint, EnvMode, EnvUsername, EnvPassword) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvEndpoint: "http://localhost:8090", }, }, { desc: "invalid URL", envVars: map[string]string{ EnvEndpoint: ":", }, expected: `httpreq: parse ":": missing protocol scheme`, }, { desc: "missing endpoint", envVars: map[string]string{ EnvEndpoint: "", }, expected: "httpreq: some credentials information are missing: HTTPREQ_ENDPOINT", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string endpoint *url.URL expected string }{ { desc: "success", endpoint: mustParse("http://localhost:8090"), }, { desc: "missing endpoint", expected: "httpreq: the endpoint is missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Endpoint = test.endpoint p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProvider_Present(t *testing.T) { envTest.RestoreEnv() testCases := []struct { desc string mode string username string password string pathPrefix string handler http.HandlerFunc expectedError string }{ { desc: "success", handler: successHandler, }, { desc: "success with path prefix", handler: successHandler, pathPrefix: "/api/acme/", }, { desc: "error", handler: http.NotFound, expectedError: "httpreq: 404: request failed: 404 page not found\n", }, { desc: "success raw mode", mode: "RAW", handler: successRawModeHandler, }, { desc: "error raw mode", mode: "RAW", handler: http.NotFound, expectedError: "httpreq: 404: request failed: 404 page not found\n", }, { desc: "basic auth", username: "bar", password: "foo", handler: func(rw http.ResponseWriter, req *http.Request) { username, password, ok := req.BasicAuth() if username != "bar" || password != "foo" || !ok { rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and password.")) http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } fmt.Fprint(rw, "lego") }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.HandleFunc(path.Join("/", test.pathPrefix, "present"), test.handler) server := httptest.NewServer(mux) t.Cleanup(server.Close) config := NewDefaultConfig() config.Endpoint = mustParse(server.URL + test.pathPrefix) config.Mode = test.mode config.Username = test.username config.Password = test.password p, err := NewDNSProviderConfig(config) require.NoError(t, err) err = p.Present("domain", "token", "key") if test.expectedError == "" { require.NoError(t, err) } else { require.EqualError(t, err, test.expectedError) } }) } } func TestNewDNSProvider_Cleanup(t *testing.T) { envTest.RestoreEnv() testCases := []struct { desc string mode string username string password string handler http.HandlerFunc expectedError string }{ { desc: "success", handler: successHandler, }, { desc: "error", handler: http.NotFound, expectedError: "httpreq: 404: request failed: 404 page not found\n", }, { desc: "success raw mode", mode: "RAW", handler: successRawModeHandler, }, { desc: "error raw mode", mode: "RAW", handler: http.NotFound, expectedError: "httpreq: 404: request failed: 404 page not found\n", }, { desc: "basic auth", username: "bar", password: "foo", handler: func(rw http.ResponseWriter, req *http.Request) { username, password, ok := req.BasicAuth() if username != "bar" || password != "foo" || !ok { rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and password.")) http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } fmt.Fprint(rw, "lego") }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() mux := http.NewServeMux() mux.HandleFunc("/cleanup", test.handler) server := httptest.NewServer(mux) t.Cleanup(server.Close) config := NewDefaultConfig() config.Endpoint = mustParse(server.URL) config.Mode = test.mode config.Username = test.username config.Password = test.password p, err := NewDNSProviderConfig(config) require.NoError(t, err) err = p.CleanUp("domain", "token", "key") if test.expectedError == "" { require.NoError(t, err) } else { require.EqualError(t, err, test.expectedError) } }) } } func successHandler(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } msg := &message{} err := json.NewDecoder(req.Body).Decode(msg) if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } fmt.Fprint(rw, "lego") } func successRawModeHandler(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } msg := &messageRaw{} err := json.NewDecoder(req.Body).Decode(msg) if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } fmt.Fprint(rw, "lego") } func mustParse(rawURL string) *url.URL { uri, err := url.Parse(rawURL) if err != nil { panic(err) } return uri } lego-4.9.1/providers/dns/hurricane/000077500000000000000000000000001434020463500172355ustar00rootroot00000000000000lego-4.9.1/providers/dns/hurricane/hurricane.go000066400000000000000000000076111434020463500215510ustar00rootroot00000000000000package hurricane import ( "context" "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/hurricane/internal" ) // Environment variables names. const ( envNamespace = "HURRICANE_" EnvTokens = envNamespace + "TOKENS" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Credentials map[string]string PropagationTimeout time.Duration PollingInterval time.Duration SequenceInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 300*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for Hurricane Electric. func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() values, err := env.Get(EnvTokens) if err != nil { return nil, fmt.Errorf("hurricane: %w", err) } credentials, err := parseCredentials(values[EnvTokens]) if err != nil { return nil, fmt.Errorf("hurricane: %w", err) } config.Credentials = credentials return NewDNSProviderConfig(config) } func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("hurricane: the configuration of the DNS provider is nil") } if len(config.Credentials) == 0 { return nil, errors.New("hurricane: credentials missing") } client := internal.NewClient(config.Credentials) return &DNSProvider{config: config, client: client}, nil } // Present updates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, _, keyAuth string) error { fqdn, txtRecord := dns01.GetRecord(domain, keyAuth) err := d.client.UpdateTxtRecord(context.Background(), dns01.UnFqdn(fqdn), txtRecord) if err != nil { return fmt.Errorf("hurricane: %w", err) } return nil } // CleanUp updates the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) err := d.client.UpdateTxtRecord(context.Background(), dns01.UnFqdn(fqdn), ".") if err != nil { return fmt.Errorf("hurricane: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Sequential All DNS challenges for this provider will be resolved sequentially. // Returns the interval between each iteration. func (d *DNSProvider) Sequential() time.Duration { return d.config.SequenceInterval } func parseCredentials(raw string) (map[string]string, error) { credentials := make(map[string]string) credStrings := strings.Split(strings.TrimSuffix(raw, ","), ",") for _, credPair := range credStrings { data := strings.Split(credPair, ":") if len(data) != 2 { return nil, fmt.Errorf("incorrect credential pair: %s", credPair) } credentials[strings.TrimSpace(data[0])] = strings.TrimSpace(data[1]) } return credentials, nil } lego-4.9.1/providers/dns/hurricane/hurricane.toml000066400000000000000000000036141434020463500221160ustar00rootroot00000000000000Name = "Hurricane Electric DNS" Description = '''''' URL = "https://dns.he.net/" Code = "hurricane" Since = "v4.3.0" Example = ''' HURRICANE_TOKENS=example.org:token \ lego --email you@example.com --dns hurricane --domains example.org --domains '*.example.org run' HURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2 \ lego --email you@example.com --dns hurricane --domains my.example.org --domains demo.example.org ''' Additional = """ Before using lego to request a certificate for a given domain or wildcard (such as `my.example.org` or `*.my.example.org`), create a TXT record named `_acme-challenge.my.example.org`, and enable dynamic updates on it. Generate a token for each URL with Hurricane Electric's UI, and copy it down. Stick to alphanumeric tokens for greatest reliability. To authenticate with the Hurricane Electric API, add each record name/token pair you want to update to the `HURRICANE_TOKENS` environment variable, as shown in the examples. Record names (without the `_acme-challenge.` component) and their tokens are separated with colons, while the credential pairs are concatenated into a comma-separated list, like so: ``` HURRICANE_TOKENS=my.example.org:token1,demo.example.org:token2 ``` If you are issuing both a wildcard certificate and a standard certificate for a given subdomain, you should not have repeat entries for that name, as both will use the same credential. ``` HURRICANE_TOKENS=example.org:token ``` """ [Configuration] [Configuration.Credentials] HURRICANE_TOKENS = "TXT record names and tokens" [Configuration.Addtional] HURRICANE_POLLING_INTERVAL = "Time between DNS propagation checks" HURRICANE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation; defaults to 300s (5 minutes)" HURRICANE_SEQUENCE_INTERVAL = "Time between sequential requests" HURRICANE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://dns.he.net/" lego-4.9.1/providers/dns/hurricane/hurricane_test.go000066400000000000000000000055531434020463500226130ustar00rootroot00000000000000package hurricane import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvTokens).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvTokens: "example.org:123", }, }, { desc: "success multiple domains", envVars: map[string]string{ EnvTokens: "example.org:123,example.com:456,example.net:789", }, }, { desc: "invalid credentials", envVars: map[string]string{ EnvTokens: ",", }, expected: "hurricane: incorrect credential pair: ", }, { desc: "invalid credentials, partial", envVars: map[string]string{ EnvTokens: "example.org:123,example.net", }, expected: "hurricane: incorrect credential pair: example.net", }, { desc: "missing credentials", envVars: map[string]string{ EnvTokens: "", }, expected: "hurricane: some credentials information are missing: HURRICANE_TOKENS", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string creds map[string]string expected string }{ { desc: "success", creds: map[string]string{"example.org": "123"}, }, { desc: "success multiple domains", creds: map[string]string{ "example.org": "123", "example.com": "456", "example.net": "789", }, }, { desc: "missing credentials", expected: "hurricane: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Credentials = test.creds p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/hurricane/internal/000077500000000000000000000000001434020463500210515ustar00rootroot00000000000000lego-4.9.1/providers/dns/hurricane/internal/client.go000066400000000000000000000064101434020463500226570ustar00rootroot00000000000000package internal import ( "bytes" "context" "fmt" "io" "log" "net/http" "net/url" "strings" "sync" "time" "golang.org/x/time/rate" ) const defaultBaseURL = "https://dyn.dns.he.net/nic/update" const ( codeGood = "good" codeNoChg = "nochg" codeAbuse = "abuse" codeBadAgent = "badagent" codeBadAuth = "badauth" codeInterval = "interval" codeNoHost = "nohost" codeNotFqdn = "notfqdn" ) const defaultBurst = 5 // Client the Hurricane Electric client. type Client struct { HTTPClient *http.Client rateLimiters sync.Map baseURL string credentials map[string]string credMu sync.Mutex } // NewClient Creates a new Client. func NewClient(credentials map[string]string) *Client { return &Client{ HTTPClient: &http.Client{Timeout: 5 * time.Second}, baseURL: defaultBaseURL, credentials: credentials, } } // UpdateTxtRecord updates a TXT record. func (c *Client) UpdateTxtRecord(ctx context.Context, hostname string, txt string) error { domain := strings.TrimPrefix(hostname, "_acme-challenge.") c.credMu.Lock() token, ok := c.credentials[domain] c.credMu.Unlock() if !ok { return fmt.Errorf("hurricane: Domain %s not found in credentials, check your credentials map", domain) } data := url.Values{} data.Set("password", token) data.Set("hostname", hostname) data.Set("txt", txt) rl, _ := c.rateLimiters.LoadOrStore(hostname, rate.NewLimiter(limit(defaultBurst), defaultBurst)) err := rl.(*rate.Limiter).Wait(ctx) if err != nil { return err } resp, err := c.HTTPClient.PostForm(c.baseURL, data) if err != nil { return err } defer func() { _ = resp.Body.Close() }() bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return err } body := string(bytes.TrimSpace(bodyBytes)) if resp.StatusCode != http.StatusOK { return fmt.Errorf("%d: attempt to change TXT record %s returned %s", resp.StatusCode, hostname, body) } return evaluateBody(body, hostname) } func evaluateBody(body string, hostname string) error { code, _, _ := strings.Cut(body, " ") switch code { case codeGood: return nil case codeNoChg: log.Printf("%s: unchanged content written to TXT record %s", body, hostname) return nil case codeAbuse: return fmt.Errorf("%s: blocked hostname for abuse: %s", body, hostname) case codeBadAgent: return fmt.Errorf("%s: user agent not sent or HTTP method not recognized; open an issue on go-acme/lego on Github", body) case codeBadAuth: return fmt.Errorf("%s: wrong authentication token provided for TXT record %s", body, hostname) case codeInterval: return fmt.Errorf("%s: TXT records update exceeded API rate limit", body) case codeNoHost: return fmt.Errorf("%s: the record provided does not exist in this account: %s", body, hostname) case codeNotFqdn: return fmt.Errorf("%s: the record provided isn't an FQDN: %s", body, hostname) default: // This is basically only server errors. return fmt.Errorf("attempt to change TXT record %s returned %s", hostname, body) } } // limit computes the rate based on burst. // The API rate limit per-record is 10 reqs / 2 minutes. // // 10 reqs / 2 minutes = freq 1/12 (burst = 1) // 6 reqs / 2 minutes = freq 1/20 (burst = 5) // // https://github.com/go-acme/lego/issues/1415 func limit(burst int) rate.Limit { return 1 / rate.Limit(120/(10-burst+1)) } lego-4.9.1/providers/dns/hurricane/internal/client_test.go000066400000000000000000000032741434020463500237230ustar00rootroot00000000000000package internal import ( "context" "fmt" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) func TestClient_UpdateTxtRecord(t *testing.T) { testCases := []struct { code string expected assert.ErrorAssertionFunc }{ { code: codeGood, expected: assert.NoError, }, { code: codeNoChg + ` "0123456789abcdef"`, expected: assert.NoError, }, { code: codeAbuse, expected: assert.Error, }, { code: codeBadAgent, expected: assert.Error, }, { code: codeBadAuth, expected: assert.Error, }, { code: codeNoHost, expected: assert.Error, }, { code: codeNotFqdn, expected: assert.Error, }, } for _, test := range testCases { test := test t.Run(test.code, func(t *testing.T) { t.Parallel() handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } if err := req.ParseForm(); err != nil { http.Error(rw, "failed to parse form data", http.StatusBadRequest) return } if req.PostForm.Encode() != "hostname=_acme-challenge.example.com&password=secret&txt=foo" { http.Error(rw, "invalid form data", http.StatusBadRequest) return } _, _ = rw.Write([]byte(test.code)) }) server := httptest.NewServer(handler) t.Cleanup(server.Close) client := NewClient(map[string]string{"example.com": "secret"}) client.baseURL = server.URL err := client.UpdateTxtRecord(context.Background(), "_acme-challenge.example.com", "foo") test.expected(t, err) }) } } lego-4.9.1/providers/dns/hyperone/000077500000000000000000000000001434020463500171065ustar00rootroot00000000000000lego-4.9.1/providers/dns/hyperone/hyperone.go000066400000000000000000000141151434020463500212700ustar00rootroot00000000000000// Package hyperone implements a DNS provider for solving the DNS-01 challenge using HyperOne. package hyperone import ( "fmt" "net/http" "os" "path/filepath" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/hyperone/internal" ) // Environment variables names. const ( envNamespace = "HYPERONE_" EnvPassportLocation = envNamespace + "PASSPORT_LOCATION" EnvAPIUrl = envNamespace + "API_URL" EnvLocationID = envNamespace + "LOCATION_ID" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIEndpoint string LocationID string PassportLocation string TTL int PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *internal.Client config *Config } // NewDNSProvider returns a DNSProvider instance configured for HyperOne. func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.PassportLocation = env.GetOrFile(EnvPassportLocation) config.LocationID = env.GetOrFile(EnvLocationID) config.APIEndpoint = env.GetOrFile(EnvAPIUrl) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for HyperOne. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config.PassportLocation == "" { var err error config.PassportLocation, err = GetDefaultPassportLocation() if err != nil { return nil, fmt.Errorf("hyperone: %w", err) } } passport, err := internal.LoadPassportFile(config.PassportLocation) if err != nil { return nil, fmt.Errorf("hyperone: %w", err) } client, err := internal.NewClient(config.APIEndpoint, config.LocationID, passport) if err != nil { return nil, fmt.Errorf("hyperone: failed to create client: %w", err) } if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{client: client, config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("hyperone: failed to get zone for fqdn=%s: %w", fqdn, err) } recordset, err := d.client.FindRecordset(zone.ID, "TXT", fqdn) if err != nil { return fmt.Errorf("hyperone: fqdn=%s, zone ID=%s: %w", fqdn, zone.ID, err) } if recordset == nil { _, err = d.client.CreateRecordset(zone.ID, "TXT", fqdn, value, d.config.TTL) if err != nil { return fmt.Errorf("hyperone: failed to create recordset: fqdn=%s, zone ID=%s, value=%s: %w", fqdn, zone.ID, value, err) } return nil } _, err = d.client.CreateRecord(zone.ID, recordset.ID, value) if err != nil { return fmt.Errorf("hyperone: failed to create record: fqdn=%s, zone ID=%s, recordset ID=%s: %w", fqdn, zone.ID, recordset.ID, err) } return nil } // CleanUp removes the TXT record matching the specified parameters and recordset if no other records are remaining. // There is a small possibility that race will cause to delete recordset with records for other DNS Challenges. func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("hyperone: failed to get zone for fqdn=%s: %w", fqdn, err) } recordset, err := d.client.FindRecordset(zone.ID, "TXT", fqdn) if err != nil { return fmt.Errorf("hyperone: fqdn=%s, zone ID=%s: %w", fqdn, zone.ID, err) } if recordset == nil { return fmt.Errorf("hyperone: recordset to remove not found: fqdn=%s", fqdn) } records, err := d.client.GetRecords(zone.ID, recordset.ID) if err != nil { return fmt.Errorf("hyperone: %w", err) } if len(records) == 1 { if records[0].Content != value { return fmt.Errorf("hyperone: record with content %s not found: fqdn=%s", value, fqdn) } err = d.client.DeleteRecordset(zone.ID, recordset.ID) if err != nil { return fmt.Errorf("hyperone: failed to delete record: fqdn=%s, zone ID=%s, recordset ID=%s: %w", fqdn, zone.ID, recordset.ID, err) } return nil } for _, record := range records { if record.Content == value { err = d.client.DeleteRecord(zone.ID, recordset.ID, record.ID) if err != nil { return fmt.Errorf("hyperone: fqdn=%s, zone ID=%s, recordset ID=%s, record ID=%s: %w", fqdn, zone.ID, recordset.ID, record.ID, err) } return nil } } return fmt.Errorf("hyperone: fqdn=%s, failed to find record with given value", fqdn) } // getHostedZone gets the hosted zone. func (d *DNSProvider) getHostedZone(fqdn string) (*internal.Zone, error) { authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return nil, err } return d.client.FindZone(authZone) } func GetDefaultPassportLocation() (string, error) { homeDir, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("failed to get user home directory: %w", err) } return filepath.Join(homeDir, ".h1", "passport.json"), nil } lego-4.9.1/providers/dns/hyperone/hyperone.toml000066400000000000000000000031261434020463500216360ustar00rootroot00000000000000Name = "HyperOne" Description = '''''' URL = "https://www.hyperone.com" Code = "hyperone" Since = "v3.9.0" Example = ''' lego --email you@example.com --dns hyperone --domains my.example.org run ''' Additional = ''' ## Description Default configuration does not require any additional environment variables, just a passport file in `~/.h1/passport.json` location. ### Generating passport file using H1 CLI To use this application you have to generate passport file for `sa`: ``` h1 iam project sa credential generate --name my-passport --project --sa --passport-output-file ~/.h1/passport.json ``` ### Required permissions The application requires following permissions: - `dns/zone/list` - `dns/zone.recordset/list` - `dns/zone.recordset/create` - `dns/zone.recordset/delete` - `dns/zone.record/create` - `dns/zone.record/list` - `dns/zone.record/delete` All required permissions are available via platform role `tool.lego`. ''' [Configuration] [Configuration.Additional] HYPERONE_PASSPORT_LOCATION = "Allows to pass custom passport file location (default ~/.h1/passport.json)" HYPERONE_API_URL = "Allows to pass custom API Endpoint to be used in the challenge (default https://api.hyperone.com/v2)" HYPERONE_LOCATION_ID = "Specifies location (region) to be used in API calls. (default pl-waw-1)" HYPERONE_TTL = "The TTL of the TXT record used for the DNS challenge" HYPERONE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" HYPERONE_POLLING_INTERVAL = "Time between DNS propagation check" [Links] API = "https://api.hyperone.com/v2/docs" lego-4.9.1/providers/dns/hyperone/hyperone_test.go000066400000000000000000000067141434020463500223350ustar00rootroot00000000000000package hyperone import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvPassportLocation, EnvAPIUrl, EnvLocationID). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvPassportLocation: "./internal/fixtures/validPassport.json", EnvAPIUrl: "", EnvLocationID: "", }, }, { desc: "invalid passport", envVars: map[string]string{ EnvPassportLocation: "./internal/fixtures/invalidPassport.json", EnvAPIUrl: "", EnvLocationID: "", }, expected: "hyperone: passport file validation failed: private key is missing", }, { desc: "non existing passport", envVars: map[string]string{ EnvPassportLocation: "./internal/fixtures/non-existing.json", EnvAPIUrl: "", EnvLocationID: "", }, expected: "hyperone: failed to open passport file: open ./internal/fixtures/non-existing.json:", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.Error(t, err) require.Contains(t, err.Error(), test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string passportLocation string apiEndpoint string locationID string expected string }{ { desc: "success", passportLocation: "./internal/fixtures/validPassport.json", apiEndpoint: "", locationID: "", }, { desc: "invalid passport", passportLocation: "./internal/fixtures/invalidPassport.json", apiEndpoint: "", locationID: "", expected: "hyperone: passport file validation failed: private key is missing", }, { desc: "non existing passport", passportLocation: "./internal/fixtures/non-existing.json", apiEndpoint: "", locationID: "", expected: "hyperone: failed to open passport file: open ./internal/fixtures/non-existing.json:", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.PassportLocation = test.passportLocation config.APIEndpoint = test.apiEndpoint config.LocationID = test.locationID p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.Error(t, err) require.Contains(t, err.Error(), test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/hyperone/internal/000077500000000000000000000000001434020463500207225ustar00rootroot00000000000000lego-4.9.1/providers/dns/hyperone/internal/client.go000066400000000000000000000207371434020463500225400ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "path" "time" ) const defaultBaseURL = "https://api.hyperone.com/v2" const defaultLocationID = "pl-waw-1" type signer interface { GetJWT() (string, error) } // Client the HyperOne client. type Client struct { HTTPClient *http.Client apiEndpoint string locationID string projectID string passport *Passport signer signer } // NewClient Creates a new HyperOne client. func NewClient(apiEndpoint, locationID string, passport *Passport) (*Client, error) { if passport == nil { return nil, errors.New("the passport is missing") } projectID, err := passport.ExtractProjectID() if err != nil { return nil, err } baseURL := defaultBaseURL if apiEndpoint != "" { baseURL = apiEndpoint } tokenSigner := &TokenSigner{ PrivateKey: passport.PrivateKey, KeyID: passport.CertificateID, Audience: baseURL, Issuer: passport.Issuer, Subject: passport.SubjectID, } client := &Client{ HTTPClient: &http.Client{Timeout: 5 * time.Second}, apiEndpoint: baseURL, locationID: locationID, passport: passport, projectID: projectID, signer: tokenSigner, } if client.locationID == "" { client.locationID = defaultLocationID } return client, nil } // FindRecordset looks for recordset with given recordType and name and returns it. // In case if recordset is not found returns nil. // https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_list func (c *Client) FindRecordset(zoneID, recordType, name string) (*Recordset, error) { // https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset resourceURL := path.Join("dns", c.locationID, "project", c.projectID, "zone", zoneID, "recordset") req, err := c.createRequest(http.MethodGet, resourceURL, nil) if err != nil { return nil, err } var recordSets []Recordset err = c.do(req, &recordSets) if err != nil { return nil, fmt.Errorf("failed to get recordsets from server: %w", err) } for _, v := range recordSets { if v.RecordType == recordType && v.Name == name { return &v, nil } } // when recordset is not present returns nil, but error is not thrown return nil, nil } // CreateRecordset creates recordset and record with given value within one request. // https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_create func (c *Client) CreateRecordset(zoneID, recordType, name, recordValue string, ttl int) (*Recordset, error) { recordsetInput := Recordset{ RecordType: recordType, Name: name, TTL: ttl, Record: &Record{Content: recordValue}, } requestBody, err := json.Marshal(recordsetInput) if err != nil { return nil, fmt.Errorf("failed to marshal recordset: %w", err) } // https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset resourceURL := path.Join("dns", c.locationID, "project", c.projectID, "zone", zoneID, "recordset") req, err := c.createRequest(http.MethodPost, resourceURL, bytes.NewBuffer(requestBody)) if err != nil { return nil, err } var recordsetResponse Recordset err = c.do(req, &recordsetResponse) if err != nil { return nil, fmt.Errorf("failed to create recordset: %w", err) } return &recordsetResponse, nil } // DeleteRecordset deletes a recordset. // https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_delete func (c *Client) DeleteRecordset(zoneID string, recordsetID string) error { // https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset/{recordsetId} resourceURL := path.Join("dns", c.locationID, "project", c.projectID, "zone", zoneID, "recordset", recordsetID) req, err := c.createRequest(http.MethodDelete, resourceURL, nil) if err != nil { return err } return c.do(req, nil) } // GetRecords gets all records within specified recordset. // https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_record_list func (c *Client) GetRecords(zoneID string, recordsetID string) ([]Record, error) { // https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset/{recordsetId}/record resourceURL := path.Join("dns", c.locationID, "project", c.projectID, "zone", zoneID, "recordset", recordsetID, "record") req, err := c.createRequest(http.MethodGet, resourceURL, nil) if err != nil { return nil, err } var records []Record err = c.do(req, &records) if err != nil { return nil, fmt.Errorf("failed to get records from server: %w", err) } return records, err } // CreateRecord creates a record. // https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_record_create func (c *Client) CreateRecord(zoneID, recordsetID, recordContent string) (*Record, error) { // https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset/{recordsetId}/record resourceURL := path.Join("dns", c.locationID, "project", c.projectID, "zone", zoneID, "recordset", recordsetID, "record") requestBody, err := json.Marshal(Record{Content: recordContent}) if err != nil { return nil, fmt.Errorf("failed to marshal record: %w", err) } req, err := c.createRequest(http.MethodPost, resourceURL, bytes.NewBuffer(requestBody)) if err != nil { return nil, err } var recordResponse Record err = c.do(req, &recordResponse) if err != nil { return nil, fmt.Errorf("failed to set record: %w", err) } return &recordResponse, nil } // DeleteRecord deletes a record. // https://api.hyperone.com/v2/docs#operation/dns_project_zone_recordset_record_delete func (c *Client) DeleteRecord(zoneID, recordsetID, recordID string) error { // https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone/{zoneId}/recordset/{recordsetId}/record/{recordId} resourceURL := path.Join("dns", c.locationID, "project", c.projectID, "zone", zoneID, "recordset", recordsetID, "record", recordID) req, err := c.createRequest(http.MethodDelete, resourceURL, nil) if err != nil { return err } return c.do(req, nil) } // FindZone looks for DNS Zone and returns nil if it does not exist. func (c *Client) FindZone(name string) (*Zone, error) { zones, err := c.GetZones() if err != nil { return nil, err } for _, zone := range zones { if zone.DNSName == name { return &zone, nil } } return nil, fmt.Errorf("failed to find zone for %s", name) } // GetZones gets all user's zones. // https://api.hyperone.com/v2/docs#operation/dns_project_zone_list func (c *Client) GetZones() ([]Zone, error) { // https://api.hyperone.com/v2/dns/{locationId}/project/{projectId}/zone resourceURL := path.Join("dns", c.locationID, "project", c.projectID, "zone") req, err := c.createRequest(http.MethodGet, resourceURL, nil) if err != nil { return nil, err } var zones []Zone err = c.do(req, &zones) if err != nil { return nil, fmt.Errorf("failed to fetch available zones: %w", err) } return zones, nil } func (c *Client) createRequest(method, uri string, body io.Reader) (*http.Request, error) { baseURL, err := url.Parse(c.apiEndpoint) if err != nil { return nil, err } endpoint, err := baseURL.Parse(path.Join(baseURL.Path, uri)) if err != nil { return nil, err } req, err := http.NewRequest(method, endpoint.String(), body) if err != nil { return nil, err } jwt, err := c.signer.GetJWT() if err != nil { return nil, fmt.Errorf("failed to sign the request: %w", err) } req.Header.Set("Authorization", "Bearer "+jwt) req.Header.Set("Content-Type", "application/json") return req, nil } func (c *Client) do(req *http.Request, v interface{}) error { resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer func() { _ = resp.Body.Close() }() err = checkResponse(resp) if err != nil { return err } if v == nil { return nil } raw, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read body: %w", err) } if err = json.Unmarshal(raw, v); err != nil { return fmt.Errorf("unmarshaling %T error: %w: %s", v, err, string(raw)) } return nil } func checkResponse(resp *http.Response) error { if resp.StatusCode/100 == 2 { return nil } var msg string if resp.StatusCode == http.StatusForbidden { msg = "forbidden: check if service account you are trying to use has permissions required for managing DNS" } else { msg = fmt.Sprintf("%d: unknown error", resp.StatusCode) } // add response body to error message if not empty responseBody, _ := io.ReadAll(resp.Body) if len(responseBody) > 0 { msg = fmt.Sprintf("%s: %s", msg, string(responseBody)) } return errors.New(msg) } lego-4.9.1/providers/dns/hyperone/internal/client_test.go000066400000000000000000000125401434020463500235700ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type signerMock struct{} func (s signerMock) GetJWT() (string, error) { return "", nil } func TestClient_FindRecordset(t *testing.T) { client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone/zone321/recordset", respFromFile("recordset.json")) recordset, err := client.FindRecordset("zone321", "SOA", "example.com.") require.NoError(t, err) expected := &Recordset{ ID: "123456789abcd", Name: "example.com.", RecordType: "SOA", TTL: 1800, } assert.Equal(t, expected, recordset) } func TestClient_CreateRecordset(t *testing.T) { expectedReqBody := Recordset{ RecordType: "TXT", Name: "test.example.com.", TTL: 3600, Record: &Record{Content: "value"}, } client := setupTest(t, http.MethodPost, "/dns/loc123/project/proj123/zone/zone123/recordset", hasReqBody(expectedReqBody), respFromFile("createRecordset.json")) rs, err := client.CreateRecordset("zone123", "TXT", "test.example.com.", "value", 3600) require.NoError(t, err) expected := &Recordset{RecordType: "TXT", Name: "test.example.com.", TTL: 3600, ID: "1234567890qwertyuiop"} assert.Equal(t, expected, rs) } func TestClient_DeleteRecordset(t *testing.T) { client := setupTest(t, http.MethodDelete, "/dns/loc123/project/proj123/zone/zone321/recordset/rs322") err := client.DeleteRecordset("zone321", "rs322") require.NoError(t, err) } func TestClient_GetRecords(t *testing.T) { client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone/321/recordset/322/record", respFromFile("record.json")) records, err := client.GetRecords("321", "322") require.NoError(t, err) expected := []Record{ { ID: "135128352183572dd", Content: "pns.hyperone.com. hostmaster.hyperone.com. 1 15 180 1209600 1800", Enabled: true, }, } assert.Equal(t, expected, records) } func TestClient_CreateRecord(t *testing.T) { expectedReqBody := Record{ Content: "value", } client := setupTest(t, http.MethodPost, "/dns/loc123/project/proj123/zone/z123/recordset/rs325/record", hasReqBody(expectedReqBody), respFromFile("createRecord.json")) rs, err := client.CreateRecord("z123", "rs325", "value") require.NoError(t, err) expected := &Record{ID: "123321qwerqwewqerq", Content: "value", Enabled: true} assert.Equal(t, expected, rs) } func TestClient_DeleteRecord(t *testing.T) { client := setupTest(t, http.MethodDelete, "/dns/loc123/project/proj123/zone/321/recordset/322/record/323") err := client.DeleteRecord("321", "322", "323") require.NoError(t, err) } func TestClient_FindZone(t *testing.T) { client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone", respFromFile("zones.json")) zone, err := client.FindZone("example.com") require.NoError(t, err) expected := &Zone{ ID: "zoneB", Name: "example.com", DNSName: "example.com", FQDN: "example.com.", URI: "", } assert.Equal(t, expected, zone) } func TestClient_GetZones(t *testing.T) { client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone", respFromFile("zones.json")) zones, err := client.GetZones() require.NoError(t, err) expected := []Zone{ { ID: "zoneA", Name: "example.org", DNSName: "example.org", FQDN: "example.org.", URI: "", }, { ID: "zoneB", Name: "example.com", DNSName: "example.com", FQDN: "example.com.", URI: "", }, } assert.Equal(t, expected, zones) } func setupTest(t *testing.T, method, path string, handlers ...assertHandler) *Client { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.Handle(path, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if req.Method != method { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } if len(handlers) != 0 { for _, handler := range handlers { code, err := handler(rw, req) if err != nil { http.Error(rw, err.Error(), code) return } } } })) passport := &Passport{ SubjectID: "/iam/project/proj123/sa/xxxxxxx", } client, err := NewClient(server.URL, "loc123", passport) require.NoError(t, err) client.signer = signerMock{} return client } type assertHandler func(http.ResponseWriter, *http.Request) (int, error) func hasReqBody(v interface{}) assertHandler { return func(rw http.ResponseWriter, req *http.Request) (int, error) { reqBody, err := io.ReadAll(req.Body) if err != nil { return http.StatusBadRequest, err } marshal, err := json.Marshal(v) if err != nil { return http.StatusInternalServerError, err } if !bytes.Equal(marshal, reqBody) { return http.StatusBadRequest, fmt.Errorf("invalid request body, got: %s, expect: %s", string(reqBody), string(marshal)) } return http.StatusOK, nil } } func respFromFile(fixtureName string) assertHandler { return func(rw http.ResponseWriter, req *http.Request) (int, error) { file, err := os.Open(filepath.Join(".", "fixtures", fixtureName)) if err != nil { return http.StatusInternalServerError, err } _, err = io.Copy(rw, file) if err != nil { return http.StatusInternalServerError, err } return http.StatusOK, nil } } lego-4.9.1/providers/dns/hyperone/internal/fixtures/000077500000000000000000000000001434020463500225735ustar00rootroot00000000000000lego-4.9.1/providers/dns/hyperone/internal/fixtures/createRecord.json000066400000000000000000000001121434020463500260620ustar00rootroot00000000000000{ "id": "123321qwerqwewqerq", "content": "value", "enabled": true } lego-4.9.1/providers/dns/hyperone/internal/fixtures/createRecordset.json000066400000000000000000000001421434020463500266010ustar00rootroot00000000000000{ "id": "1234567890qwertyuiop", "name": "test.example.com.", "type": "TXT", "ttl": 3600 } lego-4.9.1/providers/dns/hyperone/internal/fixtures/invalidPassport.json000066400000000000000000000002731434020463500266520ustar00rootroot00000000000000{ "subject_id": "/iam/project/projectId/sa/serviceAccountId", "certificate_id": "certificateID", "issuer": "https://api.hyperone.com/v2/iam/project/projectId/sa/serviceAccountId" } lego-4.9.1/providers/dns/hyperone/internal/fixtures/record.json000066400000000000000000000002221434020463500247400ustar00rootroot00000000000000[ { "id": "135128352183572dd", "content": "pns.hyperone.com. hostmaster.hyperone.com. 1 15 180 1209600 1800", "enabled": true } ] lego-4.9.1/providers/dns/hyperone/internal/fixtures/recordset.json000066400000000000000000000004571434020463500254660ustar00rootroot00000000000000[ { "id": "123456789abcd", "name": "example.com.", "type": "SOA", "ttl": 1800 }, { "id": "123456789abcde", "name": "example.com.", "type": "NS", "ttl": 3600 }, { "id": "123456789abcdf", "name": "example.com.", "type": "CNAME", "ttl": 3600 } ] lego-4.9.1/providers/dns/hyperone/internal/fixtures/validPassport.json000066400000000000000000000045421434020463500263260ustar00rootroot00000000000000{ "subject_id": "/iam/project/projectId/sa/serviceAccountId", "certificate_id": "certificateID", "issuer": "https://api.hyperone.com/v2/iam/project/projectId/sa/serviceAccountId", "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", "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" } lego-4.9.1/providers/dns/hyperone/internal/fixtures/zones.json000066400000000000000000000004001434020463500246160ustar00rootroot00000000000000[ { "id": "zoneA", "name": "example.org", "dnsName": "example.org", "fqdn": "example.org.", "uri": "" }, { "id": "zoneB", "name": "example.com", "dnsName": "example.com", "fqdn": "example.com.", "uri": "" } ]lego-4.9.1/providers/dns/hyperone/internal/models.go000066400000000000000000000010511434020463500225310ustar00rootroot00000000000000package internal type Recordset struct { RecordType string `json:"type"` Name string `json:"name"` TTL int `json:"ttl,omitempty"` ID string `json:"id,omitempty"` Record *Record `json:"record,omitempty"` } type Record struct { ID string `json:"id,omitempty"` Content string `json:"content"` Enabled bool `json:"enabled,omitempty"` } type Zone struct { ID string `json:"id"` Name string `json:"name"` DNSName string `json:"dnsName"` FQDN string `json:"fqdn"` URI string `json:"uri"` } lego-4.9.1/providers/dns/hyperone/internal/passport.go000066400000000000000000000027671434020463500231400ustar00rootroot00000000000000package internal import ( "encoding/json" "errors" "fmt" "os" "regexp" ) type Passport struct { SubjectID string `json:"subject_id"` CertificateID string `json:"certificate_id"` Issuer string `json:"issuer"` PrivateKey string `json:"private_key"` PublicKey string `json:"public_key"` } func LoadPassportFile(location string) (*Passport, error) { file, err := os.Open(location) if err != nil { return nil, fmt.Errorf("failed to open passport file: %w", err) } defer func() { _ = file.Close() }() var passport Passport err = json.NewDecoder(file).Decode(&passport) if err != nil { return nil, fmt.Errorf("failed to parse passport file: %w", err) } err = passport.validate() if err != nil { return nil, fmt.Errorf("passport file validation failed: %w", err) } return &passport, nil } func (passport *Passport) validate() error { if passport.Issuer == "" { return errors.New("issuer is empty") } if passport.CertificateID == "" { return errors.New("certificate ID is empty") } if passport.PrivateKey == "" { return errors.New("private key is missing") } if passport.SubjectID == "" { return errors.New("subject is empty") } return nil } func (passport *Passport) ExtractProjectID() (string, error) { re := regexp.MustCompile("iam/project/([a-zA-Z0-9]+)") parts := re.FindStringSubmatch(passport.SubjectID) if len(parts) != 2 { return "", fmt.Errorf("failed to extract project ID from subject ID: %s", passport.SubjectID) } return parts[1], nil } lego-4.9.1/providers/dns/hyperone/internal/passport_test.go000066400000000000000000000065271434020463500241750ustar00rootroot00000000000000package internal import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestLoadPassportFile(t *testing.T) { passport, err := LoadPassportFile("fixtures/validPassport.json") require.NoError(t, err) expected := &Passport{ SubjectID: "/iam/project/projectId/sa/serviceAccountId", CertificateID: "certificateID", Issuer: "https://api.hyperone.com/v2/iam/project/projectId/sa/serviceAccountId", PrivateKey: `-----BEGIN RSA PRIVATE KEY----- lrMAsSjjkKiRxGdgR8p5kZJj0AFgdWYa3OT2snIXnN5+/p7j13PSkseUcrAFyokc V9pgeDfitAhb9lpdjxjjuxRcuQjBfmNVLPF9MFyNOvhrprGNukUh/12oSKO9dFEt s39F/2h6Ld5IQrGt3gZaBB1aGO+tw3ill1VBy2zGPIDeuSz6DS3GG/oQ2gLSSMP4 OVfQ32Oajo496iHRkdIh/7Hho7BNzMYr1GxrYTcE9/Znr6xgeSdNT37CCeCH8cmP aEAUgSMTeIMVSpILwkKeNvBURic1EWaqXRgPRIWK0vNyOCs/+jNoFISnV4pu1ROF 92vayHDNSVw9wHcdSQ75XSE4Msawqv5U1iI7e2lD64uo1qhmJdrPcXDJQCiDbh+F hQhF+wAoLRvMNwwhg+LttL8vXqMDQl3olsWSvWPs6b/MZpB0qwd1bklzA6P+PeAU sfOvTqi9edIOfKqvXqTXEhBP8qC7ZtOKLGnryZb7W04SSVrNtuJUFRcLiqu+w/F/ MSxGSGalYpzIZ1B5HLQqISgWMXdbt39uMeeooeZjkuI3VIllFjtybecjPR9ZYQPt FFEP1XqNXjLFmGh84TXtvGLWretWM1OZmN8UKKUeATqrr7zuh5AYGAIbXd8BvweL Pigl9ei0hTculPqohvkoc5x1srPBvzHrirGlxOYjW3fc4kDgZpy+6ik5k5g7JWQD lbXCRz3HGazgUPeiwUr06a52vhgT7QuNIUZqdHb4IfCYs2pQTLHzQjAqvVk1mm2D kh4myIcTtf69BFcu/Wuptm3NaKd1nwk1squR6psvcTXOWII81pstnxNYkrokx4r2 7YVllNruOD+cMDNZbIG2CwT6V9ukIS8tl9EJp8eyb0a1uAEc22BNOjYHPF50beWF ukf3uc0SA+G3zhmXCM5sMf5OxVjKr5jgcir7kySY5KbmG71omYhczgr4H0qgxYo9 Zyj2wMKrTHLfFOpd4OOEun9Gi3srqlKZep7Hj7gNyUwZu1qiBvElmBVmp0HJxT0N mktuaVbaFgBsTS0/us1EqWvCA4REh1Ut/NoA9oG3JFt0lGDstTw1j+orDmIHOmSu 7FKYzr0uCz14AkLMSOixdPD1F0YyED1NMVnRVXw77HiAFGmb0CDi2KEg70pEKpn3 ksa8oe0MQi6oEwlMsAxVTXOB1wblTBuSBeaECzTzWE+/DHF+QQfQi8kAjjSdmmMJ yN+shdBWHYRGYnxRkTatONhcDBIY7sZV7wolYHz/rf7dpYUZf37vdQnYV8FpO1um Ya0GslyRJ5GqMBfDS1cQKne+FvVHxEE2YqEGBcOYhx/JI2soE8aA8W4XffN+DoEy ZkinJ/+BOwJ/zUI9GZtwB4JXqbNEE+j7r7/fJO9KxfPp4MPK4YWu0H0EUWONpVwe TWtbRhQUCOe4PVSC/Vv1pstvMD/D+E/0L4GQNHxr+xyFxuvILty5lvFTxoAVYpqD u8gNhk3NWefTrlSkhY4N+tPP6o7E4t3y40nOA/d9qaqiid+lYcIDB0cJTpZvgeeQ ijohxY3PHruU4vVZa37ITQnco9az6lsy18vbU0bOyK2fEZ2R9XVO8fH11jiV8oGH -----END RSA PRIVATE KEY----- `, PublicKey: `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxzYuc22QSst/dS7geYYK 5l5kLxU0tayNdixkEQ17ix+CUcUbKIsnyftZxaCYT46rQtXgCaYRdJcbB3hmyrOa vkhTpX79xJZnQmfuamMbZBqitvscxW9zRR9tBUL6vdi/0rpoUwPMEh8+Bw7CgYR0 FK0DhWYBNDfe9HKcyZEv3max8Cdq18htxjEsdYO0iwzhtKRXomBWTdhD5ykd/fAC VTr4+KEY+IeLvubHVmLUhbE5NgWXxrRpGasDqzKhCTmsa2Ysf712rl57SlH0Wz/M r3F7aM9YpErzeYLrl0GhQr9BVJxOvXcVd4kmY+XkiCcrkyS1cnghnllh+LCwQu1s YwIDAQAB -----END PUBLIC KEY----- `, } assert.Equal(t, expected, passport) } func TestLoadPassportFile_invalid(t *testing.T) { passport, err := LoadPassportFile("fixtures/invalidPassport.json") require.EqualError(t, err, "passport file validation failed: private key is missing") assert.Nil(t, passport) } func TestExtractProjectID(t *testing.T) { passport := Passport{SubjectID: "/iam/project/ddd/sa/5ef759c0ab0acab07xxxxxxx"} extractedID, err := passport.ExtractProjectID() require.NoError(t, err) assert.Equal(t, "ddd", extractedID) } func TestExtractProjectID_invalid(t *testing.T) { passport := Passport{SubjectID: "ddddddd"} extractedID, err := passport.ExtractProjectID() require.EqualError(t, err, "failed to extract project ID from subject ID: ddddddd") assert.Empty(t, extractedID) } lego-4.9.1/providers/dns/hyperone/internal/token.go000066400000000000000000000035521434020463500223760ustar00rootroot00000000000000package internal import ( "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "time" "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" ) type TokenSigner struct { PrivateKey string KeyID string Audience string Issuer string Subject string } func (input *TokenSigner) GetJWT() (string, error) { signer, err := getRSASigner(input.PrivateKey, input.KeyID) if err != nil { return "", err } issuedAt := time.Now() expiresAt := issuedAt.Add(5 * time.Minute) payload := Payload{IssuedAt: issuedAt.Unix(), Expiry: expiresAt.Unix(), Audience: input.Audience, Issuer: input.Issuer, Subject: input.Subject} token, err := payload.buildToken(&signer) return token, err } func getRSASigner(privateKey, keyID string) (jose.Signer, error) { parsedKey, err := parseRSAKey(privateKey) if err != nil { return nil, err } key := jose.SigningKey{Algorithm: jose.RS256, Key: parsedKey} signerOpts := jose.SignerOptions{} signerOpts.WithType("JWT") signerOpts.WithHeader("kid", keyID) rsaSigner, err := jose.NewSigner(key, &signerOpts) if err != nil { return nil, fmt.Errorf("failed to create JWS RSA256 signer: %w", err) } return rsaSigner, nil } type Payload struct { IssuedAt int64 `json:"iat"` Expiry int64 `json:"exp"` Audience string `json:"aud"` Issuer string `json:"iss"` Subject string `json:"sub"` } func (payload *Payload) buildToken(signer *jose.Signer) (string, error) { builder := jwt.Signed(*signer).Claims(payload) token, err := builder.CompactSerialize() if err != nil { return "", fmt.Errorf("failed to build JWT: %w", err) } return token, nil } func parseRSAKey(pemString string) (*rsa.PrivateKey, error) { block, _ := pem.Decode([]byte(pemString)) key, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse private key: %w", err) } return key, nil } lego-4.9.1/providers/dns/hyperone/internal/token_test.go000066400000000000000000000041271434020463500234340ustar00rootroot00000000000000package internal import ( "encoding/base64" "encoding/json" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const privateKey = `-----BEGIN RSA PRIVATE KEY----- MIICWgIBAAKBgGFfgMY+DuO8l0RYrMLhcl6U/NigNIiOVhoo/xnYyoQALpWxBaBR +iVJiBUYunQjKA33yAiY0AasCfSn1JB6asayQvGGn73xztLjkeCVLT+9e4nJ0A/o dK8SOKBg9FFe70KJrWjJd626el0aVDJjtCE+QxJExA0UZbQp+XIyveQXAgMBAAEC gYBHcL1XNWLRPaWx9GlUVfoGYMMd4HSKl/ueF+QKP59dt5B2LTnWhS7FOqzH5auu 17hkfx3ZCNzfeEuZn6T6F4bMtsQ6A5iT/DeRlG8tOPiCVZ/L0j6IFM78iIUT8XyA miwnSy1xGSBA67yUmsLxFg2DtGCjamAkY0C5pccadaB7oQJBAKsIPpMXMni+Oo1I kVxRyoIZgDxsMJiihG2YLVqo8rPtdErl+Lyg3ziVyg9KR6lFMaTBkYBTLoCPof3E AB/jyucCQQCRv1cVnYNx+bfnXsBlcsCFDV2HkEuLTpxj7hauD4P3GcyLidSsUkn1 PiPunZqKpsQaIoxc/BzTOCcP19ifgqdRAkBJ8Cp9FE4xfKt7YJ/WtVVCoRubA3qO wdNWPa99vgQOXN0lc/3wLevSXo8XxRjtyIgJndT1EQDNe0qglhcnsiaJAkBziAcR /VAq0tZys2szf6kYTyXqxfj8Lo5NsHeN9oKXJ346xkEtb/VsT5vQFGJishsU1HoL Y1W+IO7l4iW3G6xhAkACNwtqxSRRbVsNCUMENpKmYhsyN8QXJ8V+o2A9s+pl21Kz HIIm179mUYCgO6iAHmkqxlFHFwprUBKdPrmP8qF9 -----END RSA PRIVATE KEY-----` type Header struct { Algorithm string `json:"alg"` Type string `json:"typ"` KeyID string `json:"kid"` } func TestPayload_buildToken(t *testing.T) { signer, err := getRSASigner(privateKey, "sampleKeyId") require.NoError(t, err) payload := Payload{IssuedAt: 1234, Expiry: 4321, Audience: "api.url", Issuer: "issuer", Subject: "subject"} token, err := payload.buildToken(&signer) require.NoError(t, err) segments := strings.Split(token, ".") require.Len(t, segments, 3) headerString, err := base64.RawStdEncoding.DecodeString(segments[0]) require.NoError(t, err) var headerStruct Header err = json.Unmarshal(headerString, &headerStruct) require.NoError(t, err) payloadString, err := base64.RawStdEncoding.DecodeString(segments[1]) require.NoError(t, err) var payloadStruct Payload err = json.Unmarshal(payloadString, &payloadStruct) require.NoError(t, err) expectedHeader := Header{Algorithm: "RS256", Type: "JWT", KeyID: "sampleKeyId"} assert.Equal(t, expectedHeader, headerStruct) assert.Equal(t, payload, payloadStruct) } lego-4.9.1/providers/dns/ibmcloud/000077500000000000000000000000001434020463500170535ustar00rootroot00000000000000lego-4.9.1/providers/dns/ibmcloud/ibmcloud.go000066400000000000000000000104561434020463500212060ustar00rootroot00000000000000// Package ibmcloud implements a DNS provider for solving the DNS-01 challenge using IBM Cloud (SoftLayer). package ibmcloud import ( "errors" "fmt" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/ibmcloud/internal" "github.com/softlayer/softlayer-go/session" ) // Environment variables names. const ( envNamespace = "SOFTLAYER_" // EnvUsername the name must be the same as here: // https://github.com/softlayer/softlayer-go/blob/534185047ea683dd1e29fd23e445598295d94be4/session/session.go#L171 EnvUsername = envNamespace + "USERNAME" // EnvAPIKey the name must be the same as here: // https://github.com/softlayer/softlayer-go/blob/534185047ea683dd1e29fd23e445598295d94be4/session/session.go#L175 EnvAPIKey = envNamespace + "API_KEY" // EnvHTTPTimeout the name must be the same as here: // https://github.com/softlayer/softlayer-go/blob/534185047ea683dd1e29fd23e445598295d94be4/session/session.go#L182 EnvHTTPTimeout = envNamespace + "TIMEOUT" EnvDebug = envNamespace + "DEBUG" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Username string APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPTimeout time.Duration Debug bool } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, session.DefaultTimeout), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config wrapper *internal.Wrapper } // NewDNSProvider returns a DNSProvider instance configured for IBM Cloud (SoftLayer). // Credentials must be passed in the environment variables: // SOFTLAYER_USERNAME, SOFTLAYER_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvUsername, EnvAPIKey) if err != nil { return nil, fmt.Errorf("ibmcloud: %w", err) } config := NewDefaultConfig() config.Username = values[EnvUsername] config.APIKey = values[EnvAPIKey] config.Debug = env.GetOrDefaultBool(EnvDebug, false) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for IBM Cloud (SoftLayer). func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("ibmcloud: the configuration of the DNS provider is nil") } if config.Username == "" { return nil, errors.New("ibmcloud: username is missing") } if config.APIKey == "" { return nil, errors.New("ibmcloud: API key is missing") } sess := session.New(config.Username, config.APIKey) sess.Timeout = config.HTTPTimeout sess.Debug = config.Debug return &DNSProvider{wrapper: internal.NewWrapper(sess), config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) // TODO(ldez) replace domain by FQDN to follow CNAME. err := d.wrapper.AddTXTRecord(fqdn, domain, value, d.config.TTL) if err != nil { return fmt.Errorf("ibmcloud: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) // TODO(ldez) replace domain by FQDN to follow CNAME. err := d.wrapper.CleanupTXTRecord(fqdn, domain) if err != nil { return fmt.Errorf("ibmcloud: %w", err) } return nil } lego-4.9.1/providers/dns/ibmcloud/ibmcloud.toml000066400000000000000000000016031434020463500215460ustar00rootroot00000000000000Name = "IBM Cloud (SoftLayer)" Description = '''''' URL = "https://www.ibm.com/cloud/" Code = "ibmcloud" Since = "v4.5.0" Example = ''' SOFTLAYER_USERNAME=xxxxx \ SOFTLAYER_API_KEY=yyyyy \ lego --email you@example.com --dns ibmcloud --domains my.example.org run ''' [Configuration] [Configuration.Credentials] SOFTLAYER_USERNAME = "User name (IBM Cloud is _)" SOFTLAYER_API_KEY = "Classic Infrastructure API key" [Configuration.Additional] SOFTLAYER_POLLING_INTERVAL = "Time between DNS propagation check" SOFTLAYER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" SOFTLAYER_TTL = "The TTL of the TXT record used for the DNS challenge" SOFTLAYER_TIMEOUT = "API request timeout" [Links] API = "https://cloud.ibm.com/docs/dns?topic=dns-getting-started-with-the-dns-api" GoClient = "https://github.com/softlayer/softlayer-go" lego-4.9.1/providers/dns/ibmcloud/ibmcloud_test.go000066400000000000000000000060311434020463500222370ustar00rootroot00000000000000package ibmcloud import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvUsername, EnvAPIKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvUsername: "123", EnvAPIKey: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvUsername: "", EnvAPIKey: "", }, expected: "ibmcloud: some credentials information are missing: SOFTLAYER_USERNAME,SOFTLAYER_API_KEY", }, { desc: "missing access token", envVars: map[string]string{ EnvUsername: "", EnvAPIKey: "456", }, expected: "ibmcloud: some credentials information are missing: SOFTLAYER_USERNAME", }, { desc: "missing token secret", envVars: map[string]string{ EnvUsername: "123", EnvAPIKey: "", }, expected: "ibmcloud: some credentials information are missing: SOFTLAYER_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.wrapper) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string username string apiKey string expected string }{ { desc: "success", username: "123", apiKey: "456", }, { desc: "missing credentials", expected: "ibmcloud: username is missing", }, { desc: "missing token", apiKey: "456", expected: "ibmcloud: username is missing", }, { desc: "missing secret", username: "123", expected: "ibmcloud: API key is missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Username = test.username config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.wrapper) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/ibmcloud/internal/000077500000000000000000000000001434020463500206675ustar00rootroot00000000000000lego-4.9.1/providers/dns/ibmcloud/internal/wrapper.go000066400000000000000000000051501434020463500226770ustar00rootroot00000000000000package internal import ( "fmt" "github.com/softlayer/softlayer-go/datatypes" "github.com/softlayer/softlayer-go/services" "github.com/softlayer/softlayer-go/session" "github.com/softlayer/softlayer-go/sl" ) type Wrapper struct { session *session.Session } func NewWrapper(sess *session.Session) *Wrapper { return &Wrapper{session: sess} } func (w Wrapper) AddTXTRecord(fqdn, domain, value string, ttl int) error { service := services.GetDnsDomainService(w.session) domainID, err := getDomainID(service, domain) if err != nil { return fmt.Errorf("failed to get domain ID: %w", err) } service.Options.Id = domainID if _, err := service.CreateTxtRecord(sl.String(fqdn), sl.String(value), sl.Int(ttl)); err != nil { return fmt.Errorf("failed to create TXT record: %w", err) } return nil } func (w Wrapper) CleanupTXTRecord(fqdn, domain string) error { service := services.GetDnsDomainService(w.session) domainID, err := getDomainID(service, domain) if err != nil { return fmt.Errorf("failed to get domain ID: %w", err) } service.Options.Id = domainID records, err := findTxtRecords(service, fqdn) if err != nil { return fmt.Errorf("failed to find TXT records: %w", err) } return deleteResourceRecords(service, records) } func getDomainID(service services.Dns_Domain, domain string) (*int, error) { res, err := service.GetByDomainName(sl.String(domain)) if err != nil { return nil, err } for _, r := range res { if r.Id == nil || toString(r.Name) != domain { continue } return r.Id, nil } return nil, fmt.Errorf("no data found of domain: %s", domain) } func findTxtRecords(service services.Dns_Domain, fqdn string) ([]datatypes.Dns_Domain_ResourceRecord, error) { var results []datatypes.Dns_Domain_ResourceRecord records, err := service.GetResourceRecords() if err != nil { return nil, err } for _, record := range records { if toString(record.Host) == fqdn && toString(record.Type) == "txt" { results = append(results, record) } } if len(results) == 0 { return nil, fmt.Errorf("no data found of fqdn: %s", fqdn) } return results, nil } func deleteResourceRecords(service services.Dns_Domain, records []datatypes.Dns_Domain_ResourceRecord) error { resourceRecord := services.GetDnsDomainResourceRecordService(service.Session) // TODO maybe a bug: only the last record will be deleted for _, record := range records { resourceRecord.Options.Id = record.Id } _, err := resourceRecord.DeleteObject() if err != nil { return fmt.Errorf("no data found of fqdn: %w", err) } return nil } func toString(v *string) string { if v == nil { return "" } return *v } lego-4.9.1/providers/dns/iij/000077500000000000000000000000001434020463500160305ustar00rootroot00000000000000lego-4.9.1/providers/dns/iij/iij.go000066400000000000000000000140321434020463500171320ustar00rootroot00000000000000// Package iij implements a DNS provider for solving the DNS-01 challenge using IIJ DNS. package iij import ( "errors" "fmt" "strconv" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/iij/doapi" "github.com/iij/doapi/protocol" ) // Environment variables names. const ( envNamespace = "IIJ_" EnvAPIAccessKey = envNamespace + "API_ACCESS_KEY" EnvAPISecretKey = envNamespace + "API_SECRET_KEY" EnvDoServiceCode = envNamespace + "DO_SERVICE_CODE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { AccessKey string SecretKey string DoServiceCode string PropagationTimeout time.Duration PollingInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 300), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { api *doapi.API config *Config } // NewDNSProvider returns a DNSProvider instance configured for IIJ DNS. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIAccessKey, EnvAPISecretKey, EnvDoServiceCode) if err != nil { return nil, fmt.Errorf("iij: %w", err) } config := NewDefaultConfig() config.AccessKey = values[EnvAPIAccessKey] config.SecretKey = values[EnvAPISecretKey] config.DoServiceCode = values[EnvDoServiceCode] return NewDNSProviderConfig(config) } // NewDNSProviderConfig takes a given config // and returns a custom configured DNSProvider instance. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config.SecretKey == "" || config.AccessKey == "" || config.DoServiceCode == "" { return nil, errors.New("iij: credentials missing") } return &DNSProvider{ api: doapi.NewAPI(config.AccessKey, config.SecretKey), config: config, }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { _, value := dns01.GetRecord(domain, keyAuth) err := d.addTxtRecord(domain, value) if err != nil { return fmt.Errorf("iij: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { _, value := dns01.GetRecord(domain, keyAuth) // TODO(ldez) replace domain by FQDN to follow CNAME. err := d.deleteTxtRecord(domain, value) if err != nil { return fmt.Errorf("iij: %w", err) } return nil } func (d *DNSProvider) addTxtRecord(domain, value string) error { zones, err := d.listZones() if err != nil { return err } // TODO(ldez) replace domain by FQDN to follow CNAME. owner, zone, err := splitDomain(domain, zones) if err != nil { return err } request := protocol.RecordAdd{ DoServiceCode: d.config.DoServiceCode, ZoneName: zone, Owner: owner, TTL: strconv.Itoa(d.config.TTL), RecordType: "TXT", RData: value, } response := &protocol.RecordAddResponse{} if err := doapi.Call(*d.api, request, response); err != nil { return err } return d.commit() } func (d *DNSProvider) deleteTxtRecord(domain, value string) error { zones, err := d.listZones() if err != nil { return err } owner, zone, err := splitDomain(domain, zones) if err != nil { return err } id, err := d.findTxtRecord(owner, zone, value) if err != nil { return err } request := protocol.RecordDelete{ DoServiceCode: d.config.DoServiceCode, ZoneName: zone, RecordID: id, } response := &protocol.RecordDeleteResponse{} if err := doapi.Call(*d.api, request, response); err != nil { return err } return d.commit() } func (d *DNSProvider) commit() error { request := protocol.Commit{ DoServiceCode: d.config.DoServiceCode, } response := &protocol.CommitResponse{} return doapi.Call(*d.api, request, response) } func (d *DNSProvider) findTxtRecord(owner, zone, value string) (string, error) { request := protocol.RecordListGet{ DoServiceCode: d.config.DoServiceCode, ZoneName: zone, } response := &protocol.RecordListGetResponse{} if err := doapi.Call(*d.api, request, response); err != nil { return "", err } var id string for _, record := range response.RecordList { if record.Owner == owner && record.RecordType == "TXT" && record.RData == "\""+value+"\"" { id = record.Id } } if id == "" { return "", fmt.Errorf("%s record in %s not found", owner, zone) } return id, nil } func (d *DNSProvider) listZones() ([]string, error) { request := protocol.ZoneListGet{ DoServiceCode: d.config.DoServiceCode, } response := &protocol.ZoneListGetResponse{} if err := doapi.Call(*d.api, request, response); err != nil { return nil, err } return response.ZoneList, nil } func splitDomain(domain string, zones []string) (string, string, error) { parts := strings.Split(strings.Trim(domain, "."), ".") var owner string var zone string for i := 0; i < len(parts)-1; i++ { zone = strings.Join(parts[i:], ".") if zoneContains(zone, zones) { baseOwner := strings.Join(parts[0:i], ".") if len(baseOwner) > 0 { baseOwner = "." + baseOwner } owner = "_acme-challenge" + baseOwner break } } if owner == "" { return "", "", fmt.Errorf("%s not found", domain) } return owner, zone, nil } func zoneContains(zone string, zones []string) bool { for _, z := range zones { if zone == z { return true } } return false } lego-4.9.1/providers/dns/iij/iij.toml000066400000000000000000000014401434020463500174770ustar00rootroot00000000000000Name = "Internet Initiative Japan" Description = '''''' URL = "https://www.iij.ad.jp/en/" Code = "iij" Since = "v1.1.0" Example = ''' IIJ_API_ACCESS_KEY=xxxxxxxx \ IIJ_API_SECRET_KEY=yyyyyy \ IIJ_DO_SERVICE_CODE=zzzzzz \ lego --email you@example.com --dns iij --domains my.example.org run ''' [Configuration] [Configuration.Credentials] IIJ_API_ACCESS_KEY = "API access key" IIJ_API_SECRET_KEY = "API secret key" IIJ_DO_SERVICE_CODE = "DO service code" [Configuration.Additional] IIJ_POLLING_INTERVAL = "Time between DNS propagation check" IIJ_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" IIJ_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://manual.iij.jp/p2/pubapi/" GoClient = "https://github.com/iij/doapi" lego-4.9.1/providers/dns/iij/iij_test.go000066400000000000000000000122001434020463500201640ustar00rootroot00000000000000package iij import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "TESTDOMAIN" var envTest = tester.NewEnvTest( EnvAPIAccessKey, EnvAPISecretKey, EnvDoServiceCode). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIAccessKey: "A", EnvAPISecretKey: "B", EnvDoServiceCode: "C", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIAccessKey: "", EnvAPISecretKey: "", EnvDoServiceCode: "", }, expected: "iij: some credentials information are missing: IIJ_API_ACCESS_KEY,IIJ_API_SECRET_KEY,IIJ_DO_SERVICE_CODE", }, { desc: "missing api access key", envVars: map[string]string{ EnvAPIAccessKey: "", EnvAPISecretKey: "B", EnvDoServiceCode: "C", }, expected: "iij: some credentials information are missing: IIJ_API_ACCESS_KEY", }, { desc: "missing secret key", envVars: map[string]string{ EnvAPIAccessKey: "A", EnvAPISecretKey: "", EnvDoServiceCode: "C", }, expected: "iij: some credentials information are missing: IIJ_API_SECRET_KEY", }, { desc: "missing do service code", envVars: map[string]string{ EnvAPIAccessKey: "A", EnvAPISecretKey: "B", EnvDoServiceCode: "", }, expected: "iij: some credentials information are missing: IIJ_DO_SERVICE_CODE", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.api) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string accessKey string secretKey string doServiceCode string expected string }{ { desc: "success", accessKey: "A", secretKey: "B", doServiceCode: "C", }, { desc: "missing credentials", expected: "iij: credentials missing", }, { desc: "missing access key", accessKey: "", secretKey: "B", doServiceCode: "C", expected: "iij: credentials missing", }, { desc: "missing secret key", accessKey: "A", secretKey: "", doServiceCode: "C", expected: "iij: credentials missing", }, { desc: "missing do service code", accessKey: "A", secretKey: "B", doServiceCode: "", expected: "iij: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.AccessKey = test.accessKey config.SecretKey = test.secretKey config.DoServiceCode = test.doServiceCode p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.api) } else { require.EqualError(t, err, test.expected) } }) } } func TestSplitDomain(t *testing.T) { testCases := []struct { desc string domain string zones []string expectedOwner string expectedZone string }{ { desc: "domain equals zone", domain: "domain.com", zones: []string{"domain.com"}, expectedOwner: "_acme-challenge", expectedZone: "domain.com", }, { desc: "with a sub domain", domain: "my.domain.com", zones: []string{"domain.com"}, expectedOwner: "_acme-challenge.my", expectedZone: "domain.com", }, { desc: "with a sub domain in a zone", domain: "my.sub.domain.com", zones: []string{"sub.domain.com", "domain.com"}, expectedOwner: "_acme-challenge.my", expectedZone: "sub.domain.com", }, { desc: "with a sub sub domain", domain: "my.sub.domain.com", zones: []string{"domain1.com", "domain.com"}, expectedOwner: "_acme-challenge.my.sub", expectedZone: "domain.com", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() owner, zone, err := splitDomain(test.domain, test.zones) require.NoError(t, err) assert.Equal(t, test.expectedOwner, owner) assert.Equal(t, test.expectedZone, zone) }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/iijdpf/000077500000000000000000000000001434020463500165225ustar00rootroot00000000000000lego-4.9.1/providers/dns/iijdpf/client.go000066400000000000000000000045531434020463500203360ustar00rootroot00000000000000package iijdpf import ( "context" "errors" "fmt" dpfzones "github.com/mimuret/golang-iij-dpf/pkg/apis/dpf/v1/zones" dpfapiutils "github.com/mimuret/golang-iij-dpf/pkg/apiutils" dpftypes "github.com/mimuret/golang-iij-dpf/pkg/types" ) func (d *DNSProvider) addTxtRecord(ctx context.Context, zoneID, fqdn, rdata string) error { r, err := dpfapiutils.GetRecordFromZoneID(ctx, d.client, zoneID, fqdn, dpfzones.TypeTXT) if err != nil && !errors.Is(err, dpfapiutils.ErrRecordNotFound) { return err } if r != nil { r.RData = append(r.RData, dpfzones.RecordRDATA{Value: rdata}) _, _, err = dpfapiutils.SyncUpdate(ctx, d.client, r, nil) if err != nil { return fmt.Errorf("failed to update record: %w", err) } return nil } record := &dpfzones.Record{ AttributeMeta: dpfzones.AttributeMeta{ZoneID: zoneID}, Name: fqdn, TTL: dpftypes.NullablePositiveInt32(d.config.TTL), RRType: dpfzones.TypeTXT, RData: dpfzones.RecordRDATASlice{dpfzones.RecordRDATA{Value: rdata}}, Description: "ACME", } _, _, err = dpfapiutils.SyncCreate(ctx, d.client, record, nil) if err != nil { return fmt.Errorf("failed to create record: %w", err) } return nil } func (d *DNSProvider) deleteTxtRecord(ctx context.Context, zoneID, fqdn, rdata string) error { r, err := dpfapiutils.GetRecordFromZoneID(ctx, d.client, zoneID, fqdn, dpfzones.TypeTXT) if err != nil { if errors.Is(err, dpfapiutils.ErrRecordNotFound) { // empty target rrset return nil } return err } if len(r.RData) == 1 { // delete rrset _, _, err = dpfapiutils.SyncDelete(ctx, d.client, r) if err != nil { return fmt.Errorf("failed to delete record: %w", err) } return nil } // delete rdata rdataSlice := dpfzones.RecordRDATASlice{} for _, v := range r.RData { if v.Value != rdata { rdataSlice = append(rdataSlice, v) } } r.RData = rdataSlice _, _, err = dpfapiutils.SyncUpdate(ctx, d.client, r, nil) if err != nil { return fmt.Errorf("failed to update record: %w", err) } return nil } func (d *DNSProvider) commit(ctx context.Context, zoneID string) error { apply := &dpfzones.ZoneApply{ AttributeMeta: dpfzones.AttributeMeta{ZoneID: zoneID}, Description: "ACME Processing", } _, _, err := dpfapiutils.SyncApply(ctx, d.client, apply, nil) if err != nil { return fmt.Errorf("failed to apply zone: %w", err) } return nil } lego-4.9.1/providers/dns/iijdpf/iijdpf.go000066400000000000000000000077671434020463500203370ustar00rootroot00000000000000// Package iijdpf implements a DNS provider for solving the DNS-01 challenge using IIJ DNS Platform Service. package iijdpf import ( "context" "errors" "fmt" "time" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/miekg/dns" dpfapi "github.com/mimuret/golang-iij-dpf/pkg/api" dpfapiutils "github.com/mimuret/golang-iij-dpf/pkg/apiutils" ) // Environment variables names. const ( envNamespace = "IIJ_DPF_" EnvAPIToken = envNamespace + "API_TOKEN" EnvServiceCode = envNamespace + "DPM_SERVICE_CODE" EnvAPIEndpoint = envNamespace + "API_ENDPOINT" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Token string ServiceCode string Endpoint string PropagationTimeout time.Duration PollingInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ Endpoint: env.GetOrDefaultString(EnvAPIEndpoint, dpfapi.DefaultEndpoint), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 660*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), TTL: env.GetOrDefaultInt(EnvTTL, 300), } } var _ challenge.Provider = &DNSProvider{} // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client dpfapi.ClientInterface config *Config } // NewDNSProvider returns a DNSProvider instance configured for IIJ DNS. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIToken, EnvServiceCode) if err != nil { return nil, fmt.Errorf("iijdpf: %w", err) } config := NewDefaultConfig() config.Token = values[EnvAPIToken] config.ServiceCode = values[EnvServiceCode] return NewDNSProviderConfig(config) } // NewDNSProviderConfig takes a given config // and returns a custom configured DNSProvider instance. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config.Token == "" { return nil, errors.New("iijdpf: API token missing") } if config.ServiceCode == "" { return nil, errors.New("iijdpf: Servicecode missing") } return &DNSProvider{ client: dpfapi.NewClient(config.Token, config.Endpoint, nil), config: config, }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { ctx := context.Background() fqdn, value := dns01.GetRecord(domain, keyAuth) zoneID, err := dpfapiutils.GetZoneIdFromServiceCode(ctx, d.client, d.config.ServiceCode) if err != nil { return fmt.Errorf("iijdpf: failed to get zone id: %w", err) } err = d.addTxtRecord(ctx, zoneID, dns.CanonicalName(fqdn), `"`+value+`"`) if err != nil { return fmt.Errorf("iijdpf: %w", err) } err = d.commit(ctx, zoneID) if err != nil { return fmt.Errorf("iijdpf: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { ctx := context.Background() fqdn, value := dns01.GetRecord(domain, keyAuth) zoneID, err := dpfapiutils.GetZoneIdFromServiceCode(ctx, d.client, d.config.ServiceCode) if err != nil { return fmt.Errorf("iijdpf: failed to get zone id: %w", err) } err = d.deleteTxtRecord(ctx, zoneID, dns.CanonicalName(fqdn), `"`+value+`"`) if err != nil { return fmt.Errorf("iijdpf: %w", err) } err = d.commit(ctx, zoneID) if err != nil { return fmt.Errorf("iijdpf: %w", err) } return nil } lego-4.9.1/providers/dns/iijdpf/iijdpf.toml000066400000000000000000000016731434020463500206730ustar00rootroot00000000000000Name = "IIJ DNS Platform Service" Description = '''''' URL = "https://www.iij.ad.jp/en/biz/dns-pfm/" Code = "iijdpf" Since = "v4.7.0" Example = ''' IIJ_DPF_API_TOKEN=xxxxxxxx \ IIJ_DPF_DPM_SERVICE_CODE=yyyyyy \ lego --email you@example.com --dns iijdpf --domains my.example.org run ''' [Configuration] [Configuration.Credentials] IIJ_DPF_API_TOKEN = "API token" IIJ_DPF_DPM_SERVICE_CODE = "IIJ Managed DNS Service's service code" [Configuration.Additional] IIJ_DPF_API_ENDPOINT = "API endpoint URL, defaults to https://api.dns-platform.jp/dpf/v1" IIJ_DPF_POLLING_INTERVAL = "Time between DNS propagation check, defaults to 5 second" IIJ_DPF_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation, defaults to 660 second" IIJ_DPF_TTL = "The TTL of the TXT record used for the DNS challenge, default to 300" [Links] API = "https://manual.iij.jp/dpf/dpfapi/" GoClient = "https://github.com/mimuret/golang-iij-dpf" lego-4.9.1/providers/dns/iijdpf/iijdpf_test.go000066400000000000000000000054461434020463500213660ustar00rootroot00000000000000package iijdpf import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "TESTDOMAIN" var envTest = tester.NewEnvTest(EnvAPIToken, EnvServiceCode).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIToken: "A", EnvServiceCode: "dpmXXXXXX", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIToken: "A", }, expected: "iijdpf: some credentials information are missing: IIJ_DPF_DPM_SERVICE_CODE", }, { desc: "missing credentials", envVars: map[string]string{ EnvServiceCode: "dpmXXXXXX", }, expected: "iijdpf: some credentials information are missing: IIJ_DPF_API_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string token string servicecode string expected string }{ { desc: "success", token: "A", servicecode: "dpm00000", }, { desc: "missing credentials", servicecode: "dpm00000", expected: "iijdpf: API token missing", }, { desc: "missing credentials", token: "A", expected: "iijdpf: Servicecode missing", }, { desc: "missing credentials", expected: "iijdpf: API token missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Token = test.token config.ServiceCode = test.servicecode p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/infoblox/000077500000000000000000000000001434020463500170755ustar00rootroot00000000000000lego-4.9.1/providers/dns/infoblox/infoblox.go000066400000000000000000000140451434020463500212500ustar00rootroot00000000000000// Package infoblox implements a DNS provider for solving the DNS-01 challenge using on prem infoblox DNS. package infoblox import ( "errors" "fmt" "strconv" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" infoblox "github.com/infobloxopen/infoblox-go-client" ) // Environment variables names. const ( envNamespace = "INFOBLOX_" EnvHost = envNamespace + "HOST" EnvPort = envNamespace + "PORT" EnvUsername = envNamespace + "USERNAME" EnvPassword = envNamespace + "PASSWORD" EnvDNSView = envNamespace + "DNS_VIEW" EnvWApiVersion = envNamespace + "WAPI_VERSION" EnvSSLVerify = envNamespace + "SSL_VERIFY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) const ( defaultPoolConnections = 10 defaultUserAgent = "go-acme/lego" ) // Config is used to configure the creation of the DNSProvider. type Config struct { // Host is the URL of the grid manager. Host string // Port is the Port for the grid manager. Port string // Username the user for accessing API. Username string // Password the password for accessing API. Password string // DNSView is the dns view to put new records and search from. DNSView string // WapiVersion is the version of web api used. WapiVersion string // SSLVerify is whether or not to verify the ssl of the server being hit. SSLVerify bool PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPTimeout int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ DNSView: env.GetOrDefaultString(EnvDNSView, "External"), WapiVersion: env.GetOrDefaultString(EnvWApiVersion, "2.11"), Port: env.GetOrDefaultString(EnvPort, "443"), SSLVerify: env.GetOrDefaultBool(EnvSSLVerify, true), TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPTimeout: env.GetOrDefaultInt(EnvHTTPTimeout, 30), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config transportConfig infoblox.TransportConfig ibConfig infoblox.HostConfig recordRefs map[string]string recordRefsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for Infoblox. // Credentials must be passed in the environment variables: // INFOBLOX_USERNAME, INFOBLOX_PASSWORD // INFOBLOX_HOST, INFOBLOX_PORT // INFOBLOX_DNS_VIEW, INFOBLOX_WAPI_VERSION // INFOBLOX_SSL_VERIFY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvHost, EnvUsername, EnvPassword) if err != nil { return nil, fmt.Errorf("infoblox: %w", err) } config := NewDefaultConfig() config.Host = values[EnvHost] config.Username = values[EnvUsername] config.Password = values[EnvPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for HyperOne. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("infoblox: the configuration of the DNS provider is nil") } if config.Host == "" { return nil, errors.New("infoblox: missing host") } if config.Username == "" || config.Password == "" { return nil, errors.New("infoblox: missing credentials") } return &DNSProvider{ config: config, transportConfig: infoblox.NewTransportConfig(strconv.FormatBool(config.SSLVerify), config.HTTPTimeout, defaultPoolConnections), ibConfig: infoblox.HostConfig{ Host: config.Host, Version: config.WapiVersion, Port: config.Port, Username: config.Username, Password: config.Password, }, recordRefs: make(map[string]string), }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) connector, err := infoblox.NewConnector(d.ibConfig, d.transportConfig, &infoblox.WapiRequestBuilder{}, &infoblox.WapiHttpRequestor{}) if err != nil { return fmt.Errorf("infoblox: %w", err) } defer func() { _ = connector.Logout() }() objectManager := infoblox.NewObjectManager(connector, defaultUserAgent, "") record, err := objectManager.CreateTXTRecord(dns01.UnFqdn(fqdn), value, uint(d.config.TTL), d.config.DNSView) if err != nil { return fmt.Errorf("infoblox: could not create TXT record for %s: %w", domain, err) } d.recordRefsMu.Lock() d.recordRefs[token] = record.Ref d.recordRefsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) connector, err := infoblox.NewConnector(d.ibConfig, d.transportConfig, &infoblox.WapiRequestBuilder{}, &infoblox.WapiHttpRequestor{}) if err != nil { return fmt.Errorf("infoblox: %w", err) } defer func() { _ = connector.Logout() }() objectManager := infoblox.NewObjectManager(connector, defaultUserAgent, "") // gets the record's unique ref from when we created it d.recordRefsMu.Lock() recordRef, ok := d.recordRefs[token] d.recordRefsMu.Unlock() if !ok { return fmt.Errorf("infoblox: unknown record ID for '%s' '%s'", fqdn, token) } _, err = objectManager.DeleteTXTRecord(recordRef) if err != nil { return fmt.Errorf("infoblox: could not delete TXT record for %s: %w", domain, err) } // Delete record ref from map d.recordRefsMu.Lock() delete(d.recordRefs, token) d.recordRefsMu.Unlock() return nil } lego-4.9.1/providers/dns/infoblox/infoblox.toml000066400000000000000000000025161434020463500216160ustar00rootroot00000000000000Name = "Infoblox" Description = '''''' URL = "https://www.infoblox.com/" Code = "infoblox" Since = "v4.4.0" Example = ''' INFOBLOX_USERNAME=api-user-529 \ INFOBLOX_PASSWORD=b9841238feb177a84330febba8a83208921177bffe733 \ INFOBLOX_HOST=infoblox.example.org lego --email you@example.com --dns infoblox --domains my.example.org run ''' Additional = ''' When creating an API's user ensure it has the proper permissions for the view you are working with. ''' [Configuration] [Configuration.Credentials] INFOBLOX_USERNAME = "Account Username" INFOBLOX_PASSWORD = "Account Password" INFOBLOX_HOST = "Host URI" [Configuration.Additional] INFOBLOX_DNS_VIEW = "The view for the TXT records, default: External" INFOBLOX_WAPI_VERSION = "The version of WAPI being used, default: 2.11" INFOBLOX_PORT = "The port for the infoblox grid manager, default: 443" INFOBLOX_SSL_VERIFY = "Whether or not to verify the TLS certificate, default: true" INFOBLOX_POLLING_INTERVAL = "Time between DNS propagation check" INFOBLOX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" INFOBLOX_TTL = "The TTL of the TXT record used for the DNS challenge" INFOBLOX_HTTP_TIMEOUT = "HTTP request timeout" [Links] API = "https://your.infoblox.server/wapidoc/" GoClient = "https://github.com/infobloxopen/infoblox-go-client" lego-4.9.1/providers/dns/infoblox/infoblox_test.go000066400000000000000000000067271434020463500223170ustar00rootroot00000000000000package infoblox import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvHost, EnvPort, EnvUsername, EnvPassword, EnvSSLVerify, ).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvHost: "example.com", EnvUsername: "user", EnvPassword: "secret", EnvSSLVerify: "false", }, }, { desc: "missing host", envVars: map[string]string{ EnvHost: "", EnvUsername: "user", EnvPassword: "secret", EnvSSLVerify: "false", }, expected: "infoblox: some credentials information are missing: INFOBLOX_HOST", }, { desc: "missing username", envVars: map[string]string{ EnvHost: "example.com", EnvUsername: "", EnvPassword: "secret", EnvSSLVerify: "false", }, expected: "infoblox: some credentials information are missing: INFOBLOX_USERNAME", }, { desc: "missing password", envVars: map[string]string{ EnvHost: "example.com", EnvUsername: "user", EnvPassword: "", EnvSSLVerify: "false", }, expected: "infoblox: some credentials information are missing: INFOBLOX_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string host string username string password string expected string }{ { desc: "success", host: "example.com", username: "user", password: "secret", }, { desc: "missing host", host: "", username: "user", password: "secret", expected: "infoblox: missing host", }, { desc: "missing username", host: "example.com", username: "", password: "secret", expected: "infoblox: missing credentials", }, { desc: "missing password", host: "example.com", username: "user", password: "", expected: "infoblox: missing credentials", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Host = test.host config.Username = test.username config.Password = test.password config.SSLVerify = false p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/infomaniak/000077500000000000000000000000001434020463500173715ustar00rootroot00000000000000lego-4.9.1/providers/dns/infomaniak/infomaniak.go000066400000000000000000000123161434020463500220370ustar00rootroot00000000000000// Package infomaniak implements a DNS provider for solving the DNS-01 challenge using Infomaniak DNS. package infomaniak import ( "errors" "fmt" "net/http" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/infomaniak/internal" ) // Infomaniak API reference: https://api.infomaniak.com/doc // Create a Token: https://manager.infomaniak.com/v3/infomaniak-api // Environment variables names. const ( envNamespace = "INFOMANIAK_" EnvEndpoint = envNamespace + "ENDPOINT" EnvAccessToken = envNamespace + "ACCESS_TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) const defaultBaseURL = "https://api.infomaniak.com" // Config is used to configure the creation of the DNSProvider. type Config struct { APIEndpoint string AccessToken string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ APIEndpoint: env.GetOrDefaultString(EnvEndpoint, defaultBaseURL), TTL: env.GetOrDefaultInt(EnvTTL, 7200), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client recordIDs map[string]string recordIDsMu sync.Mutex domainIDs map[string]uint64 domainIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for Infomaniak. // Credentials must be passed in the environment variables: INFOMANIAK_ACCESS_TOKEN. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAccessToken) if err != nil { return nil, fmt.Errorf("infomaniak: %w", err) } config := NewDefaultConfig() config.AccessToken = values[EnvAccessToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Infomaniak. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("infomaniak: the configuration of the DNS provider is nil") } if config.APIEndpoint == "" { return nil, errors.New("infomaniak: missing API endpoint") } if config.AccessToken == "" { return nil, errors.New("infomaniak: missing access token") } client := internal.New(config.APIEndpoint, config.AccessToken) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]string), domainIDs: make(map[string]uint64), }, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) // TODO(ldez) replace domain by FQDN to follow CNAME. ikDomain, err := d.client.GetDomainByName(domain) if err != nil { return fmt.Errorf("infomaniak: could not get domain %q: %w", domain, err) } d.domainIDsMu.Lock() d.domainIDs[token] = ikDomain.ID d.domainIDsMu.Unlock() record := internal.Record{ Source: extractRecordName(fqdn, ikDomain.CustomerName), Target: value, Type: "TXT", TTL: d.config.TTL, } recordID, err := d.client.CreateDNSRecord(ikDomain, record) if err != nil { return fmt.Errorf("infomaniak: error when calling api to create DNS record: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = recordID d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("infomaniak: unknown record ID for '%s'", fqdn) } d.domainIDsMu.Lock() domainID, ok := d.domainIDs[token] d.domainIDsMu.Unlock() if !ok { return fmt.Errorf("infomaniak: unknown domain ID for '%s'", fqdn) } err := d.client.DeleteDNSRecord(domainID, recordID) if err != nil { return fmt.Errorf("infomaniak: could not delete record %q: %w", domain, err) } // Delete record ID from map d.recordIDsMu.Lock() delete(d.recordIDs, token) d.recordIDsMu.Unlock() // Delete domain ID from map d.domainIDsMu.Lock() delete(d.domainIDs, token) d.domainIDsMu.Unlock() return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func extractRecordName(fqdn, domain string) string { name := dns01.UnFqdn(fqdn) return name[:len(name)-len(domain)-1] } lego-4.9.1/providers/dns/infomaniak/infomaniak.toml000066400000000000000000000016331434020463500224050ustar00rootroot00000000000000Name = "Infomaniak" Description = '''''' URL = "https://www.infomaniak.com/" Code = "infomaniak" Since = "v4.1.0" Example = ''' INFOMANIAK_ACCESS_TOKEN=1234567898765432 \ lego --email you@example.com --dns infomaniak --domains my.example.org run ''' Additional = ''' ## Access token Access token can be created at the url https://manager.infomaniak.com/v3/infomaniak-api. You will need domain scope. ''' [Configuration] [Configuration.Credentials] INFOMANIAK_ACCESS_TOKEN = "Access token" [Configuration.Additional] INFOMANIAK_ENDPOINT = "https://api.infomaniak.com" INFOMANIAK_POLLING_INTERVAL = "Time between DNS propagation check" INFOMANIAK_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" INFOMANIAK_TTL = "The TTL of the TXT record used for the DNS challenge in seconds" INFOMANIAK_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.infomaniak.com/doc" lego-4.9.1/providers/dns/infomaniak/infomaniak_test.go000066400000000000000000000046551434020463500231050ustar00rootroot00000000000000package infomaniak import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvEndpoint, EnvAccessToken). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAccessToken: "123", }, }, { desc: "missing access token", envVars: map[string]string{ EnvAccessToken: "", }, expected: "infomaniak: some credentials information are missing: INFOMANIAK_ACCESS_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string accessToken string expected string }{ { desc: "success", accessToken: "123", }, { desc: "missing access token", accessToken: "", expected: "infomaniak: missing access token", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.AccessToken = test.accessToken p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/infomaniak/internal/000077500000000000000000000000001434020463500212055ustar00rootroot00000000000000lego-4.9.1/providers/dns/infomaniak/internal/client.go000066400000000000000000000076501434020463500230220ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "path" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" ) // Client the Infomaniak client. type Client struct { apiEndpoint string apiToken string HTTPClient *http.Client } // New Creates a new Infomaniak client. func New(apiEndpoint, apiToken string) *Client { return &Client{ apiEndpoint: apiEndpoint, apiToken: apiToken, HTTPClient: &http.Client{Timeout: 5 * time.Second}, } } func (c *Client) CreateDNSRecord(domain *DNSDomain, record Record) (string, error) { rawJSON, err := json.Marshal(record) if err != nil { return "", err } uri := fmt.Sprintf("/1/domain/%d/dns/record", domain.ID) req, err := c.newRequest(http.MethodPost, uri, bytes.NewBuffer(rawJSON)) if err != nil { return "", err } resp, err := c.do(req) if err != nil { return "", err } var recordID string if err = json.Unmarshal(resp.Data, &recordID); err != nil { return "", fmt.Errorf("expected record, got: %s", string(resp.Data)) } return recordID, err } func (c *Client) DeleteDNSRecord(domainID uint64, recordID string) error { uri := fmt.Sprintf("/1/domain/%d/dns/record/%s", domainID, recordID) req, err := c.newRequest(http.MethodDelete, uri, nil) if err != nil { return err } _, err = c.do(req) return err } // GetDomainByName gets a Domain object from its name. func (c *Client) GetDomainByName(name string) (*DNSDomain, error) { name = dns01.UnFqdn(name) // Try to find the most specific domain // starts with the FQDN, then remove each left label until we have a match for { i := strings.Index(name, ".") if i == -1 { break } domain, err := c.getDomainByName(name) if err != nil { return nil, err } if domain != nil { return domain, nil } log.Infof("domain %q not found, trying with %q", name, name[i+1:]) name = name[i+1:] } return nil, fmt.Errorf("domain not found %s", name) } func (c *Client) getDomainByName(name string) (*DNSDomain, error) { base, err := url.Parse("/1/product") if err != nil { return nil, err } query := base.Query() query.Add("service_name", "domain") query.Add("customer_name", name) base.RawQuery = query.Encode() req, err := c.newRequest(http.MethodGet, base.String(), nil) if err != nil { return nil, err } resp, err := c.do(req) if err != nil { return nil, err } var domains []DNSDomain if err = json.Unmarshal(resp.Data, &domains); err != nil { return nil, fmt.Errorf("failed to marshal domains: %s", string(resp.Data)) } for _, domain := range domains { if domain.CustomerName == name { return &domain, nil } } return nil, nil } func (c *Client) do(req *http.Request) (*APIResponse, error) { rawResp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to perform API request: %w", err) } defer func() { _ = rawResp.Body.Close() }() content, err := io.ReadAll(rawResp.Body) if err != nil { return nil, fmt.Errorf("failed to read the response body, status code: %d", rawResp.StatusCode) } var resp APIResponse if err := json.Unmarshal(content, &resp); err != nil { return nil, fmt.Errorf("failed to unmarshal the response body: %s", string(content)) } if resp.Result != "success" { return nil, fmt.Errorf("%d: unexpected API result (%s): %w", rawResp.StatusCode, resp.Result, resp.ErrResponse) } return &resp, nil } func (c *Client) newRequest(method, uri string, body io.Reader) (*http.Request, error) { baseURL, err := url.Parse(c.apiEndpoint) if err != nil { return nil, err } endpoint, err := baseURL.Parse(path.Join(baseURL.Path, uri)) if err != nil { return nil, err } req, err := http.NewRequest(method, endpoint.String(), body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", "Bearer "+c.apiToken) req.Header.Set("Content-Type", "application/json") return req, nil } lego-4.9.1/providers/dns/infomaniak/internal/client_test.go000066400000000000000000000073601434020463500240570ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupTest(t *testing.T) (*Client, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) return New(server.URL, "token"), mux } func TestClient_CreateDNSRecord(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/1/domain/666/dns/record", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } if req.Header.Get("Authorization") != "Bearer token" { http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } raw, err := io.ReadAll(req.Body) if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } defer func() { _ = req.Body.Close() }() if string(raw) != `{"source":"foo","type":"TXT","ttl":60,"target":"txtxtxttxt"}` { http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest) return } response := `{"result":"success","data": "123"}` _, err = rw.Write([]byte(response)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) domain := &DNSDomain{ ID: 666, CustomerName: "test", } record := Record{ Source: "foo", Target: "txtxtxttxt", Type: "TXT", TTL: 60, } recordID, err := client.CreateDNSRecord(domain, record) require.NoError(t, err) assert.Equal(t, "123", recordID) } func TestClient_GetDomainByName(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/1/product", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } if req.Header.Get("Authorization") != "Bearer token" { http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } serviceName := req.URL.Query().Get("service_name") if serviceName != "domain" { http.Error(rw, fmt.Sprintf("invalid service_name: %s", serviceName), http.StatusBadRequest) return } customerName := req.URL.Query().Get("customer_name") fmt.Println("customerName", customerName) if customerName == "" { http.Error(rw, fmt.Sprintf("invalid customer_name: %s", customerName), http.StatusBadRequest) return } response := ` { "result": "success", "data": [ { "id": 123, "customer_name": "two.three.example.com" }, { "id": 456, "customer_name": "three.example.com" } ] } ` _, err := rw.Write([]byte(response)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) domain, err := client.GetDomainByName("one.two.three.example.com.") require.NoError(t, err) expected := &DNSDomain{ID: 123, CustomerName: "two.three.example.com"} assert.Equal(t, expected, domain) } func TestClient_DeleteDNSRecord(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/1/domain/123/dns/record/456", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodDelete { http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } if req.Header.Get("Authorization") != "Bearer token" { http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } _, err := rw.Write([]byte((`{"result":"success"}`))) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) err := client.DeleteDNSRecord(123, "456") require.NoError(t, err) } lego-4.9.1/providers/dns/infomaniak/internal/models.go000066400000000000000000000017321434020463500230220ustar00rootroot00000000000000package internal import ( "encoding/json" "fmt" ) // Record a DNS record. type Record struct { ID string `json:"id,omitempty"` Source string `json:"source,omitempty"` Type string `json:"type,omitempty"` TTL int `json:"ttl,omitempty"` Target string `json:"target,omitempty"` } type DNSDomain struct { ID uint64 `json:"id,omitempty"` CustomerName string `json:"customer_name,omitempty"` } type APIResponse struct { Result string `json:"result"` Data json.RawMessage `json:"data,omitempty"` ErrResponse *APIErrorResponse `json:"error,omitempty"` } type APIErrorResponse struct { Code string `json:"code"` Description string `json:"description,omitempty"` Context map[string]string `json:"context,omitempty"` Errors []APIErrorResponse `json:"errors,omitempty"` } func (a APIErrorResponse) Error() string { return fmt.Sprintf("code: %s, description: %s", a.Code, a.Description) } lego-4.9.1/providers/dns/internal/000077500000000000000000000000001434020463500170715ustar00rootroot00000000000000lego-4.9.1/providers/dns/internal/rimuhosting/000077500000000000000000000000001434020463500214415ustar00rootroot00000000000000lego-4.9.1/providers/dns/internal/rimuhosting/client.go000066400000000000000000000070341434020463500232520ustar00rootroot00000000000000package rimuhosting import ( "encoding/xml" "errors" "io" "net/http" "net/url" "regexp" querystring "github.com/google/go-querystring/query" ) // Base URL for the RimuHosting DNS services. const ( DefaultZonomiBaseURL = "https://zonomi.com/app/dns/dyndns.jsp" DefaultRimuHostingBaseURL = "https://rimuhosting.com/app/dns/dyndns.jsp" ) // Action names. const ( SetAction = "SET" QueryAction = "QUERY" DeleteAction = "DELETE" ) // Client the RimuHosting/Zonomi client. type Client struct { apiKey string HTTPClient *http.Client BaseURL string } // NewClient Creates a RimuHosting/Zonomi client. func NewClient(apiKey string) *Client { return &Client{ HTTPClient: http.DefaultClient, BaseURL: DefaultZonomiBaseURL, apiKey: apiKey, } } // FindTXTRecords Finds TXT records. // ex: // - https://zonomi.com/app/dns/dyndns.jsp?action=QUERY&name=example.com&api_key=apikeyvaluehere // - https://zonomi.com/app/dns/dyndns.jsp?action=QUERY&name=**.example.com&api_key=apikeyvaluehere func (c Client) FindTXTRecords(domain string) ([]Record, error) { action := ActionParameter{ Action: QueryAction, Name: domain, Type: "TXT", } resp, err := c.DoActions(action) if err != nil { return nil, err } return resp.Actions.Action.Records, nil } // DoActions performs actions. func (c Client) DoActions(actions ...ActionParameter) (*DNSAPIResult, error) { if len(actions) == 0 { return nil, errors.New("no action") } resp := &DNSAPIResult{} if len(actions) == 1 { action := actionParameter{ ActionParameter: actions[0], APIKey: c.apiKey, } err := c.do(action, resp) if err != nil { return nil, err } return resp, nil } multi := c.toMultiParameters(actions) err := c.do(multi, resp) if err != nil { return nil, err } return resp, nil } func (c Client) toMultiParameters(params []ActionParameter) multiActionParameter { multi := multiActionParameter{ APIKey: c.apiKey, } for _, parameters := range params { multi.Action = append(multi.Action, parameters.Action) multi.Name = append(multi.Name, parameters.Name) multi.Type = append(multi.Type, parameters.Type) multi.Value = append(multi.Value, parameters.Value) multi.TTL = append(multi.TTL, parameters.TTL) } return multi } func (c Client) do(params, data interface{}) error { baseURL, err := url.Parse(c.BaseURL) if err != nil { return err } v, err := querystring.Values(params) if err != nil { return err } exp := regexp.MustCompile(`(%5B)(%5D)(\d+)=`) baseURL.RawQuery = exp.ReplaceAllString(v.Encode(), "${1}${3}${2}=") req, err := http.NewRequest(http.MethodGet, baseURL.String(), nil) if err != nil { return err } resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer func() { _ = resp.Body.Close() }() all, err := io.ReadAll(resp.Body) if err != nil { return err } if resp.StatusCode/100 != 2 { r := APIError{} err = xml.Unmarshal(all, &r) if err != nil { return err } return r } if data != nil { err := xml.Unmarshal(all, data) if err != nil { return err } } return nil } // AddRecord helper to create an action to add a TXT record. func AddRecord(domain, content string, ttl int) ActionParameter { return ActionParameter{ Action: SetAction, Name: domain, Type: "TXT", Value: content, TTL: ttl, } } // DeleteRecord helper to create an action to delete a TXT record. func DeleteRecord(domain, content string) ActionParameter { return ActionParameter{ Action: DeleteAction, Name: domain, Type: "TXT", Value: content, } } lego-4.9.1/providers/dns/internal/rimuhosting/client_test.go000066400000000000000000000165211434020463500243120ustar00rootroot00000000000000package rimuhosting import ( "encoding/xml" "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_FindTXTRecords(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { query := req.URL.Query() var fixture string switch query.Get("name") { case "example.com": fixture = "./fixtures/find_records.xml" case "**.example.com": fixture = "./fixtures/find_records_pattern.xml" default: fixture = "./fixtures/find_records_empty.xml" } err := writeResponse(rw, fixture) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient("apikeyvaluehere") client.BaseURL = server.URL testCases := []struct { desc string domain string expected []Record }{ { desc: "simple", domain: "example.com", expected: []Record{ { Name: "example.org", Type: "TXT", Content: "txttxtx", TTL: "3600 seconds", Priority: "0", }, }, }, { desc: "pattern", domain: "**.example.com", expected: []Record{ { Name: "_test.example.org", Type: "TXT", Content: "txttxtx", TTL: "3600 seconds", Priority: "0", }, { Name: "example.org", Type: "TXT", Content: "txttxtx", TTL: "3600 seconds", Priority: "0", }, }, }, { desc: "empty", domain: "empty.com", expected: nil, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { records, err := client.FindTXTRecords(test.domain) require.NoError(t, err) assert.Equal(t, test.expected, records) }) } } func TestClient_DoActions(t *testing.T) { type expected struct { Query string Resp *DNSAPIResult Error string } testCases := []struct { desc string actions []ActionParameter fixture string expected expected }{ { desc: "SET error", actions: []ActionParameter{ AddRecord("example.com", "txttxtx", 0), }, fixture: "./fixtures/add_record_error.xml", expected: expected{ Query: "action=SET&api_key=apikeyvaluehere&name=example.com&type=TXT&value=txttxtx", Error: "ERROR: No zone found for example.com", }, }, { desc: "SET simple", actions: []ActionParameter{ AddRecord("example.org", "txttxtx", 0), }, fixture: "./fixtures/add_record.xml", expected: expected{ Query: "action=SET&api_key=apikeyvaluehere&name=example.org&type=TXT&value=txttxtx", Resp: &DNSAPIResult{ XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, IsOk: "OK:", ResultCounts: ResultCounts{Added: "1", Changed: "0", Unchanged: "0", Deleted: "0"}, Actions: Actions{ Action: Action{ Action: "SET", Host: "example.org", Type: "TXT", Records: []Record{{ Name: "example.org", Type: "TXT", Content: "txttxtx", TTL: "3600 seconds", Priority: "0", }}, }, }, }, }, }, { desc: "SET multiple values", actions: []ActionParameter{ AddRecord("example.org", "txttxtx", 0), AddRecord("example.org", "sample", 0), }, fixture: "./fixtures/add_record_same_domain.xml", expected: expected{ Query: "action[0]=SET&action[1]=SET&api_key=apikeyvaluehere&name[0]=example.org&name[1]=example.org&ttl[0]=0&ttl[1]=0&type[0]=TXT&type[1]=TXT&value[0]=txttxtx&value[1]=sample", Resp: &DNSAPIResult{ XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, IsOk: "OK:", ResultCounts: ResultCounts{Added: "2", Changed: "0", Unchanged: "0", Deleted: "0"}, Actions: Actions{ Action: Action{ Action: "SET", Host: "example.org", Type: "TXT", Records: []Record{ { Name: "example.org", Type: "TXT", Content: "txttxtx", TTL: "0 seconds", Priority: "0", }, { Name: "example.org", Type: "TXT", Content: "sample", TTL: "0 seconds", Priority: "0", }, }, }, }, }, }, }, { desc: "DELETE error", actions: []ActionParameter{ DeleteRecord("example.com", "txttxtx"), }, fixture: "./fixtures/delete_record_error.xml", expected: expected{ Query: "action=DELETE&api_key=apikeyvaluehere&name=example.com&type=TXT&value=txttxtx", Error: "ERROR: No zone found for example.com", }, }, { desc: "DELETE nothing", actions: []ActionParameter{ DeleteRecord("example.org", "nothing"), }, fixture: "./fixtures/delete_record_nothing.xml", expected: expected{ Query: "action=DELETE&api_key=apikeyvaluehere&name=example.org&type=TXT&value=nothing", Resp: &DNSAPIResult{ XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, IsOk: "OK:", ResultCounts: ResultCounts{Added: "0", Changed: "0", Unchanged: "0", Deleted: "0"}, Actions: Actions{ Action: Action{ Action: "DELETE", Host: "example.org", Type: "TXT", Records: nil, }, }, }, }, }, { desc: "DELETE simple", actions: []ActionParameter{ DeleteRecord("example.org", "txttxtx"), }, fixture: "./fixtures/delete_record.xml", expected: expected{ Query: "action=DELETE&api_key=apikeyvaluehere&name=example.org&type=TXT&value=txttxtx", Resp: &DNSAPIResult{ XMLName: xml.Name{Space: "", Local: "dnsapi_result"}, IsOk: "OK:", ResultCounts: ResultCounts{Added: "0", Changed: "0", Unchanged: "0", Deleted: "1"}, Actions: Actions{ Action: Action{ Action: "DELETE", Host: "example.org", Type: "TXT", Records: []Record{{ Name: "example.org", Type: "TXT", Content: "txttxtx", TTL: "3600 seconds", Priority: "0", }}, }, }, }, }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { query, err := url.QueryUnescape(req.URL.RawQuery) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } if test.expected.Query != query { http.Error(rw, fmt.Sprintf("invalid query: %s", query), http.StatusBadRequest) return } if test.expected.Error != "" { rw.WriteHeader(http.StatusInternalServerError) } err = writeResponse(rw, test.fixture) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient("apikeyvaluehere") client.BaseURL = server.URL resp, err := client.DoActions(test.actions...) if test.expected.Error != "" { require.EqualError(t, err, test.expected.Error) return } require.NoError(t, err) assert.Equal(t, test.expected.Resp, resp) }) } } func writeResponse(rw io.Writer, filename string) error { file, err := os.Open(filename) if err != nil { return err } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) return err } lego-4.9.1/providers/dns/internal/rimuhosting/fixtures/000077500000000000000000000000001434020463500233125ustar00rootroot00000000000000lego-4.9.1/providers/dns/internal/rimuhosting/fixtures/add_record.xml000066400000000000000000000012511434020463500261210ustar00rootroot00000000000000]>OK: lego-4.9.1/providers/dns/internal/rimuhosting/fixtures/add_record_error.xml000066400000000000000000000002451434020463500273340ustar00rootroot00000000000000]> ERROR: No zone found for example.comlego-4.9.1/providers/dns/internal/rimuhosting/fixtures/add_record_same_domain.xml000066400000000000000000000021121434020463500304520ustar00rootroot00000000000000]>OK: lego-4.9.1/providers/dns/internal/rimuhosting/fixtures/delete_record.xml000066400000000000000000000012561434020463500266400ustar00rootroot00000000000000]>OK: lego-4.9.1/providers/dns/internal/rimuhosting/fixtures/delete_record_error.xml000066400000000000000000000002451434020463500300460ustar00rootroot00000000000000]> ERROR: No zone found for example.comlego-4.9.1/providers/dns/internal/rimuhosting/fixtures/delete_record_nothing.xml000066400000000000000000000007051434020463500303640ustar00rootroot00000000000000]>OK: lego-4.9.1/providers/dns/internal/rimuhosting/fixtures/find_records.xml000066400000000000000000000011561434020463500265000ustar00rootroot00000000000000]>OK: lego-4.9.1/providers/dns/internal/rimuhosting/fixtures/find_records_empty.xml000066400000000000000000000006501434020463500277140ustar00rootroot00000000000000]>OK: lego-4.9.1/providers/dns/internal/rimuhosting/fixtures/find_records_pattern.xml000066400000000000000000000015151434020463500302340ustar00rootroot00000000000000]>OK: lego-4.9.1/providers/dns/internal/rimuhosting/model.go000066400000000000000000000034341434020463500230740ustar00rootroot00000000000000package rimuhosting import "encoding/xml" type ActionParameter struct { Action string `url:"action,omitempty"` Name string `url:"name,omitempty"` Type string `url:"type,omitempty"` Value string `url:"value,omitempty"` TTL int `url:"ttl,omitempty"` Priority int `url:"prio,omitempty"` } type actionParameter struct { ActionParameter APIKey string `url:"api_key,omitempty"` } type multiActionParameter struct { APIKey string `url:"api_key,omitempty"` Action []string `url:"action,brackets,numbered,omitempty"` Name []string `url:"name,brackets,numbered,omitempty"` Type []string `url:"type,brackets,numbered,omitempty"` Value []string `url:"value,brackets,numbered,omitempty"` TTL []int `url:"ttl,brackets,numbered,omitempty"` Priority []int `url:"prio,brackets,numbered,omitempty"` } type APIError struct { XMLName xml.Name `xml:"error"` Text string `xml:",chardata"` } func (a APIError) Error() string { return a.Text } type DNSAPIResult struct { XMLName xml.Name `xml:"dnsapi_result"` IsOk string `xml:"is_ok"` ResultCounts ResultCounts `xml:"result_counts"` Actions Actions `xml:"actions"` } type ResultCounts struct { Added string `xml:"added,attr"` Changed string `xml:"changed,attr"` Unchanged string `xml:"unchanged,attr"` Deleted string `xml:"deleted,attr"` } type Actions struct { Action Action `xml:"action"` } type Action struct { Action string `xml:"action,attr"` Host string `xml:"host,attr"` Type string `xml:"type,attr"` Records []Record `xml:"record"` } type Record struct { Name string `xml:"name,attr"` Type string `xml:"type,attr"` Content string `xml:"content,attr"` TTL string `xml:"ttl,attr"` Priority string `xml:"prio,attr"` } lego-4.9.1/providers/dns/internal/selectel/000077500000000000000000000000001434020463500206715ustar00rootroot00000000000000lego-4.9.1/providers/dns/internal/selectel/client.go000066400000000000000000000102601434020463500224750ustar00rootroot00000000000000package selectel import ( "bytes" "encoding/json" "fmt" "io" "net/http" "strings" ) // Base URL for the Selectel/VScale DNS services. const ( DefaultSelectelBaseURL = "https://api.selectel.ru/domains/v1" DefaultVScaleBaseURL = "https://api.vscale.io/v1/domains" ) // Client represents DNS client. type Client struct { BaseURL string HTTPClient *http.Client token string } // NewClient returns a client instance. func NewClient(token string) *Client { return &Client{ token: token, BaseURL: DefaultVScaleBaseURL, HTTPClient: &http.Client{}, } } // GetDomainByName gets Domain object by its name. If `domainName` level > 2 and there is // no such domain on the account - it'll recursively search for the first // which is exists in Selectel Domain API. func (c *Client) GetDomainByName(domainName string) (*Domain, error) { uri := fmt.Sprintf("/%s", domainName) req, err := c.newRequest(http.MethodGet, uri, nil) if err != nil { return nil, err } domain := &Domain{} resp, err := c.do(req, domain) if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound && strings.Count(domainName, ".") > 1 { // Look up for the next sub domain subIndex := strings.Index(domainName, ".") return c.GetDomainByName(domainName[subIndex+1:]) } return nil, err } return domain, nil } // AddRecord adds Record for given domain. func (c *Client) AddRecord(domainID int, body Record) (*Record, error) { uri := fmt.Sprintf("/%d/records/", domainID) req, err := c.newRequest(http.MethodPost, uri, body) if err != nil { return nil, err } record := &Record{} _, err = c.do(req, record) if err != nil { return nil, err } return record, nil } // ListRecords returns list records for specific domain. func (c *Client) ListRecords(domainID int) ([]Record, error) { uri := fmt.Sprintf("/%d/records/", domainID) req, err := c.newRequest(http.MethodGet, uri, nil) if err != nil { return nil, err } var records []Record _, err = c.do(req, &records) if err != nil { return nil, err } return records, nil } // DeleteRecord deletes specific record. func (c *Client) DeleteRecord(domainID, recordID int) error { uri := fmt.Sprintf("/%d/records/%d", domainID, recordID) req, err := c.newRequest(http.MethodDelete, uri, nil) if err != nil { return err } _, err = c.do(req, nil) return err } func (c *Client) newRequest(method, uri string, body interface{}) (*http.Request, error) { buf := new(bytes.Buffer) if body != nil { err := json.NewEncoder(buf).Encode(body) if err != nil { return nil, fmt.Errorf("failed to encode request body with error: %w", err) } } req, err := http.NewRequest(method, c.BaseURL+uri, buf) if err != nil { return nil, fmt.Errorf("failed to create new http request with error: %w", err) } req.Header.Set("X-Token", c.token) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") return req, nil } func (c *Client) do(req *http.Request, to interface{}) (*http.Response, error) { resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("request failed with error: %w", err) } err = checkResponse(resp) if err != nil { return resp, err } if to != nil { if err = unmarshalBody(resp, to); err != nil { return resp, err } } return resp, nil } func checkResponse(resp *http.Response) error { if resp.StatusCode >= http.StatusBadRequest { if resp.Body == nil { return fmt.Errorf("request failed with status code %d and empty body", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return err } defer resp.Body.Close() apiError := APIError{} err = json.Unmarshal(body, &apiError) if err != nil { return fmt.Errorf("request failed with status code %d, response body: %s", resp.StatusCode, string(body)) } return fmt.Errorf("request failed with status code %d: %w", resp.StatusCode, apiError) } return nil } func unmarshalBody(resp *http.Response, to interface{}) error { body, err := io.ReadAll(resp.Body) if err != nil { return err } defer resp.Body.Close() err = json.Unmarshal(body, to) if err != nil { return fmt.Errorf("unmarshaling error: %w: %s", err, string(body)) } return nil } lego-4.9.1/providers/dns/internal/selectel/client_test.go000066400000000000000000000122101434020463500235310ustar00rootroot00000000000000package selectel import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_ListRecords(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/123/records/", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } fixture := "./fixtures/list_records.json" err := writeResponse(rw, fixture) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient("token") client.BaseURL = server.URL records, err := client.ListRecords(123) require.NoError(t, err) expected := []Record{ {ID: 123, Name: "example.com", Type: "TXT", TTL: 60, Email: "email@example.com", Content: "txttxttxtA"}, {ID: 1234, Name: "example.org", Type: "TXT", TTL: 60, Email: "email@example.org", Content: "txttxttxtB"}, {ID: 12345, Name: "example.net", Type: "TXT", TTL: 60, Email: "email@example.net", Content: "txttxttxtC"}, } assert.Equal(t, expected, records) } func TestClient_ListRecords_error(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/123/records/", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } rw.WriteHeader(http.StatusUnauthorized) err := writeResponse(rw, "./fixtures/error.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient("token") client.BaseURL = server.URL records, err := client.ListRecords(123) assert.EqualError(t, err, "request failed with status code 401: API error: 400 - error description - field that the error occurred in") assert.Nil(t, records) } func TestClient_GetDomainByName(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/sub.sub.example.org", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } rw.WriteHeader(http.StatusNotFound) }) mux.HandleFunc("/sub.example.org", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } rw.WriteHeader(http.StatusNotFound) }) mux.HandleFunc("/example.org", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } fixture := "./fixtures/domains.json" err := writeResponse(rw, fixture) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient("token") client.BaseURL = server.URL domain, err := client.GetDomainByName("sub.sub.example.org") require.NoError(t, err) expected := &Domain{ ID: 123, Name: "example.org", } assert.Equal(t, expected, domain) } func TestClient_AddRecord(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/123/records/", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } rec := Record{} err := json.NewDecoder(req.Body).Decode(&rec) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } rec.ID = 456 err = json.NewEncoder(rw).Encode(rec) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient("token") client.BaseURL = server.URL record, err := client.AddRecord(123, Record{ Name: "example.org", Type: "TXT", TTL: 60, Email: "email@example.org", Content: "txttxttxttxt", }) require.NoError(t, err) expected := &Record{ ID: 456, Name: "example.org", Type: "TXT", TTL: 60, Email: "email@example.org", Content: "txttxttxttxt", } assert.Equal(t, expected, record) } func TestClient_DeleteRecord(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodDelete { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } }) client := NewClient("token") client.BaseURL = server.URL err := client.DeleteRecord(123, 456) require.NoError(t, err) } func writeResponse(rw io.Writer, filename string) error { file, err := os.Open(filename) if err != nil { return err } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) return err } lego-4.9.1/providers/dns/internal/selectel/fixtures/000077500000000000000000000000001434020463500225425ustar00rootroot00000000000000lego-4.9.1/providers/dns/internal/selectel/fixtures/domains.json000066400000000000000000000000501434020463500250620ustar00rootroot00000000000000{ "id": 123, "name": "example.org" }lego-4.9.1/providers/dns/internal/selectel/fixtures/error.json000066400000000000000000000001401434020463500245610ustar00rootroot00000000000000{ "error": "error description", "code": 400, "field": "field that the error occurred in" }lego-4.9.1/providers/dns/internal/selectel/fixtures/list_records.json000066400000000000000000000006771434020463500261430ustar00rootroot00000000000000[ { "id": 123, "name": "example.com", "type": "TXT", "ttl": 60, "email": "email@example.com", "content": "txttxttxtA" }, { "id": 1234, "name": "example.org", "type": "TXT", "ttl": 60, "email": "email@example.org", "content": "txttxttxtB" }, { "id": 12345, "name": "example.net", "type": "TXT", "ttl": 60, "email": "email@example.net", "content": "txttxttxtC" } ] lego-4.9.1/providers/dns/internal/selectel/models.go000066400000000000000000000015601434020463500225050ustar00rootroot00000000000000package selectel import "fmt" // Domain represents domain name. type Domain struct { ID int `json:"id,omitempty"` Name string `json:"name,omitempty"` } // Record represents DNS record. type Record struct { ID int `json:"id,omitempty"` Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` // Record type (SOA, NS, A/AAAA, CNAME, SRV, MX, TXT, SPF) TTL int `json:"ttl,omitempty"` Email string `json:"email,omitempty"` // Email of domain's admin (only for SOA records) Content string `json:"content,omitempty"` // Record content (not for SRV) } // APIError API error message. type APIError struct { Description string `json:"error"` Code int `json:"code"` Field string `json:"field"` } func (a APIError) Error() string { return fmt.Sprintf("API error: %d - %s - %s", a.Code, a.Description, a.Field) } lego-4.9.1/providers/dns/internetbs/000077500000000000000000000000001434020463500174325ustar00rootroot00000000000000lego-4.9.1/providers/dns/internetbs/internal/000077500000000000000000000000001434020463500212465ustar00rootroot00000000000000lego-4.9.1/providers/dns/internetbs/internal/client.go000066400000000000000000000057021434020463500230570ustar00rootroot00000000000000package internal import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "path" "path/filepath" "strings" "time" "unicode" querystring "github.com/google/go-querystring/query" ) const baseURL = "https://api.internet.bs" // status SUCCESS, PENDING, FAILURE. const statusSuccess = "SUCCESS" // Client is the API client. type Client struct { HTTPClient *http.Client baseURL *url.URL debug bool apiKey string password string } // NewClient creates a new Client. func NewClient(apiKey string, password string) *Client { baseURL, _ := url.Parse(baseURL) return &Client{ HTTPClient: &http.Client{Timeout: 10 * time.Second}, baseURL: baseURL, apiKey: apiKey, password: password, } } // AddRecord The command is intended to add a new DNS record to a specific zone (domain). func (c Client) AddRecord(query RecordQuery) error { var r APIResponse err := c.do("Add", query, &r) if err != nil { return err } if r.Status != statusSuccess { return r } return nil } // RemoveRecord The command is intended to remove a DNS record from a specific zone. func (c Client) RemoveRecord(query RecordQuery) error { var r APIResponse err := c.do("Remove", query, &r) if err != nil { return err } if r.Status != statusSuccess { return r } return nil } // ListRecords The command is intended to retrieve the list of DNS records for a specific domain. func (c Client) ListRecords(query ListRecordQuery) ([]Record, error) { var l ListResponse err := c.do("List", query, &l) if err != nil { return nil, err } if l.Status != statusSuccess { return nil, l.APIResponse } return l.Records, nil } func (c Client) do(action string, params interface{}, response interface{}) error { endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "Domain", "DnsRecord", action)) if err != nil { return fmt.Errorf("create endpoint: %w", err) } values, err := querystring.Values(params) if err != nil { return fmt.Errorf("parse query parameters: %w", err) } values.Set("apiKey", c.apiKey) values.Set("password", c.password) values.Set("ResponseFormat", "JSON") resp, err := c.HTTPClient.PostForm(endpoint.String(), values) if err != nil { return fmt.Errorf("post request: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode/100 != 2 { data, _ := io.ReadAll(resp.Body) return fmt.Errorf("status code: %d, %s", resp.StatusCode, string(data)) } if c.debug { return dump(endpoint, resp, response) } return json.NewDecoder(resp.Body).Decode(response) } func dump(endpoint *url.URL, resp *http.Response, response interface{}) error { data, err := io.ReadAll(resp.Body) if err != nil { return err } fields := strings.FieldsFunc(endpoint.Path, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) }) err = os.WriteFile(filepath.Join("fixtures", strings.Join(fields, "_")+".json"), data, 0o666) if err != nil { return err } return json.Unmarshal(data, response) } lego-4.9.1/providers/dns/internetbs/internal/client_test.go000066400000000000000000000132061434020463500241140ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "strconv" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const testBaseURL = "https://testapi.internet.bs" const ( testAPIKey = "testapi" testPassword = "testpass" ) func TestClient_AddRecord(t *testing.T) { client := setupTest(t, "/Domain/DnsRecord/Add", "./fixtures/Domain_DnsRecord_Add_SUCCESS.json") query := RecordQuery{ FullRecordName: "www.example.com", Type: "TXT", Value: "xxx", TTL: 36000, } err := client.AddRecord(query) require.NoError(t, err) } func TestClient_AddRecord_error(t *testing.T) { client := setupTest(t, "/Domain/DnsRecord/Add", "./fixtures/Domain_DnsRecord_Add_FAILURE.json") query := RecordQuery{ FullRecordName: "www.example.com.", Type: "TXT", Value: "xxx", TTL: 36000, } err := client.AddRecord(query) require.Error(t, err) } func TestClient_AddRecord_integration(t *testing.T) { env, ok := os.LookupEnv("INTERNET_BS_DEBUG") if !ok { t.Skip("skip integration test") } client := NewClient(testAPIKey, testPassword) client.baseURL, _ = url.Parse(testBaseURL) client.debug, _ = strconv.ParseBool(env) query := RecordQuery{ FullRecordName: "www.example.com", Type: "TXT", Value: "xxx", TTL: 36000, } err := client.AddRecord(query) require.NoError(t, err) query = RecordQuery{ FullRecordName: "www.example.com", Type: "TXT", Value: "yyy", TTL: 36000, } err = client.AddRecord(query) require.NoError(t, err) } func TestClient_RemoveRecord(t *testing.T) { client := setupTest(t, "/Domain/DnsRecord/Remove", "./fixtures/Domain_DnsRecord_Remove_SUCCESS.json") query := RecordQuery{ FullRecordName: "www.example.com", Type: "TXT", Value: "", } err := client.RemoveRecord(query) require.NoError(t, err) } func TestClient_RemoveRecord_error(t *testing.T) { client := setupTest(t, "/Domain/DnsRecord/Remove", "./fixtures/Domain_DnsRecord_Remove_FAILURE.json") query := RecordQuery{ FullRecordName: "www.example.com.", Type: "TXT", Value: "", } err := client.RemoveRecord(query) require.Error(t, err) } func TestClient_RemoveRecord_integration(t *testing.T) { env, ok := os.LookupEnv("INTERNET_BS_DEBUG") if !ok { t.Skip("skip integration test") } client := NewClient(testAPIKey, testPassword) client.baseURL, _ = url.Parse(testBaseURL) client.debug, _ = strconv.ParseBool(env) query := RecordQuery{ FullRecordName: "www.example.com", Type: "TXT", Value: "", } err := client.RemoveRecord(query) require.NoError(t, err) } func TestClient_ListRecords(t *testing.T) { client := setupTest(t, "/Domain/DnsRecord/List", "./fixtures/Domain_DnsRecord_List_SUCCESS.json") query := ListRecordQuery{ Domain: "example.com", } records, err := client.ListRecords(query) require.NoError(t, err) expected := []Record{ { Name: "example.com", Value: "ns-hongkong.internet.bs", TTL: 3600, Type: "NS", }, { Name: "example.com", Value: "ns-toronto.internet.bs", TTL: 3600, Type: "NS", }, { Name: "example.com", Value: "ns-london.internet.bs", TTL: 3600, Type: "NS", }, { Name: "test.example.com", Value: "example1.com", TTL: 3600, Type: "CNAME", }, { Name: "www.example.com", Value: "xxx", TTL: 36000, Type: "TXT", }, { Name: "www.example.com", Value: "yyy", TTL: 36000, Type: "TXT", }, } assert.Equal(t, expected, records) } func TestClient_ListRecords_error(t *testing.T) { client := setupTest(t, "/Domain/DnsRecord/List", "./fixtures/Domain_DnsRecord_List_FAILURE.json") query := ListRecordQuery{ Domain: "www.example.com", } _, err := client.ListRecords(query) require.Error(t, err) } func TestClient_ListRecords_integration(t *testing.T) { env, ok := os.LookupEnv("INTERNET_BS_DEBUG") if !ok { t.Skip("skip integration test") } client := NewClient(testAPIKey, testPassword) client.baseURL, _ = url.Parse(testBaseURL) client.debug, _ = strconv.ParseBool(env) query := ListRecordQuery{ Domain: "example.com", } records, err := client.ListRecords(query) require.NoError(t, err) for _, record := range records { fmt.Println(record) } } func setupTest(t *testing.T, path, filename string) *Client { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc(path, testHandler(filename)) client := NewClient(testAPIKey, testPassword) client.baseURL, _ = url.Parse(server.URL) return client } func testHandler(filename string) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } if req.FormValue("apiKey") != testAPIKey { http.Error(rw, `{"transactid":"d46d812569acdb8b39c3933ec4351e79","status":"FAILURE","message":"Invalid API key and\/or Password","code":107002}`, http.StatusOK) return } if req.FormValue("password") != testPassword { http.Error(rw, `{"transactid":"d46d812569acdb8b39c3933ec4351e79","status":"FAILURE","message":"Invalid API key and\/or Password","code":107002}`, http.StatusOK) return } file, err := os.Open(filename) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } } } lego-4.9.1/providers/dns/internetbs/internal/fixtures/000077500000000000000000000000001434020463500231175ustar00rootroot00000000000000lego-4.9.1/providers/dns/internetbs/internal/fixtures/Domain_DnsRecord_Add_FAILURE.json000066400000000000000000000002651434020463500311060ustar00rootroot00000000000000{ "transactid": "67e4689073df2f153e7184aeb47a98f9", "status": "FAILURE", "message": "Invalid value \"www.example.com.\" for parameter \"fullrecordname\"!", "code": 100002 } lego-4.9.1/providers/dns/internetbs/internal/fixtures/Domain_DnsRecord_Add_SUCCESS.json000066400000000000000000000001161434020463500311220ustar00rootroot00000000000000{ "transactid": "548e3298130b492de23258634fd74481", "status": "SUCCESS" } lego-4.9.1/providers/dns/internetbs/internal/fixtures/Domain_DnsRecord_List_FAILURE.json000066400000000000000000000002601434020463500313240ustar00rootroot00000000000000{ "transactid": "5d554e0a5d145feb316b1805aae50706", "status": "FAILURE", "message": "The domain www.example.com does not have a supported extension!", "code": 100004 } lego-4.9.1/providers/dns/internetbs/internal/fixtures/Domain_DnsRecord_List_SUCCESS.json000066400000000000000000000014611434020463500313510ustar00rootroot00000000000000{ "transactid": "3d161c37da7c824c8b3463b25f461df0", "status": "SUCCESS", "total_records": 6, "records": [ { "name": "example.com", "value": "ns-hongkong.internet.bs", "ttl": 3600, "type": "NS" }, { "name": "example.com", "value": "ns-toronto.internet.bs", "ttl": 3600, "type": "NS" }, { "name": "example.com", "value": "ns-london.internet.bs", "ttl": 3600, "type": "NS" }, { "name": "test.example.com", "value": "example1.com", "ttl": 3600, "type": "CNAME" }, { "name": "www.example.com", "value": "xxx", "ttl": 36000, "type": "TXT" }, { "name": "www.example.com", "value": "yyy", "ttl": 36000, "type": "TXT" } ] } lego-4.9.1/providers/dns/internetbs/internal/fixtures/Domain_DnsRecord_Remove_SUCCESS.json000066400000000000000000000001161434020463500316670ustar00rootroot00000000000000{ "transactid": "221a0fe572f0505194214405f395a847", "status": "SUCCESS" } lego-4.9.1/providers/dns/internetbs/internal/types.go000066400000000000000000000017361434020463500227500ustar00rootroot00000000000000package internal import "fmt" type APIResponse struct { TransactID string `json:"transactid"` Status string `json:"status"` Message string `json:"message,omitempty"` Code int `json:"code,omitempty"` } func (a APIResponse) Error() string { return fmt.Sprintf("%s(%d): %s (%s)", a.Status, a.Code, a.Message, a.TransactID) } type ListResponse struct { APIResponse TotalRecords int `json:"total_records,omitempty"` Records []Record `json:"records,omitempty"` } type Record struct { Name string `json:"name,omitempty"` Value string `json:"value,omitempty"` TTL int `json:"ttl,omitempty"` Type string `json:"type,omitempty"` } type RecordQuery struct { FullRecordName string `url:"fullrecordname"` Type string `url:"type"` Value string `url:"value,omitempty"` TTL int `url:"ttl,omitempty"` } type ListRecordQuery struct { Domain string `url:"Domain"` FilterType string `url:"FilterType,omitempty"` } lego-4.9.1/providers/dns/internetbs/internetbs.go000066400000000000000000000074111434020463500221410ustar00rootroot00000000000000// Package internetbs implements a DNS provider for solving the DNS-01 challenge using internet.bs. package internetbs import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/internetbs/internal" ) // Environment variables names. const ( envNamespace = "INTERNET_BS_" EnvAPIKey = envNamespace + "API_KEY" EnvPassword = envNamespace + "PASSWORD" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string Password string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 3600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for internet.bs. // Credentials must be passed in the environment variables: INTERNET_BS_API_KEY, INTERNET_BS_PASSWORD. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey, EnvPassword) if err != nil { return nil, fmt.Errorf("internetbs: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] config.Password = values[EnvPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for internet.bs. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("internetbs: the configuration of the DNS provider is nil") } if config.APIKey == "" || config.Password == "" { return nil, errors.New("internetbs: missing credentials") } client := internal.NewClient(config.APIKey, config.Password) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) query := internal.RecordQuery{ FullRecordName: dns01.UnFqdn(fqdn), Type: "TXT", Value: value, TTL: d.config.TTL, } err := d.client.AddRecord(query) if err != nil { return fmt.Errorf("internetbs: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) query := internal.RecordQuery{ FullRecordName: dns01.UnFqdn(fqdn), Type: "TXT", Value: value, TTL: d.config.TTL, } err := d.client.RemoveRecord(query) if err != nil { return fmt.Errorf("internetbs: %w", err) } return nil } lego-4.9.1/providers/dns/internetbs/internetbs.toml000066400000000000000000000014421434020463500225050ustar00rootroot00000000000000Name = "Internet.bs" Description = '''''' URL = "https://internetbs.net" Code = "internetbs" Since = "v4.5.0" Example = ''' INTERNET_BS_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx \ INTERNET_BS_PASSWORD=yyyyyyyyyyyyyyyyyyyyyyyyyy \ lego --email you@example.com --dns internetbs --domains my.example.org run ''' [Configuration] [Configuration.Credentials] INTERNET_BS_API_KEY = "API key" INTERNET_BS_PASSWORD = "API password" [Configuration.Additional] INTERNET_BS_POLLING_INTERVAL = "Time between DNS propagation check" INTERNET_BS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" INTERNET_BS_TTL = "The TTL of the TXT record used for the DNS challenge" INTERNET_BS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://internetbs.net/internet-bs-api.pdf" lego-4.9.1/providers/dns/internetbs/internetbs_test.go000066400000000000000000000057421434020463500232050ustar00rootroot00000000000000package internetbs import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAPIKey, EnvPassword).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "user", EnvPassword: "secret", }, }, { desc: "missing API key", envVars: map[string]string{ EnvPassword: "secret", }, expected: "internetbs: some credentials information are missing: INTERNET_BS_API_KEY", }, { desc: "missing password", envVars: map[string]string{ EnvAPIKey: "user", }, expected: "internetbs: some credentials information are missing: INTERNET_BS_PASSWORD", }, { desc: "missing credentials", envVars: map[string]string{}, expected: "internetbs: some credentials information are missing: INTERNET_BS_API_KEY,INTERNET_BS_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string password string expected string }{ { desc: "success", apiKey: "user", password: "secret", }, { desc: "missing API key", expected: "internetbs: missing credentials", password: "secret", }, { desc: "missing password", expected: "internetbs: missing credentials", apiKey: "user", }, { desc: "missing credentials", expected: "internetbs: missing credentials", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.Password = test.password p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/inwx/000077500000000000000000000000001434020463500162425ustar00rootroot00000000000000lego-4.9.1/providers/dns/inwx/inwx.go000066400000000000000000000126671434020463500175720ustar00rootroot00000000000000// Package inwx implements a DNS provider for solving the DNS-01 challenge using inwx dom robot package inwx import ( "errors" "fmt" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/nrdcg/goinwx" "github.com/pquerna/otp/totp" ) // Environment variables names. const ( envNamespace = "INWX_" EnvUsername = envNamespace + "USERNAME" EnvPassword = envNamespace + "PASSWORD" EnvSharedSecret = envNamespace + "SHARED_SECRET" EnvSandbox = envNamespace + "SANDBOX" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Username string Password string SharedSecret string Sandbox bool PropagationTimeout time.Duration PollingInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 300), // INWX has rather unstable propagation delays, thus using a larger default value PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 360*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), Sandbox: env.GetOrDefaultBool(EnvSandbox, false), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *goinwx.Client } // NewDNSProvider returns a DNSProvider instance configured for Dyn DNS. // Credentials must be passed in the environment variables: // INWX_USERNAME, INWX_PASSWORD, and INWX_SHARED_SECRET. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvUsername, EnvPassword) if err != nil { return nil, fmt.Errorf("inwx: %w", err) } config := NewDefaultConfig() config.Username = values[EnvUsername] config.Password = values[EnvPassword] config.SharedSecret = env.GetOrFile(EnvSharedSecret) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Dyn DNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("inwx: the configuration of the DNS provider is nil") } if config.Username == "" || config.Password == "" { return nil, errors.New("inwx: credentials missing") } if config.Sandbox { log.Infof("inwx: sandbox mode is enabled") } client := goinwx.NewClient(config.Username, config.Password, &goinwx.ClientOptions{Sandbox: config.Sandbox}) return &DNSProvider{config: config, client: client}, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("inwx: %w", err) } info, err := d.client.Account.Login() if err != nil { return fmt.Errorf("inwx: %w", err) } defer func() { errL := d.client.Account.Logout() if errL != nil { log.Infof("inwx: failed to logout: %v", errL) } }() err = d.twoFactorAuth(info) if err != nil { return fmt.Errorf("inwx: %w", err) } request := &goinwx.NameserverRecordRequest{ Domain: dns01.UnFqdn(authZone), Name: dns01.UnFqdn(fqdn), Type: "TXT", Content: value, TTL: d.config.TTL, } _, err = d.client.Nameservers.CreateRecord(request) if err != nil { var er *goinwx.ErrorResponse if errors.As(err, &er) { if er.Message == "Object exists" { return nil } return fmt.Errorf("inwx: %w", err) } return fmt.Errorf("inwx: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("inwx: %w", err) } info, err := d.client.Account.Login() if err != nil { return fmt.Errorf("inwx: %w", err) } defer func() { errL := d.client.Account.Logout() if errL != nil { log.Infof("inwx: failed to logout: %v", errL) } }() err = d.twoFactorAuth(info) if err != nil { return fmt.Errorf("inwx: %w", err) } response, err := d.client.Nameservers.Info(&goinwx.NameserverInfoRequest{ Domain: dns01.UnFqdn(authZone), Name: dns01.UnFqdn(fqdn), Type: "TXT", }) if err != nil { return fmt.Errorf("inwx: %w", err) } var lastErr error for _, record := range response.Records { err = d.client.Nameservers.DeleteRecord(record.ID) if err != nil { lastErr = fmt.Errorf("inwx: %w", err) } } return lastErr } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func (d *DNSProvider) twoFactorAuth(info *goinwx.LoginResponse) error { if info.TFA != "GOOGLE-AUTH" { return nil } if d.config.SharedSecret == "" { return errors.New("two factor authentication but no shared secret is given") } tan, err := totp.GenerateCode(d.config.SharedSecret, time.Now()) if err != nil { return err } return d.client.Account.Unlock(tan) } lego-4.9.1/providers/dns/inwx/inwx.toml000066400000000000000000000017141434020463500201270ustar00rootroot00000000000000Name = "INWX" Description = '''''' URL = "https://www.inwx.de/en" Code = "inwx" Since = "v2.0.0" Example = ''' INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ lego --email you@example.com --dns inwx --domains my.example.org run # 2FA INWX_USERNAME=xxxxxxxxxx \ INWX_PASSWORD=yyyyyyyyyy \ INWX_SHARED_SECRET=zzzzzzzzzz \ lego --email you@example.com --dns inwx --domains my.example.org run ''' [Configuration] [Configuration.Credentials] INWX_USERNAME = "Username" INWX_PASSWORD = "Password" [Configuration.Additional] INWX_SHARED_SECRET = "shared secret related to 2FA" INWX_POLLING_INTERVAL = "Time between DNS propagation check" INWX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation (default 360s)" INWX_TTL = "The TTL of the TXT record used for the DNS challenge" INWX_SANDBOX = "Activate the sandbox (boolean)" [Links] API = "https://www.inwx.de/en/help/apidoc" GoClient = "https://github.com/nrdcg/goinwx" lego-4.9.1/providers/dns/inwx/inwx_test.go000066400000000000000000000057471434020463500206320ustar00rootroot00000000000000package inwx import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvUsername, EnvPassword, EnvSharedSecret, EnvSandbox, EnvTTL). WithDomain(envDomain). WithLiveTestRequirements(EnvUsername, EnvPassword, envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvUsername: "123", EnvPassword: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvUsername: "", EnvPassword: "", }, expected: "inwx: some credentials information are missing: INWX_USERNAME,INWX_PASSWORD", }, { desc: "missing username", envVars: map[string]string{ EnvUsername: "", EnvPassword: "456", }, expected: "inwx: some credentials information are missing: INWX_USERNAME", }, { desc: "missing password", envVars: map[string]string{ EnvUsername: "123", EnvPassword: "", }, expected: "inwx: some credentials information are missing: INWX_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string username string password string expected string }{ { desc: "success", username: "123", password: "456", }, { desc: "missing credentials", expected: "inwx: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Username = test.username config.Password = test.password p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresentAndCleanup(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() envTest.Apply(map[string]string{ EnvSandbox: "true", EnvTTL: "3600", // In sandbox mode, the minimum allowed TTL is 3600 }) defer envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) // Verify that no error is thrown if record already exists err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/ionos/000077500000000000000000000000001434020463500164045ustar00rootroot00000000000000lego-4.9.1/providers/dns/ionos/internal/000077500000000000000000000000001434020463500202205ustar00rootroot00000000000000lego-4.9.1/providers/dns/ionos/internal/client.go000066400000000000000000000103661434020463500220330ustar00rootroot00000000000000package internal import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "path" querystring "github.com/google/go-querystring/query" ) // defaultBaseURL represents the API endpoint to call. const defaultBaseURL = "https://api.hosting.ionos.com/dns" // Client Ionos API client. type Client struct { HTTPClient *http.Client BaseURL *url.URL apiKey string } // NewClient creates a new Client. func NewClient(apiKey string) (*Client, error) { baseURL, err := url.Parse(defaultBaseURL) if err != nil { return nil, err } return &Client{ HTTPClient: http.DefaultClient, BaseURL: baseURL, apiKey: apiKey, }, nil } // ListZones gets all zones. func (c *Client) ListZones(ctx context.Context) ([]Zone, error) { req, err := c.makeRequest(ctx, http.MethodGet, "/v1/zones", nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to call API: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return nil, readError(resp.Body, resp.StatusCode) } var zones []Zone err = json.NewDecoder(resp.Body).Decode(&zones) if err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } return zones, nil } // ReplaceRecords replaces the some records of a zones. func (c *Client) ReplaceRecords(ctx context.Context, zoneID string, records []Record) error { body, err := json.Marshal(records) if err != nil { return fmt.Errorf("failed to marshal request body: %w", err) } req, err := c.makeRequest(ctx, http.MethodPatch, path.Join("/v1/zones", zoneID), bytes.NewReader(body)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } resp, err := c.HTTPClient.Do(req) if err != nil { return fmt.Errorf("failed to call API: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return readError(resp.Body, resp.StatusCode) } return nil } // GetRecords gets the records of a zones. func (c *Client) GetRecords(ctx context.Context, zoneID string, filter *RecordsFilter) ([]Record, error) { req, err := c.makeRequest(ctx, http.MethodGet, path.Join("/v1/zones", zoneID), nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } if filter != nil { v, errQ := querystring.Values(filter) if errQ != nil { return nil, errQ } req.URL.RawQuery = v.Encode() } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to call API: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return nil, readError(resp.Body, resp.StatusCode) } var zone CustomerZone err = json.NewDecoder(resp.Body).Decode(&zone) if err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } return zone.Records, nil } // RemoveRecord removes a record. func (c *Client) RemoveRecord(ctx context.Context, zoneID, recordID string) error { req, err := c.makeRequest(ctx, http.MethodDelete, path.Join("/v1/zones", zoneID, "records", recordID), nil) if err != nil { return fmt.Errorf("failed to create request: %w", err) } resp, err := c.HTTPClient.Do(req) if err != nil { return fmt.Errorf("failed to call API: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return readError(resp.Body, resp.StatusCode) } return nil } func (c *Client) makeRequest(ctx context.Context, method, uri string, body io.Reader) (*http.Request, error) { endpoint, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, uri)) if err != nil { return nil, fmt.Errorf("failed to parse endpoint: %w", err) } req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.Header.Set("X-API-Key", c.apiKey) return req, nil } func readError(body io.Reader, statusCode int) error { bodyBytes, _ := io.ReadAll(body) cErr := &ClientError{StatusCode: statusCode} err := json.Unmarshal(bodyBytes, &cErr.errors) if err != nil { cErr.message = string(bodyBytes) return cErr } return cErr } lego-4.9.1/providers/dns/ionos/internal/client_test.go000066400000000000000000000110171434020463500230640ustar00rootroot00000000000000package internal import ( "context" "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "path" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_ListZones(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/v1/zones", mockHandler(http.MethodGet, http.StatusOK, "list_zones.json")) zones, err := client.ListZones(context.Background()) require.NoError(t, err) expected := []Zone{{ ID: "11af3414-ebba-11e9-8df5-66fbe8a334b4", Name: "test.com", Type: "NATIVE", }} assert.Equal(t, expected, zones) } func TestClient_ListZones_error(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/v1/zones", mockHandler(http.MethodGet, http.StatusUnauthorized, "list_zones_error.json")) zones, err := client.ListZones(context.Background()) require.Error(t, err) assert.Nil(t, zones) var cErr *ClientError assert.ErrorAs(t, err, &cErr) assert.Equal(t, http.StatusUnauthorized, cErr.StatusCode) } func TestClient_GetRecords(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/v1/zones/azone01", mockHandler(http.MethodGet, http.StatusOK, "get_records.json")) records, err := client.GetRecords(context.Background(), "azone01", nil) require.NoError(t, err) expected := []Record{{ ID: "22af3414-abbe-9e11-5df5-66fbe8e334b4", Name: "string", Content: "string", Type: "A", }} assert.Equal(t, expected, records) } func TestClient_GetRecords_error(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/v1/zones/azone01", mockHandler(http.MethodGet, http.StatusUnauthorized, "get_records_error.json")) records, err := client.GetRecords(context.Background(), "azone01", nil) require.Error(t, err) assert.Nil(t, records) var cErr *ClientError assert.ErrorAs(t, err, &cErr) assert.Equal(t, http.StatusUnauthorized, cErr.StatusCode) } func TestClient_RemoveRecord(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/v1/zones/azone01/records/arecord01", mockHandler(http.MethodDelete, http.StatusOK, "")) err := client.RemoveRecord(context.Background(), "azone01", "arecord01") require.NoError(t, err) } func TestClient_RemoveRecord_error(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/v1/zones/azone01/records/arecord01", mockHandler(http.MethodDelete, http.StatusInternalServerError, "remove_record_error.json")) err := client.RemoveRecord(context.Background(), "azone01", "arecord01") require.Error(t, err) var cErr *ClientError assert.ErrorAs(t, err, &cErr) assert.Equal(t, http.StatusInternalServerError, cErr.StatusCode) } func TestClient_ReplaceRecords(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/v1/zones/azone01", mockHandler(http.MethodPatch, http.StatusOK, "")) records := []Record{{ ID: "22af3414-abbe-9e11-5df5-66fbe8e334b4", Name: "string", Content: "string", Type: "A", }} err := client.ReplaceRecords(context.Background(), "azone01", records) require.NoError(t, err) } func TestClient_ReplaceRecords_error(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/v1/zones/azone01", mockHandler(http.MethodPatch, http.StatusBadRequest, "replace_records_error.json")) records := []Record{{ ID: "22af3414-abbe-9e11-5df5-66fbe8e334b4", Name: "string", Content: "string", Type: "A", }} err := client.ReplaceRecords(context.Background(), "azone01", records) require.Error(t, err) var cErr *ClientError assert.ErrorAs(t, err, &cErr) assert.Equal(t, http.StatusBadRequest, cErr.StatusCode) } func setupTest(t *testing.T) (*http.ServeMux, *Client) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client, err := NewClient("secret") require.NoError(t, err) client.BaseURL, _ = url.Parse(server.URL) return mux, client } func mockHandler(method string, statusCode int, filename string) func(http.ResponseWriter, *http.Request) { return func(rw http.ResponseWriter, req *http.Request) { if req.Method != method { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } if filename == "" { rw.WriteHeader(statusCode) return } file, err := os.Open(filepath.FromSlash(path.Join("./fixtures", filename))) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() rw.WriteHeader(statusCode) _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } } } lego-4.9.1/providers/dns/ionos/internal/fixtures/000077500000000000000000000000001434020463500220715ustar00rootroot00000000000000lego-4.9.1/providers/dns/ionos/internal/fixtures/get_records.json000066400000000000000000000005611434020463500252660ustar00rootroot00000000000000{ "id": "11af3414-ebba-11e9-8df5-66fbe8a334b4", "name": "example-zone.de", "type": "NATIVE", "records": [ { "id": "22af3414-abbe-9e11-5df5-66fbe8e334b4", "name": "string", "rootName": "string", "type": "A", "content": "string", "changeDate": "string", "ttl": 0, "prio": 0, "disabled": false } ] } lego-4.9.1/providers/dns/ionos/internal/fixtures/get_records_error.json000066400000000000000000000001561434020463500264770ustar00rootroot00000000000000[ { "code": "UNAUTHORIZED", "message": "The customer is not authorized to do this operation." } ] lego-4.9.1/providers/dns/ionos/internal/fixtures/list_zones.json000066400000000000000000000001531434020463500251540ustar00rootroot00000000000000[ { "id": "11af3414-ebba-11e9-8df5-66fbe8a334b4", "name": "test.com", "type": "NATIVE" } ] lego-4.9.1/providers/dns/ionos/internal/fixtures/list_zones_error.json000066400000000000000000000001561434020463500263700ustar00rootroot00000000000000[ { "code": "UNAUTHORIZED", "message": "The customer is not authorized to do this operation." } ] lego-4.9.1/providers/dns/ionos/internal/fixtures/remove_record_error.json000066400000000000000000000003541434020463500270320ustar00rootroot00000000000000[ { "code": "UNAUTHORIZED", "message": "The customer is not authorized to do this operation." }, { "code": "RECORD_NOT_FOUND", "message": "Record does not exist." }, { "code": "INTERNAL_SERVER_ERROR" } ] lego-4.9.1/providers/dns/ionos/internal/fixtures/replace_records_error.json000066400000000000000000000012101434020463500273230ustar00rootroot00000000000000[ { "code": "INVALID_RECORD", "message": "string", "parameters": { "errorRecord": { "id": "string", "name": "string", "disabled": false, "rootName": "string", "changeDate": "string", "type": "A", "content": "string", "ttl": 0, "prio": 0 }, "requiredFields": [ "string" ], "invalid": [ "string" ], "invalidFields": [ "string" ] } }, { "code": "UNAUTHORIZED", "message": "The customer is not authorized to do this operation." }, { "code": "INTERNAL_SERVER_ERROR" } ]lego-4.9.1/providers/dns/ionos/internal/types.go000066400000000000000000000041771434020463500217240ustar00rootroot00000000000000package internal import ( "fmt" "strconv" ) // ClientError a detailed error. type ClientError struct { errors []Error StatusCode int message string } func (f ClientError) Error() string { msg := strconv.Itoa(f.StatusCode) + ": " if f.message != "" { msg += f.message + ": " } for i, e := range f.errors { if i != 0 { msg += ", " } msg += e.Error() } return msg } func (f ClientError) Unwrap() error { if len(f.errors) == 0 { return nil } return &f.errors[0] } // Error defines model for error. type Error struct { // The error code. Code string `json:"code,omitempty"` // The error message. Message string `json:"message,omitempty"` } func (e Error) Error() string { return fmt.Sprintf("%s: %s", e.Code, e.Message) } // Zone defines model for zone. type Zone struct { // The zone id. ID string `json:"id,omitempty"` // The zone name. Name string `json:"name,omitempty"` // Represents the possible zone types. Type string `json:"type,omitempty"` } // CustomerZone defines model for customer-zone. type CustomerZone struct { // The zone id. ID string `json:"id,omitempty"` // The zone name Name string `json:"name,omitempty"` Records []Record `json:"records,omitempty"` // Represents the possible zone types. Type string `json:"type,omitempty"` } // Record defines model for record. type Record struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` Content string `json:"content,omitempty"` // Time to live for the record, recommended 3600. TTL int `json:"ttl,omitempty"` // Holds supported dns record types. Type string `json:"type,omitempty"` Priority int `json:"prio,omitempty"` // When is true, the record is not visible for lookup. Disabled bool `json:"disabled,omitempty"` } type RecordsFilter struct { // The FQDN used to filter all the record names that end with it. Suffix string `url:"suffix,omitempty"` // The record names that should be included (same as name field of Record) RecordName string `url:"recordName,omitempty"` // A comma-separated list of record types that should be included RecordType string `url:"recordType,omitempty"` } lego-4.9.1/providers/dns/ionos/ionos.go000066400000000000000000000124521434020463500200660ustar00rootroot00000000000000// Package ionos implements a DNS provider for solving the DNS-01 challenge using Ionos/1&1. package ionos import ( "context" "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/ionos/internal" ) const minTTL = 300 // Environment variables names. const ( envNamespace = "IONOS_" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for Ionos. // Credentials must be passed in the environment variables: IONOS_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("ionos: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Ionos. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("ionos: the configuration of the DNS provider is nil") } if config.APIKey == "" { return nil, errors.New("ionos: credentials missing") } if config.TTL < minTTL { return nil, fmt.Errorf("ionos: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } client, err := internal.NewClient(config.APIKey) if err != nil { return nil, fmt.Errorf("ionos: %w", err) } if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, _, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) ctx := context.Background() zones, err := d.client.ListZones(ctx) if err != nil { return fmt.Errorf("ionos: failed to get zones: %w", err) } // TODO(ldez) replace domain by FQDN to follow CNAME. zone := findZone(zones, domain) if zone == nil { return errors.New("ionos: no matching zone found for domain") } filter := &internal.RecordsFilter{ Suffix: dns01.UnFqdn(fqdn), RecordType: "TXT", } records, err := d.client.GetRecords(ctx, zone.ID, filter) if err != nil { return fmt.Errorf("ionos: failed to get records (zone=%s): %w", zone.ID, err) } records = append(records, internal.Record{ Name: dns01.UnFqdn(fqdn), Content: value, TTL: d.config.TTL, Type: "TXT", }) err = d.client.ReplaceRecords(ctx, zone.ID, records) if err != nil { return fmt.Errorf("ionos: failed to create/update records (zone=%s): %w", zone.ID, err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) ctx := context.Background() zones, err := d.client.ListZones(ctx) if err != nil { return fmt.Errorf("ionos: failed to get zones: %w", err) } // TODO(ldez) replace domain by FQDN to follow CNAME. zone := findZone(zones, domain) if zone == nil { return errors.New("ionos: no matching zone found for domain") } filter := &internal.RecordsFilter{ Suffix: dns01.UnFqdn(fqdn), RecordType: "TXT", } records, err := d.client.GetRecords(ctx, zone.ID, filter) if err != nil { return fmt.Errorf("ionos: failed to get records (zone=%s): %w", zone.ID, err) } for _, record := range records { if record.Name == dns01.UnFqdn(fqdn) && record.Content == value { err := d.client.RemoveRecord(ctx, zone.ID, record.ID) if err != nil { return fmt.Errorf("ionos: failed to remove record (zone=%s, record=%s): %w", zone.ID, record.ID, err) } return nil } } return nil } func findZone(zones []internal.Zone, domain string) *internal.Zone { var result *internal.Zone for _, zone := range zones { zone := zone if zone.Name != "" && strings.HasSuffix(domain, zone.Name) { if result == nil || len(zone.Name) > len(result.Name) { result = &zone } } } return result } lego-4.9.1/providers/dns/ionos/ionos.toml000066400000000000000000000013051434020463500204270ustar00rootroot00000000000000Name = "Ionos" Description = '''''' URL = "https://ionos.com" Code = "ionos" Since = "v4.2.0" Example = ''' IONOS_API_KEY=xxxxxxxx \ lego --email you@example.com --dns ionos --domains my.example.org run ''' [Configuration] [Configuration.Credentials] IONOS_API_KEY = "API key `.` https://developer.hosting.ionos.com/docs/getstarted" [Configuration.Additional] IONOS_POLLING_INTERVAL = "Time between DNS propagation check" IONOS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" IONOS_TTL = "The TTL of the TXT record used for the DNS challenge" IONOS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developer.hosting.ionos.com/docs/dns" lego-4.9.1/providers/dns/ionos/ionos_test.go000066400000000000000000000046611434020463500211300ustar00rootroot00000000000000package ionos import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIKey: "", }, expected: "ionos: some credentials information are missing: IONOS_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string tll int expected string }{ { desc: "success", apiKey: "123", tll: minTTL, }, { desc: "missing credentials", tll: minTTL, expected: "ionos: credentials missing", }, { desc: "invalid TTL", apiKey: "123", tll: 30, expected: "ionos: invalid TTL, TTL (30) must be greater than 300", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.TTL = test.tll p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/iwantmyname/000077500000000000000000000000001434020463500176065ustar00rootroot00000000000000lego-4.9.1/providers/dns/iwantmyname/internal/000077500000000000000000000000001434020463500214225ustar00rootroot00000000000000lego-4.9.1/providers/dns/iwantmyname/internal/client.go000066400000000000000000000027321434020463500232330ustar00rootroot00000000000000package internal import ( "context" "fmt" "io" "net/http" "net/url" "time" querystring "github.com/google/go-querystring/query" ) const defaultBaseURL = "https://iwantmyname.com/basicauth/ddns" // Record represents a record. type Record struct { Hostname string `url:"hostname,omitempty"` Type string `url:"type,omitempty"` Value string `url:"value,omitempty"` TTL int `url:"ttl,omitempty"` } // Client iwantmyname client. type Client struct { username string password string baseURL *url.URL HTTPClient *http.Client } // NewClient creates a new Client. func NewClient(username string, password string) *Client { baseURL, _ := url.Parse(defaultBaseURL) return &Client{ username: username, password: password, baseURL: baseURL, HTTPClient: &http.Client{Timeout: 10 * time.Second}, } } // Do send a request (create/add/delete) to the API. func (c Client) Do(ctx context.Context, record Record) error { values, err := querystring.Values(record) if err != nil { return err } endpoint := c.baseURL endpoint.RawQuery = values.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), http.NoBody) if err != nil { return err } req.SetBasicAuth(c.username, c.password) resp, err := c.HTTPClient.Do(req) if err != nil { return err } if resp.StatusCode/100 != 2 { data, _ := io.ReadAll(resp.Body) return fmt.Errorf("status code: %d, %s", resp.StatusCode, string(data)) } return nil } lego-4.9.1/providers/dns/iwantmyname/internal/client_test.go000066400000000000000000000032771434020463500242770ustar00rootroot00000000000000package internal import ( "context" "fmt" "net/http" "net/http/httptest" "net/url" "testing" "github.com/stretchr/testify/require" ) func checkParameter(query url.Values, key, expected string) error { if query.Get(key) != expected { return fmt.Errorf("%s: want %s got %s", key, expected, query.Get(key)) } return nil } func TestClient_Do(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) client := NewClient("user", "secret") client.HTTPClient = server.Client() client.baseURL, _ = url.Parse(server.URL) mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } username, password, ok := req.BasicAuth() if !ok { http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } if username != "user" { http.Error(rw, fmt.Sprintf("username: want %s got %s", username, "user"), http.StatusUnauthorized) return } if password != "secret" { http.Error(rw, fmt.Sprintf("password: want %s got %s", password, "secret"), http.StatusUnauthorized) return } query := req.URL.Query() values := map[string]string{ "hostname": "example.com", "type": "TXT", "value": "data", "ttl": "120", } for k, v := range values { err := checkParameter(query, k, v) if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } } }) record := Record{ Hostname: "example.com", Type: "TXT", Value: "data", TTL: 120, } err := client.Do(context.Background(), record) require.NoError(t, err) } lego-4.9.1/providers/dns/iwantmyname/iwantmyname.go000066400000000000000000000074201434020463500224710ustar00rootroot00000000000000// Package iwantmyname implements a DNS provider for solving the DNS-01 challenge using iwantmyname. package iwantmyname import ( "context" "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/iwantmyname/internal" ) // Environment variables names. const ( envNamespace = "IWANTMYNAME_" EnvUsername = envNamespace + "USERNAME" EnvPassword = envNamespace + "PASSWORD" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Username string Password string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for iwantmyname. // Credentials must be passed in the environment variables: IWANTMYNAME_USERNAME, IWANTMYNAME_PASSWORD. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvUsername, EnvPassword) if err != nil { return nil, fmt.Errorf("iwantmyname: %w", err) } config := NewDefaultConfig() config.Username = values[EnvUsername] config.Password = values[EnvPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for iwantmyname. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("iwantmyname: the configuration of the DNS provider is nil") } if config.Username == "" || config.Password == "" { return nil, errors.New("iwantmyname: credentials missing") } client := internal.NewClient(config.Username, config.Password) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, _, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) record := internal.Record{ Hostname: dns01.UnFqdn(fqdn), Type: "TXT", Value: value, TTL: d.config.TTL, } err := d.client.Do(context.Background(), record) if err != nil { return fmt.Errorf("iwantmyname: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) record := internal.Record{ Hostname: dns01.UnFqdn(fqdn), Type: "TXT", Value: "delete", TTL: d.config.TTL, } err := d.client.Do(context.Background(), record) if err != nil { return fmt.Errorf("iwantmyname: %w", err) } return nil } lego-4.9.1/providers/dns/iwantmyname/iwantmyname.toml000066400000000000000000000014161434020463500230360ustar00rootroot00000000000000Name = "iwantmyname" Description = '''''' URL = "https://iwantmyname.com" Code = "iwantmyname" Since = "v4.7.0" Example = ''' IWANTMYNAME_USERNAME=xxxxxxxx \ IWANTMYNAME_PASSWORD=xxxxxxxx \ lego --email you@example.com --dns iwantmyname --domains my.example.org run ''' [Configuration] [Configuration.Credentials] IWANTMYNAME_USERNAME = "API username" IWANTMYNAME_PASSWORD = "API password" [Configuration.Additional] IWANTMYNAME_POLLING_INTERVAL = "Time between DNS propagation check" IWANTMYNAME_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" IWANTMYNAME_TTL = "The TTL of the TXT record used for the DNS challenge" IWANTMYNAME_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://iwantmyname.com/developer/domain-dns-api" lego-4.9.1/providers/dns/iwantmyname/iwantmyname_test.go000066400000000000000000000057211434020463500235320ustar00rootroot00000000000000package iwantmyname import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvUsername, EnvPassword). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvUsername: "user", EnvPassword: "secret", }, }, { desc: "missing credentials", envVars: map[string]string{}, expected: "iwantmyname: some credentials information are missing: IWANTMYNAME_USERNAME,IWANTMYNAME_PASSWORD", }, { desc: "missing username", envVars: map[string]string{ EnvPassword: "secret", }, expected: "iwantmyname: some credentials information are missing: IWANTMYNAME_USERNAME", }, { desc: "missing password", envVars: map[string]string{ EnvUsername: "user", }, expected: "iwantmyname: some credentials information are missing: IWANTMYNAME_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string username string password string expected string }{ { desc: "success", username: "user", password: "secret", }, { desc: "missing credentials", expected: "iwantmyname: credentials missing", }, { desc: "missing username", password: "secret", expected: "iwantmyname: credentials missing", }, { desc: "missing password", username: "user", expected: "iwantmyname: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Username = test.username config.Password = test.password p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/joker/000077500000000000000000000000001434020463500163675ustar00rootroot00000000000000lego-4.9.1/providers/dns/joker/internal/000077500000000000000000000000001434020463500202035ustar00rootroot00000000000000lego-4.9.1/providers/dns/joker/internal/dmapi/000077500000000000000000000000001434020463500212755ustar00rootroot00000000000000lego-4.9.1/providers/dns/joker/internal/dmapi/client.go000066400000000000000000000125231434020463500231050ustar00rootroot00000000000000// Package dmapi Client for DMAPI joker.com. // https://joker.com/faq/category/39/22-dmapi.html package dmapi import ( "errors" "fmt" "io" "net/http" "net/url" "path" "strconv" "strings" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" ) const defaultBaseURL = "https://dmapi.joker.com/request/" // Response Joker DMAPI Response. type Response struct { Headers url.Values Body string StatusCode int StatusText string AuthSid string } type AuthInfo struct { APIKey string Username string Password string authSid string } // Client a DMAPI Client. type Client struct { HTTPClient *http.Client BaseURL string Debug bool auth AuthInfo } // NewClient creates a new DMAPI Client. func NewClient(auth AuthInfo) *Client { return &Client{ HTTPClient: http.DefaultClient, BaseURL: defaultBaseURL, Debug: false, auth: auth, } } // Login performs a login to Joker's DMAPI. func (c *Client) Login() (*Response, error) { if c.auth.authSid != "" { // already logged in return nil, nil } var values url.Values switch { case c.auth.Username != "" && c.auth.Password != "": values = url.Values{ "username": {c.auth.Username}, "password": {c.auth.Password}, } case c.auth.APIKey != "": values = url.Values{"api-key": {c.auth.APIKey}} default: return nil, errors.New("no username and password or api-key") } response, err := c.postRequest("login", values) if err != nil { return response, err } if response == nil { return nil, errors.New("login returned nil response") } if response.AuthSid == "" { return response, errors.New("login did not return valid Auth-Sid") } c.auth.authSid = response.AuthSid return response, nil } // Logout closes authenticated session with Joker's DMAPI. func (c *Client) Logout() (*Response, error) { if c.auth.authSid == "" { return nil, errors.New("already logged out") } response, err := c.postRequest("logout", url.Values{}) if err == nil { c.auth.authSid = "" } return response, err } // GetZone returns content of DNS zone for domain. func (c *Client) GetZone(domain string) (*Response, error) { if c.auth.authSid == "" { return nil, errors.New("must be logged in to get zone") } return c.postRequest("dns-zone-get", url.Values{"domain": {dns01.UnFqdn(domain)}}) } // PutZone uploads DNS zone to Joker DMAPI. func (c *Client) PutZone(domain, zone string) (*Response, error) { if c.auth.authSid == "" { return nil, errors.New("must be logged in to put zone") } return c.postRequest("dns-zone-put", url.Values{"domain": {dns01.UnFqdn(domain)}, "zone": {strings.TrimSpace(zone)}}) } // postRequest performs actual HTTP request. func (c *Client) postRequest(cmd string, data url.Values) (*Response, error) { baseURL, err := url.Parse(c.BaseURL) if err != nil { return nil, err } endpoint, err := baseURL.Parse(path.Join(baseURL.Path, cmd)) if err != nil { return nil, err } if c.auth.authSid != "" { data.Set("auth-sid", c.auth.authSid) } if c.Debug { log.Infof("postRequest:\n\tURL: %q\n\tData: %v", endpoint.String(), data) } resp, err := c.HTTPClient.PostForm(endpoint.String(), data) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP error %d [%s]: %v", resp.StatusCode, http.StatusText(resp.StatusCode), string(body)) } return parseResponse(string(body)), nil } // parseResponse parses HTTP response body. func parseResponse(message string) *Response { r := &Response{Headers: url.Values{}, StatusCode: -1} lines, body, _ := strings.Cut(message, "\n\n") for _, line := range strings.Split(lines, "\n") { if strings.TrimSpace(line) == "" { continue } k, v, _ := strings.Cut(line, ":") val := strings.TrimSpace(v) r.Headers.Add(k, val) switch k { case "Status-Code": i, err := strconv.Atoi(val) if err == nil { r.StatusCode = i } case "Status-Text": r.StatusText = val case "Auth-Sid": r.AuthSid = val } } r.Body = body return r } // Temporary workaround, until it get fixed on API side. func fixTxtLines(line string) string { fields := strings.Fields(line) if len(fields) < 6 || fields[1] != "TXT" { return line } if fields[3][0] == '"' && fields[4] == `"` { fields[3] = strings.TrimSpace(fields[3]) + `"` fields = append(fields[:4], fields[5:]...) } return strings.Join(fields, " ") } // RemoveTxtEntryFromZone clean-ups all TXT records with given name. func RemoveTxtEntryFromZone(zone, relative string) (string, bool) { prefix := fmt.Sprintf("%s TXT 0 ", relative) modified := false var zoneEntries []string for _, line := range strings.Split(zone, "\n") { if strings.HasPrefix(line, prefix) { modified = true continue } zoneEntries = append(zoneEntries, line) } return strings.TrimSpace(strings.Join(zoneEntries, "\n")), modified } // AddTxtEntryToZone returns DNS zone with added TXT record. func AddTxtEntryToZone(zone, relative, value string, ttl int) string { var zoneEntries []string for _, line := range strings.Split(zone, "\n") { zoneEntries = append(zoneEntries, fixTxtLines(line)) } newZoneEntry := fmt.Sprintf("%s TXT 0 %q %d", relative, value, ttl) zoneEntries = append(zoneEntries, newZoneEntry) return strings.TrimSpace(strings.Join(zoneEntries, "\n")) } lego-4.9.1/providers/dns/joker/internal/dmapi/client_test.go000066400000000000000000000307351434020463500241510ustar00rootroot00000000000000package dmapi import ( "io" "net/http" "net/http/httptest" "net/url" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( correctAPIKey = "123" incorrectAPIKey = "321" serverErrorAPIKey = "500" ) const ( correctUsername = "lego" incorrectUsername = "not_lego" serverErrorUsername = "error" ) func setup(t *testing.T) (*http.ServeMux, string) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) return mux, server.URL } func TestDNSProvider_login_api_key(t *testing.T) { testCases := []struct { desc string apiKey string expectedError bool expectedStatusCode int expectedAuthSid string }{ { desc: "correct key", apiKey: correctAPIKey, expectedStatusCode: 0, expectedAuthSid: correctAPIKey, }, { desc: "incorrect key", apiKey: incorrectAPIKey, expectedStatusCode: 2200, expectedError: true, }, { desc: "server error", apiKey: serverErrorAPIKey, expectedStatusCode: -500, expectedError: true, }, { desc: "non-ok status code", apiKey: "333", expectedStatusCode: 2202, expectedError: true, }, } mux, serverURL := setup(t) mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) switch r.FormValue("api-key") { case correctAPIKey: _, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: 123\n\ncom\nnet") case incorrectAPIKey: _, _ = io.WriteString(w, "Status-Code: 2200\nStatus-Text: Authentication error") case serverErrorAPIKey: http.NotFound(w, r) default: _, _ = io.WriteString(w, "Status-Code: 2202\nStatus-Text: OK\n\ncom\nnet") } }) for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { client := NewClient(AuthInfo{APIKey: test.apiKey}) client.BaseURL = serverURL response, err := client.Login() if test.expectedError { require.Error(t, err) } else { require.NoError(t, err) require.NotNil(t, response) assert.Equal(t, test.expectedStatusCode, response.StatusCode) assert.Equal(t, test.expectedAuthSid, response.AuthSid) } }) } } func TestDNSProvider_login_username(t *testing.T) { testCases := []struct { desc string username string password string expectedError bool expectedStatusCode int expectedAuthSid string }{ { desc: "correct username and password", username: correctUsername, password: "go-acme", expectedError: false, expectedStatusCode: 0, expectedAuthSid: correctAPIKey, }, { desc: "incorrect username", username: incorrectUsername, password: "go-acme", expectedStatusCode: 2200, expectedError: true, }, { desc: "server error", username: serverErrorUsername, password: "go-acme", expectedStatusCode: -500, expectedError: true, }, { desc: "non-ok status code", username: "random", password: "go-acme", expectedStatusCode: 2202, expectedError: true, }, } mux, serverURL := setup(t) mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) switch r.FormValue("username") { case correctUsername: _, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: 123\n\ncom\nnet") case incorrectUsername: _, _ = io.WriteString(w, "Status-Code: 2200\nStatus-Text: Authentication error") case serverErrorUsername: http.NotFound(w, r) default: _, _ = io.WriteString(w, "Status-Code: 2202\nStatus-Text: OK\n\ncom\nnet") } }) for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { client := NewClient(AuthInfo{Username: test.username, Password: test.password}) client.BaseURL = serverURL response, err := client.Login() if test.expectedError { require.Error(t, err) } else { require.NoError(t, err) require.NotNil(t, response) assert.Equal(t, test.expectedStatusCode, response.StatusCode) assert.Equal(t, test.expectedAuthSid, response.AuthSid) } }) } } func TestDNSProvider_logout(t *testing.T) { testCases := []struct { desc string authSid string expectedError bool expectedStatusCode int }{ { desc: "correct auth-sid", authSid: correctAPIKey, expectedStatusCode: 0, }, { desc: "incorrect auth-sid", authSid: incorrectAPIKey, expectedStatusCode: 2200, }, { desc: "already logged out", authSid: "", expectedError: true, }, { desc: "server error", authSid: serverErrorAPIKey, expectedError: true, }, } mux, serverURL := setup(t) mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) switch r.FormValue("auth-sid") { case correctAPIKey: _, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\n") case incorrectAPIKey: _, _ = io.WriteString(w, "Status-Code: 2200\nStatus-Text: Authentication error") default: http.NotFound(w, r) } }) for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { client := NewClient(AuthInfo{APIKey: "12345", authSid: test.authSid}) client.BaseURL = serverURL response, err := client.Logout() if test.expectedError { require.Error(t, err) } else { require.NoError(t, err) require.NotNil(t, response) assert.Equal(t, test.expectedStatusCode, response.StatusCode) } }) } } func TestDNSProvider_getZone(t *testing.T) { testZone := "@ A 0 192.0.2.2 3600" testCases := []struct { desc string authSid string domain string zone string expectedError bool expectedStatusCode int }{ { desc: "correct auth-sid, known domain", authSid: correctAPIKey, domain: "known", zone: testZone, expectedStatusCode: 0, }, { desc: "incorrect auth-sid, known domain", authSid: incorrectAPIKey, domain: "known", expectedStatusCode: 2202, }, { desc: "correct auth-sid, unknown domain", authSid: correctAPIKey, domain: "unknown", expectedStatusCode: 2202, }, { desc: "server error", authSid: "500", expectedError: true, }, } mux, serverURL := setup(t) mux.HandleFunc("/dns-zone-get", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) authSid := r.FormValue("auth-sid") domain := r.FormValue("domain") switch { case authSid == correctAPIKey && domain == "known": _, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\n\n"+testZone) case authSid == incorrectAPIKey || (authSid == correctAPIKey && domain == "unknown"): _, _ = io.WriteString(w, "Status-Code: 2202\nStatus-Text: Authorization error") default: http.NotFound(w, r) } }) for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { client := NewClient(AuthInfo{APIKey: "12345", authSid: test.authSid}) client.BaseURL = serverURL response, err := client.GetZone(test.domain) if test.expectedError { require.Error(t, err) } else { require.NoError(t, err) require.NotNil(t, response) assert.Equal(t, test.expectedStatusCode, response.StatusCode) assert.Equal(t, test.zone, response.Body) } }) } } func Test_parseResponse(t *testing.T) { testCases := []struct { desc string input string expected *Response }{ { desc: "Empty response", input: "", expected: &Response{ Headers: url.Values{}, StatusCode: -1, }, }, { desc: "No headers, just body", input: "\n\nTest body", expected: &Response{ Headers: url.Values{}, Body: "Test body", StatusCode: -1, }, }, { desc: "Headers and body", input: "Test-Header: value\n\nTest body", expected: &Response{ Headers: url.Values{"Test-Header": {"value"}}, Body: "Test body", StatusCode: -1, }, }, { desc: "Headers and body + Auth-Sid", input: "Test-Header: value\nAuth-Sid: 123\n\nTest body", expected: &Response{ Headers: url.Values{"Test-Header": {"value"}, "Auth-Sid": {"123"}}, Body: "Test body", StatusCode: -1, AuthSid: "123", }, }, { desc: "Headers and body + Status-Text", input: "Test-Header: value\nStatus-Text: OK\n\nTest body", expected: &Response{ Headers: url.Values{"Test-Header": {"value"}, "Status-Text": {"OK"}}, Body: "Test body", StatusText: "OK", StatusCode: -1, }, }, { desc: "Headers and body + Status-Code", input: "Test-Header: value\nStatus-Code: 2020\n\nTest body", expected: &Response{ Headers: url.Values{"Test-Header": {"value"}, "Status-Code": {"2020"}}, Body: "Test body", StatusCode: 2020, }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() response := parseResponse(test.input) assert.Equal(t, test.expected, response) }) } } func Test_removeTxtEntryFromZone(t *testing.T) { testCases := []struct { desc string input string expected string modified bool }{ { desc: "empty zone", input: "", expected: "", modified: false, }, { desc: "zone with only A entry", input: "@ A 0 192.0.2.2 3600", expected: "@ A 0 192.0.2.2 3600", modified: false, }, { desc: "zone with only clenup entry", input: "_acme-challenge TXT 0 \"old \" 120", expected: "", modified: true, }, { desc: "zone with one A and one cleanup entries", input: "@ A 0 192.0.2.2 3600\n_acme-challenge TXT 0 \"old \" 120", expected: "@ A 0 192.0.2.2 3600", modified: true, }, { desc: "zone with one A and multiple cleanup entries", input: "@ A 0 192.0.2.2 3600\n_acme-challenge TXT 0 \"old \" 120\n_acme-challenge TXT 0 \"another \" 120", expected: "@ A 0 192.0.2.2 3600", modified: true, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() zone, modified := RemoveTxtEntryFromZone(test.input, "_acme-challenge") assert.Equal(t, zone, test.expected) assert.Equal(t, modified, test.modified) }) } } func Test_addTxtEntryToZone(t *testing.T) { testCases := []struct { desc string input string expected string }{ { desc: "empty zone", input: "", expected: "_acme-challenge TXT 0 \"test\" 120", }, { desc: "zone with A entry", input: "@ A 0 192.0.2.2 3600", expected: "@ A 0 192.0.2.2 3600\n_acme-challenge TXT 0 \"test\" 120", }, { desc: "zone with required cleanup entry", input: "_acme-challenge TXT 0 \"old \" 120", expected: "_acme-challenge TXT 0 \"old\" 120\n_acme-challenge TXT 0 \"test\" 120", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { zone := AddTxtEntryToZone(test.input, "_acme-challenge", "test", 120) assert.Equal(t, zone, test.expected) }) } } func Test_fixTxtLines(t *testing.T) { testCases := []struct { desc string input string expected string }{ { desc: "clean-up", input: `_acme-challenge TXT 0 "SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE " 120`, expected: `_acme-challenge TXT 0 "SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE" 120`, }, { desc: "already cleaned", input: `_acme-challenge TXT 0 "SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE" 120`, expected: `_acme-challenge TXT 0 "SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE" 120`, }, { desc: "special DNS entry", input: "$dyndns=yes:username:password", expected: "$dyndns=yes:username:password", }, { desc: "SRV entry", input: "_jabber._tcp SRV 20/0 xmpp-server1.l.google.com:5269 300", expected: "_jabber._tcp SRV 20/0 xmpp-server1.l.google.com:5269 300", }, { desc: "MX entry", input: "@ MX 10 ASPMX.L.GOOGLE.COM 300", expected: "@ MX 10 ASPMX.L.GOOGLE.COM 300", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { line := fixTxtLines(test.input) assert.Equal(t, line, test.expected) }) } } lego-4.9.1/providers/dns/joker/internal/svc/000077500000000000000000000000001434020463500207765ustar00rootroot00000000000000lego-4.9.1/providers/dns/joker/internal/svc/client.go000066400000000000000000000025401434020463500226040ustar00rootroot00000000000000// Package svc Client for the SVC API. // https://joker.com/faq/content/6/496/en/let_s-encrypt-support.html package svc import ( "fmt" "io" "net/http" "strings" querystring "github.com/google/go-querystring/query" ) const defaultBaseURL = "https://svc.joker.com/nic/replace" type request struct { Username string `url:"username"` Password string `url:"password"` Zone string `url:"zone"` Label string `url:"label"` Type string `url:"type"` Value string `url:"value"` } type Client struct { HTTPClient *http.Client BaseURL string username string password string } func NewClient(username, password string) *Client { return &Client{ HTTPClient: http.DefaultClient, BaseURL: defaultBaseURL, username: username, password: password, } } func (c *Client) Send(zone, label, value string) error { req := request{ Username: c.username, Password: c.password, Zone: zone, Label: label, Type: "TXT", Value: value, } v, err := querystring.Values(req) if err != nil { return err } resp, err := c.HTTPClient.PostForm(c.BaseURL, v) if err != nil { return err } all, err := io.ReadAll(resp.Body) if err != nil { return err } if resp.StatusCode == http.StatusOK && strings.HasPrefix(string(all), "OK") { return nil } return fmt.Errorf("error: %d: %s", resp.StatusCode, string(all)) } lego-4.9.1/providers/dns/joker/internal/svc/client_test.go000066400000000000000000000037711434020463500236520ustar00rootroot00000000000000package svc import ( "fmt" "io" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/require" ) func TestClient_Send(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } all, _ := io.ReadAll(req.Body) if string(all) != "label=_acme-challenge&password=secret&type=TXT&username=test&value=123&zone=example.com" { http.Error(rw, fmt.Sprintf("invalid request: %q", string(all)), http.StatusBadRequest) return } _, err := rw.Write([]byte("OK: 1 inserted, 0 deleted")) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient("test", "secret") client.BaseURL = server.URL zone := "example.com" label := "_acme-challenge" value := "123" err := client.Send(zone, label, value) require.NoError(t, err) } func TestClient_Send_empty(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } all, _ := io.ReadAll(req.Body) if string(all) != "label=_acme-challenge&password=secret&type=TXT&username=test&value=&zone=example.com" { http.Error(rw, fmt.Sprintf("invalid request: %q", string(all)), http.StatusBadRequest) return } _, err := rw.Write([]byte("OK: 1 inserted, 0 deleted")) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient("test", "secret") client.BaseURL = server.URL zone := "example.com" label := "_acme-challenge" value := "" err := client.Send(zone, label, value) require.NoError(t, err) } lego-4.9.1/providers/dns/joker/joker.go000066400000000000000000000047571434020463500200450ustar00rootroot00000000000000// Package joker implements a DNS provider for solving the DNS-01 challenge using joker.com. package joker import ( "net/http" "os" "time" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "JOKER_" EnvAPIKey = envNamespace + "API_KEY" EnvUsername = envNamespace + "USERNAME" EnvPassword = envNamespace + "PASSWORD" EnvDebug = envNamespace + "DEBUG" EnvMode = envNamespace + "API_MODE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) const ( modeDMAPI = "DMAPI" modeSVC = "SVC" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Debug bool APIKey string Username string Password string APIMode string PropagationTimeout time.Duration PollingInterval time.Duration SequenceInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ APIMode: env.GetOrDefaultString(EnvMode, modeDMAPI), Debug: env.GetOrDefaultBool(EnvDebug, false), TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second), }, } } // NewDNSProvider returns a DNSProvider instance configured for Joker. // Credentials must be passed in the environment variable JOKER_API_KEY. func NewDNSProvider() (challenge.ProviderTimeout, error) { if os.Getenv(EnvMode) == modeSVC { return newSvcProvider() } return newDmapiProvider() } // NewDNSProviderConfig return a DNSProvider instance configured for Joker. func NewDNSProviderConfig(config *Config) (challenge.ProviderTimeout, error) { if config.APIMode == modeSVC { return newSvcProviderConfig(config) } return newDmapiProviderConfig(config) } lego-4.9.1/providers/dns/joker/joker.toml000066400000000000000000000042531434020463500204020ustar00rootroot00000000000000Name = "Joker" Description = '''''' URL = "https://joker.com" Code = "joker" Since = "v2.6.0" Example = ''' # SVC JOKER_API_MODE=SVC \ JOKER_USERNAME= \ JOKER_PASSWORD= \ lego --email you@example.com --dns joker --domains my.example.org run # DMAPI JOKER_API_MODE=DMAPI \ JOKER_USERNAME= \ JOKER_PASSWORD= \ lego --email you@example.com --dns joker --domains my.example.org run ## or JOKER_API_MODE=DMAPI \ JOKER_API_KEY= \ lego --email you@example.com --dns joker --domains my.example.org run ''' Additional = ''' ## SVC mode In 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. As per [Joker.com documentation](https://joker.com/faq/content/6/496/en/let_s-encrypt-support.html): > 1. please login at Joker.com, visit 'My Domains', > find the domain you want to add Let's Encrypt certificate for, and chose "DNS" in the menu > > 2. on the top right, you will find the setting for 'Dynamic DNS'. > If not already active, please activate it. > It will not affect any other already existing DNS records of this domain. > > 3. please take a note of the credentials which are now shown as 'Dynamic DNS Authentication', consisting of a 'username' and a 'password'. > > 4. this is all you have to do here - and only once per domain. ''' [Configuration] [Configuration.Credentials] JOKER_API_MODE = "'DMAPI' or 'SVC'. DMAPI is for resellers accounts. (Default: DMAPI)" JOKER_USERNAME = "Joker.com username" JOKER_PASSWORD = "Joker.com password" JOKER_API_KEY = "API key (only with DMAPI mode)" [Configuration.Additional] JOKER_POLLING_INTERVAL = "Time between DNS propagation check" JOKER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" JOKER_TTL = "The TTL of the TXT record used for the DNS challenge" JOKER_HTTP_TIMEOUT = "API request timeout" JOKER_SEQUENCE_INTERVAL = "Time between sequential requests (only with 'SVC' mode)" [Links] API = "https://joker.com/faq/category/39/22-dmapi.html" API_SVC = "https://joker.com/faq/content/6/496/en/let_s-encrypt-support.html" lego-4.9.1/providers/dns/joker/joker_test.go000066400000000000000000000050441434020463500210720ustar00rootroot00000000000000package joker import ( "fmt" "os" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAPIKey, EnvUsername, EnvPassword, EnvMode). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected interface{} }{ { desc: "mode DMAPI (default)", envVars: map[string]string{ EnvUsername: "123", EnvPassword: "123", }, expected: &dmapiProvider{}, }, { desc: "mode DMAPI", envVars: map[string]string{ EnvMode: modeDMAPI, EnvUsername: "123", EnvPassword: "123", }, expected: &dmapiProvider{}, }, { desc: "mode SVC", envVars: map[string]string{ EnvMode: modeSVC, EnvUsername: "123", EnvPassword: "123", }, expected: &svcProvider{}, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) fmt.Println(os.Getenv(EnvMode)) p, err := NewDNSProvider() require.NoError(t, err) require.NotNil(t, p) assert.IsType(t, test.expected, p) }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string mode string expected interface{} }{ { desc: "mode DMAPI (default)", expected: &dmapiProvider{}, }, { desc: "mode DMAPI", mode: modeDMAPI, expected: &dmapiProvider{}, }, { desc: "mode SVC", mode: modeSVC, expected: &svcProvider{}, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Username = "123" config.Password = "123" config.APIMode = test.mode p, err := NewDNSProviderConfig(config) require.NoError(t, err) require.NotNil(t, p) assert.IsType(t, test.expected, p) }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/joker/provider_dmapi.go000066400000000000000000000106011434020463500217200ustar00rootroot00000000000000package joker import ( "errors" "fmt" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/joker/internal/dmapi" ) // dmapiProvider implements the challenge.Provider interface. type dmapiProvider struct { config *Config client *dmapi.Client } // newDmapiProvider returns a DNSProvider instance configured for Joker. // Credentials must be passed in the environment variable: JOKER_USERNAME, JOKER_PASSWORD or JOKER_API_KEY. func newDmapiProvider() (*dmapiProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { var errU error values, errU = env.Get(EnvUsername, EnvPassword) if errU != nil { //nolint:errorlint // false-positive return nil, fmt.Errorf("joker: %v or %v", errU, err) } } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] config.Username = values[EnvUsername] config.Password = values[EnvPassword] return newDmapiProviderConfig(config) } // newDmapiProviderConfig return a DNSProvider instance configured for Joker. func newDmapiProviderConfig(config *Config) (*dmapiProvider, error) { if config == nil { return nil, errors.New("joker: the configuration of the DNS provider is nil") } if config.APIKey == "" { if config.Username == "" || config.Password == "" { return nil, errors.New("joker: credentials missing") } } client := dmapi.NewClient(dmapi.AuthInfo{ APIKey: config.APIKey, Username: config.Username, Password: config.Password, }) client.Debug = config.Debug if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &dmapiProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *dmapiProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *dmapiProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("joker: %w", err) } relative := getRelative(fqdn, zone) if d.config.Debug { log.Infof("[%s] joker: adding TXT record %q to zone %q with value %q", domain, relative, zone, value) } response, err := d.client.Login() if err != nil { return formatResponseError(response, err) } response, err = d.client.GetZone(zone) if err != nil || response.StatusCode != 0 { return formatResponseError(response, err) } dnsZone := dmapi.AddTxtEntryToZone(response.Body, relative, value, d.config.TTL) response, err = d.client.PutZone(zone, dnsZone) if err != nil || response.StatusCode != 0 { return formatResponseError(response, err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *dmapiProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("joker: %w", err) } relative := getRelative(fqdn, zone) if d.config.Debug { log.Infof("[%s] joker: removing entry %q from zone %q", domain, relative, zone) } response, err := d.client.Login() if err != nil { return formatResponseError(response, err) } defer func() { // Try to logout in case of errors _, _ = d.client.Logout() }() response, err = d.client.GetZone(zone) if err != nil || response.StatusCode != 0 { return formatResponseError(response, err) } dnsZone, modified := dmapi.RemoveTxtEntryFromZone(response.Body, relative) if modified { response, err = d.client.PutZone(zone, dnsZone) if err != nil || response.StatusCode != 0 { return formatResponseError(response, err) } } response, err = d.client.Logout() if err != nil { return formatResponseError(response, err) } return nil } func getRelative(fqdn, zone string) string { return dns01.UnFqdn(strings.TrimSuffix(fqdn, dns01.ToFqdn(zone))) } // formatResponseError formats error with optional details from DMAPI response. func formatResponseError(response *dmapi.Response, err error) error { if response != nil { return fmt.Errorf("joker: DMAPI error: %w Response: %v", err, response.Headers) } return fmt.Errorf("joker: DMAPI error: %w", err) } lego-4.9.1/providers/dns/joker/provider_dmapi_test.go000066400000000000000000000053641434020463500227710ustar00rootroot00000000000000package joker import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_newDmapiProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success API key", envVars: map[string]string{ EnvAPIKey: "123", }, }, { desc: "success username password", envVars: map[string]string{ EnvUsername: "123", EnvPassword: "123", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIKey: "", EnvUsername: "", EnvPassword: "", }, expected: "joker: some credentials information are missing: JOKER_USERNAME,JOKER_PASSWORD or some credentials information are missing: JOKER_API_KEY", }, { desc: "missing password", envVars: map[string]string{ EnvAPIKey: "", EnvUsername: "123", EnvPassword: "", }, expected: "joker: some credentials information are missing: JOKER_PASSWORD or some credentials information are missing: JOKER_API_KEY", }, { desc: "missing username", envVars: map[string]string{ EnvAPIKey: "", EnvUsername: "", EnvPassword: "123", }, expected: "joker: some credentials information are missing: JOKER_USERNAME or some credentials information are missing: JOKER_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := newDmapiProvider() if test.expected != "" { require.EqualError(t, err, test.expected) } else { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) } }) } } func Test_newDmapiProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string username string password string expected string }{ { desc: "success api key", apiKey: "123", }, { desc: "success username and password", username: "123", password: "123", }, { desc: "missing credentials", expected: "joker: credentials missing", }, { desc: "missing credentials: username", expected: "joker: credentials missing", username: "123", }, { desc: "missing credentials: password", expected: "joker: credentials missing", password: "123", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.Username = test.username config.Password = test.password p, err := newDmapiProviderConfig(config) if test.expected != "" { require.EqualError(t, err, test.expected) } else { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) } }) } } lego-4.9.1/providers/dns/joker/provider_svc.go000066400000000000000000000047631434020463500214350ustar00rootroot00000000000000package joker import ( "errors" "fmt" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/joker/internal/svc" ) // svcProvider implements the challenge.Provider interface. type svcProvider struct { config *Config client *svc.Client } // newSvcProvider returns a DNSProvider instance configured for Joker. // Credentials must be passed in the environment variable: JOKER_USERNAME, JOKER_PASSWORD. func newSvcProvider() (*svcProvider, error) { values, err := env.Get(EnvUsername, EnvPassword) if err != nil { return nil, fmt.Errorf("joker: %w", err) } config := NewDefaultConfig() config.Username = values[EnvUsername] config.Password = values[EnvPassword] return newSvcProviderConfig(config) } // newSvcProviderConfig return a DNSProvider instance configured for Joker. func newSvcProviderConfig(config *Config) (*svcProvider, error) { if config == nil { return nil, errors.New("joker: the configuration of the DNS provider is nil") } if config.Username == "" || config.Password == "" { return nil, errors.New("joker: credentials missing") } client := svc.NewClient(config.Username, config.Password) return &svcProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *svcProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *svcProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("joker: %w", err) } relative := getRelative(fqdn, zone) return d.client.Send(dns01.UnFqdn(zone), relative, value) } // CleanUp removes the TXT record matching the specified parameters. func (d *svcProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("joker: %w", err) } relative := getRelative(fqdn, zone) return d.client.Send(dns01.UnFqdn(zone), relative, "") } // Sequential All DNS challenges for this provider will be resolved sequentially. // Returns the interval between each iteration. func (d *svcProvider) Sequential() time.Duration { return d.config.SequenceInterval } lego-4.9.1/providers/dns/joker/provider_svc_test.go000066400000000000000000000044621434020463500224700ustar00rootroot00000000000000package joker import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func Test_newSvcProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success username password", envVars: map[string]string{ EnvUsername: "123", EnvPassword: "123", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvUsername: "", EnvPassword: "", }, expected: "joker: some credentials information are missing: JOKER_USERNAME,JOKER_PASSWORD", }, { desc: "missing password", envVars: map[string]string{ EnvUsername: "123", EnvPassword: "", }, expected: "joker: some credentials information are missing: JOKER_PASSWORD", }, { desc: "missing username", envVars: map[string]string{ EnvUsername: "", EnvPassword: "123", }, expected: "joker: some credentials information are missing: JOKER_USERNAME", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := newSvcProvider() if test.expected != "" { require.EqualError(t, err, test.expected) } else { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) } }) } } func Test_newSvcProviderConfig(t *testing.T) { testCases := []struct { desc string username string password string expected string }{ { desc: "success username and password", username: "123", password: "123", }, { desc: "missing credentials", expected: "joker: credentials missing", }, { desc: "missing credentials: username", expected: "joker: credentials missing", username: "123", }, { desc: "missing credentials: password", expected: "joker: credentials missing", password: "123", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Username = test.username config.Password = test.password p, err := newSvcProviderConfig(config) if test.expected != "" { require.EqualError(t, err, test.expected) } else { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) } }) } } lego-4.9.1/providers/dns/lightsail/000077500000000000000000000000001434020463500172355ustar00rootroot00000000000000lego-4.9.1/providers/dns/lightsail/lightsail.go000066400000000000000000000115721434020463500215520ustar00rootroot00000000000000// Package lightsail implements a DNS provider for solving the DNS-01 challenge using AWS Lightsail DNS. package lightsail import ( "errors" "fmt" "math/rand" "strconv" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/client" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/lightsail" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) const ( maxRetries = 5 ) // Environment variables names. const ( envNamespace = "LIGHTSAIL_" EnvRegion = envNamespace + "REGION" EnvDNSZone = "DNS_ZONE" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // customRetryer implements the client.Retryer interface by composing the DefaultRetryer. // It controls the logic for retrying recoverable request errors (e.g. when rate limits are exceeded). type customRetryer struct { client.DefaultRetryer } // RetryRules overwrites the DefaultRetryer's method. // It uses a basic exponential backoff algorithm that returns an initial // delay of ~400ms with an upper limit of ~30 seconds which should prevent // causing a high number of consecutive throttling errors. // For reference: Route 53 enforces an account-wide(!) 5req/s query limit. func (c customRetryer) RetryRules(r *request.Request) time.Duration { retryCount := r.RetryCount if retryCount > 7 { retryCount = 7 } delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200) return time.Duration(delay) * time.Millisecond } // Config is used to configure the creation of the DNSProvider. type Config struct { DNSZone string Region string PropagationTimeout time.Duration PollingInterval time.Duration } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *lightsail.Lightsail config *Config } // NewDNSProvider returns a DNSProvider instance configured for the AWS Lightsail service. // // AWS Credentials are automatically detected in the following locations // and prioritized in the following order: // 1. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, // [AWS_SESSION_TOKEN], [DNS_ZONE], [LIGHTSAIL_REGION] // 2. Shared credentials file (defaults to ~/.aws/credentials) // 3. Amazon EC2 IAM role // // public hosted zone via the FQDN. // // See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.DNSZone = env.GetOrFile(EnvDNSZone) config.Region = env.GetOrDefaultString(EnvRegion, "us-east-1") return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for AWS Lightsail. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("lightsail: the configuration of the DNS provider is nil") } retryer := customRetryer{} retryer.NumMaxRetries = maxRetries conf := aws.NewConfig().WithRegion(config.Region) sess, err := session.NewSession(request.WithRetryer(conf, retryer)) if err != nil { return nil, err } return &DNSProvider{ config: config, client: lightsail.New(sess), }, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) params := &lightsail.CreateDomainEntryInput{ DomainName: aws.String(d.config.DNSZone), DomainEntry: &lightsail.DomainEntry{ Name: aws.String(fqdn), Target: aws.String(strconv.Quote(value)), Type: aws.String("TXT"), }, } _, err := d.client.CreateDomainEntry(params) if err != nil { return fmt.Errorf("lightsail: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) params := &lightsail.DeleteDomainEntryInput{ DomainName: aws.String(d.config.DNSZone), DomainEntry: &lightsail.DomainEntry{ Name: aws.String(fqdn), Type: aws.String("TXT"), Target: aws.String(strconv.Quote(value)), }, } _, err := d.client.DeleteDomainEntry(params) if err != nil { return fmt.Errorf("lightsail: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/lightsail/lightsail.toml000066400000000000000000000043251434020463500221160ustar00rootroot00000000000000Name = "Amazon Lightsail" Description = '''''' URL = "https://aws.amazon.com/lightsail/" Code = "lightsail" Since = "v0.5.0" Example = '''''' Additional = ''' ## Description AWS Credentials are automatically detected in the following locations and prioritized in the following order: 1. Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, [`AWS_SESSION_TOKEN`] 2. Shared credentials file (defaults to `~/.aws/credentials`, profiles can be specified using `AWS_PROFILE`) 3. Amazon EC2 IAM role AWS region is not required to set as the Lightsail DNS zone is in global (us-east-1) region. ## Policy The following AWS IAM policy document describes the minimum permissions required for lego to complete the DNS challenge. ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "lightsail:DeleteDomainEntry", "lightsail:CreateDomainEntry" ], "Resource": "" } ] } ``` Replace the `Resource` value with your Lightsail DNS zone ARN. You 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). It should be in the format of `arn:aws:lightsail:global::Domain/`. You also need to replace the region in the ARN to `us-east-1` (instead of `global`). Alternatively, you can also set the `Resource` to `*` (wildcard), which allow to access all domain, but this is not recommended. ''' [Configuration] [Configuration.Credentials] 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)" 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)" DNS_ZONE = "Domain name of the DNS zone" [Configuration.Additional] AWS_SHARED_CREDENTIALS_FILE = "Managed by the AWS client. Shared credentials file." LIGHTSAIL_POLLING_INTERVAL = "Time between DNS propagation check" LIGHTSAIL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" [Links] GoClient = "https://github.com/aws/aws-sdk-go/" lego-4.9.1/providers/dns/lightsail/lightsail_integration_test.go000066400000000000000000000023131434020463500252050ustar00rootroot00000000000000package lightsail import ( "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/lightsail" "github.com/stretchr/testify/require" ) func TestLiveTTL(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) domain := envTest.GetDomain() err = provider.Present(domain, "foo", "bar") require.NoError(t, err) // we need a separate Lightsail client here as the one in the DNS provider is // unexported. fqdn := "_acme-challenge." + domain sess, err := session.NewSession() require.NoError(t, err) svc := lightsail.New(sess) require.NoError(t, err) defer func() { errC := provider.CleanUp(domain, "foo", "bar") if errC != nil { t.Log(errC) } }() params := &lightsail.GetDomainInput{ DomainName: aws.String(domain), } resp, err := svc.GetDomain(params) require.NoError(t, err) entries := resp.Domain.DomainEntries for _, entry := range entries { if aws.StringValue(entry.Type) == "TXT" && aws.StringValue(entry.Name) == fqdn { return } } t.Fatalf("Could not find a TXT record for _acme-challenge.%s", domain) } lego-4.9.1/providers/dns/lightsail/lightsail_test.go000066400000000000000000000041371434020463500226100ustar00rootroot00000000000000package lightsail import ( "os" "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/lightsail" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const ( envAwsNamespace = "AWS_" envAwsAccessKeyID = envAwsNamespace + "ACCESS_KEY_ID" envAwsSecretAccessKey = envAwsNamespace + "SECRET_ACCESS_KEY" envAwsRegion = envAwsNamespace + "REGION" envAwsHostedZoneID = envAwsNamespace + "HOSTED_ZONE_ID" ) var envTest = tester.NewEnvTest( envAwsAccessKeyID, envAwsSecretAccessKey, envAwsRegion, envAwsHostedZoneID). WithDomain(EnvDNSZone). WithLiveTestRequirements(envAwsAccessKeyID, envAwsSecretAccessKey, EnvDNSZone) func makeProvider(serverURL string) (*DNSProvider, error) { config := &aws.Config{ Credentials: credentials.NewStaticCredentials("abc", "123", " "), Endpoint: aws.String(serverURL), Region: aws.String("mock-region"), MaxRetries: aws.Int(1), } sess, err := session.NewSession(config) if err != nil { return nil, err } conf := NewDefaultConfig() client := lightsail.New(sess) return &DNSProvider{client: client, config: conf}, nil } func TestCredentialsFromEnv(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() _ = os.Setenv(envAwsAccessKeyID, "123") _ = os.Setenv(envAwsSecretAccessKey, "123") _ = os.Setenv(envAwsRegion, "us-east-1") config := &aws.Config{ CredentialsChainVerboseErrors: aws.Bool(true), } sess, err := session.NewSession(config) require.NoError(t, err) _, err = sess.Config.Credentials.Get() require.NoError(t, err, "Expected credentials to be set from environment") } func TestDNSProvider_Present(t *testing.T) { mockResponses := map[string]MockResponse{ "/": {StatusCode: 200, Body: ""}, } serverURL := newMockServer(t, mockResponses) provider, err := makeProvider(serverURL) require.NoError(t, err) domain := "example.com" keyAuth := "123456d==" err = provider.Present(domain, "", keyAuth) require.NoError(t, err, "Expected Present to return no error") } lego-4.9.1/providers/dns/lightsail/mock_server_test.go000066400000000000000000000016411434020463500231440ustar00rootroot00000000000000package lightsail import ( "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/require" ) // MockResponse represents a predefined response used by a mock server. type MockResponse struct { StatusCode int Body string } func newMockServer(t *testing.T, responses map[string]MockResponse) string { t.Helper() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path resp, ok := responses[path] if !ok { msg := fmt.Sprintf("Requested path not found in response map: %s", path) require.FailNow(t, msg) } w.Header().Set("Content-Type", "application/xml") w.WriteHeader(resp.StatusCode) _, err := w.Write([]byte(resp.Body)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } })) t.Cleanup(server.Close) time.Sleep(100 * time.Millisecond) return server.URL } lego-4.9.1/providers/dns/linode/000077500000000000000000000000001434020463500165275ustar00rootroot00000000000000lego-4.9.1/providers/dns/linode/linode.go000066400000000000000000000133751434020463500203410ustar00rootroot00000000000000// Package linode implements a DNS provider for solving the DNS-01 challenge using Linode DNS and Linode's APIv4 package linode import ( "context" "encoding/json" "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/linode/linodego" "golang.org/x/oauth2" ) const ( minTTL = 300 dnsUpdateFreqMins = 15 dnsUpdateFudgeSecs = 120 ) // Environment variables names. const ( envNamespace = "LINODE_" EnvToken = envNamespace + "TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Token string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPTimeout time.Duration } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 0), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 15*time.Second), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 0), } } type hostedZoneInfo struct { domainID int resourceName string } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *linodego.Client } // NewDNSProvider returns a DNSProvider instance configured for Linode. // Credentials must be passed in the environment variable: LINODE_TOKEN. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvToken) if err != nil { return nil, fmt.Errorf("linode: %w", err) } config := NewDefaultConfig() config.Token = values[EnvToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Linode. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("linode: the configuration of the DNS provider is nil") } if config.Token == "" { return nil, errors.New("linode: Linode Access Token missing") } if config.TTL < minTTL { return nil, fmt.Errorf("linode: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: config.Token}) oauth2Client := &http.Client{ Timeout: config.HTTPTimeout, Transport: &oauth2.Transport{ Source: tokenSource, }, } client := linodego.NewClient(oauth2Client) client.SetUserAgent("lego-dns https://github.com/linode/linodego") return &DNSProvider{ config: config, client: &client, }, nil } // Timeout returns the timeout and interval to use when checking for DNS // propagation. Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (time.Duration, time.Duration) { timeout := d.config.PropagationTimeout if d.config.PropagationTimeout <= 0 { // Since Linode only updates their zone files every X minutes, we need // to figure out how many minutes we have to wait until we hit the next // interval of X. We then wait another couple of minutes, just to be // safe. Hopefully at some point during all of this, the record will // have propagated throughout Linode's network. minsRemaining := dnsUpdateFreqMins - (time.Now().Minute() % dnsUpdateFreqMins) timeout = (time.Duration(minsRemaining) * time.Minute) + (minTTL * time.Second) + (dnsUpdateFudgeSecs * time.Second) } return timeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZoneInfo(fqdn) if err != nil { return err } createOpts := linodego.DomainRecordCreateOptions{ Name: dns01.UnFqdn(fqdn), Target: value, TTLSec: d.config.TTL, Type: linodego.RecordTypeTXT, } _, err = d.client.CreateDomainRecord(context.Background(), zone.domainID, createOpts) return err } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZoneInfo(fqdn) if err != nil { return err } // Get all TXT records for the specified domain. listOpts := linodego.NewListOptions(0, "{\"type\":\"TXT\"}") resources, err := d.client.ListDomainRecords(context.Background(), zone.domainID, listOpts) if err != nil { return err } // Remove the specified resource, if it exists. for _, resource := range resources { if (resource.Name == strings.TrimSuffix(fqdn, ".") || resource.Name == zone.resourceName) && resource.Target == value { if err := d.client.DeleteDomainRecord(context.Background(), zone.domainID, resource.ID); err != nil { return err } } } return nil } func (d *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) { // Lookup the zone that handles the specified FQDN. authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return nil, err } // Query the authority zone. data, err := json.Marshal(map[string]string{"domain": dns01.UnFqdn(authZone)}) if err != nil { return nil, err } listOpts := linodego.NewListOptions(0, string(data)) domains, err := d.client.ListDomains(context.Background(), listOpts) if err != nil { return nil, err } if len(domains) == 0 { return nil, errors.New("domain not found") } return &hostedZoneInfo{ domainID: domains[0].ID, resourceName: strings.TrimSuffix(fqdn, "."+authZone), }, nil } lego-4.9.1/providers/dns/linode/linode.toml000066400000000000000000000012661434020463500207030ustar00rootroot00000000000000Name = "Linode (v4)" Description = '''''' URL = "https://www.linode.com/" Code = "linode" Since = "v1.1.0" Example = ''' LINODE_TOKEN=xxxxx \ lego --email you@example.com --dns linode --domains my.example.org run ''' [Configuration] [Configuration.Credentials] LINODE_TOKEN = "API token" [Configuration.Additional] LINODE_POLLING_INTERVAL = "Time between DNS propagation check" LINODE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" LINODE_TTL = "The TTL of the TXT record used for the DNS challenge" LINODE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developers.linode.com/api/v4" GoClient = "https://github.com/linode/linodego" lego-4.9.1/providers/dns/linode/linode_test.go000066400000000000000000000173111434020463500213720ustar00rootroot00000000000000package linode import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/linode/linodego" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type MockResponseMap map[string]interface{} var envTest = tester.NewEnvTest(EnvToken) func setupTest(t *testing.T, responses MockResponseMap) string { t.Helper() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Ensure that we support the requested action. action := r.Method + ":" + r.URL.Path resp, ok := responses[action] if !ok { http.Error(w, fmt.Sprintf("Unsupported mock action: %q", action), http.StatusInternalServerError) return } rawResponse, err := json.Marshal(resp) if err != nil { http.Error(w, fmt.Sprintf("Failed to JSON encode response: %v", err), http.StatusInternalServerError) return } // Send the response. w.Header().Set("Content-Type", "application/json") if err, ok := resp.(linodego.APIError); ok { if err.Errors[0].Reason == "Not found" { w.WriteHeader(http.StatusNotFound) } else { w.WriteHeader(http.StatusBadRequest) } } else { w.WriteHeader(http.StatusOK) } _, err = w.Write(rawResponse) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) server := httptest.NewServer(handler) t.Cleanup(server.Close) time.Sleep(100 * time.Millisecond) return server.URL } func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvToken: "123", }, }, { desc: "missing api key", envVars: map[string]string{ EnvToken: "", }, expected: "linode: some credentials information are missing: LINODE_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string expected string }{ { desc: "success", apiKey: "123", }, { desc: "missing credentials", expected: "linode: Linode Access Token missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Token = test.apiKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestDNSProvider_Present(t *testing.T) { defer envTest.RestoreEnv() os.Setenv(EnvToken, "testing") p, err := NewDNSProvider() require.NoError(t, err) require.NotNil(t, p) domain := "example.com" keyAuth := "dGVzdGluZw==" testCases := []struct { desc string mockResponses MockResponseMap expectedError string }{ { desc: "Success", mockResponses: MockResponseMap{ "GET:/v4/domains": linodego.DomainsPagedResponse{ PageOptions: &linodego.PageOptions{ Pages: 1, Results: 1, Page: 1, }, Data: []linodego.Domain{{ Domain: domain, ID: 1234, }}, }, "POST:/v4/domains/1234/records": linodego.DomainRecord{ ID: 1234, }, }, }, { desc: "NoDomain", mockResponses: MockResponseMap{ "GET:/v4/domains": linodego.APIError{ Errors: []linodego.APIErrorReason{{ Reason: "Not found", }}, }, }, expectedError: "[404] Not found", }, { desc: "CreateFailed", mockResponses: MockResponseMap{ "GET:/v4/domains": &linodego.DomainsPagedResponse{ PageOptions: &linodego.PageOptions{ Pages: 1, Results: 1, Page: 1, }, Data: []linodego.Domain{{ Domain: "foobar.com", ID: 1234, }}, }, "POST:/v4/domains/1234/records": linodego.APIError{ Errors: []linodego.APIErrorReason{{ Reason: "Failed to create domain resource", Field: "somefield", }}, }, }, expectedError: "[400] [somefield] Failed to create domain resource", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { serverURL := setupTest(t, test.mockResponses) assert.NotNil(t, p.client) p.client.SetBaseURL(serverURL) err = p.Present(domain, "", keyAuth) if test.expectedError == "" { assert.NoError(t, err) } else { assert.EqualError(t, err, test.expectedError) } }) } } func TestDNSProvider_CleanUp(t *testing.T) { defer envTest.RestoreEnv() os.Setenv(EnvToken, "testing") p, err := NewDNSProvider() require.NoError(t, err) domain := "example.com" keyAuth := "dGVzdGluZw==" testCases := []struct { desc string mockResponses MockResponseMap expectedError string }{ { desc: "Success", mockResponses: MockResponseMap{ "GET:/v4/domains": &linodego.DomainsPagedResponse{ PageOptions: &linodego.PageOptions{ Pages: 1, Results: 1, Page: 1, }, Data: []linodego.Domain{{ Domain: "foobar.com", ID: 1234, }}, }, "GET:/v4/domains/1234/records": &linodego.DomainRecordsPagedResponse{ PageOptions: &linodego.PageOptions{ Pages: 1, Results: 1, Page: 1, }, Data: []linodego.DomainRecord{{ ID: 1234, Name: "_acme-challenge", Target: "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM", Type: "TXT", }}, }, "DELETE:/v4/domains/1234/records/1234": struct{}{}, }, }, { desc: "NoDomain", mockResponses: MockResponseMap{ "GET:/v4/domains": linodego.APIError{ Errors: []linodego.APIErrorReason{{ Reason: "Not found", }}, }, "GET:/v4/domains/1234/records": linodego.APIError{ Errors: []linodego.APIErrorReason{{ Reason: "Not found", }}, }, }, expectedError: "[404] Not found", }, { desc: "DeleteFailed", mockResponses: MockResponseMap{ "GET:/v4/domains": linodego.DomainsPagedResponse{ PageOptions: &linodego.PageOptions{ Pages: 1, Results: 1, Page: 1, }, Data: []linodego.Domain{{ ID: 1234, Domain: "example.com", }}, }, "GET:/v4/domains/1234/records": linodego.DomainRecordsPagedResponse{ PageOptions: &linodego.PageOptions{ Pages: 1, Results: 1, Page: 1, }, Data: []linodego.DomainRecord{{ ID: 1234, Name: "_acme-challenge", Target: "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM", Type: "TXT", }}, }, "DELETE:/v4/domains/1234/records/1234": linodego.APIError{ Errors: []linodego.APIErrorReason{{ Reason: "Failed to delete domain resource", }}, }, }, expectedError: "[400] Failed to delete domain resource", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { serverURL := setupTest(t, test.mockResponses) p.client.SetBaseURL(serverURL) err = p.CleanUp(domain, "", keyAuth) if test.expectedError == "" { assert.NoError(t, err) } else { assert.EqualError(t, err, test.expectedError) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("Skipping live test") } // TODO implement this test } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("Skipping live test") } // TODO implement this test } lego-4.9.1/providers/dns/liquidweb/000077500000000000000000000000001434020463500172425ustar00rootroot00000000000000lego-4.9.1/providers/dns/liquidweb/liquidweb.go000066400000000000000000000111571434020463500215630ustar00rootroot00000000000000// Package liquidweb implements a DNS provider for solving the DNS-01 challenge using Liquid Web. package liquidweb import ( "errors" "fmt" "strconv" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" lw "github.com/liquidweb/liquidweb-go/client" "github.com/liquidweb/liquidweb-go/network" ) const defaultBaseURL = "https://api.stormondemand.com" // Environment variables names. const ( envNamespace = "LIQUID_WEB_" EnvURL = envNamespace + "URL" EnvUsername = envNamespace + "USERNAME" EnvPassword = envNamespace + "PASSWORD" EnvZone = envNamespace + "ZONE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string Username string Password string Zone string TTL int PollingInterval time.Duration PropagationTimeout time.Duration HTTPTimeout time.Duration } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { config := &Config{ BaseURL: defaultBaseURL, TTL: env.GetOrDefaultInt(EnvTTL, 300), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 1*time.Minute), } return config } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *lw.API recordIDs map[string]int recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for Liquid Web. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvUsername, EnvPassword, EnvZone) if err != nil { return nil, fmt.Errorf("liquidweb: %w", err) } config := NewDefaultConfig() config.BaseURL = env.GetOrFile(EnvURL) config.Username = values[EnvUsername] config.Password = values[EnvPassword] config.Zone = values[EnvZone] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Liquid Web. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("liquidweb: the configuration of the DNS provider is nil") } if config.BaseURL == "" { config.BaseURL = defaultBaseURL } if config.Zone == "" { return nil, errors.New("liquidweb: zone is missing") } if config.Username == "" { return nil, errors.New("liquidweb: username is missing") } if config.Password == "" { return nil, errors.New("liquidweb: password is missing") } // Initialize LW client. client, err := lw.NewAPI(config.Username, config.Password, config.BaseURL, int(config.HTTPTimeout.Seconds())) if err != nil { return nil, fmt.Errorf("liquidweb: could not create Liquid Web API client: %w", err) } return &DNSProvider{ config: config, recordIDs: make(map[string]int), client: client, }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (time.Duration, time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) params := &network.DNSRecordParams{ Name: dns01.UnFqdn(fqdn), RData: strconv.Quote(value), Type: "TXT", Zone: d.config.Zone, TTL: d.config.TTL, } dnsEntry, err := d.client.NetworkDNS.Create(params) if err != nil { return fmt.Errorf("liquidweb: could not create TXT record: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = int(dnsEntry.ID) d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("liquidweb: unknown record ID for '%s'", domain) } params := &network.DNSRecordParams{ID: recordID} _, err := d.client.NetworkDNS.Delete(params) if err != nil { return fmt.Errorf("liquidweb: could not remove TXT record: %w", err) } d.recordIDsMu.Lock() delete(d.recordIDs, token) d.recordIDsMu.Unlock() return nil } lego-4.9.1/providers/dns/liquidweb/liquidweb.toml000066400000000000000000000017401434020463500221260ustar00rootroot00000000000000Name = "Liquid Web" Description = '''''' URL = "https://liquidweb.com" Code = "liquidweb" Since = "v3.1.0" Example = ''' LIQUID_WEB_USERNAME=someuser \ LIQUID_WEB_PASSWORD=somepass \ LIQUID_WEB_ZONE=tacoman.com.net \ lego --email you@example.com --dns liquidweb --domains my.example.org run ''' [Configuration] [Configuration.Credentials] LIQUID_WEB_USERNAME = "Storm API Username" LIQUID_WEB_PASSWORD = "Storm API Password" LIQUID_WEB_ZONE = "DNS Zone" [Configuration.Additional] LIQUID_WEB_URL = "Storm API endpoint" LIQUID_WEB_TTL = "The TTL of the TXT record used for the DNS challenge" LIQUID_WEB_POLLING_INTERVAL = "Time between DNS propagation check" LIQUID_WEB_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" LIQUID_WEB_HTTP_TIMEOUT = "Maximum waiting time for the DNS records to be created (not verified)" [Links] API = "https://cart.liquidweb.com/storm/api/docs/v1/" GoClient = "https://github.com/liquidweb/liquidweb-go" lego-4.9.1/providers/dns/liquidweb/liquidweb_test.go000066400000000000000000000140361434020463500226210ustar00rootroot00000000000000package liquidweb import ( "fmt" "io" "net/http" "net/http/httptest" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvURL, EnvUsername, EnvPassword, EnvZone). WithDomain(envDomain) func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) config := NewDefaultConfig() config.Username = "blars" config.Password = "tacoman" config.BaseURL = server.URL config.Zone = "tacoman.com" provider, err := NewDNSProviderConfig(config) require.NoError(t, err) return provider, mux } func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvURL: "https://storm.com", EnvUsername: "blars", EnvPassword: "tacoman", EnvZone: "blars.com", }, }, { desc: "missing credentials", envVars: map[string]string{}, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_USERNAME,LIQUID_WEB_PASSWORD,LIQUID_WEB_ZONE", }, { desc: "missing username", envVars: map[string]string{ EnvPassword: "tacoman", EnvZone: "blars.com", }, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_USERNAME", }, { desc: "missing password", envVars: map[string]string{ EnvUsername: "blars", EnvZone: "blars.com", }, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_PASSWORD", }, { desc: "missing zone", envVars: map[string]string{ EnvUsername: "blars", EnvPassword: "tacoman", }, expected: "liquidweb: some credentials information are missing: LIQUID_WEB_ZONE", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string username string password string zone string expected string }{ { desc: "success", username: "acme", password: "secret", zone: "example.com", }, { desc: "missing credentials", username: "", password: "", zone: "", expected: "liquidweb: zone is missing", }, { desc: "missing username", username: "", password: "secret", zone: "example.com", expected: "liquidweb: username is missing", }, { desc: "missing password", username: "acme", password: "", zone: "example.com", expected: "liquidweb: password is missing", }, { desc: "missing zone", username: "acme", password: "secret", zone: "", expected: "liquidweb: zone is missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Username = test.username config.Password = test.password config.Zone = test.zone p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } }) } } func TestDNSProvider_Present(t *testing.T) { provider, mux := setupTest(t) mux.HandleFunc("/v1/Network/DNS/Record/create", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) username, password, ok := r.BasicAuth() assert.Equal(t, "blars", username) assert.Equal(t, "tacoman", password) assert.True(t, ok) reqBody, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } expectedReqBody := ` { "params": { "name": "_acme-challenge.tacoman.com", "rdata": "\"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\"", "ttl": 300, "type": "TXT", "zone": "tacoman.com" } }` assert.JSONEq(t, expectedReqBody, string(reqBody)) w.WriteHeader(http.StatusOK) _, err = fmt.Fprintf(w, `{ "type": "TXT", "name": "_acme-challenge.tacoman.com", "rdata": "\"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\"", "ttl": 300, "id": 1234567, "prio": null }`) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) err := provider.Present("tacoman.com", "", "") require.NoError(t, err) } func TestDNSProvider_CleanUp(t *testing.T) { provider, mux := setupTest(t) mux.HandleFunc("/v1/Network/DNS/Record/delete", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) username, password, ok := r.BasicAuth() assert.Equal(t, "blars", username) assert.Equal(t, "tacoman", password) assert.True(t, ok) _, err := fmt.Fprintf(w, `{"deleted": "123"}`) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) provider.recordIDs["123"] = 1234567 err := provider.CleanUp("tacoman.com.", "123", "") require.NoError(t, err, "fail to remove TXT record") } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/loopia/000077500000000000000000000000001434020463500165405ustar00rootroot00000000000000lego-4.9.1/providers/dns/loopia/internal/000077500000000000000000000000001434020463500203545ustar00rootroot00000000000000lego-4.9.1/providers/dns/loopia/internal/client.go000066400000000000000000000105041434020463500221610ustar00rootroot00000000000000package internal import ( "bytes" "encoding/xml" "errors" "fmt" "io" "net/http" "strings" "time" ) // DefaultBaseURL is url to the XML-RPC api. const DefaultBaseURL = "https://api.loopia.se/RPCSERV" // Client the Loopia client. type Client struct { APIUser string APIPassword string BaseURL string HTTPClient *http.Client } // NewClient creates a new Loopia Client. func NewClient(apiUser, apiPassword string) *Client { return &Client{ APIUser: apiUser, APIPassword: apiPassword, BaseURL: DefaultBaseURL, HTTPClient: &http.Client{Timeout: 10 * time.Second}, } } // AddTXTRecord adds a TXT record. func (c *Client) AddTXTRecord(domain string, subdomain string, ttl int, value string) error { call := &methodCall{ MethodName: "addZoneRecord", Params: []param{ paramString{Value: c.APIUser}, paramString{Value: c.APIPassword}, paramString{Value: domain}, paramString{Value: subdomain}, paramStruct{ StructMembers: []structMember{ structMemberString{Name: "type", Value: "TXT"}, structMemberInt{Name: "ttl", Value: ttl}, structMemberInt{Name: "priority", Value: 0}, structMemberString{Name: "rdata", Value: value}, structMemberInt{Name: "record_id", Value: 0}, }, }, }, } resp := &responseString{} err := c.rpcCall(call, resp) if err != nil { return err } return checkResponse(resp.Value) } // RemoveTXTRecord removes a TXT record. func (c *Client) RemoveTXTRecord(domain string, subdomain string, recordID int) error { call := &methodCall{ MethodName: "removeZoneRecord", Params: []param{ paramString{Value: c.APIUser}, paramString{Value: c.APIPassword}, paramString{Value: domain}, paramString{Value: subdomain}, paramInt{Value: recordID}, }, } resp := &responseString{} err := c.rpcCall(call, resp) if err != nil { return err } return checkResponse(resp.Value) } // GetTXTRecords gets TXT records. func (c *Client) GetTXTRecords(domain string, subdomain string) ([]RecordObj, error) { call := &methodCall{ MethodName: "getZoneRecords", Params: []param{ paramString{Value: c.APIUser}, paramString{Value: c.APIPassword}, paramString{Value: domain}, paramString{Value: subdomain}, }, } resp := &recordObjectsResponse{} err := c.rpcCall(call, resp) return resp.Params, err } // RemoveSubdomain remove a sub-domain. func (c *Client) RemoveSubdomain(domain, subdomain string) error { call := &methodCall{ MethodName: "removeSubdomain", Params: []param{ paramString{Value: c.APIUser}, paramString{Value: c.APIPassword}, paramString{Value: domain}, paramString{Value: subdomain}, }, } resp := &responseString{} err := c.rpcCall(call, resp) if err != nil { return err } return checkResponse(resp.Value) } // rpcCall makes an XML-RPC call to Loopia's RPC endpoint // by marshaling the data given in the call argument to XML and sending that via HTTP Post to Loopia. // The response is then unmarshalled into the resp argument. func (c *Client) rpcCall(call *methodCall, resp response) error { body, err := xml.MarshalIndent(call, "", " ") if err != nil { return fmt.Errorf("error during unmarshalling the request body: %w", err) } body = append([]byte(``+"\n"), body...) respBody, err := c.httpPost(c.BaseURL, "text/xml", bytes.NewReader(body)) if err != nil { return err } err = xml.Unmarshal(respBody, resp) if err != nil { return fmt.Errorf("error during unmarshalling the response body: %w", err) } if resp.faultCode() != 0 { return rpcError{ faultCode: resp.faultCode(), faultString: strings.TrimSpace(resp.faultString()), } } return nil } func (c *Client) httpPost(url string, bodyType string, body io.Reader) ([]byte, error) { resp, err := c.HTTPClient.Post(url, bodyType, body) if err != nil { return nil, fmt.Errorf("HTTP Post Error: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP Post Error: %d", resp.StatusCode) } b, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("HTTP Post Error: %w", err) } return b, nil } func checkResponse(value string) error { switch v := strings.TrimSpace(value); v { case "OK": return nil case "AUTH_ERROR": return errors.New("authentication error") default: return fmt.Errorf("unknown error: %q", v) } } lego-4.9.1/providers/dns/loopia/internal/client_test.go000066400000000000000000000166521434020463500232320ustar00rootroot00000000000000package internal import ( "encoding/xml" "fmt" "io" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_AddZoneRecord(t *testing.T) { serverResponses := map[string]string{ addZoneRecordGoodAuth: responseOk, addZoneRecordBadAuth: responseAuthError, addZoneRecordNonValidDomain: responseUnknownError, addZoneRecordEmptyResponse: "", } serverURL := createFakeServer(t, serverResponses) testCases := []struct { desc string password string domain string err string }{ { desc: "auth ok", password: "goodpassword", domain: exampleDomain, }, { desc: "auth error", password: "badpassword", domain: exampleDomain, err: "authentication error", }, { desc: "unknown error", password: "goodpassword", domain: "badexample.com", err: `unknown error: "UNKNOWN_ERROR"`, }, { desc: "empty response", password: "goodpassword", domain: "empty.com", err: "error during unmarshalling the response body: EOF", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { client := NewClient("apiuser", test.password) client.BaseURL = serverURL + "/" err := client.AddTXTRecord(test.domain, exampleSubDomain, 123, "TXTrecord") if test.err == "" { require.NoError(t, err) } else { require.Error(t, err) assert.EqualError(t, err, test.err) } }) } } func TestClient_RemoveSubdomain(t *testing.T) { serverResponses := map[string]string{ removeSubdomainGoodAuth: responseOk, removeSubdomainBadAuth: responseAuthError, removeSubdomainNonValidDomain: responseUnknownError, removeSubdomainEmptyResponse: "", } serverURL := createFakeServer(t, serverResponses) testCases := []struct { desc string password string domain string err string }{ { desc: "auth ok", password: "goodpassword", domain: exampleDomain, }, { desc: "auth error", password: "badpassword", domain: exampleDomain, err: "authentication error", }, { desc: "unknown error", password: "goodpassword", domain: "badexample.com", err: `unknown error: "UNKNOWN_ERROR"`, }, { desc: "empty response", password: "goodpassword", domain: "empty.com", err: "error during unmarshalling the response body: EOF", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { client := NewClient("apiuser", test.password) client.BaseURL = serverURL + "/" err := client.RemoveSubdomain(test.domain, exampleSubDomain) if test.err == "" { require.NoError(t, err) } else { require.Error(t, err) assert.EqualError(t, err, test.err) } }) } } func TestClient_RemoveZoneRecord(t *testing.T) { serverResponses := map[string]string{ removeRecordGoodAuth: responseOk, removeRecordBadAuth: responseAuthError, removeRecordNonValidDomain: responseUnknownError, removeRecordEmptyResponse: "", } serverURL := createFakeServer(t, serverResponses) testCases := []struct { desc string password string domain string err string }{ { desc: "auth ok", password: "goodpassword", domain: exampleDomain, }, { desc: "auth error", password: "badpassword", domain: exampleDomain, err: "authentication error", }, { desc: "uknown error", password: "goodpassword", domain: "badexample.com", err: `unknown error: "UNKNOWN_ERROR"`, }, { desc: "empty response", password: "goodpassword", domain: "empty.com", err: "error during unmarshalling the response body: EOF", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { client := NewClient("apiuser", test.password) client.BaseURL = serverURL + "/" err := client.RemoveTXTRecord(test.domain, exampleSubDomain, 12345678) if test.err == "" { require.NoError(t, err) } else { require.Error(t, err) assert.EqualError(t, err, test.err) } }) } } func TestClient_GetZoneRecord(t *testing.T) { serverResponses := map[string]string{ getZoneRecords: getZoneRecordsResponse, } serverURL := createFakeServer(t, serverResponses) client := NewClient("apiuser", "goodpassword") client.BaseURL = serverURL + "/" recordObjs, err := client.GetTXTRecords(exampleDomain, exampleSubDomain) require.NoError(t, err) expected := []RecordObj{ { Type: "TXT", TTL: 300, Priority: 0, Rdata: exampleRdata, RecordID: 12345678, }, } assert.EqualValues(t, expected, recordObjs) } func TestClient_rpcCall_404(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.WriteHeader(http.StatusNotFound) _, err = fmt.Fprint(w, "") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } })) t.Cleanup(server.Close) call := &methodCall{ MethodName: "dummyMethod", Params: []param{ paramString{Value: "test1"}, }, } client := NewClient("apiuser", "apipassword") client.BaseURL = server.URL + "/" err := client.rpcCall(call, &responseString{}) assert.EqualError(t, err, "HTTP Post Error: 404") } func TestClient_rpcCall_RPCError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } _, err = fmt.Fprint(w, responseRPCError) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } })) t.Cleanup(server.Close) call := &methodCall{ MethodName: "getDomains", Params: []param{ paramString{Value: "test1"}, }, } client := NewClient("apiuser", "apipassword") client.BaseURL = server.URL + "/" err := client.rpcCall(call, &responseString{}) assert.EqualError(t, err, "RPC Error: (201) Method signature error: 42") } func TestUnmarshallFaultyRecordObject(t *testing.T) { testCases := []struct { desc string xml string }{ { desc: "faulty name", xml: "name", }, { desc: "faulty string", xml: "foo", }, { desc: "faulty int", xml: "1", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { resp := &RecordObj{} err := xml.Unmarshal([]byte(test.xml), resp) require.Error(t, err) }) } } func createFakeServer(t *testing.T, serverResponses map[string]string) string { t.Helper() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Content-Type") != "text/xml" { http.Error(w, fmt.Sprintf("invalid content type: %s", r.Header.Get("Content-Type")), http.StatusBadRequest) return } req, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } resp, ok := serverResponses[string(req)] if !ok { http.Error(w, "no response for request", http.StatusBadRequest) return } _, err = fmt.Fprint(w, resp) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) server := httptest.NewServer(handler) t.Cleanup(server.Close) return server.URL } lego-4.9.1/providers/dns/loopia/internal/mock_test.go000066400000000000000000000321751434020463500227030ustar00rootroot00000000000000package internal const ( exampleDomain = "example.com" exampleSubDomain = "_acme-challenge" exampleRdata = "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM" ) // Testdata based on real traffic between an xml-rpc client and the api. const responseOk = ` OK ` const responseAuthError = ` AUTH_ERROR ` const responseUnknownError = ` UNKNOWN_ERROR ` const responseRPCError = ` faultCode 201 faultString Method signature error: 42 ` const addZoneRecordGoodAuth = ` addZoneRecord apiuser goodpassword example.com _acme-challenge type TXT ttl 123 priority 0 rdata TXTrecord record_id 0 ` const addZoneRecordBadAuth = ` addZoneRecord apiuser badpassword example.com _acme-challenge type TXT ttl 123 priority 0 rdata TXTrecord record_id 0 ` const addZoneRecordNonValidDomain = ` addZoneRecord apiuser goodpassword badexample.com _acme-challenge type TXT ttl 123 priority 0 rdata TXTrecord record_id 0 ` const addZoneRecordEmptyResponse = ` addZoneRecord apiuser goodpassword empty.com _acme-challenge type TXT ttl 123 priority 0 rdata TXTrecord record_id 0 ` const getZoneRecords = ` getZoneRecords apiuser goodpassword example.com _acme-challenge ` const getZoneRecordsResponse = ` rdata LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM record_id 12345678 priority 0 ttl 300 type TXT ` const removeRecordGoodAuth = ` removeZoneRecord apiuser goodpassword example.com _acme-challenge 12345678 ` const removeRecordBadAuth = ` removeZoneRecord apiuser badpassword example.com _acme-challenge 12345678 ` const removeRecordNonValidDomain = ` removeZoneRecord apiuser goodpassword badexample.com _acme-challenge 12345678 ` const removeRecordEmptyResponse = ` removeZoneRecord apiuser goodpassword empty.com _acme-challenge 12345678 ` const removeSubdomainGoodAuth = ` removeSubdomain apiuser goodpassword example.com _acme-challenge ` const removeSubdomainBadAuth = ` removeSubdomain apiuser badpassword example.com _acme-challenge ` const removeSubdomainNonValidDomain = ` removeSubdomain apiuser goodpassword badexample.com _acme-challenge ` const removeSubdomainEmptyResponse = ` removeSubdomain apiuser goodpassword empty.com _acme-challenge ` lego-4.9.1/providers/dns/loopia/internal/types.go000066400000000000000000000065231434020463500220550ustar00rootroot00000000000000package internal import ( "encoding/xml" "fmt" "strings" ) // types for XML-RPC method calls and parameters type param interface { param() } type paramString struct { XMLName xml.Name `xml:"param"` Value string `xml:"value>string"` } func (p paramString) param() {} type paramInt struct { XMLName xml.Name `xml:"param"` Value int `xml:"value>int"` } func (p paramInt) param() {} type paramStruct struct { XMLName xml.Name `xml:"param"` StructMembers []structMember `xml:"value>struct>member"` } func (p paramStruct) param() {} type structMember interface { structMember() } type structMemberString struct { Name string `xml:"name"` Value string `xml:"value>string"` } func (m structMemberString) structMember() {} type structMemberInt struct { Name string `xml:"name"` Value int `xml:"value>int"` } func (m structMemberInt) structMember() {} type methodCall struct { XMLName xml.Name `xml:"methodCall"` MethodName string `xml:"methodName"` Params []param `xml:"params>param"` } // types for XML-RPC responses type response interface { faultCode() int faultString() string } type responseString struct { responseFault Value string `xml:"params>param>value>string"` } type responseFault struct { FaultCode int `xml:"fault>value>struct>member>value>int"` FaultString string `xml:"fault>value>struct>member>value>string"` } func (r responseFault) faultCode() int { return r.FaultCode } func (r responseFault) faultString() string { return r.FaultString } type rpcError struct { faultCode int faultString string } func (e rpcError) Error() string { return fmt.Sprintf("RPC Error: (%d) %s", e.faultCode, e.faultString) } type recordObjectsResponse struct { responseFault XMLName xml.Name `xml:"methodResponse"` Params []RecordObj `xml:"params>param>value>array>data>value>struct"` } type RecordObj struct { Type string TTL int Priority int Rdata string RecordID int } func (r *RecordObj) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var name string for { t, err := d.Token() if err != nil { return err } switch tt := t.(type) { case xml.StartElement: switch tt.Name.Local { case "name": // The name of the record object: var s string if err = d.DecodeElement(&s, &start); err != nil { return err } name = strings.TrimSpace(s) case "string": // A string value of the record object: if err = r.decodeValueString(name, d, start); err != nil { return err } case "int": // An int value of the record object: if err = r.decodeValueInt(name, d, start); err != nil { return err } } case xml.EndElement: if tt == start.End() { return nil } } } } func (r *RecordObj) decodeValueString(name string, d *xml.Decoder, start xml.StartElement) error { var s string if err := d.DecodeElement(&s, &start); err != nil { return err } s = strings.TrimSpace(s) switch name { case "type": r.Type = s case "rdata": r.Rdata = s } return nil } func (r *RecordObj) decodeValueInt(name string, d *xml.Decoder, start xml.StartElement) error { var i int if err := d.DecodeElement(&i, &start); err != nil { return err } switch name { case "record_id": r.RecordID = i case "ttl": r.TTL = i case "priority": r.Priority = i } return nil } lego-4.9.1/providers/dns/loopia/loopia.go000066400000000000000000000126461434020463500203630ustar00rootroot00000000000000// Package loopia implements a DNS provider for solving the DNS-01 challenge using loopia DNS. package loopia import ( "errors" "fmt" "net/http" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/loopia/internal" ) const minTTL = 300 // Environment variables names. const ( envNamespace = "LOOPIA_" EnvAPIUser = envNamespace + "API_USER" EnvAPIPassword = envNamespace + "API_PASSWORD" EnvAPIURL = envNamespace + "API_URL" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) type dnsClient interface { AddTXTRecord(domain string, subdomain string, ttl int, value string) error RemoveTXTRecord(domain string, subdomain string, recordID int) error GetTXTRecords(domain string, subdomain string) ([]internal.RecordObj, error) RemoveSubdomain(domain, subdomain string) error } // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string APIUser string APIPassword string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 40*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 60*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client dnsClient inProgressInfo map[string]int inProgressMu sync.Mutex findZoneByFqdn func(fqdn string) (string, error) } // NewDNSProvider returns a DNSProvider instance configured for Loopia. // Credentials must be passed in the environment variables: // LOOPIA_API_USER, LOOPIA_API_PASSWORD. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIUser, EnvAPIPassword) if err != nil { return nil, fmt.Errorf("loopia: %w", err) } config := NewDefaultConfig() config.APIUser = values[EnvAPIUser] config.APIPassword = values[EnvAPIPassword] config.BaseURL = env.GetOrDefaultString(EnvAPIURL, internal.DefaultBaseURL) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Loopia. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("loopia: the configuration of the DNS provider is nil") } if config.APIUser == "" || config.APIPassword == "" { return nil, errors.New("loopia: credentials missing") } // Min value for TTL is 300 if config.TTL < 300 { config.TTL = 300 } client := internal.NewClient(config.APIUser, config.APIPassword) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } if config.BaseURL != "" { client.BaseURL = config.BaseURL } return &DNSProvider{ config: config, client: client, findZoneByFqdn: dns01.FindZoneByFqdn, inProgressInfo: make(map[string]int), }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) subdomain, authZone := d.splitDomain(fqdn) err := d.client.AddTXTRecord(authZone, subdomain, d.config.TTL, value) if err != nil { return fmt.Errorf("loopia: failed to add TXT record: %w", err) } txtRecords, err := d.client.GetTXTRecords(authZone, subdomain) if err != nil { return fmt.Errorf("loopia: failed to get TXT records: %w", err) } d.inProgressMu.Lock() defer d.inProgressMu.Unlock() for _, r := range txtRecords { if r.Rdata == value { d.inProgressInfo[token] = r.RecordID return nil } } return errors.New("loopia: failed to find the stored TXT record") } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) subdomain, authZone := d.splitDomain(fqdn) d.inProgressMu.Lock() defer d.inProgressMu.Unlock() err := d.client.RemoveTXTRecord(authZone, subdomain, d.inProgressInfo[token]) if err != nil { return fmt.Errorf("loopia: failed to remove TXT record: %w", err) } records, err := d.client.GetTXTRecords(authZone, subdomain) if err != nil { return fmt.Errorf("loopia: failed to get TXT records: %w", err) } if len(records) > 0 { return nil } err = d.client.RemoveSubdomain(authZone, subdomain) if err != nil { return fmt.Errorf("loopia: failed to remove sub-domain: %w", err) } return nil } func (d *DNSProvider) splitDomain(fqdn string) (string, string) { authZone, _ := d.findZoneByFqdn(fqdn) authZone = dns01.UnFqdn(authZone) subdomain := strings.TrimSuffix(dns01.UnFqdn(fqdn), "."+authZone) return subdomain, authZone } lego-4.9.1/providers/dns/loopia/loopia.toml000066400000000000000000000020401434020463500207140ustar00rootroot00000000000000Name = "Loopia" Description = '''''' URL = "https://loopia.com" Code = "loopia" Since = "v4.2.0" Example = ''' LOOPIA_API_USER=xxxxxxxx \ LOOPIA_API_PASSWORD=yyyyyyyy \ lego --email my@email.com --dns loopia --domains my.domain.com run ''' Additional = ''' ### API user You can [generate a new API user](https://customerzone.loopia.com/api/) from your account page. It needs to have the following permissions: * addZoneRecord * getZoneRecords * removeZoneRecord * removeSubdomain ''' [Configuration] [Configuration.Credentials] LOOPIA_API_USER = "API username" LOOPIA_API_PASSWORD = "API password" [Configuration.Additional] LOOPIA_API_URL = "API endpoint. Ex: https://api.loopia.se/RPCSERV or https://api.loopia.rs/RPCSERV" LOOPIA_POLLING_INTERVAL = "Time between DNS propagation check" LOOPIA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" LOOPIA_TTL = "The TTL of the TXT record used for the DNS challenge" LOOPIA_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.loopia.com/api" lego-4.9.1/providers/dns/loopia/loopia_mock_test.go000066400000000000000000000136761434020463500224370ustar00rootroot00000000000000package loopia import ( "errors" "fmt" "testing" "github.com/go-acme/lego/v4/providers/dns/loopia/internal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) const ( exampleDomain = "example.com" exampleSubDomain = "_acme-challenge" exampleRdata = "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM" ) func TestDNSProvider_Present(t *testing.T) { mockedFindZoneByFqdn := func(fqdn string) (string, error) { return exampleDomain + ".", nil } testCases := []struct { desc string getTXTRecordsError error getTXTRecordsReturn []internal.RecordObj addTXTRecordError error callAddTXTRecord bool callGetTXTRecords bool expectedError string expectedInProgressTokenInfo int }{ { desc: "Present OK", getTXTRecordsReturn: []internal.RecordObj{{Type: "TXT", Rdata: exampleRdata, RecordID: 12345678}}, callAddTXTRecord: true, callGetTXTRecords: true, expectedInProgressTokenInfo: 12345678, }, { desc: "AddTXTRecord fails", addTXTRecordError: fmt.Errorf("unknown error: 'ADDTXT'"), callAddTXTRecord: true, expectedError: "loopia: failed to add TXT record: unknown error: 'ADDTXT'", }, { desc: "GetTXTRecords fails", getTXTRecordsError: fmt.Errorf("unknown error: 'GETTXT'"), callAddTXTRecord: true, callGetTXTRecords: true, expectedError: "loopia: failed to get TXT records: unknown error: 'GETTXT'", }, { desc: "Failed to get ID", callAddTXTRecord: true, callGetTXTRecords: true, expectedError: "loopia: failed to find the stored TXT record", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIUser = "apiuser" config.APIPassword = "password" client := &mockedClient{} provider, err := NewDNSProviderConfig(config) require.NoError(t, err) provider.findZoneByFqdn = mockedFindZoneByFqdn provider.client = client if test.callAddTXTRecord { client.On("AddTXTRecord", exampleDomain, exampleSubDomain, config.TTL, exampleRdata).Return(test.addTXTRecordError) } if test.callGetTXTRecords { client.On("GetTXTRecords", exampleDomain, exampleSubDomain).Return(test.getTXTRecordsReturn, test.getTXTRecordsError) } err = provider.Present(exampleDomain, "token", "key") client.AssertExpectations(t) if test.expectedError == "" { require.NoError(t, err) assert.Equal(t, test.expectedInProgressTokenInfo, provider.inProgressInfo["token"]) } else { require.Error(t, err) assert.EqualError(t, err, test.expectedError) } }) } } func TestDNSProvider_Cleanup(t *testing.T) { mockedFindZoneByFqdn := func(fqdn string) (string, error) { return "example.com.", nil } testCases := []struct { desc string getTXTRecordsError error getTXTRecordsReturn []internal.RecordObj removeTXTRecordError error removeSubdomainError error callAddTXTRecord bool callGetTXTRecords bool callRemoveSubdomain bool expectedError string }{ { desc: "Cleanup Ok", callAddTXTRecord: true, callGetTXTRecords: true, callRemoveSubdomain: true, }, { desc: "removeTXTRecord failed", removeTXTRecordError: errors.New("authentication error"), callAddTXTRecord: true, expectedError: "loopia: failed to remove TXT record: authentication error", }, { desc: "removeSubdomain failed", removeSubdomainError: errors.New(`unknown error: "UNKNOWN_ERROR"`), callAddTXTRecord: true, callGetTXTRecords: true, callRemoveSubdomain: true, expectedError: `loopia: failed to remove sub-domain: unknown error: "UNKNOWN_ERROR"`, }, { desc: "Dont call removeSubdomain when records", getTXTRecordsReturn: []internal.RecordObj{{Type: "TXT", Rdata: "LEFTOVER"}}, callAddTXTRecord: true, callGetTXTRecords: true, callRemoveSubdomain: false, }, { desc: "getTXTRecords failed", getTXTRecordsError: errors.New(`unknown error: "UNKNOWN_ERROR"`), callAddTXTRecord: true, callGetTXTRecords: true, callRemoveSubdomain: false, expectedError: `loopia: failed to get TXT records: unknown error: "UNKNOWN_ERROR"`, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIUser = "apiuser" config.APIPassword = "password" client := &mockedClient{} provider, err := NewDNSProviderConfig(config) require.NoError(t, err) provider.findZoneByFqdn = mockedFindZoneByFqdn provider.client = client provider.inProgressInfo["token"] = 12345678 if test.callAddTXTRecord { client.On("RemoveTXTRecord", "example.com", "_acme-challenge", 12345678).Return(test.removeTXTRecordError) } if test.callGetTXTRecords { client.On("GetTXTRecords", "example.com", "_acme-challenge").Return(test.getTXTRecordsReturn, test.getTXTRecordsError) } if test.callRemoveSubdomain { client.On("RemoveSubdomain", "example.com", "_acme-challenge").Return(test.removeSubdomainError) } err = provider.CleanUp("example.com", "token", "key") client.AssertExpectations(t) if test.expectedError == "" { require.NoError(t, err) } else { require.Error(t, err) assert.EqualError(t, err, test.expectedError) } }) } } type mockedClient struct { mock.Mock } func (c *mockedClient) RemoveTXTRecord(domain string, subdomain string, recordID int) error { args := c.Called(domain, subdomain, recordID) return args.Error(0) } func (c *mockedClient) AddTXTRecord(domain string, subdomain string, ttl int, value string) error { args := c.Called(domain, subdomain, ttl, value) return args.Error(0) } func (c *mockedClient) GetTXTRecords(domain string, subdomain string) ([]internal.RecordObj, error) { args := c.Called(domain, subdomain) return args.Get(0).([]internal.RecordObj), args.Error(1) } func (c *mockedClient) RemoveSubdomain(domain, subdomain string) error { args := c.Called(domain, subdomain) return args.Error(0) } lego-4.9.1/providers/dns/loopia/loopia_test.go000066400000000000000000000105671434020463500214220ustar00rootroot00000000000000package loopia import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIUser, EnvAPIPassword, EnvTTL, EnvPollingInterval, EnvPropagationTimeout, EnvHTTPTimeout). WithDomain(envDomain) func TestSplitDomain(t *testing.T) { provider := &DNSProvider{ findZoneByFqdn: func(fqdn string) (string, error) { return "example.com.", nil }, } testCases := []struct { desc string fqdn string subdomain string domain string }{ { desc: "single subdomain", fqdn: "subdomain.example.com", subdomain: "subdomain", domain: "example.com", }, { desc: "double subdomain", fqdn: "sub.domain.example.com", subdomain: "sub.domain", domain: "example.com", }, { desc: "asterisk subdomain", fqdn: "*.example.com", subdomain: "*", domain: "example.com", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { subdomain, domain := provider.splitDomain(test.fqdn) assert.Equal(t, test.subdomain, subdomain) assert.Equal(t, test.domain, domain) }) } } func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expectedError string }{ { desc: "success", envVars: map[string]string{ EnvAPIUser: "user", EnvAPIPassword: "secret", }, }, { desc: "missing API user", envVars: map[string]string{ EnvAPIUser: "", EnvAPIPassword: "secret", }, expectedError: "loopia: some credentials information are missing: LOOPIA_API_USER", }, { desc: "missing API password", envVars: map[string]string{ EnvAPIUser: "user", EnvAPIPassword: "", }, expectedError: "loopia: some credentials information are missing: LOOPIA_API_PASSWORD", }, { desc: "missing credentials", envVars: map[string]string{}, expectedError: "loopia: some credentials information are missing: LOOPIA_API_USER,LOOPIA_API_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expectedError == "" { require.NoError(t, err) require.NotNil(t, p) } else { require.Error(t, err) require.EqualError(t, err, test.expectedError) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string config *Config expectedTTL int expectedError string }{ { desc: "success", config: &Config{ APIUser: "user", APIPassword: "secret", TTL: 3600, }, expectedTTL: 3600, }, { desc: "nil config user", expectedError: "loopia: the configuration of the DNS provider is nil", }, { desc: "empty user", config: &Config{ APIUser: "", APIPassword: "secret", TTL: 3600, }, expectedError: "loopia: credentials missing", }, { desc: "empty password", config: &Config{ APIUser: "user", APIPassword: "", TTL: 3600, }, expectedTTL: 3600, expectedError: "loopia: credentials missing", }, { desc: "too low TTL", config: &Config{ APIUser: "user", APIPassword: "secret", TTL: 299, }, expectedTTL: 300, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { p, err := NewDNSProviderConfig(test.config) if test.expectedError == "" { require.NoError(t, err) require.NotNil(t, p) assert.Equal(t, test.expectedTTL, p.config.TTL) } else { require.Error(t, err) assert.EqualError(t, err, test.expectedError) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/luadns/000077500000000000000000000000001434020463500165435ustar00rootroot00000000000000lego-4.9.1/providers/dns/luadns/internal/000077500000000000000000000000001434020463500203575ustar00rootroot00000000000000lego-4.9.1/providers/dns/luadns/internal/client.go000066400000000000000000000073471434020463500221770ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) // defaultBaseURL represents the API endpoint to call. const defaultBaseURL = "https://api.luadns.com" // Client Lua DNS API client. type Client struct { HTTPClient *http.Client BaseURL string apiUsername string apiToken string } // NewClient creates a new Client. func NewClient(apiUsername, apiToken string) *Client { return &Client{ HTTPClient: http.DefaultClient, BaseURL: defaultBaseURL, apiUsername: apiUsername, apiToken: apiToken, } } // ListZones gets all the hosted zones. // https://luadns.com/api.html#list-zones func (d *Client) ListZones() ([]DNSZone, error) { resp, err := d.do(http.MethodGet, "/v1/zones", nil) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) var errResp errorResponse err = json.Unmarshal(bodyBytes, &errResp) if err == nil { return nil, fmt.Errorf("api call error: Status=%v: %w", resp.StatusCode, errResp) } return nil, fmt.Errorf("api call error: Status=%d: %s", resp.StatusCode, string(bodyBytes)) } var zones []DNSZone err = json.NewDecoder(resp.Body).Decode(&zones) if err != nil { return nil, fmt.Errorf("failed to unmarshal response body: %w", err) } return zones, nil } // CreateRecord creates a new record in a zone. // https://luadns.com/api.html#create-a-record func (d *Client) CreateRecord(zone DNSZone, newRecord DNSRecord) (*DNSRecord, error) { body, err := json.Marshal(newRecord) if err != nil { return nil, fmt.Errorf("failed to marshal request body: %w", err) } resource := fmt.Sprintf("/v1/zones/%d/records", zone.ID) resp, err := d.do(http.MethodPost, resource, bytes.NewReader(body)) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) var errResp errorResponse err = json.Unmarshal(bodyBytes, &errResp) if err == nil { return nil, fmt.Errorf("could not create record %v: Status=%d: %w", string(body), resp.StatusCode, errResp) } return nil, fmt.Errorf("could not create record %v: Status=%d: %s", string(body), resp.StatusCode, string(bodyBytes)) } var record *DNSRecord err = json.NewDecoder(resp.Body).Decode(&record) if err != nil { return nil, fmt.Errorf("failed to unmarshal response body: %w", err) } return record, nil } // DeleteRecord deletes a record. // https://luadns.com/api.html#delete-a-record func (d *Client) DeleteRecord(record *DNSRecord) error { body, err := json.Marshal(record) if err != nil { return fmt.Errorf("failed to marshal request body: %w", err) } resource := fmt.Sprintf("/v1/zones/%d/records/%d", record.ZoneID, record.ID) resp, err := d.do(http.MethodDelete, resource, bytes.NewReader(body)) if err != nil { return err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) var errResp errorResponse err = json.Unmarshal(bodyBytes, &errResp) if err == nil { return fmt.Errorf("could not delete record %v: Status=%d: %w", string(body), resp.StatusCode, errResp) } return fmt.Errorf("could not delete record %v: Status=%d: %s", string(body), resp.StatusCode, string(bodyBytes)) } return nil } func (d *Client) do(method, uri string, body io.Reader) (*http.Response, error) { req, err := http.NewRequest(method, fmt.Sprintf("%s%s", d.BaseURL, uri), body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.SetBasicAuth(d.apiUsername, d.apiToken) return d.HTTPClient.Do(req) } lego-4.9.1/providers/dns/luadns/internal/client_test.go000066400000000000000000000101421434020463500232210ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_ListZones(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient("me", "secretA") client.BaseURL = server.URL mux.HandleFunc("/v1/zones", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, fmt.Sprintf("invalid method: %s", req.Method), http.StatusMethodNotAllowed) return } auth := req.Header.Get("Authorization") if auth != "Basic bWU6c2VjcmV0QQ==" { http.Error(rw, fmt.Sprintf("invalid authentication: %s", auth), http.StatusUnauthorized) return } file, err := os.Open("./fixtures/list_zones.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) zones, err := client.ListZones() require.NoError(t, err) expected := []DNSZone{ { ID: 1, Name: "example.com", Synced: false, QueriesCount: 0, RecordsCount: 3, AliasesCount: 0, RedirectsCount: 0, ForwardsCount: 0, TemplateID: 0, }, { ID: 2, Name: "example.net", Synced: false, QueriesCount: 0, RecordsCount: 3, AliasesCount: 0, RedirectsCount: 0, ForwardsCount: 0, TemplateID: 0, }, } assert.Equal(t, expected, zones) } func TestClient_CreateRecord(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient("me", "secretB") client.BaseURL = server.URL mux.HandleFunc("/v1/zones/1/records", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, fmt.Sprintf("invalid method: %s", req.Method), http.StatusMethodNotAllowed) return } auth := req.Header.Get("Authorization") if auth != "Basic bWU6c2VjcmV0Qg==" { http.Error(rw, fmt.Sprintf("invalid authentication: %s", auth), http.StatusUnauthorized) return } file, err := os.Open("./fixtures/create_record.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) zone := DNSZone{ID: 1} record := DNSRecord{ Name: "example.com.", Type: "MX", Content: "10 mail.example.com.", TTL: 300, } newRecord, err := client.CreateRecord(zone, record) require.NoError(t, err) expected := &DNSRecord{ ID: 100, Name: "example.com.", Type: "MX", Content: "10 mail.example.com.", TTL: 300, ZoneID: 1, } assert.Equal(t, expected, newRecord) } func TestClient_DeleteRecord(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient("me", "secretC") client.BaseURL = server.URL mux.HandleFunc("/v1/zones/1/records/2", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodDelete { http.Error(rw, fmt.Sprintf("invalid method: %s", req.Method), http.StatusMethodNotAllowed) return } auth := req.Header.Get("Authorization") if auth != "Basic bWU6c2VjcmV0Qw==" { http.Error(rw, fmt.Sprintf("invalid authentication: %s", auth), http.StatusUnauthorized) return } file, err := os.Open("./fixtures/delete_record.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) record := &DNSRecord{ ID: 2, Name: "example.com.", Type: "MX", Content: "10 mail.example.com.", TTL: 300, ZoneID: 1, } err := client.DeleteRecord(record) require.NoError(t, err) } lego-4.9.1/providers/dns/luadns/internal/fixtures/000077500000000000000000000000001434020463500222305ustar00rootroot00000000000000lego-4.9.1/providers/dns/luadns/internal/fixtures/create_record.json000066400000000000000000000003401434020463500257210ustar00rootroot00000000000000{ "id": 100, "name": "example.com.", "type": "MX", "content": "10 mail.example.com.", "ttl": 300, "zone_id": 1, "created_at": "2015-01-17T14:04:35.251785849Z", "updated_at": "2015-01-17T14:04:35.251785972Z" }lego-4.9.1/providers/dns/luadns/internal/fixtures/delete_record.json000066400000000000000000000003401434020463500257200ustar00rootroot00000000000000{ "id": 100, "name": "example.com.", "type": "MX", "content": "10 mail.example.com.", "ttl": 300, "zone_id": 1, "created_at": "2015-01-17T14:04:35.251785849Z", "updated_at": "2015-01-17T14:04:35.251785972Z" }lego-4.9.1/providers/dns/luadns/internal/fixtures/list_zones.json000066400000000000000000000006561434020463500253230ustar00rootroot00000000000000[ { "id": 1, "name": "example.com", "synced": false, "queries_count": 0, "records_count": 3, "aliases_count": 0, "redirects_count": 0, "forwards_count": 0, "template_id": 0 }, { "id": 2, "name": "example.net", "synced": false, "queries_count": 0, "records_count": 3, "aliases_count": 0, "redirects_count": 0, "forwards_count": 0, "template_id": 0 } ]lego-4.9.1/providers/dns/luadns/internal/model.go000066400000000000000000000021031434020463500220020ustar00rootroot00000000000000package internal import "fmt" type errorResponse struct { Status string `json:"status"` RequestID string `json:"request_id"` Message string `json:"message"` } func (e errorResponse) Error() string { return fmt.Sprintf("status=%s, message=%s", e.Status, e.Message) } // DNSZone a DNS zone. type DNSZone struct { ID int `json:"id"` Name string `json:"name,omitempty"` Synced bool `json:"synced,omitempty"` QueriesCount int `json:"queries_count,omitempty"` RecordsCount int `json:"records_count,omitempty"` AliasesCount int `json:"aliases_count,omitempty"` RedirectsCount int `json:"redirects_count,omitempty"` ForwardsCount int `json:"forwards_count,omitempty"` TemplateID int `json:"template_id,omitempty"` } // DNSRecord a DNS record. type DNSRecord struct { ID int `json:"id,omitempty"` Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` Content string `json:"content,omitempty"` TTL int `json:"ttl,omitempty"` ZoneID int `json:"zone_id,omitempty"` } lego-4.9.1/providers/dns/luadns/luadns.go000066400000000000000000000116321434020463500203630ustar00rootroot00000000000000// Package luadns implements a DNS provider for solving the DNS-01 challenge using LuaDNS. package luadns import ( "errors" "fmt" "net/http" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/luadns/internal" ) const minTTL = 300 // Environment variables names. const ( envNamespace = "LUADNS_" EnvAPIUsername = envNamespace + "API_USERNAME" EnvAPIToken = envNamespace + "API_TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIUsername string APIToken string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client recordsMu sync.Mutex records map[string]*internal.DNSRecord } // NewDNSProvider returns a DNSProvider instance configured for LuaDNS. // Credentials must be passed in the environment variables: // LUADNS_API_USERNAME and LUADNS_API_TOKEN. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIUsername, EnvAPIToken) if err != nil { return nil, fmt.Errorf("luadns: %w", err) } config := NewDefaultConfig() config.APIUsername = values[EnvAPIUsername] config.APIToken = values[EnvAPIToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for LuaDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("luadns: the configuration of the DNS provider is nil") } if config.APIUsername == "" || config.APIToken == "" { return nil, errors.New("luadns: credentials missing") } if config.TTL < minTTL { return nil, fmt.Errorf("luadns: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } client := internal.NewClient(config.APIUsername, config.APIToken) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, recordsMu: sync.Mutex{}, records: make(map[string]*internal.DNSRecord), }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zones, err := d.client.ListZones() if err != nil { return fmt.Errorf("luadns: failed to get zones: %w", err) } authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("luadns: failed to find zone: %w", err) } zone := findZone(zones, authZone) if zone == nil { return fmt.Errorf("luadns: no matching zone found for domain %s", domain) } newRecord := internal.DNSRecord{ Name: fqdn, Type: "TXT", Content: value, TTL: d.config.TTL, } record, err := d.client.CreateRecord(*zone, newRecord) if err != nil { return fmt.Errorf("luadns: failed to create record: %w", err) } d.recordsMu.Lock() d.records[token] = record d.recordsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) d.recordsMu.Lock() record, ok := d.records[token] d.recordsMu.Unlock() if !ok { return fmt.Errorf("luadns: unknown record ID for '%s'", fqdn) } err := d.client.DeleteRecord(record) if err != nil { return fmt.Errorf("luadns: failed to delete record: %w", err) } // Delete record from map d.recordsMu.Lock() delete(d.records, token) d.recordsMu.Unlock() return nil } func findZone(zones []internal.DNSZone, domain string) *internal.DNSZone { var result *internal.DNSZone for _, zone := range zones { zone := zone if zone.Name != "" && strings.HasSuffix(domain, zone.Name) { if result == nil || len(zone.Name) > len(result.Name) { result = &zone } } } return result } lego-4.9.1/providers/dns/luadns/luadns.toml000066400000000000000000000013161434020463500207270ustar00rootroot00000000000000Name = "LuaDNS" Description = '''''' URL = "https://luadns.com" Code = "luadns" Since = "v3.7.0" Example = ''' LUADNS_API_USERNAME=youremail \ LUADNS_API_TOKEN=xxxxxxxx \ lego --email you@example.com --dns luadns --domains my.example.org run ''' [Configuration] [Configuration.Credentials] LUADNS_API_USERNAME = "Username (your email)" LUADNS_API_TOKEN = "API token" [Configuration.Additional] LUADNS_POLLING_INTERVAL = "Time between DNS propagation check" LUADNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" LUADNS_TTL = "The TTL of the TXT record used for the DNS challenge" LUADNS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://luadns.com/api.html" lego-4.9.1/providers/dns/luadns/luadns_test.go000066400000000000000000000110651434020463500214220ustar00rootroot00000000000000package luadns import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/go-acme/lego/v4/providers/dns/luadns/internal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIUsername, EnvAPIToken). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIUsername: "123", EnvAPIToken: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIUsername: "", EnvAPIToken: "", }, expected: "luadns: some credentials information are missing: LUADNS_API_USERNAME,LUADNS_API_TOKEN", }, { desc: "missing username", envVars: map[string]string{ EnvAPIUsername: "", EnvAPIToken: "456", }, expected: "luadns: some credentials information are missing: LUADNS_API_USERNAME", }, { desc: "missing api token", envVars: map[string]string{ EnvAPIUsername: "123", EnvAPIToken: "", }, expected: "luadns: some credentials information are missing: LUADNS_API_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string apiSecret string tll int expected string }{ { desc: "success", apiKey: "123", apiSecret: "456", tll: minTTL, }, { desc: "missing credentials", tll: minTTL, expected: "luadns: credentials missing", }, { desc: "missing username", apiSecret: "456", tll: minTTL, expected: "luadns: credentials missing", }, { desc: "missing api token", apiKey: "123", tll: minTTL, expected: "luadns: credentials missing", }, { desc: "invalid TTL", apiKey: "123", apiSecret: "456", tll: 30, expected: "luadns: invalid TTL, TTL (30) must be greater than 300", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIUsername = test.apiKey config.APIToken = test.apiSecret config.TTL = test.tll p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestDNSProvider_findZone(t *testing.T) { testCases := []struct { desc string domain string zones []internal.DNSZone expected *internal.DNSZone }{ { desc: "simple domain", domain: "example.org", zones: []internal.DNSZone{ {Name: "example.org"}, {Name: "example.com"}, }, expected: &internal.DNSZone{Name: "example.org"}, }, { desc: "sub domain", domain: "aaa.example.org", zones: []internal.DNSZone{ {Name: "example.org"}, {Name: "aaa.example.org"}, {Name: "bbb.example.org"}, {Name: "example.com"}, }, expected: &internal.DNSZone{Name: "aaa.example.org"}, }, { desc: "empty zone name", domain: "example.org", zones: []internal.DNSZone{ {}, }, }, { desc: "not found", domain: "example.org", zones: []internal.DNSZone{ {Name: "example.net"}, {Name: "aaa.example.net"}, {Name: "bbb.example.net"}, {Name: "example.com"}, }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() zone := findZone(test.zones, test.domain) assert.Equal(t, test.expected, zone) }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/mydnsjp/000077500000000000000000000000001434020463500167415ustar00rootroot00000000000000lego-4.9.1/providers/dns/mydnsjp/client.go000066400000000000000000000022221434020463500205440ustar00rootroot00000000000000package mydnsjp import ( "fmt" "io" "net/http" "net/url" "strings" ) func (d *DNSProvider) doRequest(domain, value, cmd string) error { req, err := d.buildRequest(domain, value, cmd) if err != nil { return err } resp, err := d.config.HTTPClient.Do(req) if err != nil { return fmt.Errorf("error querying API: %w", err) } defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { var content []byte content, err = io.ReadAll(resp.Body) if err != nil { return err } return fmt.Errorf("request %s failed [status code %d]: %s", req.URL, resp.StatusCode, string(content)) } return nil } func (d *DNSProvider) buildRequest(domain, value, cmd string) (*http.Request, error) { params := url.Values{} params.Set("CERTBOT_DOMAIN", domain) params.Set("CERTBOT_VALIDATION", value) params.Set("EDIT_CMD", cmd) req, err := http.NewRequest(http.MethodPost, defaultBaseURL, strings.NewReader(params.Encode())) if err != nil { return nil, fmt.Errorf("invalid request: %w", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(d.config.MasterID, d.config.Password) return req, nil } lego-4.9.1/providers/dns/mydnsjp/mydnsjp.go000066400000000000000000000063511434020463500207610ustar00rootroot00000000000000// Package mydnsjp implements a DNS provider for solving the DNS-01 challenge using MyDNS.jp. package mydnsjp import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) const defaultBaseURL = "https://www.mydns.jp/directedit.html" // Environment variables names. const ( envNamespace = "MYDNSJP_" EnvMasterID = envNamespace + "MASTER_ID" EnvPassword = envNamespace + "PASSWORD" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { MasterID string Password string PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config } // NewDNSProvider returns a DNSProvider instance configured for MyDNS.jp. // Credentials must be passed in the environment variables: MYDNSJP_MASTER_ID and MYDNSJP_PASSWORD. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvMasterID, EnvPassword) if err != nil { return nil, fmt.Errorf("mydnsjp: %w", err) } config := NewDefaultConfig() config.MasterID = values[EnvMasterID] config.Password = values[EnvPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for MyDNS.jp. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("mydnsjp: the configuration of the DNS provider is nil") } if config.MasterID == "" || config.Password == "" { return nil, errors.New("mydnsjp: some credentials information are missing") } return &DNSProvider{config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { _, value := dns01.GetRecord(domain, keyAuth) // TODO(ldez) replace domain by FQDN to follow CNAME. err := d.doRequest(domain, value, "REGIST") if err != nil { return fmt.Errorf("mydnsjp: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { _, value := dns01.GetRecord(domain, keyAuth) // TODO(ldez) replace domain by FQDN to follow CNAME. err := d.doRequest(domain, value, "DELETE") if err != nil { return fmt.Errorf("mydnsjp: %w", err) } return nil } lego-4.9.1/providers/dns/mydnsjp/mydnsjp.toml000066400000000000000000000013031434020463500213170ustar00rootroot00000000000000Name = "MyDNS.jp" Description = '''''' URL = "https://www.mydns.jp" Code = "mydnsjp" Since = "v1.2.0" Example = ''' MYDNSJP_MASTER_ID=xxxxx \ MYDNSJP_PASSWORD=xxxxx \ lego --email you@example.com --dns mydnsjp --domains my.example.org run ''' [Configuration] [Configuration.Credentials] MYDNSJP_MASTER_ID = "Master ID" MYDNSJP_PASSWORD = "Password" [Configuration.Additional] MYDNSJP_POLLING_INTERVAL = "Time between DNS propagation check" MYDNSJP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" MYDNSJP_TTL = "The TTL of the TXT record used for the DNS challenge" MYDNSJP_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.mydns.jp/?MENU=030" lego-4.9.1/providers/dns/mydnsjp/mydnsjp_test.go000066400000000000000000000060351434020463500220170ustar00rootroot00000000000000package mydnsjp import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvMasterID, EnvPassword). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvMasterID: "test@example.com", EnvPassword: "123", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvMasterID: "", EnvPassword: "", }, expected: "mydnsjp: some credentials information are missing: MYDNSJP_MASTER_ID,MYDNSJP_PASSWORD", }, { desc: "missing email", envVars: map[string]string{ EnvMasterID: "", EnvPassword: "key", }, expected: "mydnsjp: some credentials information are missing: MYDNSJP_MASTER_ID", }, { desc: "missing api key", envVars: map[string]string{ EnvMasterID: "awesome@possum.com", EnvPassword: "", }, expected: "mydnsjp: some credentials information are missing: MYDNSJP_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { assert.NoError(t, err) assert.NotNil(t, p) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string masterID string password string expected string }{ { desc: "success", masterID: "test@example.com", password: "123", }, { desc: "missing credentials", expected: "mydnsjp: some credentials information are missing", }, { desc: "missing email", password: "123", expected: "mydnsjp: some credentials information are missing", }, { desc: "missing api key", masterID: "test@example.com", expected: "mydnsjp: some credentials information are missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.MasterID = test.masterID config.Password = test.password p, err := NewDNSProviderConfig(config) if test.expected == "" { assert.NoError(t, err) assert.NotNil(t, p) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") assert.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") assert.NoError(t, err) } lego-4.9.1/providers/dns/mythicbeasts/000077500000000000000000000000001434020463500177545ustar00rootroot00000000000000lego-4.9.1/providers/dns/mythicbeasts/client.go000066400000000000000000000140111434020463500215560ustar00rootroot00000000000000package mythicbeasts import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "path" "strings" "time" ) const ( apiBaseURL = "https://api.mythic-beasts.com/dns/v2" authBaseURL = "https://auth.mythic-beasts.com/login" ) type authResponse struct { // The bearer token for use in API requests Token string `json:"access_token"` // The maximum lifetime of the token in seconds Lifetime int `json:"expires_in"` // The token type (must be 'bearer') TokenType string `json:"token_type"` Deadline time.Time `json:"-"` } type authResponseError struct { ErrorMsg string `json:"error"` ErrorDescription string `json:"error_description"` } func (a authResponseError) Error() string { return fmt.Sprintf("%s: %s", a.ErrorMsg, a.ErrorDescription) } type createTXTRequest struct { Records []createTXTRecord `json:"records"` } type createTXTRecord struct { Host string `json:"host"` TTL int `json:"ttl"` Type string `json:"type"` Data string `json:"data"` } type createTXTResponse struct { Added int `json:"records_added"` Removed int `json:"records_removed"` Message string `json:"message"` } type deleteTXTResponse struct { Removed int `json:"records_removed"` Message string `json:"message"` } // Logs into mythic beasts and acquires a bearer token for use in future API calls. // https://www.mythic-beasts.com/support/api/auth#sec-obtaining-a-token func (d *DNSProvider) login() error { d.muToken.Lock() defer d.muToken.Unlock() if d.token != nil && time.Now().Before(d.token.Deadline) { // Already authenticated, stop now return nil } req, err := http.NewRequest(http.MethodPost, d.config.AuthAPIEndpoint.String(), strings.NewReader("grant_type=client_credentials")) if err != nil { return err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(d.config.UserName, d.config.Password) resp, err := d.config.HTTPClient.Do(req) if err != nil { return err } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("login: %w", err) } if resp.StatusCode != http.StatusOK { if resp.StatusCode < 400 || resp.StatusCode > 499 { return fmt.Errorf("login: unknown error in auth API: %d", resp.StatusCode) } // Returned body should be a JSON thing errResp := &authResponseError{} err = json.Unmarshal(body, errResp) if err != nil { return fmt.Errorf("login: error parsing error: %w", err) } return fmt.Errorf("login: %d: %w", resp.StatusCode, errResp) } authResp := authResponse{} err = json.Unmarshal(body, &authResp) if err != nil { return fmt.Errorf("login: error parsing response: %w", err) } if authResp.TokenType != "bearer" { return fmt.Errorf("login: received unexpected token type: %s", authResp.TokenType) } authResp.Deadline = time.Now().Add(time.Duration(authResp.Lifetime) * time.Second) d.token = &authResp // Success return nil } // https://www.mythic-beasts.com/support/api/dnsv2#ep-get-zoneszonerecords func (d *DNSProvider) createTXTRecord(zone, leaf, value string) error { if d.token == nil { return fmt.Errorf("createTXTRecord: not logged in") } createReq := createTXTRequest{ Records: []createTXTRecord{{ Host: leaf, TTL: d.config.TTL, Type: "TXT", Data: value, }}, } reqBody, err := json.Marshal(createReq) if err != nil { return fmt.Errorf("createTXTRecord: marshaling request body failed: %w", err) } endpoint, err := d.config.APIEndpoint.Parse(path.Join(d.config.APIEndpoint.Path, "zones", zone, "records", leaf, "TXT")) if err != nil { return fmt.Errorf("createTXTRecord: failed to parse URL: %w", err) } req, err := http.NewRequest(http.MethodPost, endpoint.String(), bytes.NewReader(reqBody)) if err != nil { return fmt.Errorf("createTXTRecord: %w", err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.token.Token)) req.Header.Set("Content-Type", "application/json") resp, err := d.config.HTTPClient.Do(req) if err != nil { return fmt.Errorf("createTXTRecord: unable to perform HTTP request: %w", err) } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("createTXTRecord: %w", err) } if resp.StatusCode != http.StatusOK { return fmt.Errorf("createTXTRecord: error in API: %d", resp.StatusCode) } createResp := createTXTResponse{} err = json.Unmarshal(body, &createResp) if err != nil { return fmt.Errorf("createTXTRecord: error parsing response: %w", err) } if createResp.Added != 1 { return errors.New("createTXTRecord: did not add TXT record for some reason") } // Success return nil } // https://www.mythic-beasts.com/support/api/dnsv2#ep-delete-zoneszonerecords func (d *DNSProvider) removeTXTRecord(zone, leaf, value string) error { if d.token == nil { return fmt.Errorf("removeTXTRecord: not logged in") } endpoint, err := d.config.APIEndpoint.Parse(path.Join(d.config.APIEndpoint.Path, "zones", zone, "records", leaf, "TXT")) if err != nil { return fmt.Errorf("removeTXTRecord: failed to parse URL: %w", err) } query := endpoint.Query() query.Add("data", value) endpoint.RawQuery = query.Encode() req, err := http.NewRequest(http.MethodDelete, endpoint.String(), nil) if err != nil { return fmt.Errorf("removeTXTRecord: %w", err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.token.Token)) resp, err := d.config.HTTPClient.Do(req) if err != nil { return fmt.Errorf("removeTXTRecord: unable to perform HTTP request: %w", err) } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("removeTXTRecord: %w", err) } if resp.StatusCode != http.StatusOK { return fmt.Errorf("removeTXTRecord: error in API: %d", resp.StatusCode) } deleteResp := deleteTXTResponse{} err = json.Unmarshal(body, &deleteResp) if err != nil { return fmt.Errorf("removeTXTRecord: error parsing response: %w", err) } if deleteResp.Removed != 1 { return errors.New("removeTXTRecord: did not add TXT record for some reason") } // Success return nil } lego-4.9.1/providers/dns/mythicbeasts/mythicbeasts.go000066400000000000000000000112321434020463500230010ustar00rootroot00000000000000// Package mythicbeasts implements a DNS provider for solving the DNS-01 challenge using Mythic Beasts API. package mythicbeasts import ( "errors" "fmt" "net/http" "net/url" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "MYTHICBEASTS_" EnvUserName = envNamespace + "USERNAME" EnvPassword = envNamespace + "PASSWORD" EnvAPIEndpoint = envNamespace + "API_ENDPOINT" EnvAuthAPIEndpoint = envNamespace + "AUTH_API_ENDPOINT" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { UserName string Password string HTTPClient *http.Client PropagationTimeout time.Duration PollingInterval time.Duration APIEndpoint *url.URL AuthAPIEndpoint *url.URL TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() (*Config, error) { apiEndpoint, err := url.Parse(env.GetOrDefaultString(EnvAPIEndpoint, apiBaseURL)) if err != nil { return nil, fmt.Errorf("mythicbeasts: Unable to parse API URL: %w", err) } authEndpoint, err := url.Parse(env.GetOrDefaultString(EnvAuthAPIEndpoint, authBaseURL)) if err != nil { return nil, fmt.Errorf("mythicbeasts: Unable to parse AUTH API URL: %w", err) } return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), APIEndpoint: apiEndpoint, AuthAPIEndpoint: authEndpoint, HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, }, nil } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config // token string token *authResponse muToken sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for mythicbeasts DNSv2 API. // Credentials must be passed in the environment variables: // MYTHICBEASTS_USERNAME and MYTHICBEASTS_PASSWORD. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvUserName, EnvPassword) if err != nil { return nil, fmt.Errorf("mythicbeasts: %w", err) } config, err := NewDefaultConfig() if err != nil { return nil, fmt.Errorf("mythicbeasts: %w", err) } config.UserName = values[EnvUserName] config.Password = values[EnvPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for mythicbeasts DNSv2 API. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("mythicbeasts: the configuration of the DNS provider is nil") } if config.UserName == "" || config.Password == "" { return nil, errors.New("mythicbeasts: incomplete credentials, missing username and/or password") } return &DNSProvider{config: config}, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("mythicbeasts: %w", err) } leaf := fqdn[:len(fqdn)-(len(authZone)+1)] authZone = dns01.UnFqdn(authZone) err = d.login() if err != nil { return fmt.Errorf("mythicbeasts: %w", err) } err = d.createTXTRecord(authZone, leaf, value) if err != nil { return fmt.Errorf("mythicbeasts: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("mythicbeasts: %w", err) } leaf := fqdn[:len(fqdn)-(len(authZone)+1)] authZone = dns01.UnFqdn(authZone) err = d.login() if err != nil { return fmt.Errorf("mythicbeasts: %w", err) } err = d.removeTXTRecord(authZone, leaf, value) if err != nil { return fmt.Errorf("mythicbeasts: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/mythicbeasts/mythicbeasts.toml000066400000000000000000000022561434020463500233550ustar00rootroot00000000000000Name = "MythicBeasts" Description = '''''' URL = "https://www.mythic-beasts.com/" Code = "mythicbeasts" Since = "v0.3.7" Example = ''' MYTHICBEASTS_USERNAME=myuser \ MYTHICBEASTS_PASSWORD=mypass \ lego --email you@example.com --dns mythicbeasts --domains my.example.org run ''' Additional = ''' If you are using specific API keys, then the username is the API ID for your API key, and the password is the API secret. Your API key name is not needed to operate lego. ''' [Configuration] [Configuration.Credentials] MYTHICBEASTS_USERNAME = "User name" MYTHICBEASTS_PASSWORD = "Password" [Configuration.Additional] MYTHICBEASTS_API_ENDPOINT = "The endpoint for the API (must implement v2)" MYTHICBEASTS_AUTH_API_ENDPOINT = "The endpoint for Mythic Beasts' Authentication" MYTHICBEASTS_POLLING_INTERVAL = "Time between DNS propagation check" MYTHICBEASTS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" MYTHICBEASTS_TTL = "The TTL of the TXT record used for the DNS challenge" MYTHICBEASTS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.mythic-beasts.com/support/api/dnsv2" APIAuth = "https://auth.mythic-beasts.com/login" lego-4.9.1/providers/dns/mythicbeasts/mythicbeasts_test.go000066400000000000000000000063601434020463500240460ustar00rootroot00000000000000package mythicbeasts import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvUserName, EnvPassword). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvUserName: "123", EnvPassword: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvUserName: "", EnvPassword: "", }, expected: "mythicbeasts: some credentials information are missing: MYTHICBEASTS_USERNAME,MYTHICBEASTS_PASSWORD", }, { desc: "missing api key", envVars: map[string]string{ EnvUserName: "", EnvPassword: "api_password", }, expected: "mythicbeasts: some credentials information are missing: MYTHICBEASTS_USERNAME", }, { desc: "missing secret key", envVars: map[string]string{ EnvUserName: "api_username", EnvPassword: "", }, expected: "mythicbeasts: some credentials information are missing: MYTHICBEASTS_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string username string password string expected string }{ { desc: "success", username: "api_username", password: "api_password", }, { desc: "missing credentials", expected: "mythicbeasts: incomplete credentials, missing username and/or password", }, { desc: "missing username", username: "", password: "api_password", expected: "mythicbeasts: incomplete credentials, missing username and/or password", }, { desc: "missing password", username: "api_username", password: "", expected: "mythicbeasts: incomplete credentials, missing username and/or password", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config, err := NewDefaultConfig() require.NoError(t, err) config.UserName = test.username config.Password = test.password p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/namecheap/000077500000000000000000000000001434020463500171765ustar00rootroot00000000000000lego-4.9.1/providers/dns/namecheap/client.go000066400000000000000000000111431434020463500210030ustar00rootroot00000000000000package namecheap import ( "encoding/xml" "errors" "fmt" "io" "net/http" "net/url" "strings" ) // Record describes a DNS record returned by the Namecheap DNS gethosts API. // Namecheap uses the term "host" to refer to all DNS records that include // a host field (A, AAAA, CNAME, NS, TXT, URL). type Record struct { Type string `xml:",attr"` Name string `xml:",attr"` Address string `xml:",attr"` MXPref string `xml:",attr"` TTL string `xml:",attr"` } // apiError describes an error record in a namecheap API response. type apiError struct { Number int `xml:",attr"` Description string `xml:",innerxml"` } type setHostsResponse struct { XMLName xml.Name `xml:"ApiResponse"` Status string `xml:"Status,attr"` Errors []apiError `xml:"Errors>Error"` Result struct { IsSuccess string `xml:",attr"` } `xml:"CommandResponse>DomainDNSSetHostsResult"` } type getHostsResponse struct { XMLName xml.Name `xml:"ApiResponse"` Status string `xml:"Status,attr"` Errors []apiError `xml:"Errors>Error"` Hosts []Record `xml:"CommandResponse>DomainDNSGetHostsResult>host"` } // getHosts reads the full list of DNS host records. // https://www.namecheap.com/support/api/methods/domains-dns/get-hosts.aspx func (d *DNSProvider) getHosts(sld, tld string) ([]Record, error) { request, err := d.newRequestGet("namecheap.domains.dns.getHosts", addParam("SLD", sld), addParam("TLD", tld), ) if err != nil { return nil, err } var ghr getHostsResponse err = d.do(request, &ghr) if err != nil { return nil, err } if len(ghr.Errors) > 0 { return nil, fmt.Errorf("%s [%d]", ghr.Errors[0].Description, ghr.Errors[0].Number) } return ghr.Hosts, nil } // setHosts writes the full list of DNS host records . // https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx func (d *DNSProvider) setHosts(sld, tld string, hosts []Record) error { req, err := d.newRequestPost("namecheap.domains.dns.setHosts", addParam("SLD", sld), addParam("TLD", tld), func(values url.Values) { for i, h := range hosts { ind := fmt.Sprintf("%d", i+1) values.Add("HostName"+ind, h.Name) values.Add("RecordType"+ind, h.Type) values.Add("Address"+ind, h.Address) values.Add("MXPref"+ind, h.MXPref) values.Add("TTL"+ind, h.TTL) } }, ) if err != nil { return err } var shr setHostsResponse err = d.do(req, &shr) if err != nil { return err } if len(shr.Errors) > 0 { return fmt.Errorf("%s [%d]", shr.Errors[0].Description, shr.Errors[0].Number) } if shr.Result.IsSuccess != "true" { return errors.New("setHosts failed") } return nil } func (d *DNSProvider) do(req *http.Request, out interface{}) error { resp, err := d.config.HTTPClient.Do(req) if err != nil { return err } if resp.StatusCode >= http.StatusBadRequest { var body []byte body, err = readBody(resp) if err != nil { return fmt.Errorf("HTTP error %d [%s]: %w", resp.StatusCode, http.StatusText(resp.StatusCode), err) } return fmt.Errorf("HTTP error %d [%s]: %s", resp.StatusCode, http.StatusText(resp.StatusCode), string(body)) } body, err := readBody(resp) if err != nil { return err } return xml.Unmarshal(body, out) } func (d *DNSProvider) newRequestGet(cmd string, params ...func(url.Values)) (*http.Request, error) { query := d.makeQuery(cmd, params...) reqURL, err := url.Parse(d.config.BaseURL) if err != nil { return nil, err } reqURL.RawQuery = query.Encode() return http.NewRequest(http.MethodGet, reqURL.String(), nil) } func (d *DNSProvider) newRequestPost(cmd string, params ...func(url.Values)) (*http.Request, error) { query := d.makeQuery(cmd, params...) req, err := http.NewRequest(http.MethodPost, d.config.BaseURL, strings.NewReader(query.Encode())) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") return req, nil } func (d *DNSProvider) makeQuery(cmd string, params ...func(url.Values)) url.Values { queryParams := make(url.Values) queryParams.Set("ApiUser", d.config.APIUser) queryParams.Set("ApiKey", d.config.APIKey) queryParams.Set("UserName", d.config.APIUser) queryParams.Set("Command", cmd) queryParams.Set("ClientIp", d.config.ClientIP) for _, param := range params { param(queryParams) } return queryParams } func addParam(key, value string) func(url.Values) { return func(values url.Values) { values.Set(key, value) } } func readBody(resp *http.Response) ([]byte, error) { if resp.Body == nil { return nil, errors.New("response body is nil") } defer resp.Body.Close() rawBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return rawBody, nil } lego-4.9.1/providers/dns/namecheap/namecheap.go000066400000000000000000000163311434020463500214520ustar00rootroot00000000000000// Package namecheap implements a DNS provider for solving the DNS-01 challenge using namecheap DNS. package namecheap import ( "errors" "fmt" "io" "net/http" "strconv" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "golang.org/x/net/publicsuffix" ) // Notes about namecheap's tool API: // 1. Using the API requires registration. Once registered, use your account // name and API key to access the API. // 2. There is no API to add or modify a single DNS record. Instead you must // read the entire list of records, make modifications, and then write the // entire updated list of records. (Yuck.) // 3. Namecheap's DNS updates can be slow to propagate. I've seen them take // as long as an hour. // 4. Namecheap requires you to whitelist the IP address from which you call // its APIs. It also requires all API calls to include the whitelisted IP // address as a form or query string value. This code uses a namecheap // service to query the client's IP address. const ( defaultBaseURL = "https://api.namecheap.com/xml.response" sandboxBaseURL = "https://api.sandbox.namecheap.com/xml.response" getIPURL = "https://dynamicdns.park-your-domain.com/getip" ) // Environment variables names. const ( envNamespace = "NAMECHEAP_" EnvAPIUser = envNamespace + "API_USER" EnvAPIKey = envNamespace + "API_KEY" EnvSandbox = envNamespace + "SANDBOX" EnvDebug = envNamespace + "DEBUG" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // A challenge represents all the data needed to specify a dns-01 challenge to lets-encrypt. type challenge struct { domain string key string keyFqdn string keyValue string tld string sld string host string } // Config is used to configure the creation of the DNSProvider. type Config struct { Debug bool BaseURL string APIUser string APIKey string ClientIP string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { baseURL := defaultBaseURL if env.GetOrDefaultBool(EnvSandbox, false) { baseURL = sandboxBaseURL } return &Config{ BaseURL: baseURL, Debug: env.GetOrDefaultBool(EnvDebug, false), TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 60*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 15*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config } // NewDNSProvider returns a DNSProvider instance configured for namecheap. // Credentials must be passed in the environment variables: // NAMECHEAP_API_USER and NAMECHEAP_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIUser, EnvAPIKey) if err != nil { return nil, fmt.Errorf("namecheap: %w", err) } config := NewDefaultConfig() config.APIUser = values[EnvAPIUser] config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Namecheap. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("namecheap: the configuration of the DNS provider is nil") } if config.APIUser == "" || config.APIKey == "" { return nil, errors.New("namecheap: credentials missing") } if config.ClientIP == "" { clientIP, err := getClientIP(config.HTTPClient, config.Debug) if err != nil { return nil, fmt.Errorf("namecheap: %w", err) } config.ClientIP = clientIP } return &DNSProvider{config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Namecheap can sometimes take a long time to complete an update, so wait up to 60 minutes for the update to propagate. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present installs a TXT record for the DNS challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { // TODO(ldez) replace domain by FQDN to follow CNAME. ch, err := newChallenge(domain, keyAuth) if err != nil { return fmt.Errorf("namecheap: %w", err) } records, err := d.getHosts(ch.sld, ch.tld) if err != nil { return fmt.Errorf("namecheap: %w", err) } record := Record{ Name: ch.key, Type: "TXT", Address: ch.keyValue, MXPref: "10", TTL: strconv.Itoa(d.config.TTL), } records = append(records, record) if d.config.Debug { for _, h := range records { log.Printf("%-5.5s %-30.30s %-6s %-70.70s", h.Type, h.Name, h.TTL, h.Address) } } err = d.setHosts(ch.sld, ch.tld, records) if err != nil { return fmt.Errorf("namecheap: %w", err) } return nil } // CleanUp removes a TXT record used for a previous DNS challenge. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { // TODO(ldez) replace domain by FQDN to follow CNAME. ch, err := newChallenge(domain, keyAuth) if err != nil { return fmt.Errorf("namecheap: %w", err) } records, err := d.getHosts(ch.sld, ch.tld) if err != nil { return fmt.Errorf("namecheap: %w", err) } // Find the challenge TXT record and remove it if found. var found bool var newRecords []Record for _, h := range records { if h.Name == ch.key && h.Type == "TXT" { found = true } else { newRecords = append(newRecords, h) } } if !found { return nil } err = d.setHosts(ch.sld, ch.tld, newRecords) if err != nil { return fmt.Errorf("namecheap: %w", err) } return nil } // getClientIP returns the client's public IP address. // It uses namecheap's IP discovery service to perform the lookup. func getClientIP(client *http.Client, debug bool) (addr string, err error) { resp, err := client.Get(getIPURL) if err != nil { return "", err } defer resp.Body.Close() clientIP, err := io.ReadAll(resp.Body) if err != nil { return "", err } if debug { log.Println("Client IP:", string(clientIP)) } return string(clientIP), nil } // newChallenge builds a challenge record from a domain name and a challenge authentication key. func newChallenge(domain, keyAuth string) (*challenge, error) { domain = dns01.UnFqdn(domain) tld, _ := publicsuffix.PublicSuffix(domain) if tld == domain { return nil, fmt.Errorf("invalid domain name %q", domain) } parts := strings.Split(domain, ".") longest := len(parts) - strings.Count(tld, ".") - 1 sld := parts[longest-1] var host string if longest >= 1 { host = strings.Join(parts[:longest-1], ".") } fqdn, value := dns01.GetRecord(domain, keyAuth) return &challenge{ domain: domain, key: "_acme-challenge." + host, keyFqdn: fqdn, keyValue: value, tld: tld, sld: sld, host: host, }, nil } lego-4.9.1/providers/dns/namecheap/namecheap.toml000066400000000000000000000022771434020463500220240ustar00rootroot00000000000000Name = "Namecheap" URL = "https://www.namecheap.com" Code = "namecheap" Since = "v0.3.0" Description = ''' Configuration for [Namecheap](https://www.namecheap.com). **To enable API access on the Namecheap production environment, some opaque requirements must be met.** More information in the section [Enabling API Access](https://www.namecheap.com/support/api/intro/) of the Namecheap documentation. (2020-08: Account balance of $50+, 20+ domains in your account, or purchases totaling $50+ within the last 2 years.) ''' Example = ''' NAMECHEAP_API_USER=user \ NAMECHEAP_API_KEY=key \ lego --email you@example.com --dns namecheap --domains my.example.org run ''' [Configuration] [Configuration.Credentials] NAMECHEAP_API_USER = "API user" NAMECHEAP_API_KEY = "API key" [Configuration.Additional] NAMECHEAP_POLLING_INTERVAL = "Time between DNS propagation check" NAMECHEAP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" NAMECHEAP_TTL = "The TTL of the TXT record used for the DNS challenge" NAMECHEAP_HTTP_TIMEOUT = "API request timeout" NAMECHEAP_SANDBOX = "Activate the sandbox (boolean)" [Links] API = "https://www.namecheap.com/support/api/methods.aspx" lego-4.9.1/providers/dns/namecheap/namecheap_test.go000066400000000000000000000257641434020463500225230ustar00rootroot00000000000000package namecheap import ( "fmt" "net/http" "net/http/httptest" "net/url" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( envTestUser = "foo" envTestKey = "bar" envTestClientIP = "10.0.0.1" ) func TestDNSProvider_getHosts(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { p := setupTest(t, &test) ch, err := newChallenge(test.domain, "") require.NoError(t, err) hosts, err := p.getHosts(ch.sld, ch.tld) if test.errString != "" { assert.EqualError(t, err, test.errString) } else { assert.NoError(t, err) } next1: for _, h := range hosts { for _, th := range test.hosts { if h == th { continue next1 } } t.Errorf("getHosts case %s unexpected record [%s:%s:%s]", test.name, h.Type, h.Name, h.Address) } next2: for _, th := range test.hosts { for _, h := range hosts { if h == th { continue next2 } } t.Errorf("getHosts case %s missing record [%s:%s:%s]", test.name, th.Type, th.Name, th.Address) } }) } } func TestDNSProvider_setHosts(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { p := setupTest(t, &test) ch, err := newChallenge(test.domain, "") require.NoError(t, err) hosts, err := p.getHosts(ch.sld, ch.tld) if test.errString != "" { assert.EqualError(t, err, test.errString) } else { require.NoError(t, err) } if err != nil { return } err = p.setHosts(ch.sld, ch.tld, hosts) require.NoError(t, err) }) } } func TestDNSProvider_Present(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { p := setupTest(t, &test) err := p.Present(test.domain, "", "dummyKey") if test.errString != "" { assert.EqualError(t, err, "namecheap: "+test.errString) } else { assert.NoError(t, err) } }) } } func TestDNSProvider_CleanUp(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { p := setupTest(t, &test) err := p.CleanUp(test.domain, "", "dummyKey") if test.errString != "" { assert.EqualError(t, err, "namecheap: "+test.errString) } else { assert.NoError(t, err) } }) } } func TestDomainSplit(t *testing.T) { tests := []struct { domain string valid bool tld string sld string host string }{ {domain: "a.b.c.test.co.uk", valid: true, tld: "co.uk", sld: "test", host: "a.b.c"}, {domain: "test.co.uk", valid: true, tld: "co.uk", sld: "test"}, {domain: "test.com", valid: true, tld: "com", sld: "test"}, {domain: "test.co.com", valid: true, tld: "co.com", sld: "test"}, {domain: "www.test.com.au", valid: true, tld: "com.au", sld: "test", host: "www"}, {domain: "www.za.com", valid: true, tld: "za.com", sld: "www"}, {domain: "my.test.tf", valid: true, tld: "tf", sld: "test", host: "my"}, {}, {domain: "a"}, {domain: "com"}, {domain: "com.au"}, {domain: "co.com"}, {domain: "co.uk"}, {domain: "tf"}, {domain: "za.com"}, } for _, test := range tests { test := test t.Run(test.domain, func(t *testing.T) { valid := true ch, err := newChallenge(test.domain, "") if err != nil { valid = false } if test.valid && !valid { t.Errorf("Expected '%s' to split", test.domain) } else if !test.valid && valid { t.Errorf("Expected '%s' to produce error", test.domain) } if test.valid && valid { require.NotNil(t, ch) assert.Equal(t, test.domain, ch.domain, "domain") assert.Equal(t, test.tld, ch.tld, "tld") assert.Equal(t, test.sld, ch.sld, "sld") assert.Equal(t, test.host, ch.host, "host") } }) } } func assertHdr(t *testing.T, tc *testCase, values *url.Values) { t.Helper() ch, _ := newChallenge(tc.domain, "") assert.Equal(t, envTestUser, values.Get("ApiUser"), "ApiUser") assert.Equal(t, envTestKey, values.Get("ApiKey"), "ApiKey") assert.Equal(t, envTestUser, values.Get("UserName"), "UserName") assert.Equal(t, envTestClientIP, values.Get("ClientIp"), "ClientIp") assert.Equal(t, ch.sld, values.Get("SLD"), "SLD") assert.Equal(t, ch.tld, values.Get("TLD"), "TLD") } func setupTest(t *testing.T, tc *testCase) *DNSProvider { t.Helper() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: values := r.URL.Query() cmd := values.Get("Command") switch cmd { case "namecheap.domains.dns.getHosts": assertHdr(t, tc, &values) w.WriteHeader(http.StatusOK) fmt.Fprint(w, tc.getHostsResponse) default: t.Errorf("Unexpected GET command: %s", cmd) } case http.MethodPost: err := r.ParseForm() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } values := r.Form cmd := values.Get("Command") switch cmd { case "namecheap.domains.dns.setHosts": assertHdr(t, tc, &values) w.WriteHeader(http.StatusOK) fmt.Fprint(w, tc.setHostsResponse) default: t.Errorf("Unexpected POST command: %s", cmd) } default: t.Errorf("Unexpected http method: %s", r.Method) } }) server := httptest.NewServer(handler) t.Cleanup(server.Close) return mockDNSProvider(t, server.URL) } func mockDNSProvider(t *testing.T, baseURL string) *DNSProvider { t.Helper() config := NewDefaultConfig() config.BaseURL = baseURL config.APIUser = envTestUser config.APIKey = envTestKey config.ClientIP = envTestClientIP config.HTTPClient = &http.Client{Timeout: 60 * time.Second} provider, err := NewDNSProviderConfig(config) require.NoError(t, err) return provider } type testCase struct { name string domain string hosts []Record errString string getHostsResponse string setHostsResponse string } var testCases = []testCase{ { name: "Test:Success:1", domain: "test.example.com", hosts: []Record{ {Type: "A", Name: "home", Address: "10.0.0.1", MXPref: "10", TTL: "1799"}, {Type: "A", Name: "www", Address: "10.0.0.2", MXPref: "10", TTL: "1200"}, {Type: "AAAA", Name: "a", Address: "::0", MXPref: "10", TTL: "1799"}, {Type: "CNAME", Name: "*", Address: "example.com.", MXPref: "10", TTL: "1799"}, {Type: "MXE", Name: "example.com", Address: "10.0.0.5", MXPref: "10", TTL: "1800"}, {Type: "URL", Name: "xyz", Address: "https://google.com", MXPref: "10", TTL: "1799"}, }, getHostsResponse: responseGetHostsSuccess1, setHostsResponse: responseSetHostsSuccess1, }, { name: "Test:Success:2", domain: "example.com", hosts: []Record{ {Type: "A", Name: "@", Address: "10.0.0.2", MXPref: "10", TTL: "1200"}, {Type: "A", Name: "www", Address: "10.0.0.3", MXPref: "10", TTL: "60"}, }, getHostsResponse: responseGetHostsSuccess2, setHostsResponse: responseSetHostsSuccess2, }, { name: "Test:Error:BadApiKey:1", domain: "test.example.com", errString: "API Key is invalid or API access has not been enabled [1011102]", getHostsResponse: responseGetHostsErrorBadAPIKey1, }, } const responseGetHostsSuccess1 = ` namecheap.domains.dns.getHosts PHX01SBAPI01 --5:00 3.338 ` const responseSetHostsSuccess1 = ` namecheap.domains.dns.setHosts PHX01SBAPI01 --5:00 2.347 ` const responseGetHostsSuccess2 = ` namecheap.domains.dns.getHosts PHX01SBAPI01 --5:00 3.338 ` const responseSetHostsSuccess2 = ` namecheap.domains.dns.setHosts PHX01SBAPI01 --5:00 2.347 ` const responseGetHostsErrorBadAPIKey1 = ` API Key is invalid or API access has not been enabled PHX01SBAPI01 --5:00 0 ` lego-4.9.1/providers/dns/namedotcom/000077500000000000000000000000001434020463500174035ustar00rootroot00000000000000lego-4.9.1/providers/dns/namedotcom/namedotcom.go000066400000000000000000000124401434020463500220610ustar00rootroot00000000000000// Package namedotcom implements a DNS provider for solving the DNS-01 challenge using Name.com's DNS service. package namedotcom import ( "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/namedotcom/go/namecom" ) // according to https://www.name.com/api-docs/DNS#CreateRecord const minTTL = 300 // Environment variables names. const ( envNamespace = "NAMECOM_" EnvUsername = envNamespace + "USERNAME" EnvAPIToken = envNamespace + "API_TOKEN" EnvServer = envNamespace + "SERVER" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Username string APIToken string Server string TTL int PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 15*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 20*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *namecom.NameCom config *Config } // NewDNSProvider returns a DNSProvider instance configured for namedotcom. // Credentials must be passed in the environment variables: // NAMECOM_USERNAME and NAMECOM_API_TOKEN. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvUsername, EnvAPIToken) if err != nil { return nil, fmt.Errorf("namedotcom: %w", err) } config := NewDefaultConfig() config.Username = values[EnvUsername] config.APIToken = values[EnvAPIToken] config.Server = env.GetOrFile(EnvServer) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for namedotcom. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("namedotcom: the configuration of the DNS provider is nil") } if config.Username == "" { return nil, errors.New("namedotcom: username is required") } if config.APIToken == "" { return nil, errors.New("namedotcom: API token is required") } if config.TTL < minTTL { return nil, fmt.Errorf("namedotcom: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } client := namecom.New(config.Username, config.APIToken) client.Client = config.HTTPClient if config.Server != "" { client.Server = config.Server } return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) // TODO(ldez) replace domain by FQDN to follow CNAME. domainDetails, err := d.client.GetDomain(&namecom.GetDomainRequest{DomainName: domain}) if err != nil { return fmt.Errorf("namedotcom API call failed: %w", err) } // TODO(ldez) replace domain by FQDN to follow CNAME. request := &namecom.Record{ DomainName: domain, Host: extractRecordName(fqdn, domainDetails.DomainName), Type: "TXT", TTL: uint32(d.config.TTL), Answer: value, } _, err = d.client.CreateRecord(request) if err != nil { return fmt.Errorf("namedotcom: API call failed: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) // TODO(ldez) replace domain by FQDN to follow CNAME. records, err := d.getRecords(domain) if err != nil { return fmt.Errorf("namedotcom: %w", err) } for _, rec := range records { if rec.Fqdn == fqdn && rec.Type == "TXT" { // TODO(ldez) replace domain by FQDN to follow CNAME. request := &namecom.DeleteRecordRequest{ DomainName: domain, ID: rec.ID, } _, err := d.client.DeleteRecord(request) if err != nil { return fmt.Errorf("namedotcom: %w", err) } } } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func (d *DNSProvider) getRecords(domain string) ([]*namecom.Record, error) { request := &namecom.ListRecordsRequest{ DomainName: domain, Page: 1, } var records []*namecom.Record for request.Page > 0 { response, err := d.client.ListRecords(request) if err != nil { return nil, err } records = append(records, response.Records...) request.Page = response.NextPage } return records, nil } func extractRecordName(fqdn, zone string) string { name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+zone); idx != -1 { return name[:idx] } return name } lego-4.9.1/providers/dns/namedotcom/namedotcom.toml000066400000000000000000000014421434020463500224270ustar00rootroot00000000000000Name = "Name.com" Description = '''''' URL = "https://www.name.com" Code = "namedotcom" Since = "v0.5.0" Example = ''' NAMECOM_USERNAME=foo.bar \ NAMECOM_API_TOKEN=a379a6f6eeafb9a55e378c118034e2751e682fab \ lego --email you@example.com --dns namedotcom --domains my.example.org run ''' [Configuration] [Configuration.Credentials] NAMECOM_USERNAME = "Username" NAMECOM_API_TOKEN = "API token" [Configuration.Additional] NAMECOM_POLLING_INTERVAL = "Time between DNS propagation check" NAMECOM_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" NAMECOM_TTL = "The TTL of the TXT record used for the DNS challenge" NAMECOM_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.name.com/api-docs/DNS" GoClient = "https://github.com/namedotcom/go" lego-4.9.1/providers/dns/namedotcom/namedotcom_test.go000066400000000000000000000061001434020463500231140ustar00rootroot00000000000000package namedotcom import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvUsername, EnvAPIToken). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvUsername: "A", EnvAPIToken: "B", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvUsername: "", EnvAPIToken: "", }, expected: "namedotcom: some credentials information are missing: NAMECOM_USERNAME,NAMECOM_API_TOKEN", }, { desc: "missing username", envVars: map[string]string{ EnvUsername: "", EnvAPIToken: "B", }, expected: "namedotcom: some credentials information are missing: NAMECOM_USERNAME", }, { desc: "missing api token", envVars: map[string]string{ EnvUsername: "A", EnvAPIToken: "", }, expected: "namedotcom: some credentials information are missing: NAMECOM_API_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiToken string username string expected string }{ { desc: "success", apiToken: "A", username: "B", }, { desc: "missing credentials", expected: "namedotcom: username is required", }, { desc: "missing API token", apiToken: "", username: "B", expected: "namedotcom: API token is required", }, { desc: "missing username", apiToken: "A", username: "", expected: "namedotcom: username is required", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Username = test.username config.APIToken = test.apiToken p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/namesilo/000077500000000000000000000000001434020463500170645ustar00rootroot00000000000000lego-4.9.1/providers/dns/namesilo/namesilo.go000066400000000000000000000105421434020463500212240ustar00rootroot00000000000000// Package namesilo implements a DNS provider for solving the DNS-01 challenge using namesilo DNS. package namesilo import ( "errors" "fmt" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/nrdcg/namesilo" ) const ( defaultTTL = 3600 maxTTL = 2592000 ) // Environment variables names. const ( envNamespace = "NAMESILO_" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *namesilo.Client config *Config } // NewDNSProvider returns a DNSProvider instance configured for namesilo. // API_KEY must be passed in the environment variables: NAMESILO_API_KEY. // // See: https://www.namesilo.com/api_reference.php func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("namesilo: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Namesilo. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("namesilo: the configuration of the DNS provider is nil") } if config.TTL < defaultTTL || config.TTL > maxTTL { return nil, fmt.Errorf("namesilo: TTL should be in [%d, %d]", defaultTTL, maxTTL) } transport, err := namesilo.NewTokenTransport(config.APIKey) if err != nil { return nil, fmt.Errorf("namesilo: %w", err) } return &DNSProvider{client: namesilo.NewClient(transport.Client()), config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zoneName, err := getZoneNameByDomain(fqdn) if err != nil { return fmt.Errorf("namesilo: %w", err) } _, err = d.client.DnsAddRecord(&namesilo.DnsAddRecordParams{ Domain: zoneName, Type: "TXT", Host: getRecordName(fqdn, zoneName), Value: value, TTL: d.config.TTL, }) if err != nil { return fmt.Errorf("namesilo: failed to add record %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) zoneName, err := getZoneNameByDomain(fqdn) if err != nil { return fmt.Errorf("namesilo: %w", err) } resp, err := d.client.DnsListRecords(&namesilo.DnsListRecordsParams{Domain: zoneName}) if err != nil { return fmt.Errorf("namesilo: %w", err) } var lastErr error name := getRecordName(fqdn, zoneName) for _, r := range resp.Reply.ResourceRecord { if r.Type == "TXT" && (r.Host == name || r.Host == dns01.UnFqdn(fqdn)) { _, err := d.client.DnsDeleteRecord(&namesilo.DnsDeleteRecordParams{Domain: zoneName, ID: r.RecordID}) if err != nil { lastErr = fmt.Errorf("namesilo: %w", err) } } } return lastErr } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func getZoneNameByDomain(domain string) (string, error) { zone, err := dns01.FindZoneByFqdn(domain) if err != nil { return "", fmt.Errorf("failed to find zone for domain: %s, %w", domain, err) } return dns01.UnFqdn(zone), nil } func getRecordName(domain, zone string) string { return strings.TrimSuffix(dns01.ToFqdn(domain), "."+dns01.ToFqdn(zone)) } lego-4.9.1/providers/dns/namesilo/namesilo.toml000066400000000000000000000014071434020463500215720ustar00rootroot00000000000000Name = "Namesilo" Description = '''''' URL = "https://www.namesilo.com/" Code = "namesilo" Since = "v2.7.0" Example = ''' NAMESILO_API_KEY=b9841238feb177a84330febba8a83208921177bffe733 \ lego --email you@example.com --dns namesilo --domains my.example.org run ''' [Configuration] [Configuration.Credentials] NAMESILO_API_KEY = "Client ID" [Configuration.Additional] NAMESILO_POLLING_INTERVAL = "Time between DNS propagation check" NAMESILO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation, it is better to set larger than 15m" NAMESILO_TTL = "The TTL of the TXT record used for the DNS challenge, should be in [3600, 2592000]" [Links] API = "https://www.namesilo.com/api_reference.php" GoClient = "https://github.com/nrdcg/namesilo" lego-4.9.1/providers/dns/namesilo/namesilo_test.go000066400000000000000000000051101434020463500222560ustar00rootroot00000000000000package namesilo import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvTTL, EnvAPIKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "A", }, }, { desc: "missing API key", envVars: map[string]string{}, expected: "namesilo: some credentials information are missing: NAMESILO_API_KEY", }, { desc: "unsupported TTL", envVars: map[string]string{ EnvAPIKey: "A", EnvTTL: "180", }, expected: "namesilo: TTL should be in [3600, 2592000]", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string ttl int expected string }{ { desc: "success", apiKey: "A", ttl: defaultTTL, }, { desc: "missing API key", ttl: defaultTTL, expected: "namesilo: credentials missing: API key", }, { desc: "unavailable TTL", apiKey: "A", ttl: 100, expected: "namesilo: TTL should be in [3600, 2592000]", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.TTL = test.ttl p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/nearlyfreespeech/000077500000000000000000000000001434020463500206015ustar00rootroot00000000000000lego-4.9.1/providers/dns/nearlyfreespeech/internal/000077500000000000000000000000001434020463500224155ustar00rootroot00000000000000lego-4.9.1/providers/dns/nearlyfreespeech/internal/client.go000066400000000000000000000052111434020463500242210ustar00rootroot00000000000000package internal import ( "crypto/sha1" "encoding/json" "fmt" "io" "math/rand" "net/http" "net/url" "path" "strconv" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" querystring "github.com/google/go-querystring/query" ) const apiURL = "https://api.nearlyfreespeech.net" const authenticationHeader = "X-NFSN-Authentication" const saltBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" type Client struct { HTTPClient *http.Client baseURL *url.URL login string apiKey string } func NewClient(login string, apiKey string) *Client { baseURL, _ := url.Parse(apiURL) return &Client{ HTTPClient: &http.Client{Timeout: 10 * time.Second}, baseURL: baseURL, login: login, apiKey: apiKey, } } func (c Client) AddRecord(domain string, record Record) error { params, err := querystring.Values(record) if err != nil { return err } return c.do(path.Join("dns", dns01.UnFqdn(domain), "addRR"), params) } func (c Client) RemoveRecord(domain string, record Record) error { params, err := querystring.Values(record) if err != nil { return err } return c.do(path.Join("dns", dns01.UnFqdn(domain), "removeRR"), params) } func (c Client) do(uri string, params url.Values) error { endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, uri)) if err != nil { return err } payload := params.Encode() req, err := http.NewRequest(http.MethodPost, endpoint.String(), strings.NewReader(payload)) if err != nil { return err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set(authenticationHeader, c.createSignature(endpoint.Path, payload)) resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { data, _ := io.ReadAll(resp.Body) apiErr := &APIError{} err := json.Unmarshal(data, apiErr) if err != nil { return fmt.Errorf("%s: %s", resp.Status, data) } return apiErr } return nil } func (c Client) createSignature(uri string, body string) string { // This is the only part of this that needs to be serialized. salt := make([]byte, 16) for i := 0; i < 16; i++ { salt[i] = saltBytes[rand.Intn(len(saltBytes))] } // Header is "login;timestamp;salt;hash". // hash is SHA1("login;timestamp;salt;api-key;request-uri;body-hash") // and body-hash is SHA1(body). bodyHash := sha1.Sum([]byte(body)) timestamp := strconv.FormatInt(time.Now().Unix(), 10) hashInput := fmt.Sprintf("%s;%s;%s;%s;%s;%02x", c.login, timestamp, salt, c.apiKey, uri, bodyHash) return fmt.Sprintf("%s;%s;%s;%02x", c.login, timestamp, salt, sha1.Sum([]byte(hashInput))) } lego-4.9.1/providers/dns/nearlyfreespeech/internal/client_test.go000066400000000000000000000056201434020463500252640ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "testing" "github.com/stretchr/testify/require" ) func setupTest(t *testing.T) (*Client, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) client := NewClient("user", "secret") client.HTTPClient = server.Client() client.baseURL, _ = url.Parse(server.URL) return client, mux } func testHandler(params map[string]string) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } if req.Header.Get(authenticationHeader) == "" { http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } err := req.ParseForm() if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } for k, v := range params { if req.PostForm.Get(k) != v { http.Error(rw, fmt.Sprintf("data: got %s want %s", k, v), http.StatusBadRequest) return } } } } func testErrorHandler() http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } file, err := os.Open("./fixtures/error.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } rw.WriteHeader(http.StatusUnauthorized) _, _ = io.Copy(rw, file) } } func TestClient_AddRecord(t *testing.T) { client, mux := setupTest(t) params := map[string]string{ "data": "txtTXTtxt", "name": "sub", "type": "TXT", "ttl": "30", } mux.Handle("/dns/example.com/addRR", testHandler(params)) record := Record{ Name: "sub", Type: "TXT", Data: "txtTXTtxt", TTL: 30, } err := client.AddRecord("example.com", record) require.NoError(t, err) } func TestClient_AddRecord_error(t *testing.T) { client, mux := setupTest(t) mux.Handle("/dns/example.com/addRR", testErrorHandler()) record := Record{ Name: "sub", Type: "TXT", Data: "txtTXTtxt", TTL: 30, } err := client.AddRecord("example.com", record) require.Error(t, err) } func TestClient_RemoveRecord(t *testing.T) { client, mux := setupTest(t) params := map[string]string{ "data": "txtTXTtxt", "name": "sub", "type": "TXT", } mux.Handle("/dns/example.com/removeRR", testHandler(params)) record := Record{ Name: "sub", Type: "TXT", Data: "txtTXTtxt", } err := client.RemoveRecord("example.com", record) require.NoError(t, err) } func TestClient_RemoveRecord_error(t *testing.T) { client, mux := setupTest(t) mux.Handle("/dns/example.com/removeRR", testErrorHandler()) record := Record{ Name: "sub", Type: "TXT", Data: "txtTXTtxt", } err := client.RemoveRecord("example.com", record) require.Error(t, err) } lego-4.9.1/providers/dns/nearlyfreespeech/internal/fixtures/000077500000000000000000000000001434020463500242665ustar00rootroot00000000000000lego-4.9.1/providers/dns/nearlyfreespeech/internal/fixtures/error.json000066400000000000000000000001741434020463500263140ustar00rootroot00000000000000{ "error": "The API request could not be authenticated.", "debug": "The X-NFSN-Authentication header is not present." } lego-4.9.1/providers/dns/nearlyfreespeech/internal/types.go000066400000000000000000000005661434020463500241170ustar00rootroot00000000000000package internal import "fmt" type Record struct { Name string `url:"name,omitempty"` Type string `url:"type,omitempty"` Data string `url:"data,omitempty"` TTL int `url:"ttl,omitempty"` } type APIError struct { Message string `json:"error"` Debug string `json:"debug"` } func (a APIError) Error() string { return fmt.Sprintf("%s: %s", a.Message, a.Debug) } lego-4.9.1/providers/dns/nearlyfreespeech/nearlyfreespeech.go000066400000000000000000000110651434020463500244570ustar00rootroot00000000000000// Package nearlyfreespeech implements a DNS provider for solving the DNS-01 challenge using NearlyFreeSpeech.NET. package nearlyfreespeech import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech/internal" ) // Environment variables names. const ( envNamespace = "NEARLYFREESPEECH_" EnvLogin = envNamespace + "LOGIN" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string Login string TTL int PropagationTimeout time.Duration PollingInterval time.Duration SequenceInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 3600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for NearlyFreeSpeech.NET. // Credentials must be passed in the environment variable: NEARLYFREESPEECH_LOGIN, NEARLYFREESPEECH_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey, EnvLogin) if err != nil { return nil, fmt.Errorf("nearlyfreespeech: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] config.Login = values[EnvLogin] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for NearlyFreeSpeech.NET. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("nearlyfreespeech: the configuration of the DNS provider is nil") } if config.Login == "" || config.APIKey == "" { return nil, errors.New("nearlyfreespeech: API credentials are missing") } client := internal.NewClient(config.Login, config.APIKey) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Sequential All DNS challenges for this provider will be resolved sequentially. // Returns the interval between each iteration. func (d *DNSProvider) Sequential() time.Duration { return d.config.SequenceInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("nearlyfreespeech: could not determine zone for domain %q: %w", fqdn, err) } record := internal.Record{ Name: getRecordName(fqdn, authZone), Type: "TXT", Data: value, TTL: d.config.TTL, } err = d.client.AddRecord(authZone, record) if err != nil { return fmt.Errorf("nearlyfreespeech: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("nearlyfreespeech: could not determine zone for domain %q: %w", fqdn, err) } record := internal.Record{ Name: getRecordName(fqdn, authZone), Type: "TXT", Data: value, } err = d.client.RemoveRecord(domain, record) if err != nil { return fmt.Errorf("nearlyfreespeech: %w", err) } return nil } func getRecordName(fqdn, authZone string) string { return fqdn[0 : len(fqdn)-len(authZone)-1] } lego-4.9.1/providers/dns/nearlyfreespeech/nearlyfreespeech.toml000066400000000000000000000016551434020463500250310ustar00rootroot00000000000000Name = "NearlyFreeSpeech.NET" Description = '''''' URL = "https://nearlyfreespeech.net/" Code = "nearlyfreespeech" Since = "v4.8.0" Example = ''' NEARLYFREESPEECH_API_KEY=xxxxxx \ NEARLYFREESPEECH_LOGIN=xxxx \ lego --email you@example.com --dns nearlyfreespeech --domains my.example.org run ''' [Configuration] [Configuration.Credentials] NEARLYFREESPEECH_API_KEY = "API Key for API requests" NEARLYFREESPEECH_LOGIN = "Username for API requests" [Configuration.Additional] NEARLYFREESPEECH_POLLING_INTERVAL = "Time between DNS propagation check" NEARLYFREESPEECH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" NEARLYFREESPEECH_TTL = "The TTL of the TXT record used for the DNS challenge" NEARLYFREESPEECH_HTTP_TIMEOUT = "API request timeout" NEARLYFREESPEECH_SEQUENCE_INTERVAL = "Time between sequential requests" [Links] API = "https://members.nearlyfreespeech.net/wiki/API/Reference" lego-4.9.1/providers/dns/nearlyfreespeech/nearlyfreespeech_test.go000066400000000000000000000061041434020463500255140ustar00rootroot00000000000000package nearlyfreespeech import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAPIKey, EnvLogin).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", EnvLogin: "testuser", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIKey: "", EnvLogin: "", }, expected: "nearlyfreespeech: some credentials information are missing: NEARLYFREESPEECH_API_KEY,NEARLYFREESPEECH_LOGIN", }, { desc: "missing api key", envVars: map[string]string{ EnvAPIKey: "", EnvLogin: "testuser", }, expected: "nearlyfreespeech: some credentials information are missing: NEARLYFREESPEECH_API_KEY", }, { desc: "missing login", envVars: map[string]string{ EnvAPIKey: "123", EnvLogin: "", }, expected: "nearlyfreespeech: some credentials information are missing: NEARLYFREESPEECH_LOGIN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string login string apikey string expected string }{ { desc: "success", login: "login", apikey: "apikey", }, { desc: "missing credentials", expected: "nearlyfreespeech: API credentials are missing", }, { desc: "missing login", login: "", apikey: "apikey", expected: "nearlyfreespeech: API credentials are missing", }, { desc: "missing key", login: "login", apikey: "", expected: "nearlyfreespeech: API credentials are missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apikey config.Login = test.login p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/netcup/000077500000000000000000000000001434020463500165535ustar00rootroot00000000000000lego-4.9.1/providers/dns/netcup/internal/000077500000000000000000000000001434020463500203675ustar00rootroot00000000000000lego-4.9.1/providers/dns/netcup/internal/client.go000066400000000000000000000227211434020463500222000ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "time" ) // defaultBaseURL for reaching the jSON-based API-Endpoint of netcup. const defaultBaseURL = "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON" // success response status. const success = "success" // Request wrapper as specified in netcup wiki // needed for every request to netcup API around *Msg. // https://www.netcup-wiki.de/wiki/CCP_API#Anmerkungen_zu_JSON-Requests type Request struct { Action string `json:"action"` Param interface{} `json:"param"` } // LoginRequest as specified in netcup WSDL. // https://ccp.netcup.net/run/webservice/servers/endpoint.php#login type LoginRequest struct { CustomerNumber string `json:"customernumber"` APIKey string `json:"apikey"` APIPassword string `json:"apipassword"` ClientRequestID string `json:"clientrequestid,omitempty"` } // LogoutRequest as specified in netcup WSDL. // https://ccp.netcup.net/run/webservice/servers/endpoint.php#logout type LogoutRequest struct { CustomerNumber string `json:"customernumber"` APIKey string `json:"apikey"` APISessionID string `json:"apisessionid"` ClientRequestID string `json:"clientrequestid,omitempty"` } // UpdateDNSRecordsRequest as specified in netcup WSDL. // https://ccp.netcup.net/run/webservice/servers/endpoint.php#updateDnsRecords type UpdateDNSRecordsRequest struct { DomainName string `json:"domainname"` CustomerNumber string `json:"customernumber"` APIKey string `json:"apikey"` APISessionID string `json:"apisessionid"` ClientRequestID string `json:"clientrequestid,omitempty"` DNSRecordSet DNSRecordSet `json:"dnsrecordset"` } // DNSRecordSet as specified in netcup WSDL. // needed in UpdateDNSRecordsRequest. // https://ccp.netcup.net/run/webservice/servers/endpoint.php#Dnsrecordset type DNSRecordSet struct { DNSRecords []DNSRecord `json:"dnsrecords"` } // InfoDNSRecordsRequest as specified in netcup WSDL. // https://ccp.netcup.net/run/webservice/servers/endpoint.php#infoDnsRecords type InfoDNSRecordsRequest struct { DomainName string `json:"domainname"` CustomerNumber string `json:"customernumber"` APIKey string `json:"apikey"` APISessionID string `json:"apisessionid"` ClientRequestID string `json:"clientrequestid,omitempty"` } // DNSRecord as specified in netcup WSDL. // https://ccp.netcup.net/run/webservice/servers/endpoint.php#Dnsrecord type DNSRecord struct { ID int `json:"id,string,omitempty"` Hostname string `json:"hostname"` RecordType string `json:"type"` Priority string `json:"priority,omitempty"` Destination string `json:"destination"` DeleteRecord bool `json:"deleterecord,omitempty"` State string `json:"state,omitempty"` TTL int `json:"ttl,omitempty"` } // ResponseMsg as specified in netcup WSDL. // https://ccp.netcup.net/run/webservice/servers/endpoint.php#Responsemessage type ResponseMsg struct { ServerRequestID string `json:"serverrequestid"` ClientRequestID string `json:"clientrequestid,omitempty"` Action string `json:"action"` Status string `json:"status"` StatusCode int `json:"statuscode"` ShortMessage string `json:"shortmessage"` LongMessage string `json:"longmessage"` ResponseData json.RawMessage `json:"responsedata,omitempty"` } func (r *ResponseMsg) Error() string { return fmt.Sprintf("an error occurred during the action %s: [Status=%s, StatusCode=%d, ShortMessage=%s, LongMessage=%s]", r.Action, r.Status, r.StatusCode, r.ShortMessage, r.LongMessage) } // LoginResponse response to login action. type LoginResponse struct { APISessionID string `json:"apisessionid"` } // InfoDNSRecordsResponse response to infoDnsRecords action. type InfoDNSRecordsResponse struct { APISessionID string `json:"apisessionid"` DNSRecords []DNSRecord `json:"dnsrecords,omitempty"` } // Client netcup DNS client. type Client struct { customerNumber string apiKey string apiPassword string HTTPClient *http.Client BaseURL string } // NewClient creates a netcup DNS client. func NewClient(customerNumber, apiKey, apiPassword string) (*Client, error) { if customerNumber == "" || apiKey == "" || apiPassword == "" { return nil, errors.New("credentials missing") } return &Client{ customerNumber: customerNumber, apiKey: apiKey, apiPassword: apiPassword, BaseURL: defaultBaseURL, HTTPClient: &http.Client{ Timeout: 10 * time.Second, }, }, nil } // Login performs the login as specified by the netcup WSDL // returns sessionID needed to perform remaining actions. // https://ccp.netcup.net/run/webservice/servers/endpoint.php func (c *Client) Login() (string, error) { payload := &Request{ Action: "login", Param: &LoginRequest{ CustomerNumber: c.customerNumber, APIKey: c.apiKey, APIPassword: c.apiPassword, ClientRequestID: "", }, } var responseData LoginResponse err := c.doRequest(payload, &responseData) if err != nil { return "", fmt.Errorf("loging error: %w", err) } return responseData.APISessionID, nil } // Logout performs the logout with the supplied sessionID as specified by the netcup WSDL. // https://ccp.netcup.net/run/webservice/servers/endpoint.php func (c *Client) Logout(sessionID string) error { payload := &Request{ Action: "logout", Param: &LogoutRequest{ CustomerNumber: c.customerNumber, APIKey: c.apiKey, APISessionID: sessionID, ClientRequestID: "", }, } err := c.doRequest(payload, nil) if err != nil { return fmt.Errorf("logout error: %w", err) } return nil } // UpdateDNSRecord performs an update of the DNSRecords as specified by the netcup WSDL. // https://ccp.netcup.net/run/webservice/servers/endpoint.php func (c *Client) UpdateDNSRecord(sessionID, domainName string, records []DNSRecord) error { payload := &Request{ Action: "updateDnsRecords", Param: UpdateDNSRecordsRequest{ DomainName: domainName, CustomerNumber: c.customerNumber, APIKey: c.apiKey, APISessionID: sessionID, ClientRequestID: "", DNSRecordSet: DNSRecordSet{DNSRecords: records}, }, } err := c.doRequest(payload, nil) if err != nil { return fmt.Errorf("error when sending the request: %w", err) } return nil } // GetDNSRecords retrieves all dns records of an DNS-Zone as specified by the netcup WSDL // returns an array of DNSRecords. // https://ccp.netcup.net/run/webservice/servers/endpoint.php func (c *Client) GetDNSRecords(hostname, apiSessionID string) ([]DNSRecord, error) { payload := &Request{ Action: "infoDnsRecords", Param: InfoDNSRecordsRequest{ DomainName: hostname, CustomerNumber: c.customerNumber, APIKey: c.apiKey, APISessionID: apiSessionID, ClientRequestID: "", }, } var responseData InfoDNSRecordsResponse err := c.doRequest(payload, &responseData) if err != nil { return nil, fmt.Errorf("error when sending the request: %w", err) } return responseData.DNSRecords, nil } // doRequest marshals given body to JSON, send the request to netcup API // and returns body of response. func (c *Client) doRequest(payload, responseData interface{}) error { body, err := json.Marshal(payload) if err != nil { return err } req, err := http.NewRequest(http.MethodPost, c.BaseURL, bytes.NewReader(body)) if err != nil { return err } req.Close = true req.Header.Set("content-type", "application/json") resp, err := c.HTTPClient.Do(req) if err != nil { return err } if err = checkResponse(resp); err != nil { return err } respMsg, err := decodeResponseMsg(resp) if err != nil { return err } if respMsg.Status != success { return respMsg } if responseData != nil { err = json.Unmarshal(respMsg.ResponseData, responseData) if err != nil { return fmt.Errorf("%v: unmarshaling %T error: %w: %s", respMsg, responseData, err, string(respMsg.ResponseData)) } } return nil } func checkResponse(resp *http.Response) error { if resp.StatusCode > 299 { if resp.Body == nil { return fmt.Errorf("response body is nil, status code=%d", resp.StatusCode) } defer resp.Body.Close() raw, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("unable to read body: status code=%d, error=%w", resp.StatusCode, err) } return fmt.Errorf("status code=%d: %s", resp.StatusCode, string(raw)) } return nil } func decodeResponseMsg(resp *http.Response) (*ResponseMsg, error) { if resp.Body == nil { return nil, fmt.Errorf("response body is nil, status code=%d", resp.StatusCode) } defer resp.Body.Close() raw, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("unable to read body: status code=%d, error=%w", resp.StatusCode, err) } var respMsg ResponseMsg err = json.Unmarshal(raw, &respMsg) if err != nil { return nil, fmt.Errorf("unmarshaling %T error [status code=%d]: %w: %s", respMsg, resp.StatusCode, err, string(raw)) } return &respMsg, nil } // GetDNSRecordIdx searches a given array of DNSRecords for a given DNSRecord // equivalence is determined by Destination and RecortType attributes // returns index of given DNSRecord in given array of DNSRecords. func GetDNSRecordIdx(records []DNSRecord, record DNSRecord) (int, error) { for index, element := range records { if record.Destination == element.Destination && record.RecordType == element.RecordType { return index, nil } } return -1, errors.New("no DNS Record found") } lego-4.9.1/providers/dns/netcup/internal/client_test.go000066400000000000000000000346421434020463500232440ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "strconv" "strings" "testing" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var envTest = tester.NewEnvTest( "NETCUP_CUSTOMER_NUMBER", "NETCUP_API_KEY", "NETCUP_API_PASSWORD"). WithDomain("NETCUP_DOMAIN") func setupTest(t *testing.T) (*Client, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client, err := NewClient("a", "b", "c") require.NoError(t, err) client.HTTPClient = server.Client() client.BaseURL = server.URL return client, mux } func TestGetDNSRecordIdx(t *testing.T) { records := []DNSRecord{ { ID: 12345, Hostname: "asdf", RecordType: "TXT", Priority: "0", Destination: "randomtext", DeleteRecord: false, State: "yes", }, { ID: 23456, Hostname: "@", RecordType: "A", Priority: "0", Destination: "127.0.0.1", DeleteRecord: false, State: "yes", }, { ID: 34567, Hostname: "dfgh", RecordType: "CNAME", Priority: "0", Destination: "example.com", DeleteRecord: false, State: "yes", }, { ID: 45678, Hostname: "fghj", RecordType: "MX", Priority: "10", Destination: "mail.example.com", DeleteRecord: false, State: "yes", }, } testCases := []struct { desc string record DNSRecord expectError bool }{ { desc: "simple", record: DNSRecord{ ID: 12345, Hostname: "asdf", RecordType: "TXT", Priority: "0", Destination: "randomtext", DeleteRecord: false, State: "yes", }, }, { desc: "wrong Destination", record: DNSRecord{ ID: 12345, Hostname: "asdf", RecordType: "TXT", Priority: "0", Destination: "wrong", DeleteRecord: false, State: "yes", }, expectError: true, }, { desc: "record type CNAME", record: DNSRecord{ ID: 12345, Hostname: "asdf", RecordType: "CNAME", Priority: "0", Destination: "randomtext", DeleteRecord: false, State: "yes", }, expectError: true, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() idx, err := GetDNSRecordIdx(records, test.record) if test.expectError { assert.Error(t, err) assert.Equal(t, -1, idx) } else { assert.NoError(t, err) assert.Equal(t, records[idx], test.record) } }) } } func TestClient_Login(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { raw, err := io.ReadAll(req.Body) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } if string(raw) != `{"action":"login","param":{"customernumber":"a","apikey":"b","apipassword":"c"}}` { http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest) return } response := ` { "serverrequestid": "srv-request-id", "clientrequestid": "", "action": "login", "status": "success", "statuscode": 2000, "shortmessage": "Login successful", "longmessage": "Session has been created successful.", "responsedata": { "apisessionid": "api-session-id" } } ` _, err = rw.Write([]byte(response)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) sessionID, err := client.Login() require.NoError(t, err) assert.Equal(t, "api-session-id", sessionID) } func TestClient_Login_errors(t *testing.T) { testCases := []struct { desc string handler func(rw http.ResponseWriter, req *http.Request) }{ { desc: "HTTP error", handler: func(rw http.ResponseWriter, _ *http.Request) { http.Error(rw, "error message", http.StatusInternalServerError) }, }, { desc: "API error", handler: func(rw http.ResponseWriter, _ *http.Request) { response := ` { "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE", "clientrequestid":"", "action":"login", "status":"error", "statuscode":4013, "shortmessage":"Validation Error.", "longmessage":"Message is empty.", "responsedata":"" }` _, err := rw.Write([]byte(response)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }, }, { desc: "responsedata marshaling error", handler: func(rw http.ResponseWriter, _ *http.Request) { response := ` { "serverrequestid": "srv-request-id", "clientrequestid": "", "action": "login", "status": "success", "statuscode": 2000, "shortmessage": "Login successful", "longmessage": "Session has been created successful.", "responsedata": "" }` _, err := rw.Write([]byte(response)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() client, mux := setupTest(t) mux.HandleFunc("/", test.handler) sessionID, err := client.Login() assert.Error(t, err) assert.Equal(t, "", sessionID) }) } } func TestClient_Logout(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { raw, err := io.ReadAll(req.Body) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } if string(raw) != `{"action":"logout","param":{"customernumber":"a","apikey":"b","apisessionid":"session-id"}}` { http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest) return } response := ` { "serverrequestid": "request-id", "clientrequestid": "", "action": "logout", "status": "success", "statuscode": 2000, "shortmessage": "Logout successful", "longmessage": "Session has been terminated successful.", "responsedata": "" }` _, err = rw.Write([]byte(response)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) err := client.Logout("session-id") require.NoError(t, err) } func TestClient_Logout_errors(t *testing.T) { testCases := []struct { desc string handler func(rw http.ResponseWriter, req *http.Request) }{ { desc: "HTTP error", handler: func(rw http.ResponseWriter, _ *http.Request) { http.Error(rw, "error message", http.StatusInternalServerError) }, }, { desc: "API error", handler: func(rw http.ResponseWriter, _ *http.Request) { response := ` { "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE", "clientrequestid":"", "action":"logout", "status":"error", "statuscode":4013, "shortmessage":"Validation Error.", "longmessage":"Message is empty.", "responsedata":"" }` _, err := rw.Write([]byte(response)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() client, mux := setupTest(t) mux.HandleFunc("/", test.handler) err := client.Logout("session-id") require.Error(t, err) }) } } func TestClient_GetDNSRecords(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { raw, err := io.ReadAll(req.Body) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } if string(raw) != `{"action":"infoDnsRecords","param":{"domainname":"example.com","customernumber":"a","apikey":"b","apisessionid":"api-session-id"}}` { http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest) return } response := ` { "serverrequestid":"srv-request-id", "clientrequestid":"", "action":"infoDnsRecords", "status":"success", "statuscode":2000, "shortmessage":"Login successful", "longmessage":"Session has been created successful.", "responsedata":{ "apisessionid":"api-session-id", "dnsrecords":[ { "id":"1", "hostname":"example.com", "type":"TXT", "priority":"1", "destination":"bGVnbzE=", "state":"yes", "ttl":300 }, { "id":"2", "hostname":"example2.com", "type":"TXT", "priority":"1", "destination":"bGVnbw==", "state":"yes", "ttl":300 } ] } }` _, err = rw.Write([]byte(response)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) expected := []DNSRecord{{ ID: 1, Hostname: "example.com", RecordType: "TXT", Priority: "1", Destination: "bGVnbzE=", DeleteRecord: false, State: "yes", TTL: 300, }, { ID: 2, Hostname: "example2.com", RecordType: "TXT", Priority: "1", Destination: "bGVnbw==", DeleteRecord: false, State: "yes", TTL: 300, }} records, err := client.GetDNSRecords("example.com", "api-session-id") require.NoError(t, err) assert.Equal(t, expected, records) } func TestClient_GetDNSRecords_errors(t *testing.T) { testCases := []struct { desc string handler func(rw http.ResponseWriter, req *http.Request) }{ { desc: "HTTP error", handler: func(rw http.ResponseWriter, _ *http.Request) { http.Error(rw, "error message", http.StatusInternalServerError) }, }, { desc: "API error", handler: func(rw http.ResponseWriter, _ *http.Request) { response := ` { "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE", "clientrequestid":"", "action":"infoDnsRecords", "status":"error", "statuscode":4013, "shortmessage":"Validation Error.", "longmessage":"Message is empty.", "responsedata":"" }` _, err := rw.Write([]byte(response)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }, }, { desc: "responsedata marshaling error", handler: func(rw http.ResponseWriter, req *http.Request) { raw, err := io.ReadAll(req.Body) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } if string(raw) != `{"action":"infoDnsRecords","param":{"domainname":"example.com","customernumber":"a","apikey":"b","apisessionid":"api-session-id"}}` { http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest) return } response := ` { "serverrequestid":"srv-request-id", "clientrequestid":"", "action":"infoDnsRecords", "status":"success", "statuscode":2000, "shortmessage":"Login successful", "longmessage":"Session has been created successful.", "responsedata":"" }` _, err = rw.Write([]byte(response)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() client, mux := setupTest(t) mux.HandleFunc("/", test.handler) records, err := client.GetDNSRecords("example.com", "api-session-id") require.Error(t, err) assert.Empty(t, records) }) } } func TestLiveClientAuth(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } // Setup envTest.RestoreEnv() client, err := NewClient( envTest.GetValue("NETCUP_CUSTOMER_NUMBER"), envTest.GetValue("NETCUP_API_KEY"), envTest.GetValue("NETCUP_API_PASSWORD")) require.NoError(t, err) for i := 1; i < 4; i++ { i := i t.Run("Test_"+strconv.Itoa(i), func(t *testing.T) { t.Parallel() sessionID, err := client.Login() require.NoError(t, err) err = client.Logout(sessionID) require.NoError(t, err) }) } } func TestLiveClientGetDnsRecords(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } // Setup envTest.RestoreEnv() client, err := NewClient( envTest.GetValue("NETCUP_CUSTOMER_NUMBER"), envTest.GetValue("NETCUP_API_KEY"), envTest.GetValue("NETCUP_API_PASSWORD")) require.NoError(t, err) sessionID, err := client.Login() require.NoError(t, err) fqdn, _ := dns01.GetRecord(envTest.GetDomain(), "123d==") zone, err := dns01.FindZoneByFqdn(fqdn) require.NoError(t, err, "error finding DNSZone") zone = dns01.UnFqdn(zone) // TestMethod _, err = client.GetDNSRecords(zone, sessionID) require.NoError(t, err) // Tear down err = client.Logout(sessionID) require.NoError(t, err) } func TestLiveClientUpdateDnsRecord(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } // Setup envTest.RestoreEnv() client, err := NewClient( envTest.GetValue("NETCUP_CUSTOMER_NUMBER"), envTest.GetValue("NETCUP_API_KEY"), envTest.GetValue("NETCUP_API_PASSWORD")) require.NoError(t, err) sessionID, err := client.Login() require.NoError(t, err) fqdn, _ := dns01.GetRecord(envTest.GetDomain(), "123d==") zone, err := dns01.FindZoneByFqdn(fqdn) require.NoError(t, err, fmt.Errorf("error finding DNSZone, %w", err)) hostname := strings.Replace(fqdn, "."+zone, "", 1) record := DNSRecord{ Hostname: hostname, RecordType: "TXT", Destination: "asdf5678", DeleteRecord: false, TTL: 120, } // test zone = dns01.UnFqdn(zone) err = client.UpdateDNSRecord(sessionID, zone, []DNSRecord{record}) require.NoError(t, err) records, err := client.GetDNSRecords(zone, sessionID) require.NoError(t, err) recordIdx, err := GetDNSRecordIdx(records, record) require.NoError(t, err) assert.Equal(t, record.Hostname, records[recordIdx].Hostname) assert.Equal(t, record.RecordType, records[recordIdx].RecordType) assert.Equal(t, record.Destination, records[recordIdx].Destination) assert.Equal(t, record.DeleteRecord, records[recordIdx].DeleteRecord) records[recordIdx].DeleteRecord = true // Tear down err = client.UpdateDNSRecord(sessionID, envTest.GetDomain(), []DNSRecord{records[recordIdx]}) require.NoError(t, err, "Did not remove record! Please do so yourself.") err = client.Logout(sessionID) require.NoError(t, err) } lego-4.9.1/providers/dns/netcup/netcup.go000066400000000000000000000122241434020463500204010ustar00rootroot00000000000000// Package netcup implements a DNS Provider for solving the DNS-01 challenge using the netcup DNS API. package netcup import ( "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/netcup/internal" ) // Environment variables names. const ( envNamespace = "NETCUP_" EnvCustomerNumber = envNamespace + "CUSTOMER_NUMBER" EnvAPIKey = envNamespace + "API_KEY" EnvAPIPassword = envNamespace + "API_PASSWORD" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Key string Password string Customer string TTL int PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *internal.Client config *Config } // NewDNSProvider returns a DNSProvider instance configured for netcup. // Credentials must be passed in the environment variables: // NETCUP_CUSTOMER_NUMBER, NETCUP_API_KEY, NETCUP_API_PASSWORD. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvCustomerNumber, EnvAPIKey, EnvAPIPassword) if err != nil { return nil, fmt.Errorf("netcup: %w", err) } config := NewDefaultConfig() config.Customer = values[EnvCustomerNumber] config.Key = values[EnvAPIKey] config.Password = values[EnvAPIPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for netcup. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("netcup: the configuration of the DNS provider is nil") } client, err := internal.NewClient(config.Customer, config.Key, config.Password) if err != nil { return nil, fmt.Errorf("netcup: %w", err) } client.HTTPClient = config.HTTPClient return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("netcup: failed to find DNSZone, %w", err) } sessionID, err := d.client.Login() if err != nil { return fmt.Errorf("netcup: %w", err) } defer func() { err = d.client.Logout(sessionID) if err != nil { log.Print("netcup: %v", err) } }() hostname := strings.Replace(fqdn, "."+zone, "", 1) record := internal.DNSRecord{ Hostname: hostname, RecordType: "TXT", Destination: value, TTL: d.config.TTL, } zone = dns01.UnFqdn(zone) records, err := d.client.GetDNSRecords(zone, sessionID) if err != nil { // skip no existing records log.Infof("no existing records, error ignored: %v", err) } records = append(records, record) err = d.client.UpdateDNSRecord(sessionID, zone, records) if err != nil { return fmt.Errorf("netcup: failed to add TXT-Record: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("netcup: failed to find DNSZone, %w", err) } sessionID, err := d.client.Login() if err != nil { return fmt.Errorf("netcup: %w", err) } defer func() { err = d.client.Logout(sessionID) if err != nil { log.Print("netcup: %v", err) } }() hostname := strings.Replace(fqdn, "."+zone, "", 1) zone = dns01.UnFqdn(zone) records, err := d.client.GetDNSRecords(zone, sessionID) if err != nil { return fmt.Errorf("netcup: %w", err) } record := internal.DNSRecord{ Hostname: hostname, RecordType: "TXT", Destination: value, } idx, err := internal.GetDNSRecordIdx(records, record) if err != nil { return fmt.Errorf("netcup: %w", err) } records[idx].DeleteRecord = true err = d.client.UpdateDNSRecord(sessionID, zone, []internal.DNSRecord{records[idx]}) if err != nil { return fmt.Errorf("netcup: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/netcup/netcup.toml000066400000000000000000000014231434020463500207460ustar00rootroot00000000000000Name = "Netcup" Description = '''''' URL = "https://www.netcup.eu/" Code = "netcup" Since = "v1.1.0" Example = ''' NETCUP_CUSTOMER_NUMBER=xxxx \ NETCUP_API_KEY=yyyy \ NETCUP_API_PASSWORD=zzzz \ lego --email you@example.com --dns netcup --domains my.example.org run ''' [Configuration] [Configuration.Credentials] NETCUP_CUSTOMER_NUMBER = "Customer number" NETCUP_API_KEY = "API key" NETCUP_API_PASSWORD = "API password" [Configuration.Additional] NETCUP_POLLING_INTERVAL = "Time between DNS propagation check" NETCUP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" NETCUP_TTL = "The TTL of the TXT record used for the DNS challenge" NETCUP_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.netcup-wiki.de/wiki/DNS_API" lego-4.9.1/providers/dns/netcup/netcup_test.go000066400000000000000000000076651434020463500214550ustar00rootroot00000000000000package netcup import ( "fmt" "testing" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvCustomerNumber, EnvAPIKey, EnvAPIPassword). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvCustomerNumber: "A", EnvAPIKey: "B", EnvAPIPassword: "C", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvCustomerNumber: "", EnvAPIKey: "", EnvAPIPassword: "", }, expected: "netcup: some credentials information are missing: NETCUP_CUSTOMER_NUMBER,NETCUP_API_KEY,NETCUP_API_PASSWORD", }, { desc: "missing customer number", envVars: map[string]string{ EnvCustomerNumber: "", EnvAPIKey: "B", EnvAPIPassword: "C", }, expected: "netcup: some credentials information are missing: NETCUP_CUSTOMER_NUMBER", }, { desc: "missing API key", envVars: map[string]string{ EnvCustomerNumber: "A", EnvAPIKey: "", EnvAPIPassword: "C", }, expected: "netcup: some credentials information are missing: NETCUP_API_KEY", }, { desc: "missing api password", envVars: map[string]string{ EnvCustomerNumber: "A", EnvAPIKey: "B", EnvAPIPassword: "", }, expected: "netcup: some credentials information are missing: NETCUP_API_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string customer string key string password string expected string }{ { desc: "success", customer: "A", key: "B", password: "C", }, { desc: "missing credentials", expected: "netcup: credentials missing", }, { desc: "missing customer", customer: "", key: "B", password: "C", expected: "netcup: credentials missing", }, { desc: "missing key", customer: "A", key: "", password: "C", expected: "netcup: credentials missing", }, { desc: "missing password", customer: "A", key: "B", password: "", expected: "netcup: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Customer = test.customer config.Key = test.key config.Password = test.password p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresentAndCleanup(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() p, err := NewDNSProvider() require.NoError(t, err) fqdn, _ := dns01.GetRecord(envTest.GetDomain(), "123d==") zone, err := dns01.FindZoneByFqdn(fqdn) require.NoError(t, err, "error finding DNSZone") zone = dns01.UnFqdn(zone) testCases := []string{ zone, "sub." + zone, "*." + zone, "*.sub." + zone, } for _, test := range testCases { t.Run(fmt.Sprintf("domain(%s)", test), func(t *testing.T) { err = p.Present(test, "987d", "123d==") require.NoError(t, err) err = p.CleanUp(test, "987d", "123d==") require.NoError(t, err, "Did not clean up! Please remove record yourself.") }) } } lego-4.9.1/providers/dns/netlify/000077500000000000000000000000001434020463500167275ustar00rootroot00000000000000lego-4.9.1/providers/dns/netlify/internal/000077500000000000000000000000001434020463500205435ustar00rootroot00000000000000lego-4.9.1/providers/dns/netlify/internal/client.go000066400000000000000000000101521434020463500223470ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "path" ) const defaultBaseURL = "https://api.netlify.com/api/v1" // Client Netlify API client. type Client struct { HTTPClient *http.Client BaseURL string token string } // NewClient creates a new Client. func NewClient(token string) *Client { return &Client{ HTTPClient: http.DefaultClient, BaseURL: defaultBaseURL, token: token, } } // GetRecords gets a DNS records. func (c *Client) GetRecords(zoneID string) ([]DNSRecord, error) { endpoint, err := c.createEndpoint("dns_zones", zoneID, "dns_records") if err != nil { return nil, fmt.Errorf("failed to parse endpoint: %w", err) } req, err := http.NewRequest(http.MethodGet, endpoint, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("API call failed: %w", err) } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("invalid status code: %s: %s", resp.Status, string(body)) } var records []DNSRecord err = json.Unmarshal(body, &records) if err != nil { return nil, fmt.Errorf("failed to marshal response body: %w", err) } return records, nil } // CreateRecord creates a DNS records. func (c *Client) CreateRecord(zoneID string, record DNSRecord) (*DNSRecord, error) { endpoint, err := c.createEndpoint("dns_zones", zoneID, "dns_records") if err != nil { return nil, fmt.Errorf("failed to parse endpoint: %w", err) } marshaledRecord, err := json.Marshal(record) if err != nil { return nil, fmt.Errorf("failed to marshal request body: %w", err) } req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(marshaledRecord)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json; charset=utf-8") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("API call failed: %w", err) } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } if resp.StatusCode != http.StatusCreated { return nil, fmt.Errorf("invalid status code: %s: %s", resp.Status, string(body)) } var recordResp DNSRecord err = json.Unmarshal(body, &recordResp) if err != nil { return nil, fmt.Errorf("failed to marshal response body: %w", err) } return &recordResp, nil } // RemoveRecord removes a DNS records. func (c *Client) RemoveRecord(zoneID, recordID string) error { endpoint, err := c.createEndpoint("dns_zones", zoneID, "dns_records", recordID) if err != nil { return fmt.Errorf("failed to parse endpoint: %w", err) } req, err := http.NewRequest(http.MethodDelete, endpoint, nil) if err != nil { return fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) resp, err := c.HTTPClient.Do(req) if err != nil { return fmt.Errorf("API call failed: %w", err) } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response body: %w", err) } if resp.StatusCode != http.StatusNoContent { return fmt.Errorf("invalid status code: %s: %s", resp.Status, string(body)) } return nil } func (c *Client) createEndpoint(parts ...string) (string, error) { base, err := url.Parse(c.BaseURL) if err != nil { return "", fmt.Errorf("failed to parse base URL: %w", err) } endpoint, err := base.Parse(path.Join(base.Path, path.Join(parts...))) if err != nil { return "", fmt.Errorf("failed to parse endpoint path: %w", err) } return endpoint.String(), nil } lego-4.9.1/providers/dns/netlify/internal/client_test.go000066400000000000000000000070711434020463500234140ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_GetRecords(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/dns_zones/zoneID/dns_records", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, "unsupported method", http.StatusMethodNotAllowed) return } auth := req.Header.Get("Authorization") if auth != "Bearer tokenA" { http.Error(rw, fmt.Sprintf("invali token: %s", auth), http.StatusUnauthorized) return } rw.Header().Set("Content-Type", "application/json; charset=utf-8") file, err := os.Open("./fixtures/get_records.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient("tokenA") client.BaseURL = server.URL records, err := client.GetRecords("zoneID") require.NoError(t, err) expected := []DNSRecord{ {ID: "u6b433c15a27a2d79c6616d6", Hostname: "example.org", TTL: 3600, Type: "A", Value: "10.10.10.10"}, {ID: "u6b4764216f272872ac0ff71", Hostname: "test.example.org", TTL: 300, Type: "TXT", Value: "txtxtxtxtxtxt"}, } assert.Equal(t, expected, records) } func TestClient_CreateRecord(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/dns_zones/zoneID/dns_records", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, "unsupported method", http.StatusMethodNotAllowed) return } auth := req.Header.Get("Authorization") if auth != "Bearer tokenB" { http.Error(rw, fmt.Sprintf("invali token: %s", auth), http.StatusUnauthorized) return } rw.Header().Set("Content-Type", "application/json; charset=utf-8") rw.WriteHeader(http.StatusCreated) file, err := os.Open("./fixtures/create_record.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) client := NewClient("tokenB") client.BaseURL = server.URL record := DNSRecord{ Hostname: "_acme-challenge.example.com", TTL: 300, Type: "TXT", Value: "txtxtxtxtxtxt", } result, err := client.CreateRecord("zoneID", record) require.NoError(t, err) expected := &DNSRecord{ ID: "u6b4764216f272872ac0ff71", Hostname: "test.example.org", TTL: 300, Type: "TXT", Value: "txtxtxtxtxtxt", } assert.Equal(t, expected, result) } func TestClient_RemoveRecord(t *testing.T) { mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/dns_zones/zoneID/dns_records/recordID", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodDelete { http.Error(rw, "unsupported method", http.StatusMethodNotAllowed) return } auth := req.Header.Get("Authorization") if auth != "Bearer tokenC" { http.Error(rw, fmt.Sprintf("invali token: %s", auth), http.StatusUnauthorized) return } rw.WriteHeader(http.StatusNoContent) }) client := NewClient("tokenC") client.BaseURL = server.URL err := client.RemoveRecord("zoneID", "recordID") require.NoError(t, err) } lego-4.9.1/providers/dns/netlify/internal/fixtures/000077500000000000000000000000001434020463500224145ustar00rootroot00000000000000lego-4.9.1/providers/dns/netlify/internal/fixtures/create_record.json000066400000000000000000000004741434020463500261150ustar00rootroot00000000000000{ "hostname": "test.example.org", "type": "TXT", "ttl": 300, "priority": null, "weight": null, "port": null, "flag": null, "tag": null, "id": "u6b4764216f272872ac0ff71", "site_id": null, "dns_zone_id": "u6b4336178f002e0a06bb0b6", "errors": [], "managed": false, "value": "txtxtxtxtxtxt" }lego-4.9.1/providers/dns/netlify/internal/fixtures/get_records.json000066400000000000000000000012671434020463500256150ustar00rootroot00000000000000[ { "hostname": "example.org", "type": "A", "ttl": 3600, "priority": null, "weight": null, "port": null, "flag": null, "tag": null, "id": "u6b433c15a27a2d79c6616d6", "site_id": null, "dns_zone_id": "u6b4336178f002e0a06bb0b6", "errors": [], "managed": false, "value": "10.10.10.10" }, { "hostname": "test.example.org", "type": "TXT", "ttl": 300, "priority": null, "weight": null, "port": null, "flag": null, "tag": null, "id": "u6b4764216f272872ac0ff71", "site_id": null, "dns_zone_id": "u6b4336178f002e0a06bb0b6", "errors": [], "managed": false, "value": "txtxtxtxtxtxt" } ] lego-4.9.1/providers/dns/netlify/internal/model.go000066400000000000000000000004431434020463500221730ustar00rootroot00000000000000package internal // DNSRecord DNS record representation. type DNSRecord struct { ID string `json:"id,omitempty"` Hostname string `json:"hostname,omitempty"` TTL int `json:"ttl,omitempty"` Type string `json:"type,omitempty"` Value string `json:"value,omitempty"` } lego-4.9.1/providers/dns/netlify/netlify.go000066400000000000000000000107041434020463500207320ustar00rootroot00000000000000// Package netlify implements a DNS provider for solving the DNS-01 challenge using Netlify. package netlify import ( "errors" "fmt" "net/http" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/netlify/internal" ) // Environment variables names. const ( envNamespace = "NETLIFY_" EnvToken = envNamespace + "TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Token string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 300), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client recordIDs map[string]string recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for Netlify. // Credentials must be passed in the environment variable: NETLIFY_TOKEN. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvToken) if err != nil { return nil, fmt.Errorf("netlify: %w", err) } config := NewDefaultConfig() config.Token = values[EnvToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Netlify. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("netlify: the configuration of the DNS provider is nil") } if config.Token == "" { return nil, errors.New("netlify: incomplete credentials, missing token") } client := internal.NewClient(config.Token) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]string), }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("netlify: failed to find zone: %w", err) } authZone = dns01.UnFqdn(authZone) record := internal.DNSRecord{ Hostname: dns01.UnFqdn(fqdn), TTL: d.config.TTL, Type: "TXT", Value: value, } resp, err := d.client.CreateRecord(strings.ReplaceAll(authZone, ".", "_"), record) if err != nil { return fmt.Errorf("netlify: failed to create TXT records: fqdn=%s, authZone=%s: %w", fqdn, authZone, err) } d.recordIDsMu.Lock() d.recordIDs[token] = resp.ID d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("netlify: failed to find zone: %w", err) } authZone = dns01.UnFqdn(authZone) // gets the record's unique ID from when we created it d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("netlify: unknown record ID for '%s' '%s'", fqdn, token) } err = d.client.RemoveRecord(strings.ReplaceAll(authZone, ".", "_"), recordID) if err != nil { return fmt.Errorf("netlify: failed to delete TXT records: fqdn=%s, authZone=%s, recordID=%s: %w", fqdn, authZone, recordID, err) } // deletes record ID from map d.recordIDsMu.Lock() delete(d.recordIDs, token) d.recordIDsMu.Unlock() return nil } lego-4.9.1/providers/dns/netlify/netlify.toml000066400000000000000000000012701434020463500212760ustar00rootroot00000000000000Name = "Netlify" Description = '''''' URL = "https://www.netlify.com" Code = "netlify" Since = "v3.7.0" Example = ''' NETLIFY_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ lego --email you@example.com --dns netlify --domains my.example.org run ''' [Configuration] [Configuration.Credentials] NETLIFY_TOKEN = "Token" [Configuration.Additional] NETLIFY_POLLING_INTERVAL = "Time between DNS propagation check" NETLIFY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" NETLIFY_TTL = "The TTL of the TXT record used for the DNS challenge" NETLIFY_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://open-api.netlify.com/" lego-4.9.1/providers/dns/netlify/netlify_test.go000066400000000000000000000043101434020463500217650ustar00rootroot00000000000000package netlify import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvToken: "123", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvToken: "", }, expected: "netlify: some credentials information are missing: NETLIFY_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string expected string token string }{ { desc: "success", token: "api_key", }, { desc: "missing credentials", expected: "netlify: incomplete credentials, missing token", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Token = test.token p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/nicmanager/000077500000000000000000000000001434020463500173615ustar00rootroot00000000000000lego-4.9.1/providers/dns/nicmanager/internal/000077500000000000000000000000001434020463500211755ustar00rootroot00000000000000lego-4.9.1/providers/dns/nicmanager/internal/client.go000066400000000000000000000067551434020463500230170ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "path" "strconv" "time" "github.com/pquerna/otp/totp" ) const ( defaultBaseURL = "https://api.nicmanager.com/v1" headerTOTPToken = "X-Auth-Token" ) // Modes. const ( ModeAnycast = "anycast" ModeZone = "zone" ) // Options the Client options. type Options struct { Login string Username string Email string Password string OTP string Mode string } // Client a nicmanager DNS client. type Client struct { HTTPClient *http.Client baseURL *url.URL username string password string otp string mode string } // NewClient create a new Client. func NewClient(opts Options) *Client { c := &Client{ mode: ModeAnycast, username: opts.Email, password: opts.Password, otp: opts.OTP, HTTPClient: &http.Client{Timeout: 10 * time.Second}, } c.baseURL, _ = url.Parse(defaultBaseURL) if opts.Mode != "" { c.mode = opts.Mode } if opts.Login != "" && opts.Username != "" { c.username = fmt.Sprintf("%s.%s", opts.Login, opts.Username) } return c } func (c Client) GetZone(name string) (*Zone, error) { resp, err := c.do(http.MethodGet, name, nil) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode >= http.StatusBadRequest { b, _ := io.ReadAll(resp.Body) msg := APIError{StatusCode: resp.StatusCode} if err = json.Unmarshal(b, &msg); err != nil { return nil, fmt.Errorf("failed to get zone info for %s", name) } return nil, msg } var zone Zone err = json.NewDecoder(resp.Body).Decode(&zone) if err != nil { return nil, err } return &zone, nil } func (c Client) AddRecord(zone string, req RecordCreateUpdate) error { resp, err := c.do(http.MethodPost, path.Join(zone, "records"), req) if err != nil { return err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusAccepted { b, _ := io.ReadAll(resp.Body) msg := APIError{StatusCode: resp.StatusCode} if err = json.Unmarshal(b, &msg); err != nil { return fmt.Errorf("records create should've returned %d but returned %d", http.StatusAccepted, resp.StatusCode) } return msg } return nil } func (c Client) DeleteRecord(zone string, record int) error { resp, err := c.do(http.MethodDelete, path.Join(zone, "records", strconv.Itoa(record)), nil) if err != nil { return err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusAccepted { b, _ := io.ReadAll(resp.Body) msg := APIError{StatusCode: resp.StatusCode} if err = json.Unmarshal(b, &msg); err != nil { return fmt.Errorf("records delete should've returned %d but returned %d", http.StatusAccepted, resp.StatusCode) } return msg } return nil } func (c Client) do(method, uri string, body interface{}) (*http.Response, error) { var reqBody io.Reader if body != nil { jsonValue, err := json.Marshal(body) if err != nil { return nil, err } reqBody = bytes.NewBuffer(jsonValue) } endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, c.mode, uri)) if err != nil { return nil, err } r, err := http.NewRequest(method, endpoint.String(), reqBody) if err != nil { return nil, err } r.Header.Set("Accept", "application/json") r.Header.Set("Content-Type", "application/json") r.SetBasicAuth(c.username, c.password) if c.otp != "" { tan, err := totp.GenerateCode(c.otp, time.Now()) if err != nil { return nil, err } r.Header.Set(headerTOTPToken, tan) } return c.HTTPClient.Do(r) } lego-4.9.1/providers/dns/nicmanager/internal/client_test.go000066400000000000000000000067011434020463500240450ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_GetZone(t *testing.T) { client := setupTest(t, "/anycast/nicmanager-anycastdns4.net", testHandler(http.MethodGet, http.StatusOK, "zone.json")) zone, err := client.GetZone("nicmanager-anycastdns4.net") require.NoError(t, err) expected := &Zone{ Name: "nicmanager-anycastdns4.net", Active: true, Records: []Record{ { ID: 186, Name: "nicmanager-anycastdns4.net", Type: "A", Content: "123.123.123.123", TTL: 3600, }, }, } assert.Equal(t, expected, zone) } func TestClient_GetZone_error(t *testing.T) { client := setupTest(t, "/anycast/foo", testHandler(http.MethodGet, http.StatusNotFound, "error.json")) _, err := client.GetZone("foo") require.Error(t, err) } func TestClient_AddRecord(t *testing.T) { client := setupTest(t, "/anycast/zonedomain.tld/records", testHandler(http.MethodPost, http.StatusAccepted, "error.json")) record := RecordCreateUpdate{ Type: "TXT", Name: "lego", Value: "content", TTL: 3600, } err := client.AddRecord("zonedomain.tld", record) require.NoError(t, err) } func TestClient_AddRecord_error(t *testing.T) { client := setupTest(t, "/anycast/zonedomain.tld", testHandler(http.MethodPost, http.StatusUnauthorized, "error.json")) record := RecordCreateUpdate{ Type: "TXT", Name: "zonedomain.tld", Value: "content", TTL: 3600, } err := client.AddRecord("zonedomain.tld", record) require.Error(t, err) } func TestClient_DeleteRecord(t *testing.T) { client := setupTest(t, "/anycast/zonedomain.tld/records/6", testHandler(http.MethodDelete, http.StatusAccepted, "error.json")) err := client.DeleteRecord("zonedomain.tld", 6) require.NoError(t, err) } func TestClient_DeleteRecord_error(t *testing.T) { client := setupTest(t, "/anycast/zonedomain.tld/records/6", testHandler(http.MethodDelete, http.StatusNoContent, "")) err := client.DeleteRecord("zonedomain.tld", 7) require.Error(t, err) } func setupTest(t *testing.T, path string, handler http.Handler) *Client { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.Handle(path, handler) opts := Options{ Login: "foo", Username: "bar", Password: "foo", OTP: "2hsn", } client := NewClient(opts) client.HTTPClient = server.Client() client.baseURL, _ = url.Parse(server.URL) return client } func testHandler(method string, statusCode int, filename string) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { if req.Method != method { http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed) return } username, password, ok := req.BasicAuth() if !ok || username != "foo.bar" || password != "foo" { http.Error(rw, `{"message":"Unauthenticated"}`, http.StatusUnauthorized) return } rw.WriteHeader(statusCode) if statusCode == http.StatusNoContent { return } file, err := os.Open(filepath.Join("fixtures", filename)) if err != nil { http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError) return } } } lego-4.9.1/providers/dns/nicmanager/internal/fixtures/000077500000000000000000000000001434020463500230465ustar00rootroot00000000000000lego-4.9.1/providers/dns/nicmanager/internal/fixtures/error.json000066400000000000000000000000351434020463500250700ustar00rootroot00000000000000{ "message": "Not Found" } lego-4.9.1/providers/dns/nicmanager/internal/fixtures/zone.json000066400000000000000000000022711434020463500247160ustar00rootroot00000000000000{ "order_id": 9053, "name": "nicmanager-anycastdns4.net", "order_status": "active", "event_status": "done", "active": true, "dnssec": "inactive", "master1": null, "master2": null, "soa": { "primary": "ns1.nic53.net", "mail": "hostmaster.nicmanager.de", "serial": 1481109046, "refresh": 14400, "retry": 1800, "expire": 1209600, "default": 3600, "ttl": 86400 }, "updated_datetime": "2016-09-02T13:52:18Z", "order_datetime": "2016-09-02T13:52:18Z", "records": [ { "id": 186, "name": "nicmanager-anycastdns4.net", "type": "A", "content": "123.123.123.123", "ttl": 3600, "priority": 0, "active": true, "updated_datetime": "2016-09-02T13:52:18Z" } ], "redirects": [ { "id": 10, "name": "test.nicmanager-anycastdns4.net", "target": "https:\/\/www.nicmanager.com\/", "type": "frame", "updated_datetime": "2016-12-05T14:40:47Z", "request_uri": true, "ssl": false, "meta": { "title": "My frame", "keywords": "foo,bar", "description": "Just a Test" }, "subdomain": "test" } ] } lego-4.9.1/providers/dns/nicmanager/internal/types.go000066400000000000000000000012201434020463500226630ustar00rootroot00000000000000package internal import "fmt" type Record struct { ID int `json:"id"` Name string `json:"name"` Type string `json:"type"` Content string `json:"content"` TTL int `json:"ttl"` } type Zone struct { Name string `json:"name"` Active bool `json:"active"` Records []Record `json:"records"` } type RecordCreateUpdate struct { Name string `json:"name"` Value string `json:"value"` TTL int `json:"ttl"` Type string `json:"type"` } type APIError struct { Message string `json:"message"` StatusCode int `json:"-"` } func (a APIError) Error() string { return fmt.Sprintf("%d: %s", a.StatusCode, a.Message) } lego-4.9.1/providers/dns/nicmanager/nicmanager.go000066400000000000000000000135441434020463500220230ustar00rootroot00000000000000// Package nicmanager implements a DNS provider for solving the DNS-01 challenge using nicmanager DNS. package nicmanager import ( "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/nicmanager/internal" ) // Environment variables names. const ( envNamespace = "NICMANAGER_" EnvLogin = envNamespace + "API_LOGIN" EnvUsername = envNamespace + "API_USERNAME" EnvEmail = envNamespace + "API_EMAIL" EnvPassword = envNamespace + "API_PASSWORD" EnvOTP = envNamespace + "API_OTP" EnvMode = envNamespace + "MODE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) const minTTL = 900 // Config is used to configure the creation of the DNSProvider. type Config struct { Login string Username string Email string Password string OTPSecret string Mode string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *internal.Client config *Config } // NewDNSProvider returns a DNSProvider instance configured for nicmanager. // Credentials must be passed in the environment variables: // NICMANAGER_API_LOGIN, NICMANAGER_API_USERNAME // NICMANAGER_API_EMAIL // NICMANAGER_API_PASSWORD // NICMANAGER_API_OTP // NICMANAGER_API_MODE. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvPassword) if err != nil { return nil, fmt.Errorf("nicmanager: %w", err) } config := NewDefaultConfig() config.Password = values[EnvPassword] config.Mode = env.GetOrDefaultString(EnvMode, internal.ModeAnycast) config.Username = env.GetOrFile(EnvUsername) config.Login = env.GetOrFile(EnvLogin) config.Email = env.GetOrFile(EnvEmail) config.OTPSecret = env.GetOrFile(EnvOTP) if config.TTL < minTTL { return nil, fmt.Errorf("TTL must be higher than %d: %d", minTTL, config.TTL) } return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for nicmanager. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("nicmanager: the configuration of the DNS provider is nil") } opts := internal.Options{ Password: config.Password, OTP: config.OTPSecret, Mode: config.Mode, } switch { case config.Password == "": return nil, errors.New("nicmanager: credentials missing") case config.Email != "": opts.Email = config.Email case config.Login != "" && config.Username != "": opts.Login = config.Login opts.Username = config.Username default: return nil, errors.New("nicmanager: credentials missing") } client := internal.NewClient(opts) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{client: client, config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) rootDomain, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("nicmanager: could not determine zone for domain %q: %w", fqdn, err) } zone, err := d.client.GetZone(dns01.UnFqdn(rootDomain)) if err != nil { return fmt.Errorf("nicmanager: failed to get zone %q: %w", rootDomain, err) } // The way nic manager deals with record with multiple values is that they are completely different records with unique ids // Hence we don't check for an existing record here, but rather just create one record := internal.RecordCreateUpdate{ Name: fqdn, Type: "TXT", TTL: d.config.TTL, Value: value, } err = d.client.AddRecord(zone.Name, record) if err != nil { return fmt.Errorf("nicmanager: failed to create record [zone: %q, fqdn: %q]: %w", zone.Name, fqdn, err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) rootDomain, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("nicmanager: could not determine zone for domain %q: %w", fqdn, err) } zone, err := d.client.GetZone(dns01.UnFqdn(rootDomain)) if err != nil { return fmt.Errorf("nicmanager: failed to get zone %q: %w", rootDomain, err) } name := dns01.UnFqdn(fqdn) var existingRecord internal.Record var existingRecordFound bool for _, record := range zone.Records { if strings.EqualFold(record.Type, "TXT") && strings.EqualFold(record.Name, name) && record.Content == value { existingRecord = record existingRecordFound = true } } if existingRecordFound { err = d.client.DeleteRecord(zone.Name, existingRecord.ID) if err != nil { return fmt.Errorf("nicmanager: failed to delete record [zone: %q, domain: %q]: %w", zone.Name, name, err) } } return fmt.Errorf("nicmanager: no record found to cleanup") } lego-4.9.1/providers/dns/nicmanager/nicmanager.toml000066400000000000000000000032761434020463500223720ustar00rootroot00000000000000Name = "Nicmanager" Description = '''''' URL = "https://www.nicmanager.com/" Code = "nicmanager" Since = "v4.5.0" Example = ''' ## Login using email NICMANAGER_API_EMAIL = "you@example.com" \ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ lego --email you@example.com --dns nicmanager --domains my.example.org run ## Login using account name + username NICMANAGER_API_LOGIN = "myaccount" \ NICMANAGER_API_USERNAME = "myuser" \ NICMANAGER_API_PASSWORD = "password" \ # Optionally, if your account has TOTP enabled, set the secret here NICMANAGER_API_OTP = "long-secret" \ lego --email you@example.com --dns nicmanager --domains my.example.org run ''' Additional = ''' ## Description You can login using your account name + username or using your email address. Optionally if TOTP is configured for your account, set `NICMANAGER_API_OTP`. ''' [Configuration] [Configuration.Credentials] NICMANAGER_API_LOGIN = "Login, used for Username-based login" NICMANAGER_API_USERNAME = "Username, used for Username-based login" NICMANAGER_API_EMAIL = "Email-based login" NICMANAGER_API_PASSWORD = "Password, always required" [Configuration.Additional] NICMANAGER_API_OTP = "TOTP Secret (optional)" NICMANAGER_API_MODE = "mode: 'anycast' or 'zone' (default: 'anycast')" NICMANAGER_POLLING_INTERVAL = "Time between DNS propagation check" NICMANAGER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" NICMANAGER_TTL = "The TTL of the TXT record used for the DNS challenge" NICMANAGER_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.nicmanager.com/docs/v1/" lego-4.9.1/providers/dns/nicmanager/nicmanager_test.go000066400000000000000000000074211434020463500230570ustar00rootroot00000000000000package nicmanager import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvUsername, EnvLogin, EnvEmail, EnvPassword, EnvOTP). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success (email)", envVars: map[string]string{ EnvEmail: "foo@example.com", EnvPassword: "secret", }, }, { desc: "success (login.username)", envVars: map[string]string{ EnvLogin: "foo", EnvUsername: "bar", EnvPassword: "secret", }, }, { desc: "missing credentials", expected: "nicmanager: some credentials information are missing: NICMANAGER_API_PASSWORD", }, { desc: "missing password", envVars: map[string]string{ EnvEmail: "foo@example.com", }, expected: "nicmanager: some credentials information are missing: NICMANAGER_API_PASSWORD", }, { desc: "missing username", envVars: map[string]string{ EnvLogin: "foo", EnvPassword: "secret", }, expected: "nicmanager: credentials missing", }, { desc: "missing login", envVars: map[string]string{ EnvUsername: "bar", EnvPassword: "secret", }, expected: "nicmanager: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string login string username string email string password string otpSecret string expected string }{ { desc: "success (email)", email: "foo@example.com", password: "secret", }, { desc: "success (login.username)", login: "john", username: "doe", password: "secret", }, { desc: "missing credentials", expected: "nicmanager: credentials missing", }, { desc: "missing password", email: "foo@example.com", expected: "nicmanager: credentials missing", }, { desc: "missing login", login: "", username: "doe", password: "secret", expected: "nicmanager: credentials missing", }, { desc: "missing username", login: "john", username: "", password: "secret", expected: "nicmanager: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Login = test.login config.Username = test.username config.Email = test.email config.Password = test.password config.OTPSecret = test.otpSecret p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/nifcloud/000077500000000000000000000000001434020463500170605ustar00rootroot00000000000000lego-4.9.1/providers/dns/nifcloud/internal/000077500000000000000000000000001434020463500206745ustar00rootroot00000000000000lego-4.9.1/providers/dns/nifcloud/internal/client.go000066400000000000000000000141611434020463500225040ustar00rootroot00000000000000package internal import ( "bytes" "crypto/hmac" "crypto/sha1" "encoding/base64" "encoding/xml" "errors" "fmt" "net/http" "time" ) const ( defaultBaseURL = "https://dns.api.nifcloud.com" apiVersion = "2012-12-12N2013-12-16" // XMLNs XML NS of Route53. XMLNs = "https://route53.amazonaws.com/doc/2012-12-12/" ) // ChangeResourceRecordSetsRequest is a complex type that contains change information for the resource record set. type ChangeResourceRecordSetsRequest struct { XMLNs string `xml:"xmlns,attr"` ChangeBatch ChangeBatch `xml:"ChangeBatch"` } // ChangeResourceRecordSetsResponse is a complex type containing the response for the request. type ChangeResourceRecordSetsResponse struct { ChangeInfo ChangeInfo `xml:"ChangeInfo"` } // GetChangeResponse is a complex type that contains the ChangeInfo element. type GetChangeResponse struct { ChangeInfo ChangeInfo `xml:"ChangeInfo"` } // ErrorResponse is the information for any errors. type ErrorResponse struct { Error struct { Type string `xml:"Type"` Message string `xml:"Message"` Code string `xml:"Code"` } `xml:"Error"` RequestID string `xml:"RequestId"` } // ChangeBatch is the information for a change request. type ChangeBatch struct { Changes Changes `xml:"Changes"` Comment string `xml:"Comment"` } // Changes is array of Change. type Changes struct { Change []Change `xml:"Change"` } // Change is the information for each resource record set that you want to change. type Change struct { Action string `xml:"Action"` ResourceRecordSet ResourceRecordSet `xml:"ResourceRecordSet"` } // ResourceRecordSet is the information about the resource record set to create or delete. type ResourceRecordSet struct { Name string `xml:"Name"` Type string `xml:"Type"` TTL int `xml:"TTL"` ResourceRecords ResourceRecords `xml:"ResourceRecords"` } // ResourceRecords is array of ResourceRecord. type ResourceRecords struct { ResourceRecord []ResourceRecord `xml:"ResourceRecord"` } // ResourceRecord is the information specific to the resource record. type ResourceRecord struct { Value string `xml:"Value"` } // ChangeInfo is A complex type that describes change information about changes made to your hosted zone. type ChangeInfo struct { ID string `xml:"Id"` Status string `xml:"Status"` SubmittedAt string `xml:"SubmittedAt"` } // NewClient Creates a new client of NIFCLOUD DNS. func NewClient(accessKey, secretKey string) (*Client, error) { if accessKey == "" || secretKey == "" { return nil, errors.New("credentials missing") } return &Client{ accessKey: accessKey, secretKey: secretKey, BaseURL: defaultBaseURL, HTTPClient: &http.Client{}, }, nil } // Client client of NIFCLOUD DNS. type Client struct { accessKey string secretKey string BaseURL string HTTPClient *http.Client } // ChangeResourceRecordSets Call ChangeResourceRecordSets API and return response. func (c *Client) ChangeResourceRecordSets(hostedZoneID string, input ChangeResourceRecordSetsRequest) (*ChangeResourceRecordSetsResponse, error) { requestURL := fmt.Sprintf("%s/%s/hostedzone/%s/rrset", c.BaseURL, apiVersion, hostedZoneID) body := &bytes.Buffer{} body.WriteString(xml.Header) err := xml.NewEncoder(body).Encode(input) if err != nil { return nil, err } req, err := http.NewRequest(http.MethodPost, requestURL, body) if err != nil { return nil, err } req.Header.Set("Content-Type", "text/xml; charset=utf-8") err = c.sign(req) if err != nil { return nil, fmt.Errorf("an error occurred during the creation of the signature: %w", err) } res, err := c.HTTPClient.Do(req) if err != nil { return nil, err } if res.Body == nil { return nil, errors.New("the response body is nil") } defer res.Body.Close() if res.StatusCode != http.StatusOK { errResp := &ErrorResponse{} err = xml.NewDecoder(res.Body).Decode(errResp) if err != nil { return nil, fmt.Errorf("an error occurred while unmarshaling the error body to XML: %w", err) } return nil, fmt.Errorf("an error occurred: %s", errResp.Error.Message) } output := &ChangeResourceRecordSetsResponse{} err = xml.NewDecoder(res.Body).Decode(output) if err != nil { return nil, fmt.Errorf("an error occurred while unmarshaling the response body to XML: %w", err) } return output, err } // GetChange Call GetChange API and return response. func (c *Client) GetChange(statusID string) (*GetChangeResponse, error) { requestURL := fmt.Sprintf("%s/%s/change/%s", c.BaseURL, apiVersion, statusID) req, err := http.NewRequest(http.MethodGet, requestURL, nil) if err != nil { return nil, err } err = c.sign(req) if err != nil { return nil, fmt.Errorf("an error occurred during the creation of the signature: %w", err) } res, err := c.HTTPClient.Do(req) if err != nil { return nil, err } if res.Body == nil { return nil, errors.New("the response body is nil") } defer res.Body.Close() if res.StatusCode != http.StatusOK { errResp := &ErrorResponse{} err = xml.NewDecoder(res.Body).Decode(errResp) if err != nil { return nil, fmt.Errorf("an error occurred while unmarshaling the error body to XML: %w", err) } return nil, fmt.Errorf("an error occurred: %s", errResp.Error.Message) } output := &GetChangeResponse{} err = xml.NewDecoder(res.Body).Decode(output) if err != nil { return nil, fmt.Errorf("an error occurred while unmarshaling the response body to XML: %w", err) } return output, nil } func (c *Client) sign(req *http.Request) error { if req.Header.Get("Date") == "" { location, err := time.LoadLocation("GMT") if err != nil { return err } req.Header.Set("Date", time.Now().In(location).Format(time.RFC1123)) } if req.URL.Path == "" { req.URL.Path += "/" } mac := hmac.New(sha1.New, []byte(c.secretKey)) _, err := mac.Write([]byte(req.Header.Get("Date"))) if err != nil { return err } hashed := mac.Sum(nil) signature := base64.StdEncoding.EncodeToString(hashed) auth := fmt.Sprintf("NIFTY3-HTTPS NiftyAccessKeyId=%s,Algorithm=HmacSHA1,Signature=%s", c.accessKey, signature) req.Header.Set("X-Nifty-Authorization", auth) return nil } lego-4.9.1/providers/dns/nifcloud/internal/client_test.go000066400000000000000000000107331434020463500235440ustar00rootroot00000000000000package internal import ( "fmt" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupTest(t *testing.T, responseBody string, statusCode int) *Client { t.Helper() handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(statusCode) _, _ = fmt.Fprintln(w, responseBody) }) server := httptest.NewServer(handler) t.Cleanup(server.Close) client, err := NewClient("A", "B") require.NoError(t, err) client.HTTPClient = server.Client() client.BaseURL = server.URL return client } func TestChangeResourceRecordSets(t *testing.T) { responseBody := ` xxxxx INSYNC 2015-08-05T00:00:00.000Z ` client := setupTest(t, responseBody, http.StatusOK) res, err := client.ChangeResourceRecordSets("example.com", ChangeResourceRecordSetsRequest{}) require.NoError(t, err) assert.Equal(t, "xxxxx", res.ChangeInfo.ID) assert.Equal(t, "INSYNC", res.ChangeInfo.Status) assert.Equal(t, "2015-08-05T00:00:00.000Z", res.ChangeInfo.SubmittedAt) } func TestChangeResourceRecordSetsErrors(t *testing.T) { testCases := []struct { desc string responseBody string statusCode int expected string }{ { desc: "API error", responseBody: ` Sender AuthFailed The request signature we calculated does not match the signature you provided. `, statusCode: http.StatusUnauthorized, expected: "an error occurred: The request signature we calculated does not match the signature you provided.", }, { desc: "response body error", responseBody: "foo", statusCode: http.StatusOK, expected: "an error occurred while unmarshaling the response body to XML: EOF", }, { desc: "error message error", responseBody: "foo", statusCode: http.StatusInternalServerError, expected: "an error occurred while unmarshaling the error body to XML: EOF", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { client := setupTest(t, test.responseBody, test.statusCode) res, err := client.ChangeResourceRecordSets("example.com", ChangeResourceRecordSetsRequest{}) assert.Nil(t, res) assert.EqualError(t, err, test.expected) }) } } func TestGetChange(t *testing.T) { responseBody := ` xxxxx INSYNC 2015-08-05T00:00:00.000Z ` client := setupTest(t, responseBody, http.StatusOK) res, err := client.GetChange("12345") require.NoError(t, err) assert.Equal(t, "xxxxx", res.ChangeInfo.ID) assert.Equal(t, "INSYNC", res.ChangeInfo.Status) assert.Equal(t, "2015-08-05T00:00:00.000Z", res.ChangeInfo.SubmittedAt) } func TestGetChangeErrors(t *testing.T) { testCases := []struct { desc string responseBody string statusCode int expected string }{ { desc: "API error", responseBody: ` Sender AuthFailed The request signature we calculated does not match the signature you provided. `, statusCode: http.StatusUnauthorized, expected: "an error occurred: The request signature we calculated does not match the signature you provided.", }, { desc: "response body error", responseBody: "foo", statusCode: http.StatusOK, expected: "an error occurred while unmarshaling the response body to XML: EOF", }, { desc: "error message error", responseBody: "foo", statusCode: http.StatusInternalServerError, expected: "an error occurred while unmarshaling the error body to XML: EOF", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { client := setupTest(t, test.responseBody, test.statusCode) res, err := client.GetChange("12345") assert.Nil(t, res) assert.EqualError(t, err, test.expected) }) } } lego-4.9.1/providers/dns/nifcloud/nifcloud.go000066400000000000000000000117661434020463500212250ustar00rootroot00000000000000// Package nifcloud implements a DNS provider for solving the DNS-01 challenge using NIFCLOUD DNS. package nifcloud import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/platform/wait" "github.com/go-acme/lego/v4/providers/dns/nifcloud/internal" ) // Environment variables names. const ( envNamespace = "NIFCLOUD_" EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID" EnvSecretAccessKey = envNamespace + "SECRET_ACCESS_KEY" EnvDNSEndpoint = envNamespace + "DNS_ENDPOINT" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string AccessKey string SecretKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *internal.Client config *Config } // NewDNSProvider returns a DNSProvider instance configured for the NIFCLOUD DNS service. // Credentials must be passed in the environment variables: // NIFCLOUD_ACCESS_KEY_ID and NIFCLOUD_SECRET_ACCESS_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey) if err != nil { return nil, fmt.Errorf("nifcloud: %w", err) } config := NewDefaultConfig() config.BaseURL = env.GetOrFile(EnvDNSEndpoint) config.AccessKey = values[EnvAccessKeyID] config.SecretKey = values[EnvSecretAccessKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for NIFCLOUD. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("nifcloud: the configuration of the DNS provider is nil") } client, err := internal.NewClient(config.AccessKey, config.SecretKey) if err != nil { return nil, fmt.Errorf("nifcloud: %w", err) } if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } if len(config.BaseURL) > 0 { client.BaseURL = config.BaseURL } return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) err := d.changeRecord("CREATE", fqdn, value, d.config.TTL) if err != nil { return fmt.Errorf("nifcloud: %w", err) } return err } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) err := d.changeRecord("DELETE", fqdn, value, d.config.TTL) if err != nil { return fmt.Errorf("nifcloud: %w", err) } return err } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func (d *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { name := dns01.UnFqdn(fqdn) reqParams := internal.ChangeResourceRecordSetsRequest{ XMLNs: internal.XMLNs, ChangeBatch: internal.ChangeBatch{ Comment: "Managed by Lego", Changes: internal.Changes{ Change: []internal.Change{ { Action: action, ResourceRecordSet: internal.ResourceRecordSet{ Name: name, Type: "TXT", TTL: ttl, ResourceRecords: internal.ResourceRecords{ ResourceRecord: []internal.ResourceRecord{ { Value: value, }, }, }, }, }, }, }, }, } authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("failed to find zone: %w", err) } resp, err := d.client.ChangeResourceRecordSets(dns01.UnFqdn(authZone), reqParams) if err != nil { return fmt.Errorf("failed to change record set: %w", err) } statusID := resp.ChangeInfo.ID return wait.For("nifcloud", 120*time.Second, 4*time.Second, func() (bool, error) { resp, err := d.client.GetChange(statusID) if err != nil { return false, fmt.Errorf("failed to query change status: %w", err) } return resp.ChangeInfo.Status == "INSYNC", nil }) } lego-4.9.1/providers/dns/nifcloud/nifcloud.toml000066400000000000000000000014241434020463500215610ustar00rootroot00000000000000Name = "NIFCloud" Description = '''''' URL = "https://www.nifcloud.com/" Code = "nifcloud" Since = "v1.1.0" Example = ''' NIFCLOUD_ACCESS_KEY_ID=xxxx \ NIFCLOUD_SECRET_ACCESS_KEY=yyyy \ lego --email you@example.com --dns nifcloud --domains my.example.org run ''' [Configuration] [Configuration.Credentials] NIFCLOUD_ACCESS_KEY_ID = "Access key" NIFCLOUD_SECRET_ACCESS_KEY = "Secret access key" [Configuration.Additional] NIFCLOUD_POLLING_INTERVAL = "Time between DNS propagation check" NIFCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" NIFCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" NIFCLOUD_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://mbaas.nifcloud.com/doc/current/rest/common/format.html" lego-4.9.1/providers/dns/nifcloud/nifcloud_test.go000066400000000000000000000062201434020463500222510ustar00rootroot00000000000000package nifcloud import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAccessKeyID, EnvSecretAccessKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAccessKeyID: "123", EnvSecretAccessKey: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAccessKeyID: "", EnvSecretAccessKey: "", }, expected: "nifcloud: some credentials information are missing: NIFCLOUD_ACCESS_KEY_ID,NIFCLOUD_SECRET_ACCESS_KEY", }, { desc: "missing access key", envVars: map[string]string{ EnvAccessKeyID: "", EnvSecretAccessKey: "456", }, expected: "nifcloud: some credentials information are missing: NIFCLOUD_ACCESS_KEY_ID", }, { desc: "missing secret key", envVars: map[string]string{ EnvAccessKeyID: "123", EnvSecretAccessKey: "", }, expected: "nifcloud: some credentials information are missing: NIFCLOUD_SECRET_ACCESS_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string accessKey string secretKey string expected string }{ { desc: "success", accessKey: "123", secretKey: "456", }, { desc: "missing credentials", expected: "nifcloud: credentials missing", }, { desc: "missing api key", secretKey: "456", expected: "nifcloud: credentials missing", }, { desc: "missing secret key", accessKey: "123", expected: "nifcloud: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.AccessKey = test.accessKey config.SecretKey = test.secretKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/njalla/000077500000000000000000000000001434020463500165165ustar00rootroot00000000000000lego-4.9.1/providers/dns/njalla/internal/000077500000000000000000000000001434020463500203325ustar00rootroot00000000000000lego-4.9.1/providers/dns/njalla/internal/client.go000066400000000000000000000053511434020463500221430ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "fmt" "net/http" "time" ) const apiEndpoint = "https://njal.la/api/1/" // Client is a Njalla API client. type Client struct { HTTPClient *http.Client apiEndpoint string token string } // NewClient creates a new Client. func NewClient(token string) *Client { return &Client{ HTTPClient: &http.Client{Timeout: 5 * time.Second}, apiEndpoint: apiEndpoint, token: token, } } // AddRecord adds a record. func (c *Client) AddRecord(record Record) (*Record, error) { data := APIRequest{ Method: "add-record", Params: record, } result, err := c.do(data) if err != nil { return nil, err } var rcd Record err = json.Unmarshal(result, &rcd) if err != nil { return nil, fmt.Errorf("failed to unmarshal response result: %w", err) } return &rcd, nil } // RemoveRecord removes a record. func (c *Client) RemoveRecord(id string, domain string) error { data := APIRequest{ Method: "remove-record", Params: Record{ ID: id, Domain: domain, }, } _, err := c.do(data) if err != nil { return err } return nil } // ListRecords list the records for one domain. func (c *Client) ListRecords(domain string) ([]Record, error) { data := APIRequest{ Method: "list-records", Params: Record{ Domain: domain, }, } result, err := c.do(data) if err != nil { return nil, err } var rcds Records err = json.Unmarshal(result, &rcds) if err != nil { return nil, fmt.Errorf("failed to unmarshal response result: %w", err) } return rcds.Records, nil } func (c *Client) do(data APIRequest) (json.RawMessage, error) { req, err := c.createRequest(data) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to perform request: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected error: %d", resp.StatusCode) } apiResponse := APIResponse{} err = json.NewDecoder(resp.Body).Decode(&apiResponse) if err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } if apiResponse.Error != nil { return nil, apiResponse.Error } return apiResponse.Result, nil } func (c *Client) createRequest(data APIRequest) (*http.Request, error) { reqBody, err := json.Marshal(data) if err != nil { return nil, fmt.Errorf("failed to marshall request body: %w", err) } req, err := http.NewRequest(http.MethodPost, c.apiEndpoint, bytes.NewReader(reqBody)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Njalla "+c.token) return req, nil } lego-4.9.1/providers/dns/njalla/internal/client_test.go000066400000000000000000000115121434020463500231760ustar00rootroot00000000000000package internal import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setup(t *testing.T, handler func(http.ResponseWriter, *http.Request)) *Client { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } token := req.Header.Get("Authorization") if token != "Njalla secret" { _, _ = rw.Write([]byte(`{"jsonrpc":"2.0", "Error": {"code": 403, "message": "Invalid token."}}`)) return } if handler != nil { handler(rw, req) } else { _, _ = rw.Write([]byte(`{"jsonrpc":"2.0"}`)) } }) client := NewClient("secret") client.apiEndpoint = server.URL return client } func TestClient_AddRecord(t *testing.T) { client := setup(t, func(rw http.ResponseWriter, req *http.Request) { apiReq := struct { Method string `json:"method"` Params Record `json:"params"` }{} err := json.NewDecoder(req.Body).Decode(&apiReq) if err != nil { http.Error(rw, "failed to marshal test request body", http.StatusInternalServerError) return } apiReq.Params.ID = "123" resp := map[string]interface{}{ "jsonrpc": "2.0", "id": "897", "result": apiReq.Params, } err = json.NewEncoder(rw).Encode(resp) if err != nil { http.Error(rw, "failed to marshal test response", http.StatusInternalServerError) return } }) record := Record{ Content: "foobar", Domain: "test", Name: "example.com", TTL: 300, Type: "TXT", } result, err := client.AddRecord(record) require.NoError(t, err) expected := &Record{ ID: "123", Content: "foobar", Domain: "test", Name: "example.com", TTL: 300, Type: "TXT", } assert.Equal(t, expected, result) } func TestClient_AddRecord_error(t *testing.T) { client := setup(t, nil) client.token = "invalid" record := Record{ Content: "test", Domain: "test01", Name: "example.com", TTL: 300, Type: "TXT", } result, err := client.AddRecord(record) require.Error(t, err) assert.Nil(t, result) } func TestClient_ListRecords(t *testing.T) { client := setup(t, func(rw http.ResponseWriter, req *http.Request) { apiReq := struct { Method string `json:"method"` Params Record `json:"params"` }{} err := json.NewDecoder(req.Body).Decode(&apiReq) if err != nil { http.Error(rw, "failed to marshal test request body", http.StatusInternalServerError) return } resp := map[string]interface{}{ "jsonrpc": "2.0", "id": "897", "result": Records{ Records: []Record{ { ID: "1", Domain: apiReq.Params.Domain, Content: "test", Name: "test01", TTL: 300, Type: "TXT", }, { ID: "2", Domain: apiReq.Params.Domain, Content: "txtTxt", Name: "test02", TTL: 120, Type: "TXT", }, }, }, } err = json.NewEncoder(rw).Encode(resp) if err != nil { http.Error(rw, "failed to marshal test response", http.StatusInternalServerError) return } }) records, err := client.ListRecords("example.com") require.NoError(t, err) expected := []Record{ { ID: "1", Domain: "example.com", Content: "test", Name: "test01", TTL: 300, Type: "TXT", }, { ID: "2", Domain: "example.com", Content: "txtTxt", Name: "test02", TTL: 120, Type: "TXT", }, } assert.Equal(t, expected, records) } func TestClient_ListRecords_error(t *testing.T) { client := setup(t, nil) client.token = "invalid" records, err := client.ListRecords("example.com") require.Error(t, err) assert.Empty(t, records) } func TestClient_RemoveRecord(t *testing.T) { client := setup(t, func(rw http.ResponseWriter, req *http.Request) { apiReq := struct { Method string `json:"method"` Params Record `json:"params"` }{} err := json.NewDecoder(req.Body).Decode(&apiReq) if err != nil { http.Error(rw, "failed to marshal test request body", http.StatusInternalServerError) return } if apiReq.Params.ID == "" { _, _ = rw.Write([]byte(`{"jsonrpc":"2.0", "Error": {"code": 400, "message": ""missing ID"}}`)) return } if apiReq.Params.Domain == "" { _, _ = rw.Write([]byte(`{"jsonrpc":"2.0", "Error": {"code": 400, "message": ""missing domain"}}`)) return } _, _ = rw.Write([]byte(`{"jsonrpc":"2.0"}`)) }) err := client.RemoveRecord("123", "example.com") require.NoError(t, err) } func TestClient_RemoveRecord_error(t *testing.T) { client := setup(t, nil) client.token = "invalid" err := client.RemoveRecord("123", "example.com") require.Error(t, err) } lego-4.9.1/providers/dns/njalla/internal/types.go000066400000000000000000000020161434020463500220240ustar00rootroot00000000000000package internal import ( "encoding/json" "fmt" ) // APIRequest represents an API request body. type APIRequest struct { Method string `json:"method"` Params interface{} `json:"params"` } // APIResponse represents an API response body. type APIResponse struct { ID string `json:"id"` RPC string `json:"jsonrpc"` Error *APIError `json:"error,omitempty"` Result json.RawMessage `json:"result,omitempty"` } // APIError is an API error. type APIError struct { Code int Message string } func (a APIError) Error() string { return fmt.Sprintf("code: %d, message: %s", a.Code, a.Message) } // Record is a DNS record. type Record struct { ID string `json:"id,omitempty"` Content string `json:"content,omitempty"` Domain string `json:"domain,omitempty"` Name string `json:"name,omitempty"` TTL int `json:"ttl,omitempty"` Type string `json:"type,omitempty"` } // Records is a list of DNS records. type Records struct { Records []Record `json:"records,omitempty"` } lego-4.9.1/providers/dns/njalla/njalla.go000066400000000000000000000111431434020463500203060ustar00rootroot00000000000000// Package njalla implements a DNS provider for solving the DNS-01 challenge using Njalla. package njalla import ( "errors" "fmt" "net/http" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/njalla/internal" "github.com/miekg/dns" ) // Environment variables names. const ( envNamespace = "NJALLA_" EnvToken = envNamespace + "TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Token string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 300), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client recordIDs map[string]string recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for Njalla. // Credentials must be passed in the environment variable: NJALLA_TOKEN. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvToken) if err != nil { return nil, fmt.Errorf("njalla: %w", err) } config := NewDefaultConfig() config.Token = values[EnvToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Njalla. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("njalla: the configuration of the DNS provider is nil") } if config.Token == "" { return nil, errors.New("njalla: missing credentials") } client := internal.NewClient(config.Token) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]string), }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) rootDomain, subDomain, err := splitDomain(fqdn) if err != nil { return fmt.Errorf("njalla: %w", err) } record := internal.Record{ Name: subDomain, // TODO need to be tested Domain: dns01.UnFqdn(rootDomain), // TODO need to be tested Content: value, TTL: d.config.TTL, Type: "TXT", } resp, err := d.client.AddRecord(record) if err != nil { return fmt.Errorf("njalla: failed to add record: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = resp.ID d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) rootDomain, _, err := splitDomain(fqdn) if err != nil { return fmt.Errorf("njalla: %w", err) } // gets the record's unique ID from when we created it d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("njalla: unknown record ID for '%s' '%s'", fqdn, token) } err = d.client.RemoveRecord(recordID, dns01.UnFqdn(rootDomain)) if err != nil { return fmt.Errorf("njalla: failed to delete TXT records: fqdn=%s, recordID=%s: %w", fqdn, recordID, err) } // deletes record ID from map d.recordIDsMu.Lock() delete(d.recordIDs, token) d.recordIDsMu.Unlock() return nil } func splitDomain(full string) (string, string, error) { split := dns.Split(full) if len(split) < 2 { return "", "", fmt.Errorf("unsupported domain: %s", full) } if len(split) == 2 { return full, "", nil } domain := full[split[len(split)-2]:] subDomain := full[:split[len(split)-2]-1] return domain, subDomain, nil } lego-4.9.1/providers/dns/njalla/njalla.toml000066400000000000000000000011741434020463500206570ustar00rootroot00000000000000Name = "Njalla" Description = '''''' URL = "https://njal.la" Code = "njalla" Since = "v4.3.0" Example = ''' NJALLA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxx \ lego --email you@example.com --dns njalla --domains my.example.org run ''' [Configuration] [Configuration.Credentials] NJALLA_TOKEN = "API token" [Configuration.Additional] NJALLA_POLLING_INTERVAL = "Time between DNS propagation check" NJALLA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" NJALLA_TTL = "The TTL of the TXT record used for the DNS challenge" NJALLA_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://njal.la/api/" lego-4.9.1/providers/dns/njalla/njalla_test.go000066400000000000000000000043561434020463500213550ustar00rootroot00000000000000package njalla import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvToken).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvToken: "123", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvToken: "", }, expected: "njalla: some credentials information are missing: NJALLA_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string token string expected string }{ { desc: "success", token: "123", }, { desc: "missing credentials", expected: "njalla: missing credentials", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Token = test.token p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/ns1/000077500000000000000000000000001434020463500157565ustar00rootroot00000000000000lego-4.9.1/providers/dns/ns1/ns1.go000066400000000000000000000120011434020463500170000ustar00rootroot00000000000000// Package ns1 implements a DNS provider for solving the DNS-01 challenge using NS1 DNS. package ns1 import ( "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "gopkg.in/ns1/ns1-go.v2/rest" "gopkg.in/ns1/ns1-go.v2/rest/model/dns" ) // Environment variables names. const ( envNamespace = "NS1_" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *rest.Client config *Config } // NewDNSProvider returns a DNSProvider instance configured for NS1. // Credentials must be passed in the environment variables: NS1_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("ns1: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for NS1. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("ns1: the configuration of the DNS provider is nil") } if config.APIKey == "" { return nil, errors.New("ns1: credentials missing") } client := rest.NewClient(config.HTTPClient, rest.SetAPIKey(config.APIKey)) return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("ns1: %w", err) } record, _, err := d.client.Records.Get(zone.Zone, dns01.UnFqdn(fqdn), "TXT") // Create a new record if errors.Is(err, rest.ErrRecordMissing) || record == nil { log.Infof("Create a new record for [zone: %s, fqdn: %s, domain: %s]", zone.Zone, fqdn, domain) record = dns.NewRecord(zone.Zone, dns01.UnFqdn(fqdn), "TXT") record.TTL = d.config.TTL record.Answers = []*dns.Answer{{Rdata: []string{value}}} _, err = d.client.Records.Create(record) if err != nil { return fmt.Errorf("ns1: failed to create record [zone: %q, fqdn: %q]: %w", zone.Zone, fqdn, err) } return nil } if err != nil { return fmt.Errorf("ns1: failed to get the existing record: %w", err) } // Update the existing records record.Answers = append(record.Answers, &dns.Answer{Rdata: []string{value}}) log.Infof("Update an existing record for [zone: %s, fqdn: %s, domain: %s]", zone.Zone, fqdn, domain) _, err = d.client.Records.Update(record) if err != nil { return fmt.Errorf("ns1: failed to update record [zone: %q, fqdn: %q]: %w", zone.Zone, fqdn, err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("ns1: %w", err) } name := dns01.UnFqdn(fqdn) _, err = d.client.Records.Delete(zone.Zone, name, "TXT") if err != nil { return fmt.Errorf("ns1: failed to delete record [zone: %q, domain: %q]: %w", zone.Zone, name, err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func (d *DNSProvider) getHostedZone(fqdn string) (*dns.Zone, error) { authZone, err := getAuthZone(fqdn) if err != nil { return nil, fmt.Errorf("failed to extract auth zone from fqdn %q: %w", fqdn, err) } zone, _, err := d.client.Zones.Get(authZone) if err != nil { return nil, fmt.Errorf("failed to get zone [authZone: %q, fqdn: %q]: %w", authZone, fqdn, err) } return zone, nil } func getAuthZone(fqdn string) (string, error) { authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", err } return strings.TrimSuffix(authZone, "."), nil } lego-4.9.1/providers/dns/ns1/ns1.toml000066400000000000000000000011711434020463500173540ustar00rootroot00000000000000Name = "NS1" Description = '''''' URL = "https://ns1.com" Code = "ns1" Since = "v0.4.0" Example = ''' NS1_API_KEY=xxxx \ lego --email you@example.com --dns ns1 --domains my.example.org run ''' [Configuration] [Configuration.Credentials] NS1_API_KEY = "API key" [Configuration.Additional] NS1_POLLING_INTERVAL = "Time between DNS propagation check" NS1_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" NS1_TTL = "The TTL of the TXT record used for the DNS challenge" NS1_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://ns1.com/api" GoClient = "https://github.com/ns1/ns1-go" lego-4.9.1/providers/dns/ns1/ns1_test.go000066400000000000000000000066161434020463500200560ustar00rootroot00000000000000package ns1 import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAPIKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", }, }, { desc: "missing api key", envVars: map[string]string{ EnvAPIKey: "", }, expected: "ns1: some credentials information are missing: NS1_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string expected string }{ { desc: "success", apiKey: "123", }, { desc: "missing credentials", expected: "ns1: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func Test_getAuthZone(t *testing.T) { type expected struct { AuthZone string Error string } testCases := []struct { desc string fqdn string expected expected }{ { desc: "valid fqdn", fqdn: "_acme-challenge.myhost.sub.example.com.", expected: expected{ AuthZone: "example.com", }, }, { desc: "invalid fqdn", fqdn: "_acme-challenge.myhost.sub.example.com", expected: expected{ Error: "could not find the start of authority for _acme-challenge.myhost.sub.example.com: dns: domain must be fully qualified", }, }, { desc: "invalid authority", fqdn: "_acme-challenge.myhost.sub.domain.tld.", expected: expected{ Error: "could not find the start of authority for _acme-challenge.myhost.sub.domain.tld.: NXDOMAIN", }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() authZone, err := getAuthZone(test.fqdn) if len(test.expected.Error) > 0 { assert.EqualError(t, err, test.expected.Error) } else { require.NoError(t, err) assert.Equal(t, test.expected.AuthZone, authZone) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/oraclecloud/000077500000000000000000000000001434020463500175515ustar00rootroot00000000000000lego-4.9.1/providers/dns/oraclecloud/configprovider.go000066400000000000000000000041271434020463500231240ustar00rootroot00000000000000package oraclecloud import ( "crypto/rsa" "encoding/base64" "fmt" "os" "github.com/go-acme/lego/v4/platform/config/env" "github.com/oracle/oci-go-sdk/common" ) type configProvider struct { values map[string]string privateKeyPassphrase string } func newConfigProvider(values map[string]string) *configProvider { return &configProvider{ values: values, privateKeyPassphrase: env.GetOrFile(EnvPrivKeyPass), } } func (p *configProvider) PrivateRSAKey() (*rsa.PrivateKey, error) { privateKey, err := getPrivateKey(envPrivKey) if err != nil { return nil, err } return common.PrivateKeyFromBytes(privateKey, common.String(p.privateKeyPassphrase)) } func (p *configProvider) KeyID() (string, error) { tenancy, err := p.TenancyOCID() if err != nil { return "", err } user, err := p.UserOCID() if err != nil { return "", err } fingerprint, err := p.KeyFingerprint() if err != nil { return "", err } return fmt.Sprintf("%s/%s/%s", tenancy, user, fingerprint), nil } func (p *configProvider) TenancyOCID() (value string, err error) { return p.values[EnvTenancyOCID], nil } func (p *configProvider) UserOCID() (string, error) { return p.values[EnvUserOCID], nil } func (p *configProvider) KeyFingerprint() (string, error) { return p.values[EnvPubKeyFingerprint], nil } func (p *configProvider) Region() (string, error) { return p.values[EnvRegion], nil } func getPrivateKey(envVar string) ([]byte, error) { envVarValue := os.Getenv(envVar) if envVarValue != "" { bytes, err := base64.StdEncoding.DecodeString(envVarValue) if err != nil { return nil, fmt.Errorf("failed to read base64 value %s (defined by env var %s): %w", envVarValue, envVar, err) } return bytes, nil } fileVar := envVar + "_FILE" fileVarValue := os.Getenv(fileVar) if fileVarValue == "" { return nil, fmt.Errorf("no value provided for: %s or %s", envVar, fileVar) } fileContents, err := os.ReadFile(fileVarValue) if err != nil { return nil, fmt.Errorf("failed to read the file %s (defined by env var %s): %w", fileVarValue, fileVar, err) } return fileContents, nil } lego-4.9.1/providers/dns/oraclecloud/oraclecloud.go000066400000000000000000000144041434020463500223770ustar00rootroot00000000000000// Package oraclecloud implements a DNS provider for solving the DNS-01 challenge using Oracle Cloud DNS. package oraclecloud import ( "context" "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/oracle/oci-go-sdk/common" "github.com/oracle/oci-go-sdk/dns" ) // Environment variables names. const ( envNamespace = "OCI_" EnvCompartmentOCID = envNamespace + "COMPARTMENT_OCID" envPrivKey = envNamespace + "PRIVKEY" EnvPrivKeyFile = envPrivKey + "_FILE" EnvPrivKeyPass = envPrivKey + "_PASS" EnvTenancyOCID = envNamespace + "TENANCY_OCID" EnvUserOCID = envNamespace + "USER_OCID" EnvPubKeyFingerprint = envNamespace + "PUBKEY_FINGERPRINT" EnvRegion = envNamespace + "REGION" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { CompartmentID string OCIConfigProvider common.ConfigurationProvider PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *dns.DnsClient config *Config } // NewDNSProvider returns a DNSProvider instance configured for OracleCloud. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(envPrivKey, EnvTenancyOCID, EnvUserOCID, EnvPubKeyFingerprint, EnvRegion, EnvCompartmentOCID) if err != nil { return nil, fmt.Errorf("oraclecloud: %w", err) } config := NewDefaultConfig() config.CompartmentID = values[EnvCompartmentOCID] config.OCIConfigProvider = newConfigProvider(values) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for OracleCloud. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("oraclecloud: the configuration of the DNS provider is nil") } if config.CompartmentID == "" { return nil, errors.New("oraclecloud: CompartmentID is missing") } if config.OCIConfigProvider == nil { return nil, errors.New("oraclecloud: OCIConfigProvider is missing") } client, err := dns.NewDnsClientWithConfigurationProvider(config.OCIConfigProvider) if err != nil { return nil, fmt.Errorf("oraclecloud: %w", err) } if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{client: &client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zoneNameOrID, err1 := dns01.FindZoneByFqdn(fqdn) if err1 != nil { return fmt.Errorf("oraclecloud: could not find zone for domain %q and fqdn %q : %w", domain, fqdn, err1) } // generate request to dns.PatchDomainRecordsRequest recordOperation := dns.RecordOperation{ Domain: common.String(dns01.UnFqdn(fqdn)), Rdata: common.String(value), Rtype: common.String("TXT"), Ttl: common.Int(d.config.TTL), IsProtected: common.Bool(false), } request := dns.PatchDomainRecordsRequest{ CompartmentId: common.String(d.config.CompartmentID), ZoneNameOrId: common.String(zoneNameOrID), Domain: common.String(dns01.UnFqdn(fqdn)), PatchDomainRecordsDetails: dns.PatchDomainRecordsDetails{ Items: []dns.RecordOperation{recordOperation}, }, } _, err := d.client.PatchDomainRecords(context.Background(), request) if err != nil { return fmt.Errorf("oraclecloud: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zoneNameOrID, err1 := dns01.FindZoneByFqdn(fqdn) if err1 != nil { return fmt.Errorf("oraclecloud: could not find zone for domain %q and fqdn %q : %w", domain, fqdn, err1) } // search to TXT record's hash to delete getRequest := dns.GetDomainRecordsRequest{ ZoneNameOrId: common.String(zoneNameOrID), Domain: common.String(dns01.UnFqdn(fqdn)), CompartmentId: common.String(d.config.CompartmentID), Rtype: common.String("TXT"), } ctx := context.Background() domainRecords, err := d.client.GetDomainRecords(ctx, getRequest) if err != nil { return fmt.Errorf("oraclecloud: %w", err) } if *domainRecords.OpcTotalItems == 0 { return errors.New("oraclecloud: no record to CleanUp") } var deleteHash *string for _, record := range domainRecords.RecordCollection.Items { if record.Rdata != nil && *record.Rdata == `"`+value+`"` { deleteHash = record.RecordHash break } } if deleteHash == nil { return errors.New("oraclecloud: no record to CleanUp") } recordOperation := dns.RecordOperation{ RecordHash: deleteHash, Operation: dns.RecordOperationOperationRemove, } patchRequest := dns.PatchDomainRecordsRequest{ ZoneNameOrId: common.String(zoneNameOrID), Domain: common.String(dns01.UnFqdn(fqdn)), PatchDomainRecordsDetails: dns.PatchDomainRecordsDetails{ Items: []dns.RecordOperation{recordOperation}, }, CompartmentId: common.String(d.config.CompartmentID), } _, err = d.client.PatchDomainRecords(ctx, patchRequest) if err != nil { return fmt.Errorf("oraclecloud: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/oraclecloud/oraclecloud.toml000066400000000000000000000023341434020463500227440ustar00rootroot00000000000000Name = "Oracle Cloud" Description = '''''' URL = "https://cloud.oracle.com/home" Code = "oraclecloud" Since = "v2.3.0" Example = ''' OCI_PRIVKEY_FILE="~/.oci/oci_api_key.pem" \ OCI_PRIVKEY_PASS="secret" \ OCI_TENANCY_OCID="ocid1.tenancy.oc1..secret" \ OCI_USER_OCID="ocid1.user.oc1..secret" \ OCI_PUBKEY_FINGERPRINT="00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" \ OCI_REGION="us-phoenix-1" \ OCI_COMPARTMENT_OCID="ocid1.tenancy.oc1..secret" \ lego --email you@example.com --dns oraclecloud --domains my.example.org run ''' [Configuration] [Configuration.Credentials] OCI_PRIVKEY_FILE = "Private key file" OCI_PRIVKEY_PASS = "Private key password" OCI_TENANCY_OCID = "Tenancy OCID" OCI_USER_OCID = "User OCID" OCI_PUBKEY_FINGERPRINT = "Public key fingerprint" OCI_REGION = "Region" OCI_COMPARTMENT_OCID = "Compartment OCID" [Configuration.Additional] OCI_POLLING_INTERVAL = "Time between DNS propagation check" OCI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" OCI_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm" GoClient = "https://github.com/oracle/oci-go-sdk" lego-4.9.1/providers/dns/oraclecloud/oraclecloud_test.go000066400000000000000000000226521434020463500234420ustar00rootroot00000000000000package oraclecloud import ( "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/pem" "os" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/oracle/oci-go-sdk/common" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( envPrivKey, EnvPrivKeyFile, EnvPrivKeyPass, EnvTenancyOCID, EnvUserOCID, EnvPubKeyFingerprint, EnvRegion, EnvCompartmentOCID). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ envPrivKey: mustGeneratePrivateKey("secret1"), EnvPrivKeyPass: "secret1", EnvTenancyOCID: "ocid1.tenancy.oc1..secret", EnvUserOCID: "ocid1.user.oc1..secret", EnvPubKeyFingerprint: "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00", EnvRegion: "us-phoenix-1", EnvCompartmentOCID: "123", }, }, { desc: "success file", envVars: map[string]string{ EnvPrivKeyFile: mustGeneratePrivateKeyFile("secret1"), EnvPrivKeyPass: "secret1", EnvTenancyOCID: "ocid1.tenancy.oc1..secret", EnvUserOCID: "ocid1.user.oc1..secret", EnvPubKeyFingerprint: "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00", EnvRegion: "us-phoenix-1", EnvCompartmentOCID: "123", }, }, { desc: "missing credentials", envVars: map[string]string{}, expected: "oraclecloud: some credentials information are missing: OCI_PRIVKEY,OCI_TENANCY_OCID,OCI_USER_OCID,OCI_PUBKEY_FINGERPRINT,OCI_REGION,OCI_COMPARTMENT_OCID", }, { desc: "missing CompartmentID", envVars: map[string]string{ envPrivKey: mustGeneratePrivateKey("secret"), EnvPrivKeyPass: "secret", EnvTenancyOCID: "ocid1.tenancy.oc1..secret", EnvUserOCID: "ocid1.user.oc1..secret", EnvPubKeyFingerprint: "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00", EnvRegion: "us-phoenix-1", EnvCompartmentOCID: "", }, expected: "oraclecloud: some credentials information are missing: OCI_COMPARTMENT_OCID", }, { desc: "missing OCI_PRIVKEY", envVars: map[string]string{ envPrivKey: "", EnvPrivKeyPass: "secret", EnvTenancyOCID: "ocid1.tenancy.oc1..secret", EnvUserOCID: "ocid1.user.oc1..secret", EnvPubKeyFingerprint: "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00", EnvRegion: "us-phoenix-1", EnvCompartmentOCID: "123", }, expected: "oraclecloud: some credentials information are missing: OCI_PRIVKEY", }, { desc: "missing OCI_PRIVKEY_PASS", envVars: map[string]string{ envPrivKey: mustGeneratePrivateKey("secret"), EnvPrivKeyPass: "", EnvTenancyOCID: "ocid1.tenancy.oc1..secret", EnvUserOCID: "ocid1.user.oc1..secret", EnvPubKeyFingerprint: "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00", EnvRegion: "us-phoenix-1", EnvCompartmentOCID: "123", }, expected: "oraclecloud: can not create client, bad configuration: ", }, { desc: "missing OCI_TENANCY_OCID", envVars: map[string]string{ envPrivKey: mustGeneratePrivateKey("secret"), EnvPrivKeyPass: "secret", EnvTenancyOCID: "", EnvUserOCID: "ocid1.user.oc1..secret", EnvPubKeyFingerprint: "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00", EnvRegion: "us-phoenix-1", EnvCompartmentOCID: "123", }, expected: "oraclecloud: some credentials information are missing: OCI_TENANCY_OCID", }, { desc: "missing OCI_USER_OCID", envVars: map[string]string{ envPrivKey: mustGeneratePrivateKey("secret"), EnvPrivKeyPass: "secret", EnvTenancyOCID: "ocid1.tenancy.oc1..secret", EnvUserOCID: "", EnvPubKeyFingerprint: "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00", EnvRegion: "us-phoenix-1", EnvCompartmentOCID: "123", }, expected: "oraclecloud: some credentials information are missing: OCI_USER_OCID", }, { desc: "missing OCI_PUBKEY_FINGERPRINT", envVars: map[string]string{ envPrivKey: mustGeneratePrivateKey("secret"), EnvPrivKeyPass: "secret", EnvTenancyOCID: "ocid1.tenancy.oc1..secret", EnvUserOCID: "ocid1.user.oc1..secret", EnvPubKeyFingerprint: "", EnvRegion: "us-phoenix-1", EnvCompartmentOCID: "123", }, expected: "oraclecloud: some credentials information are missing: OCI_PUBKEY_FINGERPRINT", }, { desc: "missing OCI_REGION", envVars: map[string]string{ envPrivKey: mustGeneratePrivateKey("secret"), EnvPrivKeyPass: "secret", EnvTenancyOCID: "ocid1.tenancy.oc1..secret", EnvUserOCID: "ocid1.user.oc1..secret", EnvPubKeyFingerprint: "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00", EnvRegion: "", EnvCompartmentOCID: "123", }, expected: "oraclecloud: some credentials information are missing: OCI_REGION", }, { desc: "missing OCI_REGION", envVars: map[string]string{ envPrivKey: mustGeneratePrivateKey("secret"), EnvPrivKeyPass: "secret", EnvTenancyOCID: "ocid1.tenancy.oc1..secret", EnvUserOCID: "ocid1.user.oc1..secret", EnvPubKeyFingerprint: "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00", EnvRegion: "", EnvCompartmentOCID: "123", }, expected: "oraclecloud: some credentials information are missing: OCI_REGION", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer func() { privKeyFile := os.Getenv(EnvPrivKeyFile) if privKeyFile != "" { _ = os.Remove(privKeyFile) } envTest.RestoreEnv() }() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.Error(t, err) require.Contains(t, err.Error(), test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { envTest.ClearEnv() defer envTest.RestoreEnv() testCases := []struct { desc string compartmentID string configurationProvider common.ConfigurationProvider expected string }{ { desc: "configuration provider error", configurationProvider: mockConfigurationProvider("wrong-secret"), compartmentID: "123", expected: "oraclecloud: can not create client, bad configuration: x509: decryption password incorrect", }, { desc: "OCIConfigProvider is missing", compartmentID: "123", expected: "oraclecloud: OCIConfigProvider is missing", }, { desc: "missing CompartmentID", configurationProvider: mockConfigurationProvider("secret"), expected: "oraclecloud: CompartmentID is missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.CompartmentID = test.compartmentID config.OCIConfigProvider = test.configurationProvider p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func mockConfigurationProvider(keyPassphrase string) *configProvider { envTest.Apply(map[string]string{ envPrivKey: mustGeneratePrivateKey("secret"), }) return &configProvider{ values: map[string]string{ EnvCompartmentOCID: "test", EnvPrivKeyPass: "test", EnvTenancyOCID: "test", EnvUserOCID: "test", EnvPubKeyFingerprint: "test", EnvRegion: "test", }, privateKeyPassphrase: keyPassphrase, } } func mustGeneratePrivateKey(pwd string) string { block, err := generatePrivateKey(pwd) if err != nil { panic(err) } return base64.StdEncoding.EncodeToString(pem.EncodeToMemory(block)) } func mustGeneratePrivateKeyFile(pwd string) string { block, err := generatePrivateKey(pwd) if err != nil { panic(err) } file, err := os.CreateTemp("", "lego_oci_*.pem") if err != nil { panic(err) } err = pem.Encode(file, block) if err != nil { panic(err) } return file.Name() } func generatePrivateKey(pwd string) (*pem.Block, error) { key, err := rsa.GenerateKey(rand.Reader, 512) if err != nil { return nil, err } block := &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key), } if pwd != "" { block, err = x509.EncryptPEMBlock(rand.Reader, block.Type, block.Bytes, []byte(pwd), x509.PEMCipherAES256) if err != nil { return nil, err } } return block, nil } lego-4.9.1/providers/dns/otc/000077500000000000000000000000001434020463500160425ustar00rootroot00000000000000lego-4.9.1/providers/dns/otc/client.go000066400000000000000000000127721434020463500176600ustar00rootroot00000000000000package otc import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" ) type recordset struct { Name string `json:"name"` Description string `json:"description"` Type string `json:"type"` TTL int `json:"ttl"` Records []string `json:"records"` } type nameResponse struct { Name string `json:"name"` } type userResponse struct { Name string `json:"name"` Password string `json:"password"` Domain nameResponse `json:"domain"` } type passwordResponse struct { User userResponse `json:"user"` } type identityResponse struct { Methods []string `json:"methods"` Password passwordResponse `json:"password"` } type scopeResponse struct { Project nameResponse `json:"project"` } type authResponse struct { Identity identityResponse `json:"identity"` Scope scopeResponse `json:"scope"` } type loginResponse struct { Auth authResponse `json:"auth"` } type endpointResponse struct { Token token `json:"token"` } type token struct { Catalog []catalog `json:"catalog"` } type catalog struct { Type string `json:"type"` Endpoints []endpoint `json:"endpoints"` } type endpoint struct { URL string `json:"url"` } type zoneItem struct { ID string `json:"id"` Name string `json:"name"` } type zonesResponse struct { Zones []zoneItem `json:"zones"` } type recordSet struct { ID string `json:"id"` } type recordSetsResponse struct { RecordSets []recordSet `json:"recordsets"` } // Starts a new OTC API Session. Authenticates using userName, password // and receives a token to be used in for subsequent requests. func (d *DNSProvider) login() error { return d.loginRequest() } func (d *DNSProvider) loginRequest() error { userResp := userResponse{ Name: d.config.UserName, Password: d.config.Password, Domain: nameResponse{ Name: d.config.DomainName, }, } loginResp := loginResponse{ Auth: authResponse{ Identity: identityResponse{ Methods: []string{"password"}, Password: passwordResponse{ User: userResp, }, }, Scope: scopeResponse{ Project: nameResponse{ Name: d.config.ProjectName, }, }, }, } body, err := json.Marshal(loginResp) if err != nil { return err } req, err := http.NewRequest(http.MethodPost, d.config.IdentityEndpoint, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: d.config.HTTPClient.Timeout} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { return fmt.Errorf("OTC API request failed with HTTP status code %d", resp.StatusCode) } d.token = resp.Header.Get("X-Subject-Token") if d.token == "" { return errors.New("unable to get auth token") } var endpointResp endpointResponse err = json.NewDecoder(resp.Body).Decode(&endpointResp) if err != nil { return err } var endpoints []endpoint for _, v := range endpointResp.Token.Catalog { if v.Type == "dns" { endpoints = append(endpoints, v.Endpoints...) } } if len(endpoints) > 0 { d.baseURL = fmt.Sprintf("%s/v2", endpoints[0].URL) } else { return errors.New("unable to get dns endpoint") } return nil } func (d *DNSProvider) getZoneID(zone string) (string, error) { resource := fmt.Sprintf("zones?name=%s", zone) resp, err := d.sendRequest(http.MethodGet, resource, nil) if err != nil { return "", err } var zonesRes zonesResponse err = json.NewDecoder(resp).Decode(&zonesRes) if err != nil { return "", err } if len(zonesRes.Zones) < 1 { return "", fmt.Errorf("zone %s not found", zone) } for _, z := range zonesRes.Zones { if z.Name == zone { return z.ID, nil } } return "", fmt.Errorf("zone %s not found", zone) } func (d *DNSProvider) getRecordSetID(zoneID, fqdn string) (string, error) { resource := fmt.Sprintf("zones/%s/recordsets?type=TXT&name=%s", zoneID, fqdn) resp, err := d.sendRequest(http.MethodGet, resource, nil) if err != nil { return "", err } var recordSetsRes recordSetsResponse err = json.NewDecoder(resp).Decode(&recordSetsRes) if err != nil { return "", err } if len(recordSetsRes.RecordSets) < 1 { return "", errors.New("record not found") } if len(recordSetsRes.RecordSets) > 1 { return "", errors.New("to many records found") } if recordSetsRes.RecordSets[0].ID == "" { return "", errors.New("id not found") } return recordSetsRes.RecordSets[0].ID, nil } func (d *DNSProvider) deleteRecordSet(zoneID, recordID string) error { resource := fmt.Sprintf("zones/%s/recordsets/%s", zoneID, recordID) _, err := d.sendRequest(http.MethodDelete, resource, nil) return err } func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (io.Reader, error) { url := fmt.Sprintf("%s/%s", d.baseURL, resource) var body io.Reader if payload != nil { content, err := json.Marshal(payload) if err != nil { return nil, err } body = bytes.NewReader(content) } req, err := http.NewRequest(method, url, body) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") if len(d.token) > 0 { req.Header.Set("X-Auth-Token", d.token) } resp, err := d.config.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { return nil, fmt.Errorf("OTC API request %s failed with HTTP status code %d", url, resp.StatusCode) } body1, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return bytes.NewReader(body1), nil } lego-4.9.1/providers/dns/otc/mock_test.go000066400000000000000000000106341434020463500203650ustar00rootroot00000000000000package otc import ( "fmt" "io" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) const fakeOTCToken = "62244bc21da68d03ebac94e6636ff01f" // DNSServerMock mock. type DNSServerMock struct { t *testing.T server *httptest.Server Mux *http.ServeMux } // NewDNSServerMock create a new DNSServerMock. func NewDNSServerMock(t *testing.T) *DNSServerMock { t.Helper() mux := http.NewServeMux() return &DNSServerMock{ t: t, server: httptest.NewServer(mux), Mux: mux, } } func (m *DNSServerMock) GetServerURL() string { return m.server.URL } // ShutdownServer creates the mock server. func (m *DNSServerMock) ShutdownServer() { m.server.Close() } // HandleAuthSuccessfully Handle auth successfully. func (m *DNSServerMock) HandleAuthSuccessfully() { m.Mux.HandleFunc("/v3/auth/token", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("X-Subject-Token", fakeOTCToken) fmt.Fprintf(w, `{ "token": { "catalog": [ { "type": "dns", "id": "56cd81db1f8445d98652479afe07c5ba", "name": "", "endpoints": [ { "url": "%s", "region": "eu-de", "region_id": "eu-de", "interface": "public", "id": "0047a06690484d86afe04877074efddf" } ] } ] }}`, m.server.URL) }) } // HandleListZonesSuccessfully Handle list zones successfully. func (m *DNSServerMock) HandleListZonesSuccessfully() { m.Mux.HandleFunc("/v2/zones", func(w http.ResponseWriter, r *http.Request) { assert.Equal(m.t, r.Method, http.MethodGet) assert.Equal(m.t, r.URL.Path, "/v2/zones") assert.Equal(m.t, r.URL.RawQuery, "name=example.com.") assert.Equal(m.t, r.Header.Get("Content-Type"), "application/json") fmt.Fprintf(w, `{ "zones":[{ "id":"123123", "name":"example.com." }]} `) }) } // HandleListZonesEmpty Handle list zones empty. func (m *DNSServerMock) HandleListZonesEmpty() { m.Mux.HandleFunc("/v2/zones", func(w http.ResponseWriter, r *http.Request) { assert.Equal(m.t, r.Method, http.MethodGet) assert.Equal(m.t, r.URL.Path, "/v2/zones") assert.Equal(m.t, r.URL.RawQuery, "name=example.com.") assert.Equal(m.t, r.Header.Get("Content-Type"), "application/json") fmt.Fprintf(w, `{ "zones":[ ]} `) }) } // HandleDeleteRecordsetsSuccessfully Handle delete recordsets successfully. func (m *DNSServerMock) HandleDeleteRecordsetsSuccessfully() { m.Mux.HandleFunc("/v2/zones/123123/recordsets/321321", func(w http.ResponseWriter, r *http.Request) { assert.Equal(m.t, r.Method, http.MethodDelete) assert.Equal(m.t, r.URL.Path, "/v2/zones/123123/recordsets/321321") assert.Equal(m.t, r.Header.Get("Content-Type"), "application/json") fmt.Fprintf(w, `{ "zones":[{ "id":"123123" }]} `) }) } // HandleListRecordsetsEmpty Handle list recordsets empty. func (m *DNSServerMock) HandleListRecordsetsEmpty() { m.Mux.HandleFunc("/v2/zones/123123/recordsets", func(w http.ResponseWriter, r *http.Request) { assert.Equal(m.t, r.URL.Path, "/v2/zones/123123/recordsets") assert.Equal(m.t, r.URL.RawQuery, "type=TXT&name=_acme-challenge.example.com.") fmt.Fprintf(w, `{ "recordsets":[ ]} `) }) } // HandleListRecordsetsSuccessfully Handle list recordsets successfully. func (m *DNSServerMock) HandleListRecordsetsSuccessfully() { m.Mux.HandleFunc("/v2/zones/123123/recordsets", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { assert.Equal(m.t, r.URL.Path, "/v2/zones/123123/recordsets") assert.Equal(m.t, r.URL.RawQuery, "type=TXT&name=_acme-challenge.example.com.") assert.Equal(m.t, r.Header.Get("Content-Type"), "application/json") fmt.Fprintf(w, `{ "recordsets":[{ "id":"321321" }]} `) return } if r.Method == http.MethodPost { assert.Equal(m.t, r.Header.Get("Content-Type"), "application/json") body, err := io.ReadAll(r.Body) assert.Nil(m.t, err) exceptedString := `{ "name": "_acme-challenge.example.com.", "description": "Added TXT record for ACME dns-01 challenge using lego client", "type": "TXT", "ttl": 300, "records": ["\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\""] }` assert.JSONEq(m.t, string(body), exceptedString) fmt.Fprintf(w, `{ "recordsets":[{ "id":"321321" }]} `) return } http.Error(w, fmt.Sprintf("Expected method to be 'GET' or 'POST' but got '%s'", r.Method), http.StatusBadRequest) }) } lego-4.9.1/providers/dns/otc/otc.go000066400000000000000000000131341434020463500171600ustar00rootroot00000000000000// Package otc implements a DNS provider for solving the DNS-01 challenge using Open Telekom Cloud Managed DNS. package otc import ( "errors" "fmt" "net" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) const defaultIdentityEndpoint = "https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens" // minTTL 300 is otc minimum value for TTL. const minTTL = 300 // Environment variables names. const ( envNamespace = "OTC_" EnvDomainName = envNamespace + "DOMAIN_NAME" EnvUserName = envNamespace + "USER_NAME" EnvPassword = envNamespace + "PASSWORD" EnvProjectName = envNamespace + "PROJECT_NAME" EnvIdentityEndpoint = envNamespace + "IDENTITY_ENDPOINT" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { IdentityEndpoint string DomainName string ProjectName string UserName string Password string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), IdentityEndpoint: env.GetOrDefaultString(EnvIdentityEndpoint, defaultIdentityEndpoint), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).DialContext, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, // Workaround for keep alive bug in otc api DisableKeepAlives: true, }, }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config baseURL string token string } // NewDNSProvider returns a DNSProvider instance configured for OTC DNS. // Credentials must be passed in the environment variables: OTC_USER_NAME, // OTC_DOMAIN_NAME, OTC_PASSWORD OTC_PROJECT_NAME and OTC_IDENTITY_ENDPOINT. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvDomainName, EnvUserName, EnvPassword, EnvProjectName) if err != nil { return nil, fmt.Errorf("otc: %w", err) } config := NewDefaultConfig() config.DomainName = values[EnvDomainName] config.UserName = values[EnvUserName] config.Password = values[EnvPassword] config.ProjectName = values[EnvProjectName] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for OTC DNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("otc: the configuration of the DNS provider is nil") } if config.DomainName == "" || config.UserName == "" || config.Password == "" || config.ProjectName == "" { return nil, errors.New("otc: credentials missing") } if config.TTL < minTTL { return nil, fmt.Errorf("otc: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } if config.IdentityEndpoint == "" { config.IdentityEndpoint = defaultIdentityEndpoint } return &DNSProvider{config: config}, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("otc: %w", err) } err = d.login() if err != nil { return fmt.Errorf("otc: %w", err) } zoneID, err := d.getZoneID(authZone) if err != nil { return fmt.Errorf("otc: unable to get zone: %w", err) } resource := fmt.Sprintf("zones/%s/recordsets", zoneID) r1 := &recordset{ Name: fqdn, Description: "Added TXT record for ACME dns-01 challenge using lego client", Type: "TXT", TTL: d.config.TTL, Records: []string{fmt.Sprintf("%q", value)}, } _, err = d.sendRequest(http.MethodPost, resource, r1) if err != nil { return fmt.Errorf("otc: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("otc: %w", err) } err = d.login() if err != nil { return fmt.Errorf("otc: %w", err) } zoneID, err := d.getZoneID(authZone) if err != nil { return fmt.Errorf("otc: %w", err) } recordID, err := d.getRecordSetID(zoneID, fqdn) if err != nil { return fmt.Errorf("otc: unable go get record %s for zone %s: %w", fqdn, domain, err) } err = d.deleteRecordSet(zoneID, recordID) if err != nil { return fmt.Errorf("otc: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/otc/otc.toml000066400000000000000000000013201434020463500175200ustar00rootroot00000000000000Name = "Open Telekom Cloud" Description = '''''' URL = "https://cloud.telekom.de/en" Code = "otc" Since = "v0.4.1" Example = '''''' [Configuration] [Configuration.Credentials] OTC_USER_NAME = "User name" OTC_PASSWORD = "Password" OTC_PROJECT_NAME = "Project name" OTC_DOMAIN_NAME = "Domain name" OTC_IDENTITY_ENDPOINT = "Identity endpoint URL" [Configuration.Additional] OTC_POLLING_INTERVAL = "Time between DNS propagation check" OTC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" OTC_TTL = "The TTL of the TXT record used for the DNS challenge" OTC_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://docs.otc.t-systems.com/en-us/dns/index.html" lego-4.9.1/providers/dns/otc/otc_test.go000066400000000000000000000065041434020463500202220ustar00rootroot00000000000000package otc import ( "fmt" "os" "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/suite" ) type OTCSuite struct { suite.Suite Mock *DNSServerMock envTest *tester.EnvTest } func (s *OTCSuite) SetupTest() { s.Mock = NewDNSServerMock(s.T()) s.Mock.HandleAuthSuccessfully() s.envTest = tester.NewEnvTest( EnvDomainName, EnvUserName, EnvPassword, EnvProjectName, EnvIdentityEndpoint, ) } func (s *OTCSuite) TearDownTest() { s.envTest.RestoreEnv() s.Mock.ShutdownServer() } func TestTestSuite(t *testing.T) { suite.Run(t, new(OTCSuite)) } func (s *OTCSuite) createDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() config.UserName = "UserName" config.Password = "Password" config.DomainName = "DomainName" config.ProjectName = "ProjectName" config.IdentityEndpoint = fmt.Sprintf("%s/v3/auth/token", s.Mock.GetServerURL()) return NewDNSProviderConfig(config) } func (s *OTCSuite) TestLogin() { provider, err := s.createDNSProvider() s.Require().NoError(err) err = provider.loginRequest() s.Require().NoError(err) s.Equal(provider.baseURL, fmt.Sprintf("%s/v2", s.Mock.GetServerURL())) s.Equal(fakeOTCToken, provider.token) } func (s *OTCSuite) TestLoginEnv() { s.envTest.ClearEnv() s.envTest.Apply(map[string]string{ EnvDomainName: "unittest1", EnvUserName: "unittest2", EnvPassword: "unittest3", EnvProjectName: "unittest4", EnvIdentityEndpoint: "unittest5", }) provider, err := NewDNSProvider() s.Require().NoError(err) s.Equal(provider.config.DomainName, "unittest1") s.Equal(provider.config.UserName, "unittest2") s.Equal(provider.config.Password, "unittest3") s.Equal(provider.config.ProjectName, "unittest4") s.Equal(provider.config.IdentityEndpoint, "unittest5") os.Setenv(EnvIdentityEndpoint, "") provider, err = NewDNSProvider() s.Require().NoError(err) s.Equal(provider.config.IdentityEndpoint, "https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens") } func (s *OTCSuite) TestLoginEnvEmpty() { s.envTest.ClearEnv() _, err := NewDNSProvider() s.EqualError(err, "otc: some credentials information are missing: OTC_DOMAIN_NAME,OTC_USER_NAME,OTC_PASSWORD,OTC_PROJECT_NAME") } func (s *OTCSuite) TestDNSProvider_Present() { s.Mock.HandleListZonesSuccessfully() s.Mock.HandleListRecordsetsSuccessfully() provider, err := s.createDNSProvider() s.Require().NoError(err) err = provider.Present("example.com", "", "foobar") s.Require().NoError(err) } func (s *OTCSuite) TestDNSProvider_Present_EmptyZone() { s.Mock.HandleListZonesEmpty() s.Mock.HandleListRecordsetsSuccessfully() provider, err := s.createDNSProvider() s.Require().NoError(err) err = provider.Present("example.com", "", "foobar") s.NotNil(err) } func (s *OTCSuite) TestDNSProvider_CleanUp() { s.Mock.HandleListZonesSuccessfully() s.Mock.HandleListRecordsetsSuccessfully() s.Mock.HandleDeleteRecordsetsSuccessfully() provider, err := s.createDNSProvider() s.Require().NoError(err) err = provider.CleanUp("example.com", "", "foobar") s.Require().NoError(err) } func (s *OTCSuite) TestDNSProvider_CleanUp_EmptyRecordset() { s.Mock.HandleListZonesSuccessfully() s.Mock.HandleListRecordsetsEmpty() provider, err := s.createDNSProvider() s.Require().NoError(err) err = provider.CleanUp("example.com", "", "foobar") s.Require().Error(err) } lego-4.9.1/providers/dns/ovh/000077500000000000000000000000001434020463500160515ustar00rootroot00000000000000lego-4.9.1/providers/dns/ovh/ovh.go000066400000000000000000000144411434020463500172000ustar00rootroot00000000000000// Package ovh implements a DNS provider for solving the DNS-01 challenge using OVH DNS. package ovh import ( "errors" "fmt" "net/http" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/ovh/go-ovh/ovh" ) // OVH API reference: https://eu.api.ovh.com/ // Create a Token: https://eu.api.ovh.com/createToken/ // Environment variables names. const ( envNamespace = "OVH_" EnvEndpoint = envNamespace + "ENDPOINT" EnvApplicationKey = envNamespace + "APPLICATION_KEY" EnvApplicationSecret = envNamespace + "APPLICATION_SECRET" EnvConsumerKey = envNamespace + "CONSUMER_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Record a DNS record. type Record struct { ID int64 `json:"id,omitempty"` FieldType string `json:"fieldType,omitempty"` SubDomain string `json:"subDomain,omitempty"` Target string `json:"target,omitempty"` TTL int `json:"ttl,omitempty"` Zone string `json:"zone,omitempty"` } // Config is used to configure the creation of the DNSProvider. type Config struct { APIEndpoint string ApplicationKey string ApplicationSecret string ConsumerKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, ovh.DefaultTimeout), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *ovh.Client recordIDs map[string]int64 recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for OVH // Credentials must be passed in the environment variables: // OVH_ENDPOINT (must be either "ovh-eu" or "ovh-ca"), OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvEndpoint, EnvApplicationKey, EnvApplicationSecret, EnvConsumerKey) if err != nil { return nil, fmt.Errorf("ovh: %w", err) } config := NewDefaultConfig() config.APIEndpoint = values[EnvEndpoint] config.ApplicationKey = values[EnvApplicationKey] config.ApplicationSecret = values[EnvApplicationSecret] config.ConsumerKey = values[EnvConsumerKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for OVH. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("ovh: the configuration of the DNS provider is nil") } if config.APIEndpoint == "" || config.ApplicationKey == "" || config.ApplicationSecret == "" || config.ConsumerKey == "" { return nil, errors.New("ovh: credentials missing") } client, err := ovh.NewClient( config.APIEndpoint, config.ApplicationKey, config.ApplicationSecret, config.ConsumerKey, ) if err != nil { return nil, fmt.Errorf("ovh: %w", err) } client.Client = config.HTTPClient return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]int64), }, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) // Parse domain name authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("ovh: could not determine zone for domain %q: %w", fqdn, err) } authZone = dns01.UnFqdn(authZone) subDomain := extractRecordName(fqdn, authZone) reqURL := fmt.Sprintf("/domain/zone/%s/record", authZone) reqData := Record{FieldType: "TXT", SubDomain: subDomain, Target: value, TTL: d.config.TTL} // Create TXT record var respData Record err = d.client.Post(reqURL, reqData, &respData) if err != nil { return fmt.Errorf("ovh: error when call api to add record (%s): %w", reqURL, err) } // Apply the change reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone) err = d.client.Post(reqURL, nil, nil) if err != nil { return fmt.Errorf("ovh: error when call api to refresh zone (%s): %w", reqURL, err) } d.recordIDsMu.Lock() d.recordIDs[token] = respData.ID d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) // get the record's unique ID from when we created it d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("ovh: unknown record ID for '%s'", fqdn) } authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("ovh: could not determine zone for domain %q: %w", fqdn, err) } authZone = dns01.UnFqdn(authZone) reqURL := fmt.Sprintf("/domain/zone/%s/record/%d", authZone, recordID) err = d.client.Delete(reqURL, nil) if err != nil { return fmt.Errorf("ovh: error when call OVH api to delete challenge record (%s): %w", reqURL, err) } // Apply the change reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone) err = d.client.Post(reqURL, nil, nil) if err != nil { return fmt.Errorf("ovh: error when call api to refresh zone (%s): %w", reqURL, err) } // Delete record ID from map d.recordIDsMu.Lock() delete(d.recordIDs, token) d.recordIDsMu.Unlock() return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func extractRecordName(fqdn, zone string) string { name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+zone); idx != -1 { return name[:idx] } return "" } lego-4.9.1/providers/dns/ovh/ovh.toml000066400000000000000000000025711434020463500175470ustar00rootroot00000000000000Name = "OVH" Description = '''''' URL = "https://www.ovh.com/" Code = "ovh" Since = "v0.4.0" Example = ''' OVH_APPLICATION_KEY=1234567898765432 \ OVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \ OVH_CONSUMER_KEY=256vfsd347245sdfg \ OVH_ENDPOINT=ovh-eu \ lego --email you@example.com --dns ovh --domains my.example.org run ''' Additional = ''' ## Application Key and Secret Application key and secret can be created by following the [OVH guide](https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/). When requesting the consumer key, the following configuration can be use to define access rights: ```json { "accessRules": [ { "method": "POST", "path": "/domain/zone/*" }, { "method": "DELETE", "path": "/domain/zone/*" } ] } ``` ''' [Configuration] [Configuration.Credentials] OVH_ENDPOINT = "Endpoint URL (ovh-eu or ovh-ca)" OVH_APPLICATION_KEY = "Application key" OVH_APPLICATION_SECRET = "Application secret" OVH_CONSUMER_KEY = "Consumer key" [Configuration.Additional] OVH_POLLING_INTERVAL = "Time between DNS propagation check" OVH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" OVH_TTL = "The TTL of the TXT record used for the DNS challenge" OVH_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://eu.api.ovh.com/" GoClient = "https://github.com/ovh/go-ovh" lego-4.9.1/providers/dns/ovh/ovh_test.go000066400000000000000000000130511434020463500202330ustar00rootroot00000000000000package ovh import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvEndpoint, EnvApplicationKey, EnvApplicationSecret, EnvConsumerKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvEndpoint: "ovh-eu", EnvApplicationKey: "B", EnvApplicationSecret: "C", EnvConsumerKey: "D", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvEndpoint: "", EnvApplicationKey: "", EnvApplicationSecret: "", EnvConsumerKey: "", }, expected: "ovh: some credentials information are missing: OVH_ENDPOINT,OVH_APPLICATION_KEY,OVH_APPLICATION_SECRET,OVH_CONSUMER_KEY", }, { desc: "missing endpoint", envVars: map[string]string{ EnvEndpoint: "", EnvApplicationKey: "B", EnvApplicationSecret: "C", EnvConsumerKey: "D", }, expected: "ovh: some credentials information are missing: OVH_ENDPOINT", }, { desc: "missing invalid endpoint", envVars: map[string]string{ EnvEndpoint: "foobar", EnvApplicationKey: "B", EnvApplicationSecret: "C", EnvConsumerKey: "D", }, expected: "ovh: unknown endpoint 'foobar', consider checking 'Endpoints' list of using an URL", }, { desc: "missing application key", envVars: map[string]string{ EnvEndpoint: "ovh-eu", EnvApplicationKey: "", EnvApplicationSecret: "C", EnvConsumerKey: "D", }, expected: "ovh: some credentials information are missing: OVH_APPLICATION_KEY", }, { desc: "missing application secret", envVars: map[string]string{ EnvEndpoint: "ovh-eu", EnvApplicationKey: "B", EnvApplicationSecret: "", EnvConsumerKey: "D", }, expected: "ovh: some credentials information are missing: OVH_APPLICATION_SECRET", }, { desc: "missing consumer key", envVars: map[string]string{ EnvEndpoint: "ovh-eu", EnvApplicationKey: "B", EnvApplicationSecret: "C", EnvConsumerKey: "", }, expected: "ovh: some credentials information are missing: OVH_CONSUMER_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiEndpoint string applicationKey string applicationSecret string consumerKey string expected string }{ { desc: "success", apiEndpoint: "ovh-eu", applicationKey: "B", applicationSecret: "C", consumerKey: "D", }, { desc: "missing credentials", expected: "ovh: credentials missing", }, { desc: "missing api endpoint", apiEndpoint: "", applicationKey: "B", applicationSecret: "C", consumerKey: "D", expected: "ovh: credentials missing", }, { desc: "missing invalid api endpoint", apiEndpoint: "foobar", applicationKey: "B", applicationSecret: "C", consumerKey: "D", expected: "ovh: unknown endpoint 'foobar', consider checking 'Endpoints' list of using an URL", }, { desc: "missing application key", apiEndpoint: "ovh-eu", applicationKey: "", applicationSecret: "C", consumerKey: "D", expected: "ovh: credentials missing", }, { desc: "missing application secret", apiEndpoint: "ovh-eu", applicationKey: "B", applicationSecret: "", consumerKey: "D", expected: "ovh: credentials missing", }, { desc: "missing consumer key", apiEndpoint: "ovh-eu", applicationKey: "B", applicationSecret: "C", consumerKey: "", expected: "ovh: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIEndpoint = test.apiEndpoint config.ApplicationKey = test.applicationKey config.ApplicationSecret = test.applicationSecret config.ConsumerKey = test.consumerKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/pdns/000077500000000000000000000000001434020463500162215ustar00rootroot00000000000000lego-4.9.1/providers/dns/pdns/client.go000066400000000000000000000107031434020463500200270ustar00rootroot00000000000000package pdns import ( "encoding/json" "errors" "fmt" "io" "net/http" "path" "strconv" "strings" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/miekg/dns" ) type Record struct { Content string `json:"content"` Disabled bool `json:"disabled"` // pre-v1 API Name string `json:"name"` Type string `json:"type"` TTL int `json:"ttl,omitempty"` } type hostedZone struct { ID string `json:"id"` Name string `json:"name"` URL string `json:"url"` RRSets []rrSet `json:"rrsets"` // pre-v1 API Records []Record `json:"records"` } type rrSet struct { Name string `json:"name"` Type string `json:"type"` Kind string `json:"kind"` ChangeType string `json:"changetype"` Records []Record `json:"records,omitempty"` TTL int `json:"ttl,omitempty"` } type rrSets struct { RRSets []rrSet `json:"rrsets"` } type apiError struct { ShortMsg string `json:"error"` } func (a apiError) Error() string { return a.ShortMsg } type apiVersion struct { URL string `json:"url"` Version int `json:"version"` } func (d *DNSProvider) getHostedZone(fqdn string) (*hostedZone, error) { authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return nil, err } p := path.Join("/servers", d.config.ServerName, "/zones/", dns.Fqdn(authZone)) result, err := d.sendRequest(http.MethodGet, p, nil) if err != nil { return nil, err } var zone hostedZone err = json.Unmarshal(result, &zone) if err != nil { return nil, err } // convert pre-v1 API result if len(zone.Records) > 0 { zone.RRSets = []rrSet{} for _, record := range zone.Records { set := rrSet{ Name: record.Name, Type: record.Type, Records: []Record{record}, } zone.RRSets = append(zone.RRSets, set) } } return &zone, nil } func (d *DNSProvider) findTxtRecord(fqdn string) (*rrSet, error) { zone, err := d.getHostedZone(fqdn) if err != nil { return nil, err } _, err = d.sendRequest(http.MethodGet, zone.URL, nil) if err != nil { return nil, err } for _, set := range zone.RRSets { if set.Type == "TXT" && (set.Name == dns01.UnFqdn(fqdn) || set.Name == fqdn) { return &set, nil } } return nil, nil } func (d *DNSProvider) getAPIVersion() (int, error) { result, err := d.sendRequest(http.MethodGet, "/api", nil) if err != nil { return 0, err } var versions []apiVersion err = json.Unmarshal(result, &versions) if err != nil { return 0, err } latestVersion := 0 for _, v := range versions { if v.Version > latestVersion { latestVersion = v.Version } } return latestVersion, err } func (d *DNSProvider) notify(zoneURL string) error { if d.apiVersion >= 1 { p := path.Join(zoneURL, "/notify") _, err := d.sendRequest(http.MethodPut, p, nil) if err != nil { return err } } return nil } func (d *DNSProvider) sendRequest(method, uri string, body io.Reader) (json.RawMessage, error) { req, err := d.makeRequest(method, uri, body) if err != nil { return nil, err } resp, err := d.config.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("error talking to PDNS API: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusUnprocessableEntity && (resp.StatusCode < 200 || resp.StatusCode >= 300) { return nil, fmt.Errorf("unexpected HTTP status code %d when %sing '%s'", resp.StatusCode, req.Method, req.URL) } var msg json.RawMessage err = json.NewDecoder(resp.Body).Decode(&msg) if err != nil { if errors.Is(err, io.EOF) { // empty body return nil, nil } // other error return nil, err } // check for PowerDNS error message if len(msg) > 0 && msg[0] == '{' { var errInfo apiError err = json.Unmarshal(msg, &errInfo) if err != nil { return nil, err } if errInfo.ShortMsg != "" { return nil, fmt.Errorf("error talking to PDNS API: %w", errInfo) } } return msg, nil } func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (*http.Request, error) { p := path.Join("/", uri) if p != "/api" && d.apiVersion > 0 && !strings.HasPrefix(p, "/api/v") { p = path.Join("/api", "v"+strconv.Itoa(d.apiVersion), p) } u, err := d.config.Host.Parse(path.Join(d.config.Host.Path, p)) if err != nil { return nil, err } req, err := http.NewRequest(method, u.String(), body) if err != nil { return nil, err } req.Header.Set("X-API-Key", d.config.APIKey) if method != http.MethodGet && method != http.MethodDelete { req.Header.Set("Content-Type", "application/json") } return req, nil } lego-4.9.1/providers/dns/pdns/client_test.go000066400000000000000000000050711434020463500210700ustar00rootroot00000000000000package pdns import ( "net/http" "net/url" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDNSProvider_makeRequest(t *testing.T) { testCases := []struct { desc string apiVersion int baseURL string uri string expected string }{ { desc: "host with path", apiVersion: 1, baseURL: "https://example.com/test", uri: "/foo", expected: "https://example.com/test/api/v1/foo", }, { desc: "host with path + trailing slash", apiVersion: 1, baseURL: "https://example.com/test/", uri: "/foo", expected: "https://example.com/test/api/v1/foo", }, { desc: "no URI", apiVersion: 1, baseURL: "https://example.com/test", uri: "", expected: "https://example.com/test/api/v1", }, { desc: "host without path", apiVersion: 1, baseURL: "https://example.com", uri: "/foo", expected: "https://example.com/api/v1/foo", }, { desc: "api", apiVersion: 1, baseURL: "https://example.com", uri: "/api", expected: "https://example.com/api", }, { desc: "API version 0, host with path", apiVersion: 0, baseURL: "https://example.com/test", uri: "/foo", expected: "https://example.com/test/foo", }, { desc: "API version 0, host with path + trailing slash", apiVersion: 0, baseURL: "https://example.com/test/", uri: "/foo", expected: "https://example.com/test/foo", }, { desc: "API version 0, no URI", apiVersion: 0, baseURL: "https://example.com/test", uri: "", expected: "https://example.com/test", }, { desc: "API version 0, host without path", apiVersion: 0, baseURL: "https://example.com", uri: "/foo", expected: "https://example.com/foo", }, { desc: "API version 0, api", apiVersion: 0, baseURL: "https://example.com", uri: "/api", expected: "https://example.com/api", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() host, err := url.Parse(test.baseURL) require.NoError(t, err) config := &Config{Host: host, APIKey: "secret"} p := &DNSProvider{ config: config, apiVersion: test.apiVersion, } req, err := p.makeRequest(http.MethodGet, test.uri, nil) require.NoError(t, err) assert.Equal(t, test.expected, req.URL.String()) assert.Equal(t, "secret", req.Header.Get("X-API-Key")) }) } } lego-4.9.1/providers/dns/pdns/pdns.go000066400000000000000000000126171434020463500175230ustar00rootroot00000000000000// Package pdns implements a DNS provider for solving the DNS-01 challenge using PowerDNS nameserver. package pdns import ( "bytes" "encoding/json" "errors" "fmt" "net/http" "net/url" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "PDNS_" EnvAPIKey = envNamespace + "API_KEY" EnvAPIURL = envNamespace + "API_URL" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" EnvServerName = envNamespace + "SERVER_NAME" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string Host *url.URL ServerName string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), ServerName: env.GetOrDefaultString(EnvServerName, "localhost"), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { apiVersion int config *Config } // NewDNSProvider returns a DNSProvider instance configured for pdns. // Credentials must be passed in the environment variable: // PDNS_API_URL and PDNS_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey, EnvAPIURL) if err != nil { return nil, fmt.Errorf("pdns: %w", err) } hostURL, err := url.Parse(values[EnvAPIURL]) if err != nil { return nil, fmt.Errorf("pdns: %w", err) } config := NewDefaultConfig() config.Host = hostURL config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for pdns. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("pdns: the configuration of the DNS provider is nil") } if config.APIKey == "" { return nil, errors.New("pdns: API key missing") } if config.Host == nil || config.Host.Host == "" { return nil, errors.New("pdns: API URL missing") } d := &DNSProvider{config: config} apiVersion, err := d.getAPIVersion() if err != nil { log.Warnf("pdns: failed to get API version %v", err) } d.apiVersion = apiVersion return d, nil } // Timeout returns the timeout and interval to use when checking for DNS // propagation. Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("pdns: %w", err) } name := fqdn // pre-v1 API wants non-fqdn if d.apiVersion == 0 { name = dns01.UnFqdn(fqdn) } rec := Record{ Content: "\"" + value + "\"", Disabled: false, // pre-v1 API Type: "TXT", Name: name, TTL: d.config.TTL, } // Look for existing records. existingRrSet, err := d.findTxtRecord(fqdn) if err != nil { return fmt.Errorf("pdns: %w", err) } // merge the existing and new records var records []Record if existingRrSet != nil { records = existingRrSet.Records } records = append(records, rec) rrsets := rrSets{ RRSets: []rrSet{ { Name: name, ChangeType: "REPLACE", Type: "TXT", Kind: "Master", TTL: d.config.TTL, Records: records, }, }, } body, err := json.Marshal(rrsets) if err != nil { return fmt.Errorf("pdns: %w", err) } _, err = d.sendRequest(http.MethodPatch, zone.URL, bytes.NewReader(body)) if err != nil { return fmt.Errorf("pdns: %w", err) } if d.apiVersion < 1 { return nil } err = d.notify(zone.URL) if err != nil { return fmt.Errorf("pdns: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("pdns: %w", err) } set, err := d.findTxtRecord(fqdn) if err != nil { return fmt.Errorf("pdns: %w", err) } if set == nil { return fmt.Errorf("pdns: no existing record found for %s", fqdn) } rrsets := rrSets{ RRSets: []rrSet{ { Name: set.Name, Type: set.Type, ChangeType: "DELETE", }, }, } body, err := json.Marshal(rrsets) if err != nil { return fmt.Errorf("pdns: %w", err) } _, err = d.sendRequest(http.MethodPatch, zone.URL, bytes.NewReader(body)) if err != nil { return fmt.Errorf("pdns: %w", err) } if d.apiVersion < 1 { return nil } err = d.notify(zone.URL) if err != nil { return fmt.Errorf("pdns: %w", err) } return nil } lego-4.9.1/providers/dns/pdns/pdns.toml000066400000000000000000000026401434020463500200640ustar00rootroot00000000000000Name = "PowerDNS" Description = '''''' URL = "https://www.powerdns.com/" Code = "pdns" Since = "v0.4.0" Example = ''' PDNS_API_URL=http://pdns-server:80/ \ PDNS_API_KEY=xxxx \ lego --email you@example.com --dns pdns --domains my.example.org run ''' Additional = ''' ## Information Tested 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. PowerDNS Notes: - 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. - 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 ''' [Configuration] [Configuration.Credentials] PDNS_API_KEY = "API key" PDNS_API_URL = "API URL" [Configuration.Additional] PDNS_POLLING_INTERVAL = "Time between DNS propagation check" PDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" PDNS_TTL = "The TTL of the TXT record used for the DNS challenge" PDNS_HTTP_TIMEOUT = "API request timeout" PDNS_SERVER_NAME = "Name of the server in the URL, 'localhost' by default" [Links] API = "https://doc.powerdns.com/md/httpapi/README/" lego-4.9.1/providers/dns/pdns/pdns_test.go000066400000000000000000000055501434020463500205600ustar00rootroot00000000000000package pdns import ( "net/url" "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAPIURL, EnvAPIKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", EnvAPIURL: "http://example.com", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIKey: "", EnvAPIURL: "", }, expected: "pdns: some credentials information are missing: PDNS_API_KEY,PDNS_API_URL", }, { desc: "missing api key", envVars: map[string]string{ EnvAPIKey: "", EnvAPIURL: "http://example.com", }, expected: "pdns: some credentials information are missing: PDNS_API_KEY", }, { desc: "missing API URL", envVars: map[string]string{ EnvAPIKey: "123", EnvAPIURL: "", }, expected: "pdns: some credentials information are missing: PDNS_API_URL", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string host *url.URL expected string }{ { desc: "success", apiKey: "123", host: func() *url.URL { u, _ := url.Parse("http://example.com") return u }(), }, { desc: "missing credentials", expected: "pdns: API key missing", }, { desc: "missing API key", apiKey: "", host: func() *url.URL { u, _ := url.Parse("http://example.com") return u }(), expected: "pdns: API key missing", }, { desc: "missing host", apiKey: "123", expected: "pdns: API URL missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.Host = test.host p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresentAndCleanup(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/porkbun/000077500000000000000000000000001434020463500167355ustar00rootroot00000000000000lego-4.9.1/providers/dns/porkbun/porkbun.go000066400000000000000000000114471434020463500207530ustar00rootroot00000000000000// Package porkbun implements a DNS provider for solving the DNS-01 challenge using Porkbun. package porkbun import ( "context" "errors" "fmt" "net/http" "strconv" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/nrdcg/porkbun" ) // Environment variables names. const ( envNamespace = "PORKBUN_" EnvSecretAPIKey = envNamespace + "SECRET_API_KEY" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) const minTTL = 300 // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string SecretAPIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), TTL: env.GetOrDefaultInt(EnvTTL, minTTL), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *porkbun.Client recordIDs map[string]int recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for Porkbun. // Credentials must be passed in the environment variables: // PORKBUN_SECRET_API_KEY, PORKBUN_PAPI_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvSecretAPIKey, EnvAPIKey) if err != nil { return nil, fmt.Errorf("porkbun: %w", err) } config := NewDefaultConfig() config.SecretAPIKey = values[EnvSecretAPIKey] config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Porkbun. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("porkbun: the configuration of the DNS provider is nil") } if config.SecretAPIKey == "" || config.APIKey == "" { return nil, errors.New("porkbun: some credentials information are missing") } if config.TTL < minTTL { return nil, fmt.Errorf("porkbun: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } client := porkbun.New(config.SecretAPIKey, config.APIKey) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]int), }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zoneName, hostName, err := splitDomain(fqdn) if err != nil { return fmt.Errorf("porkbun: %w", err) } record := porkbun.Record{ Name: hostName, Type: "TXT", Content: value, TTL: strconv.Itoa(d.config.TTL), } ctx := context.Background() recordID, err := d.client.CreateRecord(ctx, dns01.UnFqdn(zoneName), record) if err != nil { return fmt.Errorf("porkbun: failed to create record: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = recordID d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) // gets the record's unique ID from when we created it d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("porkbun: unknown record ID for '%s' '%s'", fqdn, token) } zoneName, _, err := splitDomain(fqdn) if err != nil { return fmt.Errorf("porkbun: %w", err) } ctx := context.Background() err = d.client.DeleteRecord(ctx, dns01.UnFqdn(zoneName), recordID) if err != nil { return fmt.Errorf("porkbun: failed to delete record: %w", err) } return nil } // splitDomain splits the hostname from the authoritative zone, and returns both parts. func splitDomain(fqdn string) (string, string, error) { zone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", "", err } host := dns01.UnFqdn(strings.TrimSuffix(fqdn, zone)) return zone, host, nil } lego-4.9.1/providers/dns/porkbun/porkbun.toml000066400000000000000000000013371434020463500213160ustar00rootroot00000000000000Name = "Porkbun" Description = '''''' URL = "https://porkbun.com/" Code = "porkbun" Since = "v4.4.0" Example = ''' PORKBUN_SECRET_API_KEY=xxxxxx \ PORKBUN_API_KEY=yyyyyy \ lego --email you@example.com --dns porkbun --domains my.example.org run ''' [Configuration] [Configuration.Credentials] PORKBUN_SECRET_API_KEY = "secret API key" PORKBUN_API_KEY = "API key" [Configuration.Additional] PORKBUN_POLLING_INTERVAL = "Time between DNS propagation check" PORKBUN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" PORKBUN_TTL = "The TTL of the TXT record used for the DNS challenge" PORKBUN_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://porkbun.com/api/json/v3/documentation" lego-4.9.1/providers/dns/porkbun/porkbun_test.go000066400000000000000000000062771434020463500220170ustar00rootroot00000000000000package porkbun import ( "fmt" "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvSecretAPIKey, EnvAPIKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvSecretAPIKey: "secret", EnvAPIKey: "key", }, }, { desc: "missing secret API key", envVars: map[string]string{ EnvSecretAPIKey: "", EnvAPIKey: "key", }, expected: "porkbun: some credentials information are missing: PORKBUN_SECRET_API_KEY", }, { desc: "missing API key", envVars: map[string]string{ EnvSecretAPIKey: "secret", EnvAPIKey: "", }, expected: "porkbun: some credentials information are missing: PORKBUN_API_KEY", }, { desc: "missing all credentials", envVars: map[string]string{ EnvSecretAPIKey: "", EnvAPIKey: "", }, expected: "porkbun: some credentials information are missing: PORKBUN_SECRET_API_KEY,PORKBUN_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string secretAPIKey string apiKey string expected string }{ { desc: "success", secretAPIKey: "secret", apiKey: "key", }, { desc: "missing secret API key", apiKey: "key", expected: "porkbun: some credentials information are missing", }, { desc: "missing API key", secretAPIKey: "secret", expected: "porkbun: some credentials information are missing", }, { desc: "missing all credentials", expected: "porkbun: some credentials information are missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.SecretAPIKey = test.secretAPIKey config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestName(t *testing.T) { fmt.Println(splitDomain("_acme-challenge.example.com.")) } lego-4.9.1/providers/dns/rackspace/000077500000000000000000000000001434020463500172115ustar00rootroot00000000000000lego-4.9.1/providers/dns/rackspace/client.go000066400000000000000000000116111434020463500210160ustar00rootroot00000000000000package rackspace import ( "bytes" "encoding/json" "fmt" "io" "net/http" "github.com/go-acme/lego/v4/challenge/dns01" ) // APIKeyCredentials API credential. type APIKeyCredentials struct { Username string `json:"username"` APIKey string `json:"apiKey"` } // Auth auth credentials. type Auth struct { APIKeyCredentials `json:"RAX-KSKEY:apiKeyCredentials"` } // AuthData Auth data. type AuthData struct { Auth `json:"auth"` } // Identity Identity. type Identity struct { Access Access `json:"access"` } // Access Access. type Access struct { ServiceCatalog []ServiceCatalog `json:"serviceCatalog"` Token Token `json:"token"` } // Token Token. type Token struct { ID string `json:"id"` } // ServiceCatalog ServiceCatalog. type ServiceCatalog struct { Endpoints []Endpoint `json:"endpoints"` Name string `json:"name"` } // Endpoint Endpoint. type Endpoint struct { PublicURL string `json:"publicURL"` TenantID string `json:"tenantId"` } // ZoneSearchResponse represents the response when querying Rackspace DNS zones. type ZoneSearchResponse struct { TotalEntries int `json:"totalEntries"` HostedZones []HostedZone `json:"domains"` } // HostedZone HostedZone. type HostedZone struct { ID string `json:"id"` Name string `json:"name"` } // Records is the list of records sent/received from the DNS API. type Records struct { Record []Record `json:"records"` } // Record represents a Rackspace DNS record. type Record struct { Name string `json:"name"` Type string `json:"type"` Data string `json:"data"` TTL int `json:"ttl,omitempty"` ID string `json:"id,omitempty"` } // getHostedZoneID performs a lookup to get the DNS zone which needs // modifying for a given FQDN. func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) { authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", err } result, err := d.makeRequest(http.MethodGet, fmt.Sprintf("/domains?name=%s", dns01.UnFqdn(authZone)), nil) if err != nil { return "", err } var zoneSearchResponse ZoneSearchResponse err = json.Unmarshal(result, &zoneSearchResponse) if err != nil { return "", err } // If nothing was returned, or for whatever reason more than 1 was returned (the search uses exact match, so should not occur) if zoneSearchResponse.TotalEntries != 1 { return "", fmt.Errorf("found %d zones for %s in Rackspace for domain %s", zoneSearchResponse.TotalEntries, authZone, fqdn) } return zoneSearchResponse.HostedZones[0].ID, nil } // findTxtRecord searches a DNS zone for a TXT record with a specific name. func (d *DNSProvider) findTxtRecord(fqdn string, zoneID string) (*Record, error) { result, err := d.makeRequest(http.MethodGet, fmt.Sprintf("/domains/%s/records?type=TXT&name=%s", zoneID, dns01.UnFqdn(fqdn)), nil) if err != nil { return nil, err } var records Records err = json.Unmarshal(result, &records) if err != nil { return nil, err } switch len(records.Record) { case 1: case 0: return nil, fmt.Errorf("no TXT record found for %s", fqdn) default: return nil, fmt.Errorf("more than 1 TXT record found for %s", fqdn) } return &records.Record[0], nil } // makeRequest is a wrapper function used for making DNS API requests. func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) { url := d.cloudDNSEndpoint + uri req, err := http.NewRequest(method, url, body) if err != nil { return nil, err } req.Header.Set("X-Auth-Token", d.token) req.Header.Set("Content-Type", "application/json") resp, err := d.config.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("error querying DNS API: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { return nil, fmt.Errorf("request failed for %s %s. Response code: %d", method, url, resp.StatusCode) } var r json.RawMessage err = json.NewDecoder(resp.Body).Decode(&r) if err != nil { return nil, fmt.Errorf("JSON decode failed for %s %s. Response code: %d", method, url, resp.StatusCode) } return r, nil } func login(config *Config) (*Identity, error) { authData := AuthData{ Auth: Auth{ APIKeyCredentials: APIKeyCredentials{ Username: config.APIUser, APIKey: config.APIKey, }, }, } body, err := json.Marshal(authData) if err != nil { return nil, err } req, err := http.NewRequest(http.MethodPost, config.BaseURL, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") resp, err := config.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("error querying Identity API: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("authentication failed: response code: %d", resp.StatusCode) } var identity Identity err = json.NewDecoder(resp.Body).Decode(&identity) if err != nil { return nil, err } return &identity, nil } lego-4.9.1/providers/dns/rackspace/rackspace.go000066400000000000000000000114071434020463500214770ustar00rootroot00000000000000// Package rackspace implements a DNS provider for solving the DNS-01 challenge using rackspace DNS. package rackspace import ( "bytes" "encoding/json" "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // defaultBaseURL represents the Identity API endpoint to call. const defaultBaseURL = "https://identity.api.rackspacecloud.com/v2.0/tokens" // Environment variables names. const ( envNamespace = "RACKSPACE_" EnvUser = envNamespace + "USER" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string APIUser string APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ BaseURL: defaultBaseURL, TTL: env.GetOrDefaultInt(EnvTTL, 300), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config token string cloudDNSEndpoint string } // NewDNSProvider returns a DNSProvider instance configured for Rackspace. // Credentials must be passed in the environment variables: // RACKSPACE_USER and RACKSPACE_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvUser, EnvAPIKey) if err != nil { return nil, fmt.Errorf("rackspace: %w", err) } config := NewDefaultConfig() config.APIUser = values[EnvUser] config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Rackspace. // It authenticates against the API, also grabbing the DNS Endpoint. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("rackspace: the configuration of the DNS provider is nil") } if config.APIUser == "" || config.APIKey == "" { return nil, errors.New("rackspace: credentials missing") } identity, err := login(config) if err != nil { return nil, fmt.Errorf("rackspace: %w", err) } // Iterate through the Service Catalog to get the DNS Endpoint var dnsEndpoint string for _, service := range identity.Access.ServiceCatalog { if service.Name == "cloudDNS" { dnsEndpoint = service.Endpoints[0].PublicURL break } } if dnsEndpoint == "" { return nil, errors.New("rackspace: failed to populate DNS endpoint, check Rackspace API for changes") } return &DNSProvider{ config: config, token: identity.Access.Token.ID, cloudDNSEndpoint: dnsEndpoint, }, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zoneID, err := d.getHostedZoneID(fqdn) if err != nil { return fmt.Errorf("rackspace: %w", err) } rec := Records{ Record: []Record{{ Name: dns01.UnFqdn(fqdn), Type: "TXT", Data: value, TTL: d.config.TTL, }}, } body, err := json.Marshal(rec) if err != nil { return fmt.Errorf("rackspace: %w", err) } _, err = d.makeRequest(http.MethodPost, fmt.Sprintf("/domains/%s/records", zoneID), bytes.NewReader(body)) if err != nil { return fmt.Errorf("rackspace: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) zoneID, err := d.getHostedZoneID(fqdn) if err != nil { return fmt.Errorf("rackspace: %w", err) } record, err := d.findTxtRecord(fqdn, zoneID) if err != nil { return fmt.Errorf("rackspace: %w", err) } _, err = d.makeRequest(http.MethodDelete, fmt.Sprintf("/domains/%s/records?id=%s", zoneID, record.ID), nil) if err != nil { return fmt.Errorf("rackspace: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/rackspace/rackspace.toml000066400000000000000000000013421434020463500220420ustar00rootroot00000000000000Name = "Rackspace" Description = '''''' URL = "https://www.rackspace.com/" Code = "rackspace" Since = "v0.4.0" Example = ''' RACKSPACE_USER=xxxx \ RACKSPACE_API_KEY=yyyy \ lego --email you@example.com --dns rackspace --domains my.example.org run ''' [Configuration] [Configuration.Credentials] RACKSPACE_USER = "API user" RACKSPACE_API_KEY = "API key" [Configuration.Additional] RACKSPACE_POLLING_INTERVAL = "Time between DNS propagation check" RACKSPACE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" RACKSPACE_TTL = "The TTL of the TXT record used for the DNS challenge" RACKSPACE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developer.rackspace.com/docs/cloud-dns/v1/" lego-4.9.1/providers/dns/rackspace/rackspace_mock_test.go000066400000000000000000000040331434020463500235440ustar00rootroot00000000000000package rackspace const recordDeleteMock = ` { "status": "RUNNING", "verb": "DELETE", "jobId": "00000000-0000-0000-0000-0000000000", "callbackUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000", "requestUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/recordsid=TXT-654321" } ` const recordDetailsMock = ` { "records": [ { "name": "_acme-challenge.example.com", "id": "TXT-654321", "type": "TXT", "data": "pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM", "ttl": 300, "updated": "1970-01-01T00:00:00.000+0000", "created": "1970-01-01T00:00:00.000+0000" } ] } ` const zoneDetailsMock = ` { "domains": [ { "name": "example.com", "id": "112233", "emailAddress": "hostmaster@example.com", "updated": "1970-01-01T00:00:00.000+0000", "created": "1970-01-01T00:00:00.000+0000" } ], "totalEntries": 1 } ` const identityResponseMock = ` { "access": { "token": { "id": "testToken", "expires": "1970-01-01T00:00:00.000Z", "tenant": { "id": "123456", "name": "123456" }, "RAX-AUTH:authenticatedBy": [ "APIKEY" ] }, "serviceCatalog": [ { "type": "rax:dns", "endpoints": [ { "publicURL": "https://dns.api.rackspacecloud.com/v1.0/123456", "tenantId": "123456" } ], "name": "cloudDNS" } ], "user": { "id": "fakeUseID", "name": "testUser" } } } ` const recordResponseMock = ` { "request": "{\"records\":[{\"name\":\"_acme-challenge.example.com\",\"type\":\"TXT\",\"data\":\"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\",\"ttl\":300}]}", "status": "RUNNING", "verb": "POST", "jobId": "00000000-0000-0000-0000-0000000000", "callbackUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000", "requestUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/records" } ` lego-4.9.1/providers/dns/rackspace/rackspace_test.go000066400000000000000000000116151434020463500225370ustar00rootroot00000000000000package rackspace import ( "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvUser, EnvAPIKey). WithDomain(envDomain) func TestNewDNSProviderConfig(t *testing.T) { config := setupTest(t) provider, err := NewDNSProviderConfig(config) require.NoError(t, err) assert.NotNil(t, provider.config) assert.Equal(t, provider.token, "testToken", "The token should match") } func TestNewDNSProviderConfig_MissingCredErr(t *testing.T) { _, err := NewDNSProviderConfig(NewDefaultConfig()) assert.EqualError(t, err, "rackspace: credentials missing") } func TestDNSProvider_Present(t *testing.T) { config := setupTest(t) provider, err := NewDNSProviderConfig(config) if assert.NoError(t, err) { err = provider.Present("example.com", "token", "keyAuth") require.NoError(t, err) } } func TestDNSProvider_CleanUp(t *testing.T) { config := setupTest(t) provider, err := NewDNSProviderConfig(config) if assert.NoError(t, err) { err = provider.CleanUp("example.com", "token", "keyAuth") require.NoError(t, err) } } func TestLiveNewDNSProvider_ValidEnv(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) assert.Contains(t, provider.cloudDNSEndpoint, "https://dns.api.rackspacecloud.com/v1.0/", "The endpoint URL should contain the base") } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "112233445566==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(15 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "112233445566==") require.NoError(t, err) } func setupTest(t *testing.T) *Config { t.Helper() dnsAPI := httptest.NewServer(dnsHandler()) t.Cleanup(dnsAPI.Close) identityAPI := httptest.NewServer(identityHandler(dnsAPI.URL + "/123456")) t.Cleanup(identityAPI.Close) config := NewDefaultConfig() config.APIUser = "testUser" config.APIKey = "testKey" config.HTTPClient = identityAPI.Client() config.BaseURL = identityAPI.URL + "/" return config } func identityHandler(dnsEndpoint string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reqBody, err := io.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if string(reqBody) != `{"auth":{"RAX-KSKEY:apiKeyCredentials":{"username":"testUser","apiKey":"testKey"}}}` { w.WriteHeader(http.StatusBadRequest) return } resp := strings.Replace(identityResponseMock, "https://dns.api.rackspacecloud.com/v1.0/123456", dnsEndpoint, 1) w.WriteHeader(http.StatusOK) fmt.Fprint(w, resp) }) } func dnsHandler() *http.ServeMux { mux := http.NewServeMux() // Used by `getHostedZoneID()` finding `zoneID` "?name=example.com" mux.HandleFunc("/123456/domains", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("name") == "example.com" { w.WriteHeader(http.StatusOK) fmt.Fprint(w, zoneDetailsMock) return } w.WriteHeader(http.StatusBadRequest) }) mux.HandleFunc("/123456/domains/112233/records", func(w http.ResponseWriter, r *http.Request) { switch r.Method { // Used by `Present()` creating the TXT record case http.MethodPost: reqBody, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if string(reqBody) != `{"records":[{"name":"_acme-challenge.example.com","type":"TXT","data":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM","ttl":300}]}` { w.WriteHeader(http.StatusBadRequest) return } w.WriteHeader(http.StatusAccepted) fmt.Fprint(w, recordResponseMock) // Used by `findTxtRecord()` finding `record.ID` "?type=TXT&name=_acme-challenge.example.com" case http.MethodGet: if r.URL.Query().Get("type") == "TXT" && r.URL.Query().Get("name") == "_acme-challenge.example.com" { w.WriteHeader(http.StatusOK) fmt.Fprint(w, recordDetailsMock) return } w.WriteHeader(http.StatusBadRequest) return // Used by `CleanUp()` deleting the TXT record "?id=445566" case http.MethodDelete: if r.URL.Query().Get("id") == "TXT-654321" { w.WriteHeader(http.StatusOK) fmt.Fprint(w, recordDeleteMock) return } w.WriteHeader(http.StatusBadRequest) } }) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) fmt.Printf("Not Found for Request: (%+v)\n\n", r) }) return mux } lego-4.9.1/providers/dns/regru/000077500000000000000000000000001434020463500164015ustar00rootroot00000000000000lego-4.9.1/providers/dns/regru/internal/000077500000000000000000000000001434020463500202155ustar00rootroot00000000000000lego-4.9.1/providers/dns/regru/internal/client.go000066400000000000000000000056261434020463500220330ustar00rootroot00000000000000package internal import ( "encoding/json" "fmt" "io" "net/http" "net/url" "path" ) const defaultBaseURL = "https://api.reg.ru/api/regru2/" // Client the reg.ru client. type Client struct { username string password string BaseURL string HTTPClient *http.Client } // NewClient Creates a reg.ru client. func NewClient(username, password string) *Client { return &Client{ username: username, password: password, BaseURL: defaultBaseURL, HTTPClient: http.DefaultClient, } } // RemoveTxtRecord removes a TXT record. // https://www.reg.ru/support/help/api2#zone_remove_record func (c Client) RemoveTxtRecord(domain, subDomain, content string) error { request := RemoveRecordRequest{ Username: c.username, Password: c.password, Domains: []Domain{ {DName: domain}, }, SubDomain: subDomain, Content: content, RecordType: "TXT", OutputContentType: "plain", } resp, err := c.do(request, "zone", "remove_record") if err != nil { return err } return resp.HasError() } // AddTXTRecord adds a TXT record. // https://www.reg.ru/support/help/api2#zone_add_txt func (c Client) AddTXTRecord(domain, subDomain, content string) error { request := AddTxtRequest{ Username: c.username, Password: c.password, Domains: []Domain{ {DName: domain}, }, SubDomain: subDomain, Text: content, OutputContentType: "plain", } resp, err := c.do(request, "zone", "add_txt") if err != nil { return err } return resp.HasError() } func (c Client) do(request interface{}, fragments ...string) (*APIResponse, error) { endpoint, err := c.createEndpoint(fragments...) if err != nil { return nil, err } inputData, err := json.Marshal(request) if err != nil { return nil, err } query := endpoint.Query() query.Add("input_data", string(inputData)) query.Add("input_format", "json") endpoint.RawQuery = query.Encode() resp, err := http.Get(endpoint.String()) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode/100 != 2 { all, errB := io.ReadAll(resp.Body) if errB != nil { return nil, fmt.Errorf("API error, status code: %d", resp.StatusCode) } var apiResp APIResponse errB = json.Unmarshal(all, &apiResp) if errB != nil { return nil, fmt.Errorf("API error, status code: %d, %s", resp.StatusCode, string(all)) } return nil, fmt.Errorf("%w, status code: %d", apiResp, resp.StatusCode) } all, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var apiResp APIResponse err = json.Unmarshal(all, &apiResp) if err != nil { return nil, err } return &apiResp, nil } func (c Client) createEndpoint(fragments ...string) (*url.URL, error) { baseURL, err := url.Parse(c.BaseURL) if err != nil { return nil, err } endpoint, err := baseURL.Parse(path.Join(baseURL.Path, path.Join(fragments...))) if err != nil { return nil, err } return endpoint, nil } lego-4.9.1/providers/dns/regru/internal/client_test.go000066400000000000000000000053351434020463500230670ustar00rootroot00000000000000package internal import ( "testing" "github.com/stretchr/testify/require" ) const ( noopBaseURL = "https://api.reg.ru/api/regru2/nop" officialTestUser = "test" officialTestPassword = "test" ) func TestRemoveRecord(t *testing.T) { // TODO(ldez): remove skip when the reg.ru API will be fixed. t.Skip("there is a bug with the reg.ru API: INTERNAL_API_ERROR: Внутренняя ошибка, status code: 503") client := NewClient(officialTestUser, officialTestPassword) err := client.RemoveTxtRecord("test.ru", "_acme-challenge", "txttxttxt") require.NoError(t, err) } func TestRemoveRecord_errors(t *testing.T) { testCases := []struct { desc string domain string username string password string baseURL string expected string }{ { desc: "authentication failed", domain: "test.ru", username: "", password: "", baseURL: noopBaseURL, expected: "API error: NO_AUTH: No authorization mechanism selected", }, { desc: "domain error", domain: "", username: officialTestUser, password: officialTestPassword, baseURL: defaultBaseURL, expected: "API error: NO_DOMAIN: domain_name not given or empty", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() client := NewClient(test.username, test.username) err := client.RemoveTxtRecord(test.domain, "_acme-challenge", "txttxttxt") require.EqualError(t, err, test.expected) }) } } func TestAddTXTRecord(t *testing.T) { // TODO(ldez): remove skip when the reg.ru API will be fixed. t.Skip("there is a bug with the reg.ru API: INTERNAL_API_ERROR: Внутренняя ошибка, status code: 503") client := NewClient(officialTestUser, officialTestPassword) err := client.AddTXTRecord("test.ru", "_acme-challenge", "txttxttxt") require.NoError(t, err) } func TestAddTXTRecord_errors(t *testing.T) { testCases := []struct { desc string domain string username string password string baseURL string expected string }{ { desc: "authentication failed", domain: "test.ru", username: "", password: "", baseURL: noopBaseURL, expected: "API error: NO_AUTH: No authorization mechanism selected", }, { desc: "domain error", domain: "", username: officialTestUser, password: officialTestPassword, baseURL: defaultBaseURL, expected: "API error: NO_DOMAIN: domain_name not given or empty", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() client := NewClient(test.username, test.username) err := client.AddTXTRecord(test.domain, "_acme-challenge", "txttxttxt") require.EqualError(t, err, test.expected) }) } } lego-4.9.1/providers/dns/regru/internal/model.go000066400000000000000000000043401434020463500216450ustar00rootroot00000000000000package internal import "fmt" const successResult = "success" // APIResponse is the representation of an API response. type APIResponse struct { Result string `json:"result"` Answer *Answer `json:"answer,omitempty"` ErrorCode string `json:"error_code,omitempty"` ErrorText string `json:"error_text,omitempty"` } func (a APIResponse) Error() string { return fmt.Sprintf("API %s: %s: %s", a.Result, a.ErrorCode, a.ErrorText) } // HasError returns an error is the response contains an error. func (a APIResponse) HasError() error { if a.Result != successResult { return a } if a.Answer != nil { for _, domResp := range a.Answer.Domains { if domResp.Result != successResult { return domResp } } } return nil } // Answer is the representation of an API response answer. type Answer struct { Domains []DomainResponse `json:"domains,omitempty"` } // DomainResponse is the representation of an API response answer domain. type DomainResponse struct { Result string `json:"result"` DName string `json:"dname"` ErrorCode string `json:"error_code,omitempty"` ErrorText string `json:"error_text,omitempty"` } func (d DomainResponse) Error() string { return fmt.Sprintf("API %s: %s: %s", d.Result, d.ErrorCode, d.ErrorText) } // AddTxtRequest is the representation of the payload of a request to add a TXT record. type AddTxtRequest struct { Username string `json:"username"` Password string `json:"password"` Domains []Domain `json:"domains,omitempty"` SubDomain string `json:"subdomain,omitempty"` Text string `json:"text,omitempty"` OutputContentType string `json:"output_content_type,omitempty"` } // RemoveRecordRequest is the representation of the payload of a request to remove a record. type RemoveRecordRequest struct { Username string `json:"username"` Password string `json:"password"` Domains []Domain `json:"domains,omitempty"` SubDomain string `json:"subdomain,omitempty"` Content string `json:"content,omitempty"` RecordType string `json:"record_type,omitempty"` OutputContentType string `json:"output_content_type,omitempty"` } // Domain is the representation of a Domain. type Domain struct { DName string `json:"dname"` } lego-4.9.1/providers/dns/regru/regru.go000066400000000000000000000102101434020463500200460ustar00rootroot00000000000000// Package regru implements a DNS provider for solving the DNS-01 challenge using reg.ru DNS. package regru import ( "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/regru/internal" ) // Environment variables names. const ( envNamespace = "REGRU_" EnvUsername = envNamespace + "USERNAME" EnvPassword = envNamespace + "PASSWORD" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Username string Password string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 300), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for reg.ru. // Credentials must be passed in the environment variables: // REGRU_USERNAME and REGRU_PASSWORD. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvUsername, EnvPassword) if err != nil { return nil, fmt.Errorf("regru: %w", err) } config := NewDefaultConfig() config.Username = values[EnvUsername] config.Password = values[EnvPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for reg.ru. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("regru: the configuration of the DNS provider is nil") } if config.Username == "" || config.Password == "" { return nil, errors.New("regru: incomplete credentials, missing username and/or password") } client := internal.NewClient(config.Username, config.Password) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("regru: could not find zone for domain %q and fqdn %q : %w", domain, fqdn, err) } subDomain := dns01.UnFqdn(strings.TrimSuffix(fqdn, authZone)) err = d.client.AddTXTRecord(dns01.UnFqdn(authZone), subDomain, value) if err != nil { return fmt.Errorf("regru: failed to create TXT records [domain: %s, sub domain: %s]: %w", dns01.UnFqdn(authZone), subDomain, err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("regru: could not find zone for domain %q and fqdn %q : %w", domain, fqdn, err) } subDomain := dns01.UnFqdn(strings.TrimSuffix(fqdn, authZone)) err = d.client.RemoveTxtRecord(dns01.UnFqdn(authZone), subDomain, value) if err != nil { return fmt.Errorf("regru: failed to remove TXT records [domain: %s, sub domain: %s]: %w", dns01.UnFqdn(authZone), subDomain, err) } return nil } lego-4.9.1/providers/dns/regru/regru.toml000066400000000000000000000012711434020463500204230ustar00rootroot00000000000000Name = "reg.ru" Description = '''''' URL = "https://www.reg.ru/" Code = "regru" Since = "v3.5.0" Example = ''' REGRU_USERNAME=xxxxxx \ REGRU_PASSWORD=yyyyyy \ lego --email you@example.com --dns regru --domains my.example.org run ''' [Configuration] [Configuration.Credentials] REGRU_USERNAME = "API username" REGRU_PASSWORD = "API password" [Configuration.Additional] REGRU_POLLING_INTERVAL = "Time between DNS propagation check" REGRU_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" REGRU_TTL = "The TTL of the TXT record used for the DNS challenge" REGRU_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.reg.ru/support/help/api2" lego-4.9.1/providers/dns/regru/regru_test.go000066400000000000000000000062031434020463500211140ustar00rootroot00000000000000package regru import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvUsername, EnvPassword). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvUsername: "123", EnvPassword: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvUsername: "", EnvPassword: "", }, expected: "regru: some credentials information are missing: REGRU_USERNAME,REGRU_PASSWORD", }, { desc: "missing api key", envVars: map[string]string{ EnvUsername: "", EnvPassword: "api_password", }, expected: "regru: some credentials information are missing: REGRU_USERNAME", }, { desc: "missing secret key", envVars: map[string]string{ EnvUsername: "api_username", EnvPassword: "", }, expected: "regru: some credentials information are missing: REGRU_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string username string password string expected string }{ { desc: "success", username: "api_username", password: "api_password", }, { desc: "missing credentials", expected: "regru: incomplete credentials, missing username and/or password", }, { desc: "missing username", username: "", password: "api_password", expected: "regru: incomplete credentials, missing username and/or password", }, { desc: "missing password", username: "api_username", password: "", expected: "regru: incomplete credentials, missing username and/or password", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Username = test.username config.Password = test.password p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/rfc2136/000077500000000000000000000000001434020463500163435ustar00rootroot00000000000000lego-4.9.1/providers/dns/rfc2136/rfc2136.go000066400000000000000000000146751434020463500177750ustar00rootroot00000000000000// Package rfc2136 implements a DNS provider for solving the DNS-01 challenge using the rfc2136 dynamic update. package rfc2136 import ( "errors" "fmt" "net" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/miekg/dns" ) // Environment variables names. const ( envNamespace = "RFC2136_" EnvTSIGKey = envNamespace + "TSIG_KEY" EnvTSIGSecret = envNamespace + "TSIG_SECRET" EnvTSIGAlgorithm = envNamespace + "TSIG_ALGORITHM" EnvNameserver = envNamespace + "NAMESERVER" EnvDNSTimeout = envNamespace + "DNS_TIMEOUT" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Nameserver string TSIGAlgorithm string TSIGKey string TSIGSecret string PropagationTimeout time.Duration PollingInterval time.Duration TTL int SequenceInterval time.Duration DNSTimeout time.Duration } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TSIGAlgorithm: env.GetOrDefaultString(EnvTSIGAlgorithm, dns.HmacSHA1), TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, env.GetOrDefaultSecond("RFC2136_TIMEOUT", 60*time.Second)), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), DNSTimeout: env.GetOrDefaultSecond(EnvDNSTimeout, 10*time.Second), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config } // NewDNSProvider returns a DNSProvider instance configured for rfc2136 // dynamic update. Configured with environment variables: // RFC2136_NAMESERVER: Network address in the form "host" or "host:port". // RFC2136_TSIG_ALGORITHM: Defaults to hmac-md5.sig-alg.reg.int. (HMAC-MD5). // See https://github.com/miekg/dns/blob/master/tsig.go for supported values. // RFC2136_TSIG_KEY: Name of the secret key as defined in DNS server configuration. // RFC2136_TSIG_SECRET: Secret key payload. // RFC2136_PROPAGATION_TIMEOUT: DNS propagation timeout in time.ParseDuration format. (60s) // To disable TSIG authentication, leave the RFC2136_TSIG* variables unset. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvNameserver) if err != nil { return nil, fmt.Errorf("rfc2136: %w", err) } config := NewDefaultConfig() config.Nameserver = values[EnvNameserver] config.TSIGKey = env.GetOrFile(EnvTSIGKey) config.TSIGSecret = env.GetOrFile(EnvTSIGSecret) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for rfc2136. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("rfc2136: the configuration of the DNS provider is nil") } if config.Nameserver == "" { return nil, errors.New("rfc2136: nameserver missing") } if config.TSIGAlgorithm == "" { config.TSIGAlgorithm = dns.HmacSHA1 } // Append the default DNS port if none is specified. if _, _, err := net.SplitHostPort(config.Nameserver); err != nil { if strings.Contains(err.Error(), "missing port") { config.Nameserver = net.JoinHostPort(config.Nameserver, "53") } else { return nil, fmt.Errorf("rfc2136: %w", err) } } if config.TSIGKey == "" || config.TSIGSecret == "" { config.TSIGKey = "" config.TSIGSecret = "" } return &DNSProvider{config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Sequential All DNS challenges for this provider will be resolved sequentially. // Returns the interval between each iteration. func (d *DNSProvider) Sequential() time.Duration { return d.config.SequenceInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) err := d.changeRecord("INSERT", fqdn, value, d.config.TTL) if err != nil { return fmt.Errorf("rfc2136: failed to insert: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) err := d.changeRecord("REMOVE", fqdn, value, d.config.TTL) if err != nil { return fmt.Errorf("rfc2136: failed to remove: %w", err) } return nil } func (d *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { // Find the zone for the given fqdn zone, err := dns01.FindZoneByFqdnCustom(fqdn, []string{d.config.Nameserver}) if err != nil { return err } // Create RR rr := new(dns.TXT) rr.Hdr = dns.RR_Header{Name: fqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(ttl)} rr.Txt = []string{value} rrs := []dns.RR{rr} // Create dynamic update packet m := new(dns.Msg) m.SetUpdate(zone) switch action { case "INSERT": // Always remove old challenge left over from who knows what. m.RemoveRRset(rrs) m.Insert(rrs) case "REMOVE": m.Remove(rrs) default: return fmt.Errorf("unexpected action: %s", action) } // Setup client c := &dns.Client{Timeout: d.config.DNSTimeout} c.SingleInflight = true // TSIG authentication / msg signing if len(d.config.TSIGKey) > 0 && len(d.config.TSIGSecret) > 0 { key := strings.ToLower(dns.Fqdn(d.config.TSIGKey)) alg := dns.Fqdn(d.config.TSIGAlgorithm) m.SetTsig(key, alg, 300, time.Now().Unix()) // secret(s) for Tsig map[], // zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2) c.TsigSecret = map[string]string{key: d.config.TSIGSecret} } // Send the query reply, _, err := c.Exchange(m, d.config.Nameserver) if err != nil { return fmt.Errorf("DNS update failed: %w", err) } if reply != nil && reply.Rcode != dns.RcodeSuccess { return fmt.Errorf("DNS update failed: server replied: %s", dns.RcodeToString[reply.Rcode]) } return nil } lego-4.9.1/providers/dns/rfc2136/rfc2136.toml000066400000000000000000000034021434020463500203250ustar00rootroot00000000000000Name = "RFC2136" Description = '''''' URL = "https://www.rfc-editor.org/rfc/rfc2136.html" Code = "rfc2136" Since = "v0.3.0" Example = ''' RFC2136_NAMESERVER=127.0.0.1 \ RFC2136_TSIG_KEY=lego \ RFC2136_TSIG_ALGORITHM=hmac-sha256. \ RFC2136_TSIG_SECRET=YWJjZGVmZGdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNDU= \ lego --email you@example.com --dns rfc2136 --domains my.example.org run ## --- keyname=lego; keyfile=lego.key; tsig-keygen $keyname > $keyfile RFC2136_NAMESERVER=127.0.0.1 \ RFC2136_TSIG_KEY="$keyname" \ RFC2136_TSIG_ALGORITHM="$( awk -F'[ ";]' '/algorithm/ { print $2 }' $keyfile )." \ RFC2136_TSIG_SECRET="$( awk -F'[ ";]' '/secret/ { print $3 }' $keyfile )" \ lego --email you@example.com --dns rfc2136 --domains my.example.org run ''' [Configuration] [Configuration.Credentials] RFC2136_TSIG_KEY = "Name of the secret key as defined in DNS server configuration. To disable TSIG authentication, leave the `RFC2136_TSIG*` variables unset." RFC2136_TSIG_SECRET = "Secret key payload. To disable TSIG authentication, leave the` RFC2136_TSIG*` variables unset." RFC2136_TSIG_ALGORITHM = "TSIG algorithm. See [miekg/dns#tsig.go](https://github.com/miekg/dns/blob/master/tsig.go) for supported values. To disable TSIG authentication, leave the `RFC2136_TSIG*` variables unset." RFC2136_NAMESERVER = 'Network address in the form "host" or "host:port"' [Configuration.Additional] RFC2136_POLLING_INTERVAL = "Time between DNS propagation check" RFC2136_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" RFC2136_TTL = "The TTL of the TXT record used for the DNS challenge" RFC2136_DNS_TIMEOUT = "API request timeout" RFC2136_SEQUENCE_INTERVAL = "Time between sequential requests" [Links] API = "https://www.rfc-editor.org/rfc/rfc2136.html" lego-4.9.1/providers/dns/rfc2136/rfc2136_test.go000066400000000000000000000153601434020463500210240ustar00rootroot00000000000000package rfc2136 import ( "bytes" "fmt" "net" "strings" "sync" "testing" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/miekg/dns" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( fakeDomain = "123456789.www.example.com" fakeKeyAuth = "123d==" fakeValue = "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo" fakeFqdn = "_acme-challenge.123456789.www.example.com." fakeZone = "example.com." fakeTTL = 120 fakeTsigKey = "example.com." fakeTsigSecret = "IwBTJx9wrDp4Y1RyC3H0gA==" ) func TestCanaryLocalTestServer(t *testing.T) { dns01.ClearFqdnCache() dns.HandleFunc("example.com.", serverHandlerHello) defer dns.HandleRemove("example.com.") server, addr, err := runLocalDNSTestServer(false) require.NoError(t, err, "Failed to start test server") defer func() { _ = server.Shutdown() }() c := new(dns.Client) m := new(dns.Msg) m.SetQuestion("example.com.", dns.TypeTXT) r, _, err := c.Exchange(m, addr) require.NoError(t, err, "Failed to communicate with test server") assert.Len(t, r.Extra, 1, "Failed to communicate with test server") txt := r.Extra[0].(*dns.TXT).Txt[0] assert.Equal(t, "Hello world", txt) } func TestServerSuccess(t *testing.T) { dns01.ClearFqdnCache() dns.HandleFunc(fakeZone, serverHandlerReturnSuccess) defer dns.HandleRemove(fakeZone) server, addr, err := runLocalDNSTestServer(false) require.NoError(t, err, "Failed to start test server") defer func() { _ = server.Shutdown() }() config := NewDefaultConfig() config.Nameserver = addr provider, err := NewDNSProviderConfig(config) require.NoError(t, err) err = provider.Present(fakeDomain, "", fakeKeyAuth) require.NoError(t, err) } func TestServerError(t *testing.T) { dns01.ClearFqdnCache() dns.HandleFunc(fakeZone, serverHandlerReturnErr) defer dns.HandleRemove(fakeZone) server, addr, err := runLocalDNSTestServer(false) require.NoError(t, err, "Failed to start test server") defer func() { _ = server.Shutdown() }() config := NewDefaultConfig() config.Nameserver = addr provider, err := NewDNSProviderConfig(config) require.NoError(t, err) err = provider.Present(fakeDomain, "", fakeKeyAuth) require.Error(t, err) if !strings.Contains(err.Error(), "NOTZONE") { t.Errorf("Expected Present() to return an error with the 'NOTZONE' rcode string but it did not: %v", err) } } func TestTsigClient(t *testing.T) { dns01.ClearFqdnCache() dns.HandleFunc(fakeZone, serverHandlerReturnSuccess) defer dns.HandleRemove(fakeZone) server, addr, err := runLocalDNSTestServer(true) require.NoError(t, err, "Failed to start test server") defer func() { _ = server.Shutdown() }() config := NewDefaultConfig() config.Nameserver = addr config.TSIGKey = fakeTsigKey config.TSIGSecret = fakeTsigSecret provider, err := NewDNSProviderConfig(config) require.NoError(t, err) err = provider.Present(fakeDomain, "", fakeKeyAuth) require.NoError(t, err) } func TestValidUpdatePacket(t *testing.T) { reqChan := make(chan *dns.Msg, 10) dns01.ClearFqdnCache() dns.HandleFunc(fakeZone, serverHandlerPassBackRequest(reqChan)) defer dns.HandleRemove(fakeZone) server, addr, err := runLocalDNSTestServer(false) require.NoError(t, err, "Failed to start test server") defer func() { _ = server.Shutdown() }() txtRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN TXT %s", fakeFqdn, fakeTTL, fakeValue)) rrs := []dns.RR{txtRR} m := new(dns.Msg) m.SetUpdate(fakeZone) m.RemoveRRset(rrs) m.Insert(rrs) expectStr := m.String() expect, err := m.Pack() require.NoError(t, err, "error packing") config := NewDefaultConfig() config.Nameserver = addr provider, err := NewDNSProviderConfig(config) require.NoError(t, err) err = provider.Present(fakeDomain, "", "1234d==") require.NoError(t, err) rcvMsg := <-reqChan rcvMsg.Id = m.Id actual, err := rcvMsg.Pack() require.NoError(t, err, "error packing") if !bytes.Equal(actual, expect) { tmp := new(dns.Msg) if err := tmp.Unpack(actual); err != nil { t.Fatalf("Error unpacking actual msg: %v", err) } t.Errorf("Expected msg:\n%s", expectStr) t.Errorf("Actual msg:\n%v", tmp) } } func runLocalDNSTestServer(tsig bool) (*dns.Server, string, error) { pc, err := net.ListenPacket("udp", "127.0.0.1:0") if err != nil { return nil, "", err } server := &dns.Server{ PacketConn: pc, ReadTimeout: time.Hour, WriteTimeout: time.Hour, MsgAcceptFunc: func(dh dns.Header) dns.MsgAcceptAction { // bypass defaultMsgAcceptFunc to allow dynamic update (https://github.com/miekg/dns/pull/830) return dns.MsgAccept }, } if tsig { server.TsigSecret = map[string]string{fakeTsigKey: fakeTsigSecret} } waitLock := sync.Mutex{} waitLock.Lock() server.NotifyStartedFunc = waitLock.Unlock go func() { _ = server.ActivateAndServe() pc.Close() }() waitLock.Lock() return server, pc.LocalAddr().String(), nil } func serverHandlerHello(w dns.ResponseWriter, req *dns.Msg) { m := new(dns.Msg) m.SetReply(req) m.Extra = make([]dns.RR, 1) m.Extra[0] = &dns.TXT{ Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 0}, Txt: []string{"Hello world"}, } _ = w.WriteMsg(m) } func serverHandlerReturnSuccess(w dns.ResponseWriter, req *dns.Msg) { m := new(dns.Msg) m.SetReply(req) if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET { // Return SOA to appease findZoneByFqdn() soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", fakeZone, fakeTTL, fakeZone, fakeZone)) m.Answer = []dns.RR{soaRR} } if t := req.IsTsig(); t != nil { if w.TsigStatus() == nil { // Validated m.SetTsig(fakeZone, dns.HmacSHA1, 300, time.Now().Unix()) } } _ = w.WriteMsg(m) } func serverHandlerReturnErr(w dns.ResponseWriter, req *dns.Msg) { m := new(dns.Msg) m.SetRcode(req, dns.RcodeNotZone) _ = w.WriteMsg(m) } func serverHandlerPassBackRequest(reqChan chan *dns.Msg) func(w dns.ResponseWriter, req *dns.Msg) { return func(w dns.ResponseWriter, req *dns.Msg) { m := new(dns.Msg) m.SetReply(req) if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET { // Return SOA to appease findZoneByFqdn() soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", fakeZone, fakeTTL, fakeZone, fakeZone)) m.Answer = []dns.RR{soaRR} } if t := req.IsTsig(); t != nil { if w.TsigStatus() == nil { // Validated m.SetTsig(fakeZone, dns.HmacSHA1, 300, time.Now().Unix()) } } _ = w.WriteMsg(m) if req.Opcode != dns.OpcodeQuery || req.Question[0].Qtype != dns.TypeSOA || req.Question[0].Qclass != dns.ClassINET { // Only talk back when it is not the SOA RR. reqChan <- req } } } lego-4.9.1/providers/dns/rimuhosting/000077500000000000000000000000001434020463500176255ustar00rootroot00000000000000lego-4.9.1/providers/dns/rimuhosting/rimuhosting.go000066400000000000000000000076011434020463500225300ustar00rootroot00000000000000// Package rimuhosting implements a DNS provider for solving the DNS-01 challenge using RimuHosting DNS. package rimuhosting import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/internal/rimuhosting" ) // Environment variables names. const ( envNamespace = "RIMUHOSTING_" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 3600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *rimuhosting.Client } // NewDNSProvider returns a DNSProvider instance configured for RimuHosting. // Credentials must be passed in the environment variables. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("rimuhosting: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for RimuHosting. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("rimuhosting: the configuration of the DNS provider is nil") } if config.APIKey == "" { return nil, errors.New("rimuhosting: incomplete credentials, missing API key") } client := rimuhosting.NewClient(config.APIKey) client.BaseURL = rimuhosting.DefaultRimuHostingBaseURL if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) records, err := d.client.FindTXTRecords(dns01.UnFqdn(fqdn)) if err != nil { return fmt.Errorf("rimuhosting: failed to find record(s) for %s: %w", domain, err) } actions := []rimuhosting.ActionParameter{ rimuhosting.AddRecord(dns01.UnFqdn(fqdn), value, d.config.TTL), } for _, record := range records { actions = append(actions, rimuhosting.AddRecord(record.Name, record.Content, d.config.TTL)) } _, err = d.client.DoActions(actions...) if err != nil { return fmt.Errorf("rimuhosting: failed to add record(s) for %s: %w", domain, err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) action := rimuhosting.DeleteRecord(dns01.UnFqdn(fqdn), value) _, err := d.client.DoActions(action) if err != nil { return fmt.Errorf("rimuhosting: failed to delete record for %s: %w", domain, err) } return nil } lego-4.9.1/providers/dns/rimuhosting/rimuhosting.toml000066400000000000000000000013261434020463500230740ustar00rootroot00000000000000Name = "RimuHosting" Description = '''''' URL = "https://rimuhosting.com" Code = "rimuhosting" Since = "v0.3.5" Example = ''' RIMUHOSTING_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ lego --email you@example.com --dns rimuhosting --domains my.example.org run ''' [Configuration] [Configuration.Credentials] RIMUHOSTING_API_KEY = "User API key" [Configuration.Additional] RIMUHOSTING_POLLING_INTERVAL = "Time between DNS propagation check" RIMUHOSTING_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" RIMUHOSTING_TTL = "The TTL of the TXT record used for the DNS challenge" RIMUHOSTING_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://rimuhosting.com/dns/dyndns.jsp" lego-4.9.1/providers/dns/rimuhosting/rimuhosting_test.go000066400000000000000000000045031434020463500235650ustar00rootroot00000000000000package rimuhosting import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", }, }, { desc: "missing api key", envVars: map[string]string{ EnvAPIKey: "", }, expected: "rimuhosting: some credentials information are missing: RIMUHOSTING_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string expected string apiKey string secretKey string }{ { desc: "success", apiKey: "api_key", secretKey: "api_secret", }, { desc: "missing api key", apiKey: "", secretKey: "api_secret", expected: "rimuhosting: incomplete credentials, missing API key", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/route53/000077500000000000000000000000001434020463500165635ustar00rootroot00000000000000lego-4.9.1/providers/dns/route53/fixtures_test.go000066400000000000000000000026401434020463500220240ustar00rootroot00000000000000package route53 const ChangeResourceRecordSetsResponse = ` /change/123456 PENDING 2016-02-10T01:36:41.958Z ` const ListHostedZonesByNameResponse = ` /hostedzone/ABCDEFG example.com. D2224C5B-684A-DB4A-BB9A-E09E3BAFEA7A Test comment false 10 true example2.com ZLT12321321124 1 ` const GetChangeResponse = ` 123456 INSYNC 2016-02-10T01:36:41.958Z ` lego-4.9.1/providers/dns/route53/mock_test.go000066400000000000000000000021021434020463500210750ustar00rootroot00000000000000package route53 import ( "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/require" ) // MockResponse represents a predefined response used by a mock server. type MockResponse struct { StatusCode int Body string } // MockResponseMap maps request paths to responses. type MockResponseMap map[string]MockResponse func setupTest(t *testing.T, responses MockResponseMap) string { t.Helper() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path resp, ok := responses[path] if !ok { resp, ok = responses[r.RequestURI] if !ok { msg := fmt.Sprintf("Requested path not found in response map: %s", path) require.FailNow(t, msg) } } w.Header().Set("Content-Type", "application/xml") w.WriteHeader(resp.StatusCode) _, err := w.Write([]byte(resp.Body)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) server := httptest.NewServer(handler) t.Cleanup(server.Close) time.Sleep(100 * time.Millisecond) return server.URL } lego-4.9.1/providers/dns/route53/route53.go000066400000000000000000000223401434020463500204210ustar00rootroot00000000000000// Package route53 implements a DNS provider for solving the DNS-01 challenge using AWS Route 53 DNS. package route53 import ( "errors" "fmt" "math/rand" "strings" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/client" "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/route53" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/platform/wait" ) // Environment variables names. const ( envNamespace = "AWS_" EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID" EnvSecretAccessKey = envNamespace + "SECRET_ACCESS_KEY" EnvRegion = envNamespace + "REGION" EnvHostedZoneID = envNamespace + "HOSTED_ZONE_ID" EnvMaxRetries = envNamespace + "MAX_RETRIES" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvAssumeRoleArn = envNamespace + "ASSUME_ROLE_ARN" ) // Config is used to configure the creation of the DNSProvider. type Config struct { HostedZoneID string MaxRetries int AssumeRoleArn string TTL int PropagationTimeout time.Duration PollingInterval time.Duration Client *route53.Route53 } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ HostedZoneID: env.GetOrFile(EnvHostedZoneID), MaxRetries: env.GetOrDefaultInt(EnvMaxRetries, 5), AssumeRoleArn: env.GetOrDefaultString(EnvAssumeRoleArn, ""), TTL: env.GetOrDefaultInt(EnvTTL, 10), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *route53.Route53 config *Config } // customRetryer implements the client.Retryer interface by composing the DefaultRetryer. // It controls the logic for retrying recoverable request errors (e.g. when rate limits are exceeded). type customRetryer struct { client.DefaultRetryer } // RetryRules overwrites the DefaultRetryer's method. // It uses a basic exponential backoff algorithm: // that returns an initial delay of ~400ms with an upper limit of ~30 seconds, // which should prevent causing a high number of consecutive throttling errors. // For reference: Route 53 enforces an account-wide(!) 5req/s query limit. func (d customRetryer) RetryRules(r *request.Request) time.Duration { retryCount := r.RetryCount if retryCount > 7 { retryCount = 7 } delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200) return time.Duration(delay) * time.Millisecond } // NewDNSProvider returns a DNSProvider instance configured for the AWS Route 53 service. // // AWS Credentials are automatically detected in the following locations and prioritized in the following order: // 1. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, // AWS_REGION, [AWS_SESSION_TOKEN] // 2. Shared credentials file (defaults to ~/.aws/credentials) // 3. Amazon EC2 IAM role // // If AWS_HOSTED_ZONE_ID is not set, Lego tries to determine the correct public hosted zone via the FQDN. // // See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(NewDefaultConfig()) } // NewDNSProviderConfig takes a given config ans returns a custom configured DNSProvider instance. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("route53: the configuration of the Route53 DNS provider is nil") } if config.Client != nil { return &DNSProvider{client: config.Client, config: config}, nil } sess, err := createSession(config) if err != nil { return nil, err } return &DNSProvider{ client: route53.New(sess), config: config, }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) hostedZoneID, err := d.getHostedZoneID(fqdn) if err != nil { return fmt.Errorf("route53: failed to determine hosted zone ID: %w", err) } records, err := d.getExistingRecordSets(hostedZoneID, fqdn) if err != nil { return fmt.Errorf("route53: %w", err) } realValue := `"` + value + `"` var found bool for _, record := range records { if aws.StringValue(record.Value) == realValue { found = true } } if !found { records = append(records, &route53.ResourceRecord{Value: aws.String(realValue)}) } recordSet := &route53.ResourceRecordSet{ Name: aws.String(fqdn), Type: aws.String("TXT"), TTL: aws.Int64(int64(d.config.TTL)), ResourceRecords: records, } err = d.changeRecord(route53.ChangeActionUpsert, hostedZoneID, recordSet) if err != nil { return fmt.Errorf("route53: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) hostedZoneID, err := d.getHostedZoneID(fqdn) if err != nil { return fmt.Errorf("failed to determine Route 53 hosted zone ID: %w", err) } records, err := d.getExistingRecordSets(hostedZoneID, fqdn) if err != nil { return fmt.Errorf("route53: %w", err) } if len(records) == 0 { return nil } recordSet := &route53.ResourceRecordSet{ Name: aws.String(fqdn), Type: aws.String("TXT"), TTL: aws.Int64(int64(d.config.TTL)), ResourceRecords: records, } err = d.changeRecord(route53.ChangeActionDelete, hostedZoneID, recordSet) if err != nil { return fmt.Errorf("route53: %w", err) } return nil } func (d *DNSProvider) changeRecord(action, hostedZoneID string, recordSet *route53.ResourceRecordSet) error { recordSetInput := &route53.ChangeResourceRecordSetsInput{ HostedZoneId: aws.String(hostedZoneID), ChangeBatch: &route53.ChangeBatch{ Comment: aws.String("Managed by Lego"), Changes: []*route53.Change{{ Action: aws.String(action), ResourceRecordSet: recordSet, }}, }, } resp, err := d.client.ChangeResourceRecordSets(recordSetInput) if err != nil { return fmt.Errorf("failed to change record set: %w", err) } changeID := resp.ChangeInfo.Id return wait.For("route53", d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) { reqParams := &route53.GetChangeInput{Id: changeID} resp, err := d.client.GetChange(reqParams) if err != nil { return false, fmt.Errorf("failed to query change status: %w", err) } if aws.StringValue(resp.ChangeInfo.Status) == route53.ChangeStatusInsync { return true, nil } return false, fmt.Errorf("unable to retrieve change: ID=%s", aws.StringValue(changeID)) }) } func (d *DNSProvider) getExistingRecordSets(hostedZoneID, fqdn string) ([]*route53.ResourceRecord, error) { listInput := &route53.ListResourceRecordSetsInput{ HostedZoneId: aws.String(hostedZoneID), StartRecordName: aws.String(fqdn), StartRecordType: aws.String("TXT"), } recordSetsOutput, err := d.client.ListResourceRecordSets(listInput) if err != nil { return nil, err } if recordSetsOutput == nil { return nil, nil } var records []*route53.ResourceRecord for _, recordSet := range recordSetsOutput.ResourceRecordSets { if aws.StringValue(recordSet.Name) == fqdn { records = append(records, recordSet.ResourceRecords...) } } return records, nil } func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) { if d.config.HostedZoneID != "" { return d.config.HostedZoneID, nil } authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", err } // .DNSName should not have a trailing dot reqParams := &route53.ListHostedZonesByNameInput{ DNSName: aws.String(dns01.UnFqdn(authZone)), } resp, err := d.client.ListHostedZonesByName(reqParams) if err != nil { return "", err } var hostedZoneID string for _, hostedZone := range resp.HostedZones { // .Name has a trailing dot if !aws.BoolValue(hostedZone.Config.PrivateZone) && aws.StringValue(hostedZone.Name) == authZone { hostedZoneID = aws.StringValue(hostedZone.Id) break } } if hostedZoneID == "" { return "", fmt.Errorf("zone %s not found for domain %s", authZone, fqdn) } hostedZoneID = strings.TrimPrefix(hostedZoneID, "/hostedzone/") return hostedZoneID, nil } func createSession(config *Config) (*session.Session, error) { retry := customRetryer{} retry.NumMaxRetries = config.MaxRetries sessionCfg := request.WithRetryer(aws.NewConfig(), retry) sess, err := session.NewSessionWithOptions(session.Options{Config: *sessionCfg}) if err != nil { return nil, err } if config.AssumeRoleArn == "" { return sess, nil } return session.NewSession(&aws.Config{ Region: sess.Config.Region, Credentials: stscreds.NewCredentials(sess, config.AssumeRoleArn), }) } lego-4.9.1/providers/dns/route53/route53.toml000066400000000000000000000115761434020463500210000ustar00rootroot00000000000000Name = "Amazon Route 53" Description = '''''' URL = "https://aws.amazon.com/route53/" Code = "route53" Since = "v0.3.0" Example = '''''' Additional = ''' ## Description AWS Credentials are automatically detected in the following locations and prioritized in the following order: 1. Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, [`AWS_SESSION_TOKEN`] 2. Shared credentials file (defaults to `~/.aws/credentials`, profiles can be specified using `AWS_PROFILE`) 3. Amazon EC2 IAM role The AWS Region is automatically detected in the following locations and prioritized in the following order: 1. Environment variables: `AWS_REGION` 2. Shared configuration file if `AWS_SDK_LOAD_CONFIG` is set (defaults to `~/.aws/config`, profiles can be specified using `AWS_PROFILE`) If `AWS_HOSTED_ZONE_ID` is not set, Lego tries to determine the correct public hosted zone via the FQDN. See also: - [sessions](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/sessions.html) - [Setting AWS Credentials](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials) - [Setting AWS Region](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-the-region) ## IAM Policy Examples ### Broad privileges for testing purposes The 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. A word of caution: These permissions grant write access to any DNS record in any hosted zone, so it is recommended to narrow them down as much as possible if you are using this policy in production. ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "route53:GetChange", "route53:ChangeResourceRecordSets", "route53:ListResourceRecordSets" ], "Resource": [ "arn:aws:route53:::hostedzone/*", "arn:aws:route53:::change/*" ] }, { "Effect": "Allow", "Action": "route53:ListHostedZonesByName", "Resource": "*" } ] } ``` ### Least privilege policy for production purposes The following AWS IAM policy document describes least privilege permissions required for lego to complete the DNS challenge. Write access is limited to a specified hosted zone's DNS TXT records with a key of `_acme-challenge.example.com`. Replace `Z11111112222222333333` with your hosted zone ID and `example.com` with your domain name to use this policy. ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "route53:GetChange", "Resource": "arn:aws:route53:::change/*" }, { "Effect": "Allow", "Action": "route53:ListHostedZonesByName", "Resource": "*" }, { "Effect": "Allow", "Action": [ "route53:ListResourceRecordSets" ], "Resource": [ "arn:aws:route53:::hostedzone/Z11111112222222333333" ] }, { "Effect": "Allow", "Action": [ "route53:ChangeResourceRecordSets" ], "Resource": [ "arn:aws:route53:::hostedzone/Z11111112222222333333" ], "Condition": { "ForAllValues:StringEquals": { "route53:ChangeResourceRecordSetsNormalizedRecordNames": [ "_acme-challenge.example.com" ], "route53:ChangeResourceRecordSetsRecordTypes": [ "TXT" ] } } } ] } ``` ''' [Configuration] [Configuration.Credentials] 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)" 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)" AWS_REGION = "Managed by the AWS client (`AWS_REGION_FILE` is not supported)" AWS_HOSTED_ZONE_ID = "Override the hosted zone ID." AWS_PROFILE = "Managed by the AWS client (`AWS_PROFILE_FILE` is not supported)" 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)" AWS_ASSUME_ROLE_ARN = "Managed by the AWS Role ARN (`AWS_ASSUME_ROLE_ARN` is not supported)" [Configuration.Additional] AWS_SHARED_CREDENTIALS_FILE = "Managed by the AWS client. Shared credentials file." AWS_MAX_RETRIES = "The number of maximum returns the service will use to make an individual API request" AWS_POLLING_INTERVAL = "Time between DNS propagation check" AWS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" AWS_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://docs.aws.amazon.com/Route53/latest/APIReference/API_Operations_Amazon_Route_53.html" GoClient = "https://github.com/aws/aws-sdk-go/aws" lego-4.9.1/providers/dns/route53/route53_integration_test.go000066400000000000000000000024271434020463500240670ustar00rootroot00000000000000package route53 import ( "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/route53" "github.com/stretchr/testify/require" ) func TestLiveTTL(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) domain := envTest.GetDomain() err = provider.Present(domain, "foo", "bar") require.NoError(t, err) // we need a separate R53 client here as the one in the DNS provider is unexported. fqdn := "_acme-challenge." + domain + "." sess, err := session.NewSession() require.NoError(t, err) svc := route53.New(sess) defer func() { errC := provider.CleanUp(domain, "foo", "bar") if errC != nil { t.Log(errC) } }() zoneID, err := provider.getHostedZoneID(fqdn) require.NoError(t, err) params := &route53.ListResourceRecordSetsInput{ HostedZoneId: aws.String(zoneID), } resp, err := svc.ListResourceRecordSets(params) require.NoError(t, err) for _, v := range resp.ResourceRecordSets { if aws.StringValue(v.Name) == fqdn && aws.StringValue(v.Type) == "TXT" && aws.Int64Value(v.TTL) == 10 { return } } t.Fatalf("Could not find a TXT record for _acme-challenge.%s with a TTL of 10", domain) } lego-4.9.1/providers/dns/route53/route53_test.go000066400000000000000000000104131434020463500214560ustar00rootroot00000000000000package route53 import ( "os" "testing" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/route53" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = "R53_DOMAIN" var envTest = tester.NewEnvTest( EnvAccessKeyID, EnvSecretAccessKey, EnvRegion, EnvHostedZoneID, EnvMaxRetries, EnvTTL, EnvPropagationTimeout, EnvPollingInterval). WithDomain(envDomain). WithLiveTestRequirements(EnvAccessKeyID, EnvSecretAccessKey, EnvRegion, envDomain) func makeTestProvider(t *testing.T, serverURL string) *DNSProvider { t.Helper() config := &aws.Config{ Credentials: credentials.NewStaticCredentials("abc", "123", " "), Endpoint: aws.String(serverURL), Region: aws.String("mock-region"), MaxRetries: aws.Int(1), } sess, err := session.NewSession(config) require.NoError(t, err) return &DNSProvider{ client: route53.New(sess), config: NewDefaultConfig(), } } func Test_loadCredentials_FromEnv(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() _ = os.Setenv(EnvAccessKeyID, "123") _ = os.Setenv(EnvSecretAccessKey, "456") _ = os.Setenv(EnvRegion, "us-east-1") config := &aws.Config{ CredentialsChainVerboseErrors: aws.Bool(true), } sess, err := session.NewSession(config) require.NoError(t, err) value, err := sess.Config.Credentials.Get() require.NoError(t, err, "Expected credentials to be set from environment") expected := credentials.Value{ AccessKeyID: "123", SecretAccessKey: "456", SessionToken: "", ProviderName: "EnvConfigCredentials", } assert.Equal(t, expected, value) } func Test_loadRegion_FromEnv(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() os.Setenv(EnvRegion, route53.CloudWatchRegionUsEast1) sess, err := session.NewSession(aws.NewConfig()) require.NoError(t, err) region := aws.StringValue(sess.Config.Region) assert.Equal(t, route53.CloudWatchRegionUsEast1, region, "Region") } func Test_getHostedZoneID_FromEnv(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() expectedZoneID := "zoneID" os.Setenv(EnvHostedZoneID, expectedZoneID) provider, err := NewDNSProvider() require.NoError(t, err) hostedZoneID, err := provider.getHostedZoneID("whatever") require.NoError(t, err, "HostedZoneID") assert.Equal(t, expectedZoneID, hostedZoneID) } func TestNewDefaultConfig(t *testing.T) { defer envTest.RestoreEnv() testCases := []struct { desc string envVars map[string]string expected *Config }{ { desc: "default configuration", expected: &Config{ MaxRetries: 5, TTL: 10, PropagationTimeout: 2 * time.Minute, PollingInterval: 4 * time.Second, }, }, { desc: "", envVars: map[string]string{ EnvMaxRetries: "10", EnvTTL: "99", EnvPropagationTimeout: "60", EnvPollingInterval: "60", EnvHostedZoneID: "abc123", }, expected: &Config{ MaxRetries: 10, TTL: 99, PropagationTimeout: 60 * time.Second, PollingInterval: 60 * time.Second, HostedZoneID: "abc123", }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { envTest.ClearEnv() for key, value := range test.envVars { os.Setenv(key, value) } config := NewDefaultConfig() assert.Equal(t, test.expected, config) }) } } func TestDNSProvider_Present(t *testing.T) { mockResponses := MockResponseMap{ "/2013-04-01/hostedzonesbyname": {StatusCode: 200, Body: ListHostedZonesByNameResponse}, "/2013-04-01/hostedzone/ABCDEFG/rrset/": {StatusCode: 200, Body: ChangeResourceRecordSetsResponse}, "/2013-04-01/change/123456": {StatusCode: 200, Body: GetChangeResponse}, "/2013-04-01/hostedzone/ABCDEFG/rrset?name=_acme-challenge.example.com.&type=TXT": { StatusCode: 200, Body: "", }, } serverURL := setupTest(t, mockResponses) defer envTest.RestoreEnv() envTest.ClearEnv() provider := makeTestProvider(t, serverURL) domain := "example.com" keyAuth := "123456d==" err := provider.Present(domain, "", keyAuth) require.NoError(t, err, "Expected Present to return no error") } lego-4.9.1/providers/dns/safedns/000077500000000000000000000000001434020463500167005ustar00rootroot00000000000000lego-4.9.1/providers/dns/safedns/internal/000077500000000000000000000000001434020463500205145ustar00rootroot00000000000000lego-4.9.1/providers/dns/safedns/internal/client.go000066400000000000000000000060021434020463500223170ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "path" "strconv" "time" "github.com/go-acme/lego/v4/challenge/dns01" ) const defaultBaseURL = "https://api.ukfast.io/safedns/v1" // Client the UKFast SafeDNS client. type Client struct { authToken string baseURL *url.URL HTTPClient *http.Client } // NewClient Creates a new Client. func NewClient(authToken string) *Client { baseURL, _ := url.Parse(defaultBaseURL) return &Client{ authToken: authToken, baseURL: baseURL, HTTPClient: &http.Client{Timeout: 5 * time.Second}, } } // AddRecord adds a DNS record. func (c *Client) AddRecord(zone string, record Record) (*AddRecordResponse, error) { body, err := json.Marshal(record) if err != nil { return nil, err } endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "zones", dns01.UnFqdn(zone), "records")) if err != nil { return nil, err } req, err := c.newRequest(http.MethodPost, endpoint.String(), bytes.NewReader(body)) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode >= http.StatusBadRequest { return nil, readError(req, resp) } content, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.New(toUnreadableBodyMessage(req, content)) } respData := &AddRecordResponse{} err = json.Unmarshal(content, respData) if err != nil { return nil, fmt.Errorf("%w: %s", err, toUnreadableBodyMessage(req, content)) } return respData, nil } // RemoveRecord removes a DNS record. func (c *Client) RemoveRecord(zone string, recordID int) error { endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "zones", dns01.UnFqdn(zone), "records", strconv.Itoa(recordID))) if err != nil { return err } req, err := c.newRequest(http.MethodDelete, endpoint.String(), nil) if err != nil { return err } resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode >= http.StatusBadRequest { return readError(req, resp) } return nil } func (c *Client) newRequest(method, endpoint string, body io.Reader) (*http.Request, error) { req, err := http.NewRequest(method, endpoint, body) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", c.authToken) return req, nil } func readError(req *http.Request, resp *http.Response) error { content, err := io.ReadAll(resp.Body) if err != nil { return errors.New(toUnreadableBodyMessage(req, content)) } var errInfo APIError err = json.Unmarshal(content, &errInfo) if err != nil { return fmt.Errorf("unmarshaling error: %w: %s", err, toUnreadableBodyMessage(req, content)) } return errInfo } func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string { return fmt.Sprintf("the request %s received a response with an invalid format: %q", req.URL, string(rawBody)) } lego-4.9.1/providers/dns/safedns/internal/client_test.go000066400000000000000000000053251434020463500233650ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "net/url" "testing" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupTest(t *testing.T) (*Client, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient("secret") client.baseURL, _ = url.Parse(server.URL) return client, mux } func TestClient_AddRecord(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/zones/example.com/records", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } if req.Header.Get("Authorization") != "secret" { http.Error(rw, `{"message":"Unauthenticated"}`, http.StatusUnauthorized) return } reqBody, err := io.ReadAll(req.Body) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } expectedReqBody := `{"name":"_acme-challenge.example.com","type":"TXT","content":"\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\"","ttl":120}` if string(reqBody) != expectedReqBody { http.Error(rw, `{"message":"invalid request"}`, http.StatusBadRequest) return } resp := `{ "data": { "id": 1234567 }, "meta": { "location": "https://api.ukfast.io/safedns/v1/zones/example.com/records/1234567" } }` rw.WriteHeader(http.StatusCreated) _, err = fmt.Fprint(rw, resp) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) record := Record{ Name: "_acme-challenge.example.com", Type: "TXT", Content: `"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI"`, TTL: dns01.DefaultTTL, } response, err := client.AddRecord("example.com", record) require.NoError(t, err) expected := &AddRecordResponse{ Data: struct { ID int `json:"id"` }{ ID: 1234567, }, Meta: struct { Location string `json:"location"` }{ Location: "https://api.ukfast.io/safedns/v1/zones/example.com/records/1234567", }, } assert.Equal(t, expected, response) } func TestClient_RemoveRecord(t *testing.T) { client, mux := setupTest(t) mux.HandleFunc("/zones/example.com/records/1234567", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodDelete { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } if req.Header.Get("Authorization") != "secret" { http.Error(rw, `{"message":"Unauthenticated"}`, http.StatusUnauthorized) return } rw.WriteHeader(http.StatusNoContent) }) err := client.RemoveRecord("example.com", 1234567) require.NoError(t, err) } lego-4.9.1/providers/dns/safedns/internal/types.go000066400000000000000000000006441434020463500222130ustar00rootroot00000000000000package internal type AddRecordResponse struct { Data struct { ID int `json:"id"` } `json:"data"` Meta struct { Location string `json:"location"` } } type Record struct { Name string `json:"name"` Type string `json:"type"` Content string `json:"content"` TTL int `json:"ttl"` } type APIError struct { Message string `json:"message"` } func (a APIError) Error() string { return a.Message } lego-4.9.1/providers/dns/safedns/safedns.go000066400000000000000000000077411434020463500206630ustar00rootroot00000000000000// Package safedns implements a DNS provider for solving the DNS-01 challenge using UKFast SafeDNS. package safedns import ( "errors" "fmt" "net/http" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/safedns/internal" ) // Environment variables. const ( envNamespace = "SAFEDNS_" EnvAuthToken = envNamespace + "AUTH_TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { AuthToken string TTL int PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client recordIDs map[string]int recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAuthToken) if err != nil { return nil, fmt.Errorf("safedns: %w", err) } config := NewDefaultConfig() config.AuthToken = values[EnvAuthToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for UKFast SafeDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("safedns: supplied configuration was nil") } if config.AuthToken == "" { return nil, errors.New("safedns: credentials missing") } client := internal.NewClient(config.AuthToken) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]int), }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(fqdn)) if err != nil { return fmt.Errorf("safedns: could not determine zone for domain: %q: %w", fqdn, err) } record := internal.Record{ Name: dns01.UnFqdn(fqdn), Type: "TXT", Content: fmt.Sprintf("%q", value), TTL: d.config.TTL, } resp, err := d.client.AddRecord(zone, record) if err != nil { return fmt.Errorf("safedns: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = resp.Data.ID d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record previously created. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("safedns: %w", err) } d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("safedns: unknown record ID for '%s'", fqdn) } err = d.client.RemoveRecord(authZone, recordID) if err != nil { return fmt.Errorf("safedns: %w", err) } d.recordIDsMu.Lock() delete(d.recordIDs, token) d.recordIDsMu.Unlock() return nil } lego-4.9.1/providers/dns/safedns/safedns.toml000066400000000000000000000013051434020463500212170ustar00rootroot00000000000000Name = "UKFast SafeDNS" Description = '''''' URL = "https://www.ukfast.co.uk/dns-hosting.html" Code = "safedns" Since = "v4.6.0" Example = ''' SAFEDNS_AUTH_TOKEN=xxxxxx \ lego --email you@example.com --dns safedns --domains my.example.org run ''' [Configuration] [Configuration.Credentials] SAFEDNS_AUTH_TOKEN = "Authentication token" [Configuration.Additional] SAFEDNS_POLLING_INTERVAL = "Time between DNS propagation check" SAFEDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" SAFEDNS_TTL = "The TTL of the TXT record used for the DNS challenge" SAFEDNS_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developers.ukfast.io/documentation/safedns" lego-4.9.1/providers/dns/safedns/safedns_test.go000066400000000000000000000044341434020463500217160ustar00rootroot00000000000000package safedns import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAuthToken).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAuthToken: "123", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAuthToken: "", }, expected: "safedns: some credentials information are missing: SAFEDNS_AUTH_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string authToken string expected string }{ { desc: "success", authToken: "123", }, { desc: "missing credentials", expected: "safedns: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.AuthToken = test.authToken p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/sakuracloud/000077500000000000000000000000001434020463500175725ustar00rootroot00000000000000lego-4.9.1/providers/dns/sakuracloud/client.go000066400000000000000000000046461434020463500214110ustar00rootroot00000000000000package sakuracloud import ( "context" "fmt" "strings" "sync" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/sacloud/iaas-api-go" "github.com/sacloud/iaas-api-go/search" ) // This mutex is required for concurrent updates. // see: https://github.com/go-acme/lego/pull/850 var mu sync.Mutex func (d *DNSProvider) addTXTRecord(fqdn, value string, ttl int) error { mu.Lock() defer mu.Unlock() zone, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("%w", err) } name := extractRecordName(fqdn, zone.Name) records := append(zone.Records, &iaas.DNSRecord{ Name: name, Type: "TXT", RData: value, TTL: ttl, }) _, err = d.client.UpdateSettings(context.Background(), zone.ID, &iaas.DNSUpdateSettingsRequest{ Records: records, SettingsHash: zone.SettingsHash, }) if err != nil { return fmt.Errorf("API call failed: %w", err) } return nil } func (d *DNSProvider) cleanupTXTRecord(fqdn, value string) error { mu.Lock() defer mu.Unlock() zone, err := d.getHostedZone(fqdn) if err != nil { return err } recordName := extractRecordName(fqdn, zone.Name) var updRecords iaas.DNSRecords for _, r := range zone.Records { if !(r.Name == recordName && r.Type == "TXT" && r.RData == value) { updRecords = append(updRecords, r) } } settings := &iaas.DNSUpdateSettingsRequest{ Records: updRecords, SettingsHash: zone.SettingsHash, } _, err = d.client.UpdateSettings(context.Background(), zone.ID, settings) if err != nil { return fmt.Errorf("API call failed: %w", err) } return nil } func (d *DNSProvider) getHostedZone(domain string) (*iaas.DNS, error) { authZone, err := dns01.FindZoneByFqdn(domain) if err != nil { return nil, err } zoneName := dns01.UnFqdn(authZone) conditions := &iaas.FindCondition{ Filter: search.Filter{ search.Key("Name"): search.ExactMatch(zoneName), }, } res, err := d.client.Find(context.Background(), conditions) if err != nil { if iaas.IsNotFoundError(err) { return nil, fmt.Errorf("zone %s not found on SakuraCloud DNS: %w", zoneName, err) } return nil, fmt.Errorf("API call failed: %w", err) } for _, zone := range res.DNS { if zone.Name == zoneName { return zone, nil } } return nil, fmt.Errorf("zone %s not found", zoneName) } func extractRecordName(fqdn, zone string) string { name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+zone); idx != -1 { return name[:idx] } return name } lego-4.9.1/providers/dns/sakuracloud/client_test.go000066400000000000000000000062301434020463500224370ustar00rootroot00000000000000package sakuracloud import ( "context" "fmt" "sync" "testing" client "github.com/sacloud/api-client-go" "github.com/sacloud/iaas-api-go" "github.com/sacloud/iaas-api-go/helper/api" "github.com/stretchr/testify/require" ) func setupTest(t *testing.T) { t.Helper() t.Setenv("SAKURACLOUD_FAKE_MODE", "1") createDummyZone(t, fakeCaller()) } func fakeCaller() iaas.APICaller { return api.NewCallerWithOptions(&api.CallerOptions{ Options: &client.Options{ AccessToken: "dummy", AccessTokenSecret: "dummy", }, FakeMode: true, }) } func createDummyZone(t *testing.T, caller iaas.APICaller) { t.Helper() ctx := context.Background() dnsOp := iaas.NewDNSOp(caller) // cleanup zones, err := dnsOp.Find(ctx, &iaas.FindCondition{}) require.NoError(t, err) for _, zone := range zones.DNS { if zone.Name == "example.com" { err = dnsOp.Delete(ctx, zone.ID) require.NoError(t, err) break } } // create dummy zone _, err = iaas.NewDNSOp(caller).Create(context.Background(), &iaas.DNSCreateRequest{Name: "example.com"}) require.NoError(t, err) } func TestDNSProvider_addAndCleanupRecords(t *testing.T) { setupTest(t) config := NewDefaultConfig() config.Token = "token1" config.Secret = "secret1" p, err := NewDNSProviderConfig(config) require.NoError(t, err) t.Run("addTXTRecord", func(t *testing.T) { err = p.addTXTRecord("test.example.com.", "dummyValue", 10) require.NoError(t, err) updZone, e := p.getHostedZone("test.example.com.") require.NoError(t, e) require.NotNil(t, updZone) require.Len(t, updZone.Records, 1) }) t.Run("cleanupTXTRecord", func(t *testing.T) { err = p.cleanupTXTRecord("test.example.com.", "dummyValue") require.NoError(t, err) updZone, e := p.getHostedZone("test.example.com.") require.NoError(t, e) require.NotNil(t, updZone) require.Len(t, updZone.Records, 0) }) } func TestDNSProvider_concurrentAddAndCleanupRecords(t *testing.T) { setupTest(t) dummyRecordCount := 10 var providers []*DNSProvider for i := 0; i < dummyRecordCount; i++ { config := NewDefaultConfig() config.Token = "token3" config.Secret = "secret3" p, err := NewDNSProviderConfig(config) require.NoError(t, err) providers = append(providers, p) } var wg sync.WaitGroup t.Run("addTXTRecord", func(t *testing.T) { wg.Add(len(providers)) for i, p := range providers { go func(j int, client *DNSProvider) { err := client.addTXTRecord(fmt.Sprintf("test%d.example.com.", j), "dummyValue", 10) require.NoError(t, err) wg.Done() }(i, p) } wg.Wait() updZone, err := providers[0].getHostedZone("example.com.") require.NoError(t, err) require.NotNil(t, updZone) require.Len(t, updZone.Records, dummyRecordCount) }) t.Run("cleanupTXTRecord", func(t *testing.T) { wg.Add(len(providers)) for i, p := range providers { go func(i int, client *DNSProvider) { err := client.cleanupTXTRecord(fmt.Sprintf("test%d.example.com.", i), "dummyValue") require.NoError(t, err) wg.Done() }(i, p) } wg.Wait() updZone, err := providers[0].getHostedZone("example.com.") require.NoError(t, err) require.NotNil(t, updZone) require.Len(t, updZone.Records, 0) }) } lego-4.9.1/providers/dns/sakuracloud/sakuracloud.go000066400000000000000000000077761434020463500224570ustar00rootroot00000000000000// Package sakuracloud implements a DNS provider for solving the DNS-01 challenge using SakuraCloud DNS. package sakuracloud import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" client "github.com/sacloud/api-client-go" "github.com/sacloud/iaas-api-go" "github.com/sacloud/iaas-api-go/helper/api" ) // Environment variables names. const ( envNamespace = "SAKURACLOUD_" EnvAccessToken = envNamespace + "ACCESS_TOKEN" EnvAccessTokenSecret = envNamespace + "ACCESS_TOKEN_SECRET" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Token string Secret string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client iaas.DNSAPI } // NewDNSProvider returns a DNSProvider instance configured for SakuraCloud. // Credentials must be passed in the environment variables: // SAKURACLOUD_ACCESS_TOKEN & SAKURACLOUD_ACCESS_TOKEN_SECRET. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAccessToken, EnvAccessTokenSecret) if err != nil { return nil, fmt.Errorf("sakuracloud: %w", err) } config := NewDefaultConfig() config.Token = values[EnvAccessToken] config.Secret = values[EnvAccessTokenSecret] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for SakuraCloud. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("sakuracloud: the configuration of the DNS provider is nil") } if config.Token == "" { return nil, errors.New("sakuracloud: AccessToken is missing") } if config.Secret == "" { return nil, errors.New("sakuracloud: AccessSecret is missing") } defaultOption, err := api.DefaultOption() if err != nil { return nil, fmt.Errorf("sakuracloud: %w", err) } options := &api.CallerOptions{ Options: &client.Options{ AccessToken: config.Token, AccessTokenSecret: config.Secret, HttpClient: config.HTTPClient, UserAgent: fmt.Sprintf("go-acme/lego %s", iaas.DefaultUserAgent), }, } return &DNSProvider{ client: iaas.NewDNSOp(api.NewCallerWithOptions(api.MergeOptions(defaultOption, options))), config: config, }, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) err := d.addTXTRecord(fqdn, value, d.config.TTL) if err != nil { return fmt.Errorf("sakuracloud: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) err := d.cleanupTXTRecord(fqdn, value) if err != nil { return fmt.Errorf("sakuracloud: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } lego-4.9.1/providers/dns/sakuracloud/sakuracloud.toml000066400000000000000000000015451434020463500230110ustar00rootroot00000000000000Name = "Sakura Cloud" Description = '''''' URL = "https://cloud.sakura.ad.jp/" Code = "sakuracloud" Since = "v1.1.0" Example = ''' SAKURACLOUD_ACCESS_TOKEN=xxxxx \ SAKURACLOUD_ACCESS_TOKEN_SECRET=yyyyy \ lego --email you@example.com --dns sakuracloud --domains my.example.org run ''' [Configuration] [Configuration.Credentials] SAKURACLOUD_ACCESS_TOKEN = "Access token" SAKURACLOUD_ACCESS_TOKEN_SECRET = "Access token secret" [Configuration.Additional] SAKURACLOUD_POLLING_INTERVAL = "Time between DNS propagation check" SAKURACLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" SAKURACLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" SAKURACLOUD_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developer.sakura.ad.jp/cloud/api/1.1/" GoClient = "https://github.com/sacloud/iaas-api-go" lego-4.9.1/providers/dns/sakuracloud/sakuracloud_test.go000066400000000000000000000062541434020463500235040ustar00rootroot00000000000000package sakuracloud import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAccessToken, EnvAccessTokenSecret). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAccessToken: "123", EnvAccessTokenSecret: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAccessToken: "", EnvAccessTokenSecret: "", }, expected: "sakuracloud: some credentials information are missing: SAKURACLOUD_ACCESS_TOKEN,SAKURACLOUD_ACCESS_TOKEN_SECRET", }, { desc: "missing access token", envVars: map[string]string{ EnvAccessToken: "", EnvAccessTokenSecret: "456", }, expected: "sakuracloud: some credentials information are missing: SAKURACLOUD_ACCESS_TOKEN", }, { desc: "missing token secret", envVars: map[string]string{ EnvAccessToken: "123", EnvAccessTokenSecret: "", }, expected: "sakuracloud: some credentials information are missing: SAKURACLOUD_ACCESS_TOKEN_SECRET", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string token string secret string expected string }{ { desc: "success", token: "123", secret: "456", }, { desc: "missing credentials", expected: "sakuracloud: AccessToken is missing", }, { desc: "missing token", secret: "456", expected: "sakuracloud: AccessToken is missing", }, { desc: "missing secret", token: "123", expected: "sakuracloud: AccessSecret is missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Token = test.token config.Secret = test.secret p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/scaleway/000077500000000000000000000000001434020463500170655ustar00rootroot00000000000000lego-4.9.1/providers/dns/scaleway/scaleway.go000066400000000000000000000112731434020463500212300ustar00rootroot00000000000000// Package scaleway implements a DNS provider for solving the DNS-01 challenge using Scaleway Domains API. // Token: https://www.scaleway.com/en/docs/generate-an-api-token/ package scaleway import ( "errors" "fmt" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" scwdomain "github.com/scaleway/scaleway-sdk-go/api/domain/v2beta1" "github.com/scaleway/scaleway-sdk-go/scw" ) const ( minTTL = 60 defaultPollingInterval = 10 * time.Second defaultPropagationTimeout = 120 * time.Second ) // Environment variables names. const ( envNamespace = "SCALEWAY_" EnvAPIToken = envNamespace + "API_TOKEN" EnvProjectID = envNamespace + "PROJECT_ID" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { ProjectID string Token string PropagationTimeout time.Duration PollingInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *scwdomain.API } // NewDNSProvider returns a DNSProvider instance configured for Scaleway Domains API. // Credentials must be passed in the environment variables: // SCALEWAY_API_TOKEN, SCALEWAY_PROJECT_ID. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIToken) if err != nil { return nil, fmt.Errorf("scaleway: %w", err) } config := NewDefaultConfig() config.Token = values[EnvAPIToken] config.ProjectID = env.GetOrFile(EnvProjectID) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for scaleway. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("scaleway: the configuration of the DNS provider is nil") } if config.Token == "" { return nil, errors.New("scaleway: credentials missing") } if config.TTL < minTTL { config.TTL = minTTL } configuration := []scw.ClientOption{ scw.WithAuth("SCWXXXXXXXXXXXXXXXXX", config.Token), scw.WithUserAgent("Scaleway Lego's provider"), } if config.ProjectID != "" { configuration = append(configuration, scw.WithDefaultProjectID(config.ProjectID)) } // Create a Scaleway client clientScw, err := scw.NewClient(configuration...) if err != nil { return nil, fmt.Errorf("scaleway: %w", err) } return &DNSProvider{config: config, client: scwdomain.NewAPI(clientScw)}, nil } // Timeout returns the Timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill DNS-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) records := []*scwdomain.Record{{ Data: fmt.Sprintf(`%q`, value), Name: fqdn, TTL: uint32(d.config.TTL), Type: scwdomain.RecordTypeTXT, Comment: scw.StringPtr("used by lego"), }} // TODO(ldez) replace domain by FQDN to follow CNAME. req := &scwdomain.UpdateDNSZoneRecordsRequest{ DNSZone: domain, Changes: []*scwdomain.RecordChange{{ Add: &scwdomain.RecordChangeAdd{Records: records}, }}, ReturnAllRecords: scw.BoolPtr(false), } _, err := d.client.UpdateDNSZoneRecords(req) if err != nil { return fmt.Errorf("scaleway: %w", err) } return nil } // CleanUp removes a TXT record used for DNS-01 challenge. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) recordIdentifier := &scwdomain.RecordIdentifier{ Name: fqdn, Type: scwdomain.RecordTypeTXT, Data: scw.StringPtr(fmt.Sprintf(`%q`, value)), } // TODO(ldez) replace domain by FQDN to follow CNAME. req := &scwdomain.UpdateDNSZoneRecordsRequest{ DNSZone: domain, Changes: []*scwdomain.RecordChange{{ Delete: &scwdomain.RecordChangeDelete{IDFields: recordIdentifier}, }}, ReturnAllRecords: scw.BoolPtr(false), } _, err := d.client.UpdateDNSZoneRecords(req) if err != nil { return fmt.Errorf("scaleway: %w", err) } return nil } lego-4.9.1/providers/dns/scaleway/scaleway.toml000066400000000000000000000013251434020463500215730ustar00rootroot00000000000000Name = "Scaleway" Description = '''''' URL = "https://developers.scaleway.com/" Code = "scaleway" Since = "v3.4.0" Example = ''' SCALEWAY_API_TOKEN=xxxxxxx-xxxxx-xxxx-xxx-xxxxxx \ lego --email you@example.com --dns scaleway --domains my.example.org run ''' [Configuration] [Configuration.Credentials] SCALEWAY_API_TOKEN = "API token" SCALEWAY_PROJECT_ID = "Project to use (optional)" [Configuration.Additional] SCALEWAY_POLLING_INTERVAL = "Time between DNS propagation check" SCALEWAY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" SCALEWAY_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://developers.scaleway.com/en/products/domain/dns/api/" lego-4.9.1/providers/dns/scaleway/scaleway_test.go000066400000000000000000000050161434020463500222650ustar00rootroot00000000000000package scaleway import ( "fmt" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAPIToken, EnvProjectID). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIToken: "00000000-0000-0000-0000-000000000000", EnvProjectID: "", }, }, { desc: "missing api key", envVars: map[string]string{ EnvAPIToken: "", EnvProjectID: "", }, expected: fmt.Sprintf("scaleway: some credentials information are missing: %s", EnvAPIToken), }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string token string ttl int expected string }{ { desc: "success", token: "00000000-0000-0000-0000-000000000000", ttl: minTTL, }, { desc: "missing api key", token: "", ttl: minTTL, expected: "scaleway: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.TTL = test.ttl config.Token = test.token p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/selectel/000077500000000000000000000000001434020463500170555ustar00rootroot00000000000000lego-4.9.1/providers/dns/selectel/selectel.go000066400000000000000000000106301434020463500212040ustar00rootroot00000000000000// Package selectel implements a DNS provider for solving the DNS-01 challenge using Selectel Domains API. // Selectel Domain API reference: https://kb.selectel.com/23136054.html // Token: https://my.selectel.ru/profile/apikeys package selectel import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/internal/selectel" ) const minTTL = 60 // Environment variables names. const ( envNamespace = "SELECTEL_" EnvBaseURL = envNamespace + "BASE_URL" EnvAPIToken = envNamespace + "API_TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string Token string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ BaseURL: env.GetOrDefaultString(EnvBaseURL, selectel.DefaultSelectelBaseURL), TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *selectel.Client } // NewDNSProvider returns a DNSProvider instance configured for Selectel Domains API. // API token must be passed in the environment variable SELECTEL_API_TOKEN. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIToken) if err != nil { return nil, fmt.Errorf("selectel: %w", err) } config := NewDefaultConfig() config.Token = values[EnvAPIToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for selectel. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("selectel: the configuration of the DNS provider is nil") } if config.Token == "" { return nil, errors.New("selectel: credentials missing") } if config.TTL < minTTL { return nil, fmt.Errorf("selectel: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } client := selectel.NewClient(config.Token) client.BaseURL = config.BaseURL client.HTTPClient = config.HTTPClient return &DNSProvider{config: config, client: client}, nil } // Timeout returns the Timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill DNS-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) // TODO(ldez) replace domain by FQDN to follow CNAME. domainObj, err := d.client.GetDomainByName(domain) if err != nil { return fmt.Errorf("selectel: %w", err) } txtRecord := selectel.Record{ Type: "TXT", TTL: d.config.TTL, Name: fqdn, Content: value, } _, err = d.client.AddRecord(domainObj.ID, txtRecord) if err != nil { return fmt.Errorf("selectel: %w", err) } return nil } // CleanUp removes a TXT record used for DNS-01 challenge. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) recordName := dns01.UnFqdn(fqdn) // TODO(ldez) replace domain by FQDN to follow CNAME. domainObj, err := d.client.GetDomainByName(domain) if err != nil { return fmt.Errorf("selectel: %w", err) } records, err := d.client.ListRecords(domainObj.ID) if err != nil { return fmt.Errorf("selectel: %w", err) } // Delete records with specific FQDN var lastErr error for _, record := range records { if record.Name == recordName { err = d.client.DeleteRecord(domainObj.ID, record.ID) if err != nil { lastErr = fmt.Errorf("selectel: %w", err) } } } return lastErr } lego-4.9.1/providers/dns/selectel/selectel.toml000066400000000000000000000013061434020463500215520ustar00rootroot00000000000000Name = "Selectel" Description = '''''' URL = "https://kb.selectel.com/" Code = "selectel" Since = "v1.2.0" Example = ''' SELECTEL_API_TOKEN=xxxxx \ lego --email you@example.com --dns selectel --domains my.example.org run ''' [Configuration] [Configuration.Credentials] SELECTEL_API_TOKEN = "API token" [Configuration.Additional] SELECTEL_BASE_URL = "API endpoint URL" SELECTEL_POLLING_INTERVAL = "Time between DNS propagation check" SELECTEL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" SELECTEL_TTL = "The TTL of the TXT record used for the DNS challenge" SELECTEL_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://kb.selectel.com/23136054.html" lego-4.9.1/providers/dns/selectel/selectel_test.go000066400000000000000000000047661434020463500222600ustar00rootroot00000000000000package selectel import ( "fmt" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var envTest = tester.NewEnvTest(EnvAPIToken, EnvTTL) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIToken: "123", }, }, { desc: "missing api key", envVars: map[string]string{ EnvAPIToken: "", }, expected: fmt.Sprintf("selectel: some credentials information are missing: %s", EnvAPIToken), }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string token string ttl int expected string }{ { desc: "success", token: "123", ttl: 60, }, { desc: "missing api key", token: "", ttl: 60, expected: "selectel: credentials missing", }, { desc: "bad TTL value", token: "123", ttl: 59, expected: fmt.Sprintf("selectel: invalid TTL, TTL (59) must be greater than %d", minTTL), }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.TTL = test.ttl config.Token = test.token p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/servercow/000077500000000000000000000000001434020463500172745ustar00rootroot00000000000000lego-4.9.1/providers/dns/servercow/internal/000077500000000000000000000000001434020463500211105ustar00rootroot00000000000000lego-4.9.1/providers/dns/servercow/internal/client.go000066400000000000000000000072251434020463500227230ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" ) const baseAPIURL = "https://api.servercow.de/dns/v1/domains" // Client the Servercow client. type Client struct { BaseURL string HTTPClient *http.Client username string password string } // NewClient Creates a Servercow client. func NewClient(username, password string) *Client { return &Client{ HTTPClient: http.DefaultClient, BaseURL: baseAPIURL, username: username, password: password, } } // GetRecords from API. func (c *Client) GetRecords(domain string) ([]Record, error) { req, err := c.createRequest(http.MethodGet, domain, nil) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() // Note the API always return 200 even if the authentication failed. if resp.StatusCode/100 != 2 { return nil, fmt.Errorf("error: status code %d", resp.StatusCode) } raw, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read body: %w", err) } var records []Record err = unmarshal(raw, &records) if err != nil { return nil, err } return records, nil } // CreateUpdateRecord creates or updates a record. func (c *Client) CreateUpdateRecord(domain string, data Record) (*Message, error) { req, err := c.createRequest(http.MethodPost, domain, &data) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() // Note the API always return 200 even if the authentication failed. if resp.StatusCode/100 != 2 { return nil, fmt.Errorf("error: status code %d", resp.StatusCode) } raw, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read body: %w", err) } var msg Message err = json.Unmarshal(raw, &msg) if err != nil { return nil, err } if msg.ErrorMsg != "" { return nil, msg } return &msg, nil } // DeleteRecord deletes a record. func (c *Client) DeleteRecord(domain string, data Record) (*Message, error) { req, err := c.createRequest(http.MethodDelete, domain, &data) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() // Note the API always return 200 even if the authentication failed. if resp.StatusCode/100 != 2 { return nil, fmt.Errorf("error: status code %d", resp.StatusCode) } raw, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read body: %w", err) } var msg Message err = json.Unmarshal(raw, &msg) if err != nil { return nil, fmt.Errorf("unmarshaling %T error: %w: %s", msg, err, string(raw)) } if msg.ErrorMsg != "" { return nil, msg } return &msg, nil } func (c *Client) createRequest(method, domain string, payload *Record) (*http.Request, error) { body, err := json.Marshal(payload) if err != nil { return nil, err } req, err := http.NewRequest(method, c.BaseURL+"/"+domain, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("X-Auth-Username", c.username) req.Header.Set("X-Auth-Password", c.password) req.Header.Set("Content-Type", "application/json") return req, nil } func unmarshal(raw []byte, v interface{}) error { err := json.Unmarshal(raw, v) if err == nil { return nil } var e *json.UnmarshalTypeError if errors.As(err, &e) { var apiError Message errU := json.Unmarshal(raw, &apiError) if errU != nil { return fmt.Errorf("unmarshaling %T error: %w: %s", v, err, string(raw)) } return apiError } return fmt.Errorf("unmarshaling %T error: %w: %s", v, err, string(raw)) } lego-4.9.1/providers/dns/servercow/internal/client_test.go000066400000000000000000000122741434020463500237620ustar00rootroot00000000000000package internal import ( "encoding/json" "io" "net/http" "net/http/httptest" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupTest(t *testing.T) (*Client, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient("", "") client.BaseURL = server.URL return client, mux } func TestClient_GetRecords(t *testing.T) { client, handler := setupTest(t) handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } file, err := os.Open("./fixtures/records-01.json") if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) records, err := client.GetRecords("lego.wtf") require.NoError(t, err) recordsJSON, err := json.Marshal(records) require.NoError(t, err) expectedContent, err := os.ReadFile("./fixtures/records-01.json") require.NoError(t, err) assert.JSONEq(t, string(expectedContent), string(recordsJSON)) } func TestClient_GetRecords_error(t *testing.T) { client, handler := setupTest(t) handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } err := json.NewEncoder(rw).Encode(Message{ErrorMsg: "authentication failed"}) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) records, err := client.GetRecords("lego.wtf") require.Error(t, err) assert.Nil(t, records) } func TestClient_CreateUpdateRecord(t *testing.T) { client, handler := setupTest(t) handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } content, err := io.ReadAll(req.Body) if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } expectedRequest := `{"name":"_acme-challenge.www","type":"TXT","ttl":30,"content":["aaa","bbb"]}` if !assert.JSONEq(t, expectedRequest, string(content)) { http.Error(rw, "invalid content", http.StatusBadRequest) return } err = json.NewEncoder(rw).Encode(Message{Message: "ok"}) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) record := Record{ Name: "_acme-challenge.www", Type: "TXT", TTL: 30, Content: Value{"aaa", "bbb"}, } msg, err := client.CreateUpdateRecord("lego.wtf", record) require.NoError(t, err) expected := &Message{Message: "ok"} assert.Equal(t, expected, msg) } func TestClient_CreateUpdateRecord_error(t *testing.T) { client, handler := setupTest(t) handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } err := json.NewEncoder(rw).Encode(Message{ErrorMsg: "parameter type must be cname, txt, tlsa, caa, a or aaaa"}) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) record := Record{ Name: "_acme-challenge.www", } msg, err := client.CreateUpdateRecord("lego.wtf", record) require.Error(t, err) assert.Nil(t, msg) } func TestClient_DeleteRecord(t *testing.T) { client, handler := setupTest(t) handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodDelete { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } content, err := io.ReadAll(req.Body) if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } expectedRequest := `{"name":"_acme-challenge.www","type":"TXT"}` if !assert.JSONEq(t, expectedRequest, string(content)) { http.Error(rw, "invalid content", http.StatusBadRequest) return } err = json.NewEncoder(rw).Encode(Message{Message: "ok"}) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) record := Record{ Name: "_acme-challenge.www", Type: "TXT", } msg, err := client.DeleteRecord("lego.wtf", record) require.NoError(t, err) expected := &Message{Message: "ok"} assert.Equal(t, expected, msg) } func TestClient_DeleteRecord_error(t *testing.T) { client, handler := setupTest(t) handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodDelete { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } err := json.NewEncoder(rw).Encode(Message{ErrorMsg: "parameter type must be cname, txt, tlsa, caa, a or aaaa"}) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) record := Record{ Name: "_acme-challenge.www", } msg, err := client.DeleteRecord("lego.wtf", record) require.Error(t, err) assert.Nil(t, msg) } lego-4.9.1/providers/dns/servercow/internal/fixtures/000077500000000000000000000000001434020463500227615ustar00rootroot00000000000000lego-4.9.1/providers/dns/servercow/internal/fixtures/records-01.json000066400000000000000000000037411434020463500255400ustar00rootroot00000000000000[ { "name": "letsencrypt", "ttl": 120, "type": "A", "content": "1.1.1.1" }, { "name": "diskover", "ttl": 120, "type": "CAA", "content": "0 issue \"letsencrypt.org\"" }, { "name": "diskover", "ttl": 120, "type": "AAAA", "content": ":::::" }, { "name": "diskover", "ttl": 120, "type": "A", "content": "1.1.1.1" }, { "name": "portainer", "ttl": 120, "type": "CAA", "content": "0 issue \"letsencrypt.org\"" }, { "name": "portainer", "ttl": 120, "type": "AAAA", "content": ":::::" }, { "name": "portainer", "ttl": 120, "type": "A", "content": "1.1.1.1" }, { "name": "lego", "ttl": 120, "type": "A", "content": "1.1.1.1" }, { "name": "traefik", "ttl": 120, "type": "CAA", "content": "0 issue \"letsencrypt.org\"" }, { "name": "traefik", "ttl": 120, "type": "AAAA", "content": ":::::" }, { "name": "traefik", "ttl": 120, "type": "A", "content": "1.1.1.1" }, { "name": "spaghetti", "ttl": 120, "type": "CAA", "content": "0 issue \"letsencrypt.org\"" }, { "name": "spaghetti", "ttl": 120, "type": "AAAA", "content": ":::::" }, { "name": "spaghetti", "ttl": 120, "type": "A", "content": "1.1.1.1" }, { "name": "dragonstone", "ttl": 120, "type": "CAA", "content": "0 issue \"letsencrypt.org\"" }, { "name": "dragonstone", "ttl": 120, "type": "A", "content": "1.1.1.1" }, { "name": "_acme-challenge.sample", "ttl": 20, "type": "TXT", "content": [ "txtxtxtxtxtxtxt", "acbdefghijklmnopqrstuvwxyz" ] }, { "name": "", "ttl": 120, "type": "CAA", "content": "0 issue \"letsencrypt.org\"" }, { "name": "", "ttl": 120, "type": "AAAA", "content": ":::::" }, { "name": "", "ttl": 120, "type": "A", "content": "1.1.1.1" } ] lego-4.9.1/providers/dns/servercow/internal/model.go000066400000000000000000000020431434020463500225360ustar00rootroot00000000000000package internal import "encoding/json" // Record is the record representation. type Record struct { Name string `json:"name"` Type string `json:"type"` TTL int `json:"ttl,omitempty"` Content Value `json:"content,omitempty"` } // Value is the value of a record. // Allows to handle dynamic type (string and string array). type Value []string func (v Value) MarshalJSON() ([]byte, error) { if len(v) == 0 { return nil, nil } if len(v) == 1 { return json.Marshal(v[0]) } content, err := json.Marshal([]string(v)) if err != nil { return nil, err } return content, nil } func (v *Value) UnmarshalJSON(b []byte) error { if b[0] == '[' { return json.Unmarshal(b, (*[]string)(v)) } var s string if err := json.Unmarshal(b, &s); err != nil { return err } *v = append(*v, s) return nil } // Message is the basic response representation. // Can be an error. type Message struct { Message string `json:"message,omitempty"` ErrorMsg string `json:"error,omitempty"` } func (a Message) Error() string { return a.ErrorMsg } lego-4.9.1/providers/dns/servercow/internal/model_test.go000066400000000000000000000045101434020463500235760ustar00rootroot00000000000000package internal import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestValue_MarshalJSON(t *testing.T) { testCases := []struct { desc string record Record expected string }{ { desc: "empty content", record: Record{ Name: "_acme-challenge.www", Type: "TXT", TTL: 30, Content: Value{}, }, expected: `{"name":"_acme-challenge.www","type":"TXT","ttl":30}`, }, { desc: "content with a single value", record: Record{ Name: "_acme-challenge.www", Type: "TXT", TTL: 30, Content: Value{"aaa"}, }, expected: `{"name":"_acme-challenge.www","type":"TXT","ttl":30,"content":"aaa"}`, }, { desc: "content with multiple values", record: Record{ Name: "_acme-challenge.www", Type: "TXT", TTL: 30, Content: Value{"aaa", "bbb", "ccc"}, }, expected: `{"name":"_acme-challenge.www","type":"TXT","ttl":30,"content":["aaa","bbb","ccc"]}`, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { content, err := json.Marshal(test.record) require.NoError(t, err) assert.JSONEq(t, test.expected, string(content)) }) } } func TestValue_UnmarshalJSON(t *testing.T) { testCases := []struct { desc string data string expected Record }{ { desc: "empty content", data: `{"name":"_acme-challenge.www","type":"TXT","ttl":30}`, expected: Record{ Name: "_acme-challenge.www", Type: "TXT", TTL: 30, Content: Value(nil), }, }, { desc: "content with a single value", data: `{"name":"_acme-challenge.www","type":"TXT","ttl":30,"content":"aaa"}`, expected: Record{ Name: "_acme-challenge.www", Type: "TXT", TTL: 30, Content: Value{"aaa"}, }, }, { desc: "content with multiple values", data: `{"name":"_acme-challenge.www","type":"TXT","ttl":30,"content":["aaa","bbb","ccc"]}`, expected: Record{ Name: "_acme-challenge.www", Type: "TXT", TTL: 30, Content: Value{"aaa", "bbb", "ccc"}, }, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { record := Record{} err := json.Unmarshal([]byte(test.data), &record) require.NoError(t, err) assert.Equal(t, test.expected, record) }) } } lego-4.9.1/providers/dns/servercow/servercow.go000066400000000000000000000134561434020463500216530ustar00rootroot00000000000000// Package servercow implements a DNS provider for solving the DNS-01 challenge using Servercow DNS. package servercow import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/servercow/internal" ) const defaultTTL = 120 // Environment variables names. const ( envNamespace = "SERVERCOW_" EnvUsername = envNamespace + "USERNAME" EnvPassword = envNamespace + "PASSWORD" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Username string Password string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvUsername, EnvPassword) if err != nil { return nil, fmt.Errorf("servercow: %w", err) } config := NewDefaultConfig() config.Username = values[EnvUsername] config.Password = values[EnvPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Servercow. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config.Username == "" || config.Password == "" { return nil, errors.New("servercow: incomplete credentials, missing username and/or password") } if config.HTTPClient == nil { config.HTTPClient = http.DefaultClient } client := internal.NewClient(config.Username, config.Password) client.HTTPClient = config.HTTPClient return &DNSProvider{ config: config, client: client, }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := getAuthZone(fqdn) if err != nil { return fmt.Errorf("servercow: %w", err) } records, err := d.client.GetRecords(authZone) if err != nil { return fmt.Errorf("servercow: %w", err) } recordName := getRecordName(fqdn, authZone) record := findRecords(records, recordName) // TXT record entry already existing if record != nil { if containsValue(record, value) { return nil } request := internal.Record{ Name: record.Name, TTL: record.TTL, Type: record.Type, Content: append(record.Content, value), } _, err = d.client.CreateUpdateRecord(authZone, request) if err != nil { return fmt.Errorf("servercow: failed to update TXT records: %w", err) } return nil } request := internal.Record{ Type: "TXT", Name: recordName, TTL: d.config.TTL, Content: internal.Value{value}, } _, err = d.client.CreateUpdateRecord(authZone, request) if err != nil { return fmt.Errorf("servercow: failed to create TXT record %s: %w", fqdn, err) } return nil } // CleanUp removes the TXT record previously created. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := getAuthZone(fqdn) if err != nil { return fmt.Errorf("servercow: %w", err) } records, err := d.client.GetRecords(authZone) if err != nil { return fmt.Errorf("servercow: failed to get TXT records: %w", err) } recordName := getRecordName(fqdn, authZone) record := findRecords(records, recordName) if record == nil { return nil } if !containsValue(record, value) { return nil } // only 1 record value, the whole record must be deleted. if len(record.Content) == 1 { _, err = d.client.DeleteRecord(authZone, *record) if err != nil { return fmt.Errorf("servercow: failed to delete TXT records: %w", err) } return nil } request := internal.Record{ Name: record.Name, Type: record.Type, TTL: record.TTL, } for _, val := range record.Content { if val != value { request.Content = append(request.Content, val) } } _, err = d.client.CreateUpdateRecord(authZone, request) if err != nil { return fmt.Errorf("servercow: failed to update TXT records: %w", err) } return nil } func getAuthZone(domain string) (string, error) { authZone, err := dns01.FindZoneByFqdn(domain) if err != nil { return "", fmt.Errorf("could not find zone for domain %q: %w", domain, err) } zoneName := dns01.UnFqdn(authZone) return zoneName, nil } func findRecords(records []internal.Record, name string) *internal.Record { for _, r := range records { if r.Type == "TXT" && r.Name == name { return &r } } return nil } func containsValue(record *internal.Record, value string) bool { for _, val := range record.Content { if val == value { return true } } return false } func getRecordName(fqdn, authZone string) string { return fqdn[0 : len(fqdn)-len(authZone)-2] } lego-4.9.1/providers/dns/servercow/servercow.toml000066400000000000000000000014371434020463500222150ustar00rootroot00000000000000Name = "Servercow" Description = '''''' URL = "https://servercow.de/" Code = "servercow" Since = "v3.4.0" Example = ''' SERVERCOW_USERNAME=xxxxxxxx \ SERVERCOW_PASSWORD=xxxxxxxx \ lego --email you@example.com --dns servercow --domains my.example.org run ''' [Configuration] [Configuration.Credentials] SERVERCOW_USERNAME = "API username" SERVERCOW_PASSWORD = "API password" [Configuration.Additional] SERVERCOW_POLLING_INTERVAL = "Time between DNS propagation check" SERVERCOW_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" SERVERCOW_TTL = "The TTL of the TXT record used for the DNS challenge" SERVERCOW_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://cp.servercow.de/client/plugin/support_manager/knowledgebase/view/34/dns-api-v1/7/" lego-4.9.1/providers/dns/servercow/servercow_test.go000066400000000000000000000062571434020463500227130ustar00rootroot00000000000000package servercow import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvUsername, EnvPassword). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvUsername: "123", EnvPassword: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvUsername: "", EnvPassword: "", }, expected: "servercow: some credentials information are missing: SERVERCOW_USERNAME,SERVERCOW_PASSWORD", }, { desc: "missing username", envVars: map[string]string{ EnvUsername: "", EnvPassword: "api_password", }, expected: "servercow: some credentials information are missing: SERVERCOW_USERNAME", }, { desc: "missing password", envVars: map[string]string{ EnvUsername: "api_username", EnvPassword: "", }, expected: "servercow: some credentials information are missing: SERVERCOW_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string expected string username string password string }{ { desc: "success", username: "api_username", password: "api_password", }, { desc: "missing credentials", expected: "servercow: incomplete credentials, missing username and/or password", }, { desc: "missing api key", username: "", password: "api_password", expected: "servercow: incomplete credentials, missing username and/or password", }, { desc: "missing secret key", username: "api_username", password: "", expected: "servercow: incomplete credentials, missing username and/or password", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Username = test.username config.Password = test.password p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/simply/000077500000000000000000000000001434020463500165725ustar00rootroot00000000000000lego-4.9.1/providers/dns/simply/internal/000077500000000000000000000000001434020463500204065ustar00rootroot00000000000000lego-4.9.1/providers/dns/simply/internal/client.go000066400000000000000000000065361434020463500222250ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "errors" "fmt" "net/http" "net/url" "path" "time" ) const defaultBaseURL = "https://api.simply.com/1/" // Client is a Simply.com API client. type Client struct { HTTPClient *http.Client baseURL *url.URL accountName string apiKey string } // NewClient creates a new Client. func NewClient(accountName string, apiKey string) (*Client, error) { if accountName == "" { return nil, errors.New("credentials missing: accountName") } if apiKey == "" { return nil, errors.New("credentials missing: apiKey") } baseURL, err := url.Parse(defaultBaseURL) if err != nil { return nil, err } return &Client{ HTTPClient: &http.Client{Timeout: 5 * time.Second}, baseURL: baseURL, accountName: accountName, apiKey: apiKey, }, nil } // GetRecords lists all the records in the zone. func (c *Client) GetRecords(zoneName string) ([]Record, error) { resp, err := c.do(zoneName, "/", http.MethodGet, nil) if err != nil { return nil, err } var records []Record err = json.Unmarshal(resp.Records, &records) if err != nil { return nil, fmt.Errorf("failed to unmarshal response result: %w", err) } return records, nil } // AddRecord adds a record. func (c *Client) AddRecord(zoneName string, record Record) (int64, error) { reqBody, err := json.Marshal(record) if err != nil { return 0, fmt.Errorf("failed to marshall request body: %w", err) } resp, err := c.do(zoneName, "/", http.MethodPost, reqBody) if err != nil { return 0, err } var rcd recordHeader err = json.Unmarshal(resp.Record, &rcd) if err != nil { return 0, fmt.Errorf("failed to unmarshal response result: %w", err) } return rcd.ID, nil } // EditRecord updates a record. func (c *Client) EditRecord(zoneName string, id int64, record Record) error { reqBody, err := json.Marshal(record) if err != nil { return fmt.Errorf("failed to marshall request body: %w", err) } _, err = c.do(zoneName, fmt.Sprintf("%d", id), http.MethodPut, reqBody) return err } // DeleteRecord deletes a record. func (c *Client) DeleteRecord(zoneName string, id int64) error { _, err := c.do(zoneName, fmt.Sprintf("%d", id), http.MethodDelete, nil) return err } func (c *Client) do(zoneName string, endpoint string, reqMethod string, reqBody []byte) (*apiResponse, error) { reqURL, err := c.baseURL.Parse(path.Join(c.baseURL.Path, c.accountName, c.apiKey, "my", "products", zoneName, "dns", "records", endpoint)) if err != nil { return nil, fmt.Errorf("failed to parse endpoint: %w", err) } req, err := http.NewRequest(reqMethod, reqURL.String(), bytes.NewReader(reqBody)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to perform request: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode >= http.StatusInternalServerError { return nil, fmt.Errorf("unexpected error: %d", resp.StatusCode) } response := apiResponse{} err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } if response.Status != http.StatusOK { return nil, fmt.Errorf("unexpected error: %s", response.Message) } return &response, nil } lego-4.9.1/providers/dns/simply/internal/client_test.go000066400000000000000000000113301434020463500232500ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "path" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestClient_GetRecords(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records", mockHandler(http.MethodGet, http.StatusOK, "get_records.json")) records, err := client.GetRecords("azone01") require.NoError(t, err) expected := []Record{ { ID: 1, Name: "@", TTL: 3600, Data: "ns1.simply.com", Type: "NS", Priority: 0, }, { ID: 2, Name: "@", TTL: 3600, Data: "ns2.simply.com", Type: "NS", Priority: 0, }, { ID: 3, Name: "@", TTL: 3600, Data: "ns3.simply.com", Type: "NS", Priority: 0, }, { ID: 4, Name: "@", TTL: 3600, Data: "ns4.simply.com", Type: "NS", Priority: 0, }, } assert.Equal(t, expected, records) } func TestClient_GetRecords_error(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records", mockHandler(http.MethodGet, http.StatusBadRequest, "bad_auth_error.json")) records, err := client.GetRecords("azone01") require.Error(t, err) assert.Nil(t, records) } func TestClient_AddRecord(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records", mockHandler(http.MethodPost, http.StatusOK, "add_record.json")) record := Record{ Name: "arecord01", Data: "content", Type: "TXT", TTL: 120, Priority: 0, } recordID, err := client.AddRecord("azone01", record) require.NoError(t, err) assert.EqualValues(t, 123456789, recordID) } func TestClient_AddRecord_error(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records", mockHandler(http.MethodPost, http.StatusNotFound, "bad_zone_error.json")) record := Record{ Name: "arecord01", Data: "content", Type: "TXT", TTL: 120, Priority: 0, } recordID, err := client.AddRecord("azone01", record) require.Error(t, err) assert.Zero(t, recordID) } func TestClient_EditRecord(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records/123456789", mockHandler(http.MethodPut, http.StatusOK, "success.json")) record := Record{ Name: "arecord01", Data: "content", Type: "TXT", TTL: 120, Priority: 0, } err := client.EditRecord("azone01", 123456789, record) require.NoError(t, err) } func TestClient_EditRecord_error(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records/123456789", mockHandler(http.MethodPut, http.StatusNotFound, "invalid_record_id.json")) record := Record{ Name: "arecord01", Data: "content", Type: "TXT", TTL: 120, Priority: 0, } err := client.EditRecord("azone01", 123456789, record) require.Error(t, err) } func TestClient_DeleteRecord(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records/123456789", mockHandler(http.MethodDelete, http.StatusOK, "success.json")) err := client.DeleteRecord("azone01", 123456789) require.NoError(t, err) } func TestClient_DeleteRecord_error(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records/123456789", mockHandler(http.MethodDelete, http.StatusNotFound, "invalid_record_id.json")) err := client.DeleteRecord("azone01", 123456789) require.Error(t, err) } func setupTest(t *testing.T) (*http.ServeMux, *Client) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client, err := NewClient("accountname", "apikey") require.NoError(t, err) client.baseURL, _ = url.Parse(server.URL) return mux, client } func mockHandler(method string, statusCode int, filename string) func(http.ResponseWriter, *http.Request) { return func(rw http.ResponseWriter, req *http.Request) { if req.Method != method { http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) return } if filename == "" { rw.WriteHeader(statusCode) return } file, err := os.Open(filepath.FromSlash(path.Join("./fixtures", filename))) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() rw.WriteHeader(statusCode) _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } } } lego-4.9.1/providers/dns/simply/internal/fixtures/000077500000000000000000000000001434020463500222575ustar00rootroot00000000000000lego-4.9.1/providers/dns/simply/internal/fixtures/add_record.json000066400000000000000000000001231434020463500252340ustar00rootroot00000000000000{ "status": 200, "message": "success", "record": { "id": 123456789 } } lego-4.9.1/providers/dns/simply/internal/fixtures/bad_auth_error.json000066400000000000000000000001021434020463500261230ustar00rootroot00000000000000{ "status": 400, "message": "Invalid account authorization" } lego-4.9.1/providers/dns/simply/internal/fixtures/bad_zone_error.json000066400000000000000000000001111434020463500261350ustar00rootroot00000000000000{ "status": 404, "message": "Unknown or invalid product reference" } lego-4.9.1/providers/dns/simply/internal/fixtures/get_records.json000066400000000000000000000012031434020463500254460ustar00rootroot00000000000000{ "status": 200, "message": "success", "records": [ { "record_id": 1, "name": "@", "ttl": 3600, "data": "ns1.simply.com", "type": "NS", "priority": 0 }, { "record_id": 2, "name": "@", "ttl": 3600, "data": "ns2.simply.com", "type": "NS", "priority": 0 }, { "record_id": 3, "name": "@", "ttl": 3600, "data": "ns3.simply.com", "type": "NS", "priority": 0 }, { "record_id": 4, "name": "@", "ttl": 3600, "data": "ns4.simply.com", "type": "NS", "priority": 0 } ] } lego-4.9.1/providers/dns/simply/internal/fixtures/invalid_record_id_error.json000066400000000000000000000000671434020463500300260ustar00rootroot00000000000000{ "status": 404, "message": "Unknown DNS record" } lego-4.9.1/providers/dns/simply/internal/fixtures/success.json000066400000000000000000000000541434020463500246210ustar00rootroot00000000000000{ "status": 200, "message": "success" } lego-4.9.1/providers/dns/simply/internal/types.go000066400000000000000000000012471434020463500221050ustar00rootroot00000000000000package internal import "encoding/json" // Record represents the content of a DNS record. type Record struct { ID int64 `json:"record_id,omitempty"` Name string `json:"name,omitempty"` Data string `json:"data,omitempty"` Type string `json:"type,omitempty"` TTL int `json:"ttl,omitempty"` Priority int `json:"priority,omitempty"` } // apiResponse represents an API response. type apiResponse struct { Status int `json:"status"` Message string `json:"message"` Records json.RawMessage `json:"records,omitempty"` Record json.RawMessage `json:"record,omitempty"` } type recordHeader struct { ID int64 `json:"id"` } lego-4.9.1/providers/dns/simply/simply.go000066400000000000000000000114121434020463500204350ustar00rootroot00000000000000// Package simply implements a DNS provider for solving the DNS-01 challenge using Simply.com. package simply import ( "errors" "fmt" "net/http" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/simply/internal" ) // Environment variables names. const ( envNamespace = "SIMPLY_" EnvAccountName = envNamespace + "ACCOUNT_NAME" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { AccountName string APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client recordIDs map[string]int64 recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for Simply.com. // Credentials must be passed in the environment variable: SIMPLY_ACCOUNT_NAME, SIMPLY_API_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAccountName, EnvAPIKey) if err != nil { return nil, fmt.Errorf("simply: %w", err) } config := NewDefaultConfig() config.AccountName = values[EnvAccountName] config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Simply.com. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("simply: the configuration of the DNS provider is nil") } if config.AccountName == "" { return nil, errors.New("simply: missing credentials: account name") } if config.APIKey == "" { return nil, errors.New("simply: missing credentials: api key") } client, err := internal.NewClient(config.AccountName, config.APIKey) if err != nil { return nil, fmt.Errorf("simply: failed to create client: %w", err) } if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]int64), }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("simply: could not determine zone for domain %q: %w", domain, err) } authZone = dns01.UnFqdn(authZone) subDomain := dns01.UnFqdn(strings.TrimSuffix(fqdn, authZone)) recordBody := internal.Record{ Name: subDomain, Data: value, Type: "TXT", TTL: d.config.TTL, } recordID, err := d.client.AddRecord(authZone, recordBody) if err != nil { return fmt.Errorf("simply: failed to add record: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = recordID d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("simply: could not determine zone for domain %q: %w", domain, err) } authZone = dns01.UnFqdn(authZone) // gets the record's unique ID from when we created it d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("simply: unknown record ID for '%s' '%s'", fqdn, token) } err = d.client.DeleteRecord(authZone, recordID) if err != nil { return fmt.Errorf("simply: failed to delete TXT records: fqdn=%s, recordID=%d: %w", fqdn, recordID, err) } // deletes record ID from map d.recordIDsMu.Lock() delete(d.recordIDs, token) d.recordIDsMu.Unlock() return nil } lego-4.9.1/providers/dns/simply/simply.toml000066400000000000000000000013261434020463500210060ustar00rootroot00000000000000Name = "Simply.com" Description = '''''' URL = "https://www.simply.com/en/domains/" Code = "simply" Since = "v4.4.0" Example = ''' SIMPLY_ACCOUNT_NAME=xxxxxx \ SIMPLY_API_KEY=yyyyyy \ lego --email you@example.com --dns simply --domains my.example.org run ''' [Configuration] [Configuration.Credentials] SIMPLY_ACCOUNT_NAME = "Account name" SIMPLY_API_KEY = "API key" [Configuration.Additional] SIMPLY_POLLING_INTERVAL = "Time between DNS propagation check" SIMPLY_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" SIMPLY_TTL = "The TTL of the TXT record used for the DNS challenge" SIMPLY_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.simply.com/en/docs/api/" lego-4.9.1/providers/dns/simply/simply_test.go000066400000000000000000000060311434020463500214750ustar00rootroot00000000000000package simply import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAccountName, EnvAPIKey).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAccountName: "S000000", EnvAPIKey: "secret", }, }, { desc: "missing credentials: account name", envVars: map[string]string{ EnvAccountName: "", EnvAPIKey: "secret", }, expected: "simply: some credentials information are missing: SIMPLY_ACCOUNT_NAME", }, { desc: "missing credentials: api key", envVars: map[string]string{ EnvAccountName: "S000000", EnvAPIKey: "", }, expected: "simply: some credentials information are missing: SIMPLY_API_KEY", }, { desc: "missing credentials: all", envVars: map[string]string{ EnvAccountName: "", EnvAPIKey: "", }, expected: "simply: some credentials information are missing: SIMPLY_ACCOUNT_NAME,SIMPLY_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string accountName string apiKey string expected string }{ { desc: "success", accountName: "S000000", apiKey: "secret", }, { desc: "missing account name", apiKey: "secret", expected: "simply: missing credentials: account name", }, { desc: "missing api key", accountName: "S000000", expected: "simply: missing credentials: api key", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.AccountName = test.accountName config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/sonic/000077500000000000000000000000001434020463500163705ustar00rootroot00000000000000lego-4.9.1/providers/dns/sonic/internal/000077500000000000000000000000001434020463500202045ustar00rootroot00000000000000lego-4.9.1/providers/dns/sonic/internal/client.go000066400000000000000000000041061434020463500220120ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "time" ) const baseURL = "https://public-api.sonic.net/dyndns" type APIResponse struct { Message string `json:"message"` Result int `json:"result"` } // Record holds the Sonic API representation of a Domain Record. type Record struct { UserID string `json:"userid"` APIKey string `json:"apikey"` Hostname string `json:"hostname"` Value string `json:"value"` TTL int `json:"ttl"` Type string `json:"type"` } // Client Sonic client. type Client struct { userID string apiKey string baseURL string HTTPClient *http.Client } // NewClient creates a Client. func NewClient(userID, apiKey string) (*Client, error) { if userID == "" || apiKey == "" { return nil, errors.New("credentials are missing") } return &Client{ userID: userID, apiKey: apiKey, baseURL: baseURL, HTTPClient: &http.Client{Timeout: 10 * time.Second}, }, nil } // SetRecord creates or updates a TXT records. // Sonic does not provide a delete record API endpoint. // https://public-api.sonic.net/dyndns#updating_or_adding_host_records func (c *Client) SetRecord(hostname string, value string, ttl int) error { payload := &Record{ UserID: c.userID, APIKey: c.apiKey, Hostname: hostname, Value: value, TTL: ttl, Type: "TXT", } body, err := json.Marshal(payload) if err != nil { return err } req, err := http.NewRequest(http.MethodPut, c.baseURL+"/host", bytes.NewReader(body)) if err != nil { return err } req.Header.Set("content-type", "application/json") resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer func() { _ = resp.Body.Close() }() raw, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response body: %w", err) } r := APIResponse{} err = json.Unmarshal(raw, &r) if err != nil { return fmt.Errorf("failed to unmarshal response: %w: %s", err, string(raw)) } if r.Result != 200 { return fmt.Errorf("API response code: %d, %s", r.Result, r.Message) } return nil } lego-4.9.1/providers/dns/sonic/internal/client_test.go000066400000000000000000000022031434020463500230450ustar00rootroot00000000000000package internal import ( "fmt" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/require" ) func setup(t *testing.T, body string) *Client { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/host", func(rw http.ResponseWriter, req *http.Request) { _, _ = fmt.Fprintln(rw, body) }) client, err := NewClient("foo", "secret") require.NoError(t, err) client.baseURL = server.URL return client } func TestClient_SetRecord(t *testing.T) { testCases := []struct { desc string response string assert require.ErrorAssertionFunc }{ { desc: "success", response: `{"message":"OK","result":200}`, assert: require.NoError, }, { desc: "failure", response: `{"message":"Not Found : the information you requested was not found.","result":404}`, assert: require.Error, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() client := setup(t, test.response) err := client.SetRecord("example.com", "txttxttxt", 10) test.assert(t, err) }) } } lego-4.9.1/providers/dns/sonic/sonic.go000066400000000000000000000075711434020463500200440ustar00rootroot00000000000000// Package sonic implements a DNS provider for solving the DNS-01 challenge using Sonic. package sonic import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/sonic/internal" ) // Environment variables names. const ( envNamespace = "SONIC_" EnvUserID = envNamespace + "USER_ID" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { UserID string APIKey string HTTPClient *http.Client PropagationTimeout time.Duration PollingInterval time.Duration SequenceInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for Sonic. // Credentials must be passed in the environment variables: // SONIC_USERID and SONIC_APIKEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvUserID, EnvAPIKey) if err != nil { return nil, fmt.Errorf("sonic: %w", err) } config := NewDefaultConfig() config.UserID = values[EnvUserID] config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Sonic. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("sonic: the configuration of the DNS provider is nil") } client, err := internal.NewClient(config.UserID, config.APIKey) if err != nil { return nil, fmt.Errorf("sonic: %w", err) } if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) err := d.client.SetRecord(dns01.UnFqdn(fqdn), value, d.config.TTL) if err != nil { return fmt.Errorf("sonic: unable to create record for %s: %w", fqdn, err) } return nil } // CleanUp removes the TXT records matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) err := d.client.SetRecord(dns01.UnFqdn(fqdn), "_", d.config.TTL) if err != nil { return fmt.Errorf("sonic: unable to clean record for %s: %w", fqdn, err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Sequential All DNS challenges for this provider will be resolved sequentially. // Returns the interval between each iteration. func (d *DNSProvider) Sequential() time.Duration { return d.config.SequenceInterval } lego-4.9.1/providers/dns/sonic/sonic.toml000066400000000000000000000027271434020463500204100ustar00rootroot00000000000000Name = "Sonic" Description = '''''' URL = "https://www.sonic.com/" Code = "sonic" Since = "v4.4.0" Example = ''' SONIC_USER_ID=12345 \ SONIC_API_KEY=4d6fbf2f9ab0fa11697470918d37625851fc0c51 \ lego --email you@example.com --dns sonic --domains my.example.org run ''' Additional = ''' ## API keys The API keys must be generated by calling the `dyndns/api_key` endpoint. Example: ```bash $ curl -X POST -H "Content-Type: application/json" --data '{"username":"notarealuser","password":"notarealpassword","hostname":"example.com"}' https://public-api.sonic.net/dyndns/api_key {"userid":"12345","apikey":"4d6fbf2f9ab0fa11697470918d37625851fc0c51","result":200,"message":"OK"} ``` See https://public-api.sonic.net/dyndns/#requesting_an_api_key for additional details. This `userid` and `apikey` combo allow modifications to any DNS entries connected to the managed domain (hostname). Hostname should be the toplevel domain managed e.g `example.com` not `www.example.com`. ''' [Configuration] [Configuration.Credentials] SONIC_USER_ID = "User ID" SONIC_API_KEY = "API Key" [Configuration.Additional] SONIC_POLLING_INTERVAL = "Time between DNS propagation check" SONIC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" SONIC_TTL = "The TTL of the TXT record used for the DNS challenge" SONIC_HTTP_TIMEOUT = "API request timeout" SONIC_SEQUENCE_INTERVAL = "Time between sequential requests" [Links] API = "https://public-api.sonic.net/dyndns/" lego-4.9.1/providers/dns/sonic/sonic_test.go000066400000000000000000000055251434020463500211000ustar00rootroot00000000000000package sonic import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAPIKey, EnvUserID). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvUserID: "dummy", EnvAPIKey: "dummy", }, }, { desc: "missing all credentials", envVars: map[string]string{}, expected: "sonic: some credentials information are missing: SONIC_USER_ID,SONIC_API_KEY", }, { desc: "no userid", envVars: map[string]string{ EnvAPIKey: "dummy", }, expected: "sonic: some credentials information are missing: SONIC_USER_ID", }, { desc: "no apikey", envVars: map[string]string{ EnvUserID: "dummy", }, expected: `sonic: some credentials information are missing: SONIC_API_KEY`, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string userID string apiKey string expected string }{ { desc: "success", userID: "dummy", apiKey: "dummy", }, { desc: "missing all credentials", expected: "sonic: credentials are missing", }, { desc: "missing userid", apiKey: "dummy", expected: "sonic: credentials are missing", }, { desc: "missing apikey", userID: "dummy", expected: "sonic: credentials are missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.UserID = test.userID config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() assert.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") assert.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() assert.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") assert.NoError(t, err) } lego-4.9.1/providers/dns/stackpath/000077500000000000000000000000001434020463500172375ustar00rootroot00000000000000lego-4.9.1/providers/dns/stackpath/client.go000066400000000000000000000111461434020463500210470ustar00rootroot00000000000000package stackpath import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "path" "github.com/go-acme/lego/v4/challenge/dns01" "golang.org/x/net/publicsuffix" ) // Zones is the response struct from the Stackpath api GetZones. type Zones struct { Zones []Zone `json:"zones"` } // Zone a DNS zone representation. type Zone struct { ID string Domain string } // Records is the response struct from the Stackpath api GetZoneRecords. type Records struct { Records []Record `json:"records"` } // Record a DNS record representation. type Record struct { ID string `json:"id,omitempty"` Name string `json:"name"` Type string `json:"type"` TTL int `json:"ttl"` Data string `json:"data"` } // ErrorResponse the API error response representation. type ErrorResponse struct { Code int `json:"code"` Message string `json:"error"` } func (e *ErrorResponse) Error() string { return fmt.Sprintf("%d %s", e.Code, e.Message) } // https://developer.stackpath.com/en/api/dns/#operation/GetZones func (d *DNSProvider) getZones(domain string) (*Zone, error) { tld, err := publicsuffix.EffectiveTLDPlusOne(dns01.UnFqdn(domain)) if err != nil { return nil, err } req, err := d.newRequest(http.MethodGet, "/zones", nil) if err != nil { return nil, err } query := req.URL.Query() query.Add("page_request.filter", fmt.Sprintf("domain='%s'", tld)) req.URL.RawQuery = query.Encode() var zones Zones err = d.do(req, &zones) if err != nil { return nil, err } if len(zones.Zones) == 0 { return nil, fmt.Errorf("did not find zone with domain %s", domain) } return &zones.Zones[0], nil } // https://developer.stackpath.com/en/api/dns/#operation/GetZoneRecords func (d *DNSProvider) getZoneRecords(name string, zone *Zone) ([]Record, error) { u := fmt.Sprintf("/zones/%s/records", zone.ID) req, err := d.newRequest(http.MethodGet, u, nil) if err != nil { return nil, err } query := req.URL.Query() query.Add("page_request.filter", fmt.Sprintf("name='%s' and type='TXT'", name)) req.URL.RawQuery = query.Encode() var records Records err = d.do(req, &records) if err != nil { return nil, err } if len(records.Records) == 0 { return nil, fmt.Errorf("did not find record with name %s", name) } return records.Records, nil } // https://developer.stackpath.com/en/api/dns/#operation/CreateZoneRecord func (d *DNSProvider) createZoneRecord(zone *Zone, record Record) error { u := fmt.Sprintf("/zones/%s/records", zone.ID) req, err := d.newRequest(http.MethodPost, u, record) if err != nil { return err } return d.do(req, nil) } // https://developer.stackpath.com/en/api/dns/#operation/DeleteZoneRecord func (d *DNSProvider) deleteZoneRecord(zone *Zone, record Record) error { u := fmt.Sprintf("/zones/%s/records/%s", zone.ID, record.ID) req, err := d.newRequest(http.MethodDelete, u, nil) if err != nil { return err } return d.do(req, nil) } func (d *DNSProvider) newRequest(method, urlStr string, body interface{}) (*http.Request, error) { u, err := d.BaseURL.Parse(path.Join(d.config.StackID, urlStr)) if err != nil { return nil, err } if body == nil { var req *http.Request req, err = http.NewRequest(method, u.String(), nil) if err != nil { return nil, err } return req, nil } reqBody, err := json.Marshal(body) if err != nil { return nil, err } req, err := http.NewRequest(method, u.String(), bytes.NewBuffer(reqBody)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") return req, nil } func (d *DNSProvider) do(req *http.Request, v interface{}) error { resp, err := d.client.Do(req) if err != nil { return err } err = checkResponse(resp) if err != nil { return err } if v == nil { return nil } raw, err := readBody(resp) if err != nil { return fmt.Errorf("failed to read body: %w", err) } err = json.Unmarshal(raw, v) if err != nil { return fmt.Errorf("unmarshaling error: %w: %s", err, string(raw)) } return nil } func checkResponse(resp *http.Response) error { if resp.StatusCode > 299 { data, err := readBody(resp) if err != nil { return &ErrorResponse{Code: resp.StatusCode, Message: err.Error()} } errResp := &ErrorResponse{} err = json.Unmarshal(data, errResp) if err != nil { return &ErrorResponse{Code: resp.StatusCode, Message: fmt.Sprintf("unmarshaling error: %v: %s", err, string(data))} } return errResp } return nil } func readBody(resp *http.Response) ([]byte, error) { if resp.Body == nil { return nil, errors.New("response body is nil") } defer resp.Body.Close() rawBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return rawBody, nil } lego-4.9.1/providers/dns/stackpath/stackpath.go000066400000000000000000000111041434020463500215450ustar00rootroot00000000000000// Package stackpath implements a DNS provider for solving the DNS-01 challenge using Stackpath DNS. // https://developer.stackpath.com/en/api/dns/ package stackpath import ( "context" "errors" "fmt" "net/http" "net/url" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "golang.org/x/oauth2/clientcredentials" ) const ( defaultBaseURL = "https://gateway.stackpath.com/dns/v1/stacks/" defaultAuthURL = "https://gateway.stackpath.com/identity/v1/oauth2/token" ) // Environment variables names. const ( envNamespace = "STACKPATH_" EnvClientID = envNamespace + "CLIENT_ID" EnvClientSecret = envNamespace + "CLIENT_SECRET" EnvStackID = envNamespace + "STACK_ID" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { ClientID string ClientSecret string StackID string TTL int PropagationTimeout time.Duration PollingInterval time.Duration } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 120), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { BaseURL *url.URL client *http.Client config *Config } // NewDNSProvider returns a DNSProvider instance configured for Stackpath. // Credentials must be passed in the environment variables: // STACKPATH_CLIENT_ID, STACKPATH_CLIENT_SECRET, and STACKPATH_STACK_ID. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvClientID, EnvClientSecret, EnvStackID) if err != nil { return nil, fmt.Errorf("stackpath: %w", err) } config := NewDefaultConfig() config.ClientID = values[EnvClientID] config.ClientSecret = values[EnvClientSecret] config.StackID = values[EnvStackID] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Stackpath. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("stackpath: the configuration of the DNS provider is nil") } if config.ClientID == "" || config.ClientSecret == "" { return nil, errors.New("stackpath: credentials missing") } if config.StackID == "" { return nil, errors.New("stackpath: stack id missing") } baseURL, _ := url.Parse(defaultBaseURL) return &DNSProvider{ BaseURL: baseURL, client: getOathClient(config), config: config, }, nil } func getOathClient(config *Config) *http.Client { oathConfig := &clientcredentials.Config{ TokenURL: defaultAuthURL, ClientID: config.ClientID, ClientSecret: config.ClientSecret, } return oathConfig.Client(context.Background()) } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getZones(fqdn) if err != nil { return fmt.Errorf("stackpath: %w", err) } record := Record{ Name: extractRecordName(fqdn, zone.Domain), Type: "TXT", TTL: d.config.TTL, Data: value, } return d.createZoneRecord(zone, record) } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := d.getZones(fqdn) if err != nil { return fmt.Errorf("stackpath: %w", err) } recordName := extractRecordName(fqdn, zone.Domain) records, err := d.getZoneRecords(recordName, zone) if err != nil { return err } for _, record := range records { err = d.deleteZoneRecord(zone, record) if err != nil { log.Printf("stackpath: failed to delete TXT record: %v", err) } } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func extractRecordName(fqdn, zone string) string { name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+zone); idx != -1 { return name[:idx] } return name } lego-4.9.1/providers/dns/stackpath/stackpath.toml000066400000000000000000000014171434020463500221210ustar00rootroot00000000000000Name = "Stackpath" Description = '''''' URL = "https://www.stackpath.com/" Code = "stackpath" Since = "v1.1.0" Example = ''' STACKPATH_CLIENT_ID=xxxxx \ STACKPATH_CLIENT_SECRET=yyyyy \ STACKPATH_STACK_ID=zzzzz \ lego --email you@example.com --dns stackpath --domains my.example.org run ''' [Configuration] [Configuration.Credentials] STACKPATH_CLIENT_ID = "Client ID" STACKPATH_CLIENT_SECRET = "Client secret" STACKPATH_STACK_ID = "Stack ID" [Configuration.Additional] STACKPATH_POLLING_INTERVAL = "Time between DNS propagation check" STACKPATH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" STACKPATH_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://developer.stackpath.com/en/api/dns/#tag/Zone" lego-4.9.1/providers/dns/stackpath/stackpath_test.go000066400000000000000000000147201434020463500226130ustar00rootroot00000000000000package stackpath import ( "net/http" "net/http/httptest" "net/url" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvClientID, EnvClientSecret, EnvStackID). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvClientID: "test@example.com", EnvClientSecret: "123", EnvStackID: "ID", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvClientID: "", EnvClientSecret: "", EnvStackID: "", }, expected: "stackpath: some credentials information are missing: STACKPATH_CLIENT_ID,STACKPATH_CLIENT_SECRET,STACKPATH_STACK_ID", }, { desc: "missing client id", envVars: map[string]string{ EnvClientID: "", EnvClientSecret: "123", EnvStackID: "ID", }, expected: "stackpath: some credentials information are missing: STACKPATH_CLIENT_ID", }, { desc: "missing client secret", envVars: map[string]string{ EnvClientID: "test@example.com", EnvClientSecret: "", EnvStackID: "ID", }, expected: "stackpath: some credentials information are missing: STACKPATH_CLIENT_SECRET", }, { desc: "missing stack id", envVars: map[string]string{ EnvClientID: "test@example.com", EnvClientSecret: "123", EnvStackID: "", }, expected: "stackpath: some credentials information are missing: STACKPATH_STACK_ID", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) assert.NotNil(t, p) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := map[string]struct { config *Config expectedErr string }{ "no_config": { config: nil, expectedErr: "stackpath: the configuration of the DNS provider is nil", }, "no_client_id": { config: &Config{ ClientSecret: "secret", StackID: "stackID", }, expectedErr: "stackpath: credentials missing", }, "no_client_secret": { config: &Config{ ClientID: "clientID", StackID: "stackID", }, expectedErr: "stackpath: credentials missing", }, "no_stack_id": { config: &Config{ ClientID: "clientID", ClientSecret: "secret", }, expectedErr: "stackpath: stack id missing", }, } for desc, test := range testCases { test := test t.Run(desc, func(t *testing.T) { t.Parallel() p, err := NewDNSProviderConfig(test.config) require.EqualError(t, err, test.expectedErr) assert.Nil(t, p) }) } } func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) config := NewDefaultConfig() config.ClientID = "CLIENT_ID" config.ClientSecret = "CLIENT_SECRET" config.StackID = "STACK_ID" provider, err := NewDNSProviderConfig(config) require.NoError(t, err) provider.client = http.DefaultClient provider.BaseURL, _ = url.Parse(server.URL + "/") return provider, mux } func TestDNSProvider_getZoneRecords(t *testing.T) { provider, mux := setupTest(t) mux.HandleFunc("/STACK_ID/zones/A/records", func(w http.ResponseWriter, _ *http.Request) { content := ` { "records": [ {"id":"1","name":"foo1","type":"TXT","ttl":120,"data":"txtTXTtxt"}, {"id":"2","name":"foo2","type":"TXT","ttl":121,"data":"TXTtxtTXT"} ] }` _, err := w.Write([]byte(content)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) records, err := provider.getZoneRecords("foo1", &Zone{ID: "A", Domain: "test"}) require.NoError(t, err) expected := []Record{ {ID: "1", Name: "foo1", Type: "TXT", TTL: 120, Data: "txtTXTtxt"}, {ID: "2", Name: "foo2", Type: "TXT", TTL: 121, Data: "TXTtxtTXT"}, } assert.Equal(t, expected, records) } func TestDNSProvider_getZoneRecords_apiError(t *testing.T) { provider, mux := setupTest(t) mux.HandleFunc("/STACK_ID/zones/A/records", func(w http.ResponseWriter, _ *http.Request) { content := ` { "code": 401, "error": "an unauthorized request is attempted." }` w.WriteHeader(http.StatusUnauthorized) _, err := w.Write([]byte(content)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) _, err := provider.getZoneRecords("foo1", &Zone{ID: "A", Domain: "test"}) expected := &ErrorResponse{Code: 401, Message: "an unauthorized request is attempted."} assert.Equal(t, expected, err) } func TestDNSProvider_getZones(t *testing.T) { provider, mux := setupTest(t) mux.HandleFunc("/STACK_ID/zones", func(w http.ResponseWriter, _ *http.Request) { content := ` { "pageInfo": { "totalCount": "5", "hasPreviousPage": false, "hasNextPage": false, "startCursor": "1", "endCursor": "1" }, "zones": [ { "stackId": "my_stack", "accountId": "my_account", "id": "A", "domain": "foo.com", "version": "1", "labels": { "property1": "val1", "property2": "val2" }, "created": "2018-10-07T02:31:49Z", "updated": "2018-10-07T02:31:49Z", "nameservers": [ "1.1.1.1" ], "verified": "2018-10-07T02:31:49Z", "status": "ACTIVE", "disabled": false } ] }` _, err := w.Write([]byte(content)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) zone, err := provider.getZones("sub.foo.com") require.NoError(t, err) expected := &Zone{ID: "A", Domain: "foo.com"} assert.Equal(t, expected, zone) } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/tencentcloud/000077500000000000000000000000001434020463500177445ustar00rootroot00000000000000lego-4.9.1/providers/dns/tencentcloud/client.go000066400000000000000000000044751434020463500215630ustar00rootroot00000000000000package tencentcloud import ( "errors" "fmt" "strings" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" errorsdk "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" "golang.org/x/net/idna" ) func (d *DNSProvider) getHostedZone(domain string) (*dnspod.DomainListItem, error) { request := dnspod.NewDescribeDomainListRequest() var domains []*dnspod.DomainListItem for { response, err := d.client.DescribeDomainList(request) if err != nil { return nil, fmt.Errorf("API call failed: %w", err) } domains = append(domains, response.Response.DomainList...) if uint64(len(domains)) >= *response.Response.DomainCountInfo.AllTotal { break } request.Offset = common.Int64Ptr(int64(len(domains))) } authZone, err := dns01.FindZoneByFqdn(domain) if err != nil { return nil, err } var hostedZone *dnspod.DomainListItem for _, zone := range domains { if *zone.Name == dns01.UnFqdn(authZone) { hostedZone = zone } } if hostedZone == nil { return nil, fmt.Errorf("zone %s not found in dnspod for domain %s", authZone, domain) } return hostedZone, nil } func (d *DNSProvider) findTxtRecords(zone *dnspod.DomainListItem, fqdn string) ([]*dnspod.RecordListItem, error) { recordName, err := extractRecordName(fqdn, *zone.Name) if err != nil { return nil, err } request := dnspod.NewDescribeRecordListRequest() request.Domain = zone.Name request.DomainId = zone.DomainId request.Subdomain = common.StringPtr(recordName) request.RecordType = common.StringPtr("TXT") request.RecordLine = common.StringPtr("默认") response, err := d.client.DescribeRecordList(request) if err != nil { var sdkError *errorsdk.TencentCloudSDKError if errors.As(err, &sdkError) { if sdkError.Code == dnspod.RESOURCENOTFOUND_NODATAOFRECORD { return nil, nil } } return nil, err } return response.Response.RecordList, nil } func extractRecordName(fqdn, zone string) (string, error) { asciiDomain, err := idna.ToASCII(zone) if err != nil { return "", fmt.Errorf("fail to convert punycode: %w", err) } name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+asciiDomain); idx != -1 { return name[:idx], nil } return name, nil } lego-4.9.1/providers/dns/tencentcloud/tencentcloud.go000066400000000000000000000127061434020463500227700ustar00rootroot00000000000000// Package tencentcloud implements a DNS provider for solving the DNS-01 challenge using Tencent Cloud DNS. package tencentcloud import ( "errors" "fmt" "math" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" ) // Environment variables names. const ( envNamespace = "TENCENTCLOUD_" EnvSecretID = envNamespace + "SECRET_ID" EnvSecretKey = envNamespace + "SECRET_KEY" EnvRegion = envNamespace + "REGION" EnvSessionToken = envNamespace + "SESSION_TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { SecretID string SecretKey string Region string SessionToken string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPTimeout time.Duration } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *dnspod.Client } // NewDNSProvider returns a DNSProvider instance configured for Tencent Cloud DNS. // Credentials must be passed in the environment variable: TENCENTCLOUD_SECRET_ID, TENCENTCLOUD_SECRET_KEY. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvSecretID, EnvSecretKey) if err != nil { return nil, fmt.Errorf("tencentcloud: %w", err) } config := NewDefaultConfig() config.SecretID = values[EnvSecretID] config.SecretKey = values[EnvSecretKey] config.Region = env.GetOrDefaultString(EnvRegion, "") config.SessionToken = env.GetOrDefaultString(EnvSessionToken, "") return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Tencent Cloud DNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("tencentcloud: the configuration of the DNS provider is nil") } var credential *common.Credential switch { case config.SecretID != "" && config.SecretKey != "" && config.SessionToken != "": credential = common.NewTokenCredential(config.SecretID, config.SecretKey, config.SessionToken) case config.SecretID != "" && config.SecretKey != "": credential = common.NewCredential(config.SecretID, config.SecretKey) default: return nil, errors.New("tencentcloud: credentials missing") } cpf := profile.NewClientProfile() cpf.HttpProfile.Endpoint = "dnspod.tencentcloudapi.com" cpf.HttpProfile.ReqTimeout = int(math.Round(config.HTTPTimeout.Seconds())) client, err := dnspod.NewClient(credential, config.Region, cpf) if err != nil { return nil, fmt.Errorf("tencentcloud: %w", err) } return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("tencentcloud: failed to get hosted zone: %w", err) } recordName, err := extractRecordName(fqdn, *zone.Name) if err != nil { return fmt.Errorf("tencentcloud: failed to extract record name: %w", err) } request := dnspod.NewCreateRecordRequest() request.Domain = zone.Name request.DomainId = zone.DomainId request.SubDomain = common.StringPtr(recordName) request.RecordType = common.StringPtr("TXT") request.RecordLine = common.StringPtr("默认") request.Value = common.StringPtr(value) request.TTL = common.Uint64Ptr(uint64(d.config.TTL)) _, err = d.client.CreateRecord(request) if err != nil { return fmt.Errorf("dnspod: API call failed: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("tencentcloud: failed to get hosted zone: %w", err) } records, err := d.findTxtRecords(zone, fqdn) if err != nil { return fmt.Errorf("tencentcloud: failed to find TXT records: %w", err) } for _, record := range records { request := dnspod.NewDeleteRecordRequest() request.Domain = zone.Name request.DomainId = zone.DomainId request.RecordId = record.RecordId _, err := d.client.DeleteRecord(request) if err != nil { return fmt.Errorf("tencentcloud: delete record failed: %w", err) } } return nil } lego-4.9.1/providers/dns/tencentcloud/tencentcloud.toml000066400000000000000000000017561434020463500233410ustar00rootroot00000000000000Name = "Tencent Cloud DNS" Description = '''''' URL = "https://cloud.tencent.com/product/cns" Code = "tencentcloud" Since = "v4.6.0" Example = ''' TENCENTCLOUD_SECRET_ID=abcdefghijklmnopqrstuvwx \ TENCENTCLOUD_SECRET_KEY=your-secret-key \ lego --email you@example.com --dns tencentcloud --domains my.example.org run ''' [Configuration] [Configuration.Credentials] TENCENTCLOUD_SECRET_ID = "Access key ID" TENCENTCLOUD_SECRET_KEY = "Access Key secret" [Configuration.Additional] TENCENTCLOUD_SESSION_TOKEN = "Access Key token" TENCENTCLOUD_REGION = "Region" TENCENTCLOUD_POLLING_INTERVAL = "Time between DNS propagation check" TENCENTCLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" TENCENTCLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" TENCENTCLOUD_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://cloud.tencent.com/document/product/1427/56153" GoClient = "https://github.com/tencentcloud/tencentcloud-sdk-go" lego-4.9.1/providers/dns/tencentcloud/tencentcloud_test.go000066400000000000000000000061461434020463500240300ustar00rootroot00000000000000package tencentcloud import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvSecretID, EnvSecretKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvSecretID: "123", EnvSecretKey: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvSecretID: "", EnvSecretKey: "", }, expected: "tencentcloud: some credentials information are missing: TENCENTCLOUD_SECRET_ID,TENCENTCLOUD_SECRET_KEY", }, { desc: "missing access id", envVars: map[string]string{ EnvSecretID: "", EnvSecretKey: "456", }, expected: "tencentcloud: some credentials information are missing: TENCENTCLOUD_SECRET_ID", }, { desc: "missing secret key", envVars: map[string]string{ EnvSecretID: "123", EnvSecretKey: "", }, expected: "tencentcloud: some credentials information are missing: TENCENTCLOUD_SECRET_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string secretID string secretKey string expected string }{ { desc: "success", secretID: "123", secretKey: "456", }, { desc: "missing credentials", expected: "tencentcloud: credentials missing", }, { desc: "missing secret id", secretKey: "456", expected: "tencentcloud: credentials missing", }, { desc: "missing secret key", secretID: "123", expected: "tencentcloud: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.SecretID = test.secretID config.SecretKey = test.secretKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/transip/000077500000000000000000000000001434020463500167355ustar00rootroot00000000000000lego-4.9.1/providers/dns/transip/fakeclient_test.go000066400000000000000000000053251434020463500224350ustar00rootroot00000000000000package transip import ( "encoding/json" "fmt" "time" "github.com/transip/gotransip/v6/domain" "github.com/transip/gotransip/v6/rest" ) type dnsEntryWrapper struct { DNSEntry domain.DNSEntry `json:"dnsEntry"` } type dnsEntriesWrapper struct { DNSEntries []domain.DNSEntry `json:"dnsEntries"` } type fakeClient struct { dnsEntries []domain.DNSEntry setDNSEntriesLatency time.Duration getInfoLatency time.Duration domainName string } func (f *fakeClient) Get(request rest.Request, dest interface{}) error { if f.getInfoLatency != 0 { time.Sleep(f.getInfoLatency) } if request.Endpoint != fmt.Sprintf("/domains/%s/dns", f.domainName) { return fmt.Errorf("function GET for endpoint %s not implemented", request.Endpoint) } entries := dnsEntriesWrapper{DNSEntries: f.dnsEntries} body, err := json.Marshal(entries) if err != nil { return fmt.Errorf("can't encode json: %w", err) } err = json.Unmarshal(body, dest) if err != nil { return fmt.Errorf("can't decode json: %w", err) } return nil } func (f *fakeClient) Put(request rest.Request) error { if f.getInfoLatency != 0 { time.Sleep(f.getInfoLatency) } return fmt.Errorf("function PUT for endpoint %s not implemented", request.Endpoint) } func (f *fakeClient) Post(request rest.Request) error { if f.getInfoLatency != 0 { time.Sleep(f.getInfoLatency) } if request.Endpoint != fmt.Sprintf("/domains/%s/dns", f.domainName) { return fmt.Errorf("function POST for endpoint %s not implemented", request.Endpoint) } body, err := request.GetJSONBody() if err != nil { return fmt.Errorf("unable get request body") } var entry dnsEntryWrapper if err := json.Unmarshal(body, &entry); err != nil { return fmt.Errorf("unable to decode request body") } f.dnsEntries = append(f.dnsEntries, entry.DNSEntry) return nil } func (f *fakeClient) Delete(request rest.Request) error { if f.getInfoLatency != 0 { time.Sleep(f.getInfoLatency) } if request.Endpoint != fmt.Sprintf("/domains/%s/dns", f.domainName) { return fmt.Errorf("function DELETE for endpoint %s not implemented", request.Endpoint) } body, err := request.GetJSONBody() if err != nil { return fmt.Errorf("unable get request body") } var entry dnsEntryWrapper if err := json.Unmarshal(body, &entry); err != nil { return fmt.Errorf("unable to decode request body") } cp := make([]domain.DNSEntry, 0) for _, e := range f.dnsEntries { if e.Name == entry.DNSEntry.Name { continue } cp = append(cp, e) } f.dnsEntries = cp return nil } func (f *fakeClient) Patch(request rest.Request) error { if f.getInfoLatency != 0 { time.Sleep(f.getInfoLatency) } return fmt.Errorf("function PATCH for endpoint %s not implemented", request.Endpoint) } lego-4.9.1/providers/dns/transip/fixtures/000077500000000000000000000000001434020463500206065ustar00rootroot00000000000000lego-4.9.1/providers/dns/transip/fixtures/private.key000066400000000000000000000000001434020463500227600ustar00rootroot00000000000000lego-4.9.1/providers/dns/transip/transip.go000066400000000000000000000104371434020463500207510ustar00rootroot00000000000000// Package transip implements a DNS provider for solving the DNS-01 challenge using TransIP. package transip import ( "errors" "fmt" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/transip/gotransip/v6" transipdomain "github.com/transip/gotransip/v6/domain" ) // Environment variables names. const ( envNamespace = "TRANSIP_" EnvAccountName = envNamespace + "ACCOUNT_NAME" EnvPrivateKeyPath = envNamespace + "PRIVATE_KEY_PATH" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { AccountName string PrivateKeyPath string PropagationTimeout time.Duration PollingInterval time.Duration TTL int64 } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: int64(env.GetOrDefaultInt(EnvTTL, 10)), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config repository transipdomain.Repository } // NewDNSProvider returns a DNSProvider instance configured for TransIP. // Credentials must be passed in the environment variables: // TRANSIP_ACCOUNTNAME, TRANSIP_PRIVATEKEYPATH. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAccountName, EnvPrivateKeyPath) if err != nil { return nil, fmt.Errorf("transip: %w", err) } config := NewDefaultConfig() config.AccountName = values[EnvAccountName] config.PrivateKeyPath = values[EnvPrivateKeyPath] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for TransIP. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("transip: the configuration of the DNS provider is nil") } client, err := gotransip.NewClient(gotransip.ClientConfiguration{ AccountName: config.AccountName, PrivateKeyPath: config.PrivateKeyPath, }) if err != nil { return nil, fmt.Errorf("transip: %w", err) } repo := transipdomain.Repository{Client: client} return &DNSProvider{repository: repo, config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return err } domainName := dns01.UnFqdn(authZone) // get the subDomain subDomain := strings.TrimSuffix(dns01.UnFqdn(fqdn), "."+domainName) entry := transipdomain.DNSEntry{ Name: subDomain, Expire: int(d.config.TTL), Type: "TXT", Content: value, } err = d.repository.AddDNSEntry(domainName, entry) if err != nil { return fmt.Errorf("transip: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return err } domainName := dns01.UnFqdn(authZone) // get the subDomain subDomain := strings.TrimSuffix(dns01.UnFqdn(fqdn), "."+domainName) // get all DNS entries dnsEntries, err := d.repository.GetDNSEntries(domainName) if err != nil { return fmt.Errorf("transip: error for %s in CleanUp: %w", fqdn, err) } // loop through the existing entries and remove the specific record for _, entry := range dnsEntries { if entry.Name == subDomain && entry.Content == value { if err = d.repository.RemoveDNSEntry(domainName, entry); err != nil { return fmt.Errorf("transip: couldn't get Record ID in CleanUp: %w", err) } return nil } } return nil } lego-4.9.1/providers/dns/transip/transip.toml000066400000000000000000000014061434020463500213130ustar00rootroot00000000000000Name = "TransIP" Description = '''''' URL = "https://www.transip.nl/" Code = "transip" Since = "v2.0.0" Example = ''' TRANSIP_ACCOUNT_NAME = "Account name" \ TRANSIP_PRIVATE_KEY_PATH = "transip.key" \ lego --email you@example.com --dns transip --domains my.example.org run ''' [Configuration] [Configuration.Credentials] TRANSIP_ACCOUNT_NAME = "Account name" TRANSIP_PRIVATE_KEY_PATH = "Private key path" [Configuration.Additional] TRANSIP_POLLING_INTERVAL = "Time between DNS propagation check" TRANSIP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" TRANSIP_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://api.transip.eu/rest/docs.html" GoClient = "https://github.com/transip/gotransip" lego-4.9.1/providers/dns/transip/transip_test.go000066400000000000000000000146471434020463500220170ustar00rootroot00000000000000package transip import ( "fmt" "os" "strings" "sync" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/transip/gotransip/v6/domain" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest( EnvAccountName, EnvPrivateKeyPath). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAccountName: "johndoe", EnvPrivateKeyPath: "./fixtures/private.key", }, }, { desc: "missing all credentials", envVars: map[string]string{ EnvAccountName: "", EnvPrivateKeyPath: "", }, expected: "transip: some credentials information are missing: TRANSIP_ACCOUNT_NAME,TRANSIP_PRIVATE_KEY_PATH", }, { desc: "missing account name", envVars: map[string]string{ EnvAccountName: "", EnvPrivateKeyPath: "./fixtures/private.key", }, expected: "transip: some credentials information are missing: TRANSIP_ACCOUNT_NAME", }, { desc: "missing private key path", envVars: map[string]string{ EnvAccountName: "johndoe", EnvPrivateKeyPath: "", }, expected: "transip: some credentials information are missing: TRANSIP_PRIVATE_KEY_PATH", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.repository) } else { require.EqualError(t, err, test.expected) } }) } // The error message for a file not existing is different on Windows and Linux. // Therefore we test if the error type is the same. t.Run("could not open private key path", func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(map[string]string{ EnvAccountName: "johndoe", EnvPrivateKeyPath: "./fixtures/non/existent/private.key", }) _, err := NewDNSProvider() assert.ErrorIs(t, err, os.ErrNotExist) }) } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string accountName string privateKeyPath string expected string }{ { desc: "success", accountName: "johndoe", privateKeyPath: "./fixtures/private.key", }, { desc: "missing all credentials", expected: "transip: AccountName is required", }, { desc: "missing account name", privateKeyPath: "./fixtures/private.key", expected: "transip: AccountName is required", }, { desc: "missing private key path", accountName: "johndoe", expected: "transip: PrivateKeyReader, token or PrivateKeyReader is required", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.AccountName = test.accountName config.PrivateKeyPath = test.privateKeyPath p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.repository) } else { require.EqualError(t, err, test.expected) } }) } // The error message for a file not existing is different on Windows and Linux. // Therefore we test if the error type is the same. t.Run("could not open private key path", func(t *testing.T) { config := NewDefaultConfig() config.AccountName = "johndoe" config.PrivateKeyPath = "./fixtures/non/existent/private.key" _, err := NewDNSProviderConfig(config) assert.ErrorIs(t, err, os.ErrNotExist) }) } func TestDNSProvider_concurrentGetDNSEntries(t *testing.T) { client := &fakeClient{ getInfoLatency: 50 * time.Millisecond, setDNSEntriesLatency: 500 * time.Millisecond, domainName: "lego.wtf", } repo := domain.Repository{Client: client} p := &DNSProvider{ config: NewDefaultConfig(), repository: repo, } var wg sync.WaitGroup wg.Add(2) solve := func(domain1, suffix string, timeoutPresent, timeoutSolve, timeoutCleanup time.Duration) error { time.Sleep(timeoutPresent) err := p.Present(domain1, "", "") if err != nil { return err } time.Sleep(timeoutSolve) var found bool for _, entry := range client.dnsEntries { if strings.HasSuffix(entry.Name, suffix) { found = true } } if !found { return fmt.Errorf("record %s not found: %v", suffix, client.dnsEntries) } time.Sleep(timeoutCleanup) return p.CleanUp(domain1, "", "") } go func() { defer wg.Done() err := solve("bar.lego.wtf", ".bar", 500*time.Millisecond, 100*time.Millisecond, 100*time.Millisecond) require.NoError(t, err) }() go func() { defer wg.Done() err := solve("foo.lego.wtf", ".foo", 500*time.Millisecond, 200*time.Millisecond, 100*time.Millisecond) require.NoError(t, err) }() wg.Wait() assert.Empty(t, client.dnsEntries) } func TestDNSProvider_concurrentAddDNSEntry(t *testing.T) { client := &fakeClient{ domainName: "lego.wtf", } repo := domain.Repository{Client: client} p := &DNSProvider{ config: NewDefaultConfig(), repository: repo, } var wg sync.WaitGroup wg.Add(2) solve := func(domain1 string, timeoutPresent, timeoutCleanup time.Duration) error { time.Sleep(timeoutPresent) err := p.Present(domain1, "", "") if err != nil { return err } time.Sleep(timeoutCleanup) return p.CleanUp(domain1, "", "") } go func() { defer wg.Done() err := solve("bar.lego.wtf", 550*time.Millisecond, 500*time.Millisecond) require.NoError(t, err) }() go func() { defer wg.Done() err := solve("foo.lego.wtf", 500*time.Millisecond, 100*time.Millisecond) require.NoError(t, err) }() wg.Wait() assert.Empty(t, client.dnsEntries) } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/variomedia/000077500000000000000000000000001434020463500173755ustar00rootroot00000000000000lego-4.9.1/providers/dns/variomedia/internal/000077500000000000000000000000001434020463500212115ustar00rootroot00000000000000lego-4.9.1/providers/dns/variomedia/internal/client.go000066400000000000000000000051671434020463500230270ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "path" "time" ) const defaultBaseURL = "https://api.variomedia.de" type Client struct { apiToken string baseURL *url.URL HTTPClient *http.Client } func NewClient(apiToken string) *Client { baseURL, _ := url.Parse(defaultBaseURL) return &Client{ apiToken: apiToken, baseURL: baseURL, HTTPClient: &http.Client{Timeout: 10 * time.Second}, } } func (c Client) CreateDNSRecord(record DNSRecord) (*CreateDNSRecordResponse, error) { endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "dns-records")) if err != nil { return nil, err } data := CreateDNSRecordRequest{Data: Data{ Type: "dns-record", Attributes: record, }} body, err := json.Marshal(data) if err != nil { return nil, err } req, err := http.NewRequest(http.MethodPost, endpoint.String(), bytes.NewReader(body)) if err != nil { return nil, err } var result CreateDNSRecordResponse err = c.do(req, &result) if err != nil { return nil, err } return &result, nil } func (c Client) DeleteDNSRecord(id string) (*DeleteRecordResponse, error) { endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "dns-records", id)) if err != nil { return nil, err } req, err := http.NewRequest(http.MethodDelete, endpoint.String(), nil) if err != nil { return nil, err } var result DeleteRecordResponse err = c.do(req, &result) if err != nil { return nil, err } return &result, nil } func (c Client) GetJob(id string) (*GetJobResponse, error) { endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "queue-jobs", id)) if err != nil { return nil, err } req, err := http.NewRequest(http.MethodGet, endpoint.String(), nil) if err != nil { return nil, err } var result GetJobResponse err = c.do(req, &result) if err != nil { return nil, err } return &result, nil } func (c Client) do(req *http.Request, data interface{}) error { req.Header.Set("Content-Type", "application/vnd.api+json") req.Header.Set("Accept", "application/vnd.variomedia.v1+json") req.Header.Set("Authorization", "token "+c.apiToken) resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode/100 != 2 { all, _ := io.ReadAll(resp.Body) var e APIError err = json.Unmarshal(all, &e) if err != nil { return fmt.Errorf("%d: %s", resp.StatusCode, string(all)) } return e } content, err := io.ReadAll(resp.Body) if err != nil { return err } err = json.Unmarshal(content, data) if err != nil { return fmt.Errorf("%w: %s", err, string(content)) } return nil } lego-4.9.1/providers/dns/variomedia/internal/client_test.go000066400000000000000000000101701434020463500240540ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setup(t *testing.T) (*Client, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient("secret") client.baseURL, _ = url.Parse(server.URL) return client, mux } func mockHandler(method string, filename string) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { if req.Method != method { http.Error(rw, fmt.Sprintf("invalid method, got %s want %s", req.Method, method), http.StatusBadRequest) return } filename = "./fixtures/" + filename statusCode := http.StatusOK if req.Header.Get("Authorization") != "token secret" { statusCode = http.StatusUnauthorized filename = "./fixtures/error.json" } rw.WriteHeader(statusCode) file, err := os.Open(filename) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } defer func() { _ = file.Close() }() _, err = io.Copy(rw, file) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } } } func TestClient_CreateDNSRecord(t *testing.T) { client, mux := setup(t) mux.HandleFunc("/dns-records", mockHandler(http.MethodPost, "POST_dns-records.json")) record := DNSRecord{ RecordType: "TXT", Name: "_acme-challenge", Domain: "example.com", Data: "test", TTL: 300, } resp, err := client.CreateDNSRecord(record) require.NoError(t, err) expected := &CreateDNSRecordResponse{ Data: struct { Type string `json:"type"` ID string `json:"id"` Attributes struct { Status string `json:"status"` } `json:"attributes"` Links struct { QueueJob string `json:"queue-job"` DNSRecord string `json:"dns-record"` } `json:"links"` }{ Type: "queue-job", ID: "18181818", Attributes: struct { Status string `json:"status"` }{ Status: "pending", }, Links: struct { QueueJob string `json:"queue-job"` DNSRecord string `json:"dns-record"` }{ QueueJob: "https://api.variomedia.de/queue-jobs/18181818", DNSRecord: "https://api.variomedia.de/dns-records/19191919", }, }, } assert.Equal(t, expected, resp) } func TestClient_DeleteDNSRecord(t *testing.T) { client, mux := setup(t) mux.HandleFunc("/dns-records/test", mockHandler(http.MethodDelete, "DELETE_dns-records_pending.json")) resp, err := client.DeleteDNSRecord("test") require.NoError(t, err) expected := &DeleteRecordResponse{ Data: struct { ID string `json:"id"` Type string `json:"type"` Attributes struct { JobType string `json:"job_type"` Status string `json:"status"` } `json:"attributes"` Links struct { Self string `json:"self"` Object string `json:"object"` } `json:"links"` }{ ID: "303030", Type: "queue-job", Attributes: struct { JobType string `json:"job_type"` Status string `json:"status"` }{ Status: "pending", }, }, } assert.Equal(t, expected, resp) } func TestClient_GetJob(t *testing.T) { client, mux := setup(t) mux.HandleFunc("/queue-jobs/test", mockHandler(http.MethodGet, "GET_queue-jobs.json")) resp, err := client.GetJob("test") require.NoError(t, err) expected := &GetJobResponse{ Data: struct { ID string `json:"id"` Type string `json:"type"` Attributes struct { JobType string `json:"job_type"` Status string `json:"status"` } `json:"attributes"` Links struct { Self string `json:"self"` Object string `json:"object"` } `json:"links"` }{ ID: "171717", Type: "queue-job", Attributes: struct { JobType string `json:"job_type"` Status string `json:"status"` }{ JobType: "dns-record", Status: "done", }, Links: struct { Self string `json:"self"` Object string `json:"object"` }{ Self: "https://api.variomedia.de/queue-jobs/171717", Object: "https://api.variomedia.de/dns-records/212121", }, }, } assert.Equal(t, expected, resp) } lego-4.9.1/providers/dns/variomedia/internal/fixtures/000077500000000000000000000000001434020463500230625ustar00rootroot00000000000000lego-4.9.1/providers/dns/variomedia/internal/fixtures/DELETE_dns-records_done.json000066400000000000000000000007721434020463500302350ustar00rootroot00000000000000{ "data": { "id": "303030", "type": "queue-job", "attributes": { "job_type": "dns-record", "status": "done" }, "relationships": { "owner": { "data": { "id": "505050", "type": "customer" } } }, "links": { "self": "https://api.variomedia.de/queue-jobs/303030", "object": "https://api.variomedia.de/dns-records/212121" } }, "links": { "self": "https://api.variomedia.de/queue-jobs/303030" } } lego-4.9.1/providers/dns/variomedia/internal/fixtures/DELETE_dns-records_pending.json000066400000000000000000000004311434020463500307240ustar00rootroot00000000000000{ "data": { "id": "303030", "type": "queue-job", "attributes": { "status": "pending" }, "links": { "queue-job": "https://api.variomedia.de/queue-jobs/303030" } }, "links": { "self": "https://api.variomedia.de/dns-records/212121" } } lego-4.9.1/providers/dns/variomedia/internal/fixtures/GET_dns-records.json000066400000000000000000000007731434020463500267060ustar00rootroot00000000000000{ "data": { "id": "20202020", "type": "dns-record", "links": { "self": "https://api.variomedia.de/dns-records/20202020" }, "attributes": { "record_type": "TXT", "fqdn": "my-test-record.example.com", "fqdn_ace": "my-test-record.example.com", "name": "my-test-record", "name_ace": "my-test-record", "domain": "example.com", "data": "test", "ttl": 300 } }, "links": { "self": "https://api.variomedia.de/dns-records" } } lego-4.9.1/providers/dns/variomedia/internal/fixtures/GET_queue-jobs.json000066400000000000000000000007721434020463500265410ustar00rootroot00000000000000{ "data": { "id": "171717", "type": "queue-job", "links": { "self": "https://api.variomedia.de/queue-jobs/171717", "object": "https://api.variomedia.de/dns-records/212121" }, "attributes": { "job_type": "dns-record", "status": "done" }, "relationships": { "owner": { "data": { "id": "505050", "type": "customer" } } } }, "links": { "self": "https://api.variomedia.de/queue-jobs/171717" } } lego-4.9.1/providers/dns/variomedia/internal/fixtures/POST_dns-records.json000066400000000000000000000005341434020463500270470ustar00rootroot00000000000000{ "data": { "type": "queue-job", "id": "18181818", "attributes": { "status": "pending" }, "links": { "queue-job": "https://api.variomedia.de/queue-jobs/18181818", "dns-record": "https://api.variomedia.de/dns-records/19191919" } }, "links": { "self": "https://api.variomedia.de/dns-records" } } lego-4.9.1/providers/dns/variomedia/internal/fixtures/error.json000066400000000000000000000006111434020463500251040ustar00rootroot00000000000000{ "errors": [ { "status": "401", "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.", "id": "unauthorized" } ], "links": { "self": "https://api.variomedia.de/dns-records" } } lego-4.9.1/providers/dns/variomedia/internal/types.go000066400000000000000000000036151434020463500227110ustar00rootroot00000000000000package internal import ( "fmt" "strings" ) type CreateDNSRecordRequest struct { Data Data `json:"data"` } type Data struct { Type string `json:"type"` Attributes DNSRecord `json:"attributes"` } type DNSRecord struct { RecordType string `json:"record_type,omitempty"` Name string `json:"name,omitempty"` Domain string `json:"domain,omitempty"` Data string `json:"data,omitempty"` TTL int `json:"ttl,omitempty"` } type APIError struct { Errors []ErrorItem `json:"errors"` } func (a APIError) Error() string { var parts []string for _, data := range a.Errors { parts = append(parts, fmt.Sprintf("status: %s, title: %s, id: %s", data.Status, data.Title, data.ID)) } return strings.Join(parts, ", ") } type ErrorItem struct { Status string `json:"status,omitempty"` Title string `json:"title,omitempty"` ID string `json:"id,omitempty"` } type CreateDNSRecordResponse struct { Data struct { Type string `json:"type"` ID string `json:"id"` Attributes struct { Status string `json:"status"` } `json:"attributes"` Links struct { QueueJob string `json:"queue-job"` DNSRecord string `json:"dns-record"` } `json:"links"` } `json:"data"` } type GetJobResponse struct { Data struct { ID string `json:"id"` Type string `json:"type"` Attributes struct { JobType string `json:"job_type"` Status string `json:"status"` } `json:"attributes"` Links struct { Self string `json:"self"` Object string `json:"object"` } `json:"links"` } `json:"data"` } type DeleteRecordResponse struct { Data struct { ID string `json:"id"` Type string `json:"type"` Attributes struct { JobType string `json:"job_type"` Status string `json:"status"` } `json:"attributes"` Links struct { Self string `json:"self"` Object string `json:"object"` } `json:"links"` } `json:"data"` } lego-4.9.1/providers/dns/variomedia/variomedia.go000066400000000000000000000121561434020463500220510ustar00rootroot00000000000000// Package variomedia implements a DNS provider for solving the DNS-01 challenge using Variomedia DNS. package variomedia import ( "errors" "fmt" "net/http" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/platform/wait" "github.com/go-acme/lego/v4/providers/dns/variomedia/internal" ) const defaultTTL = 300 // Environment variables names. const ( envNamespace = "VARIOMEDIA_" EnvAPIToken = envNamespace + "API_TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIToken string PropagationTimeout time.Duration PollingInterval time.Duration SequenceInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client recordIDs map[string]string recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIToken) if err != nil { return nil, fmt.Errorf("variomedia: %w", err) } config := NewDefaultConfig() config.APIToken = values[EnvAPIToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Variomedia. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config.APIToken == "" { return nil, errors.New("variomedia: missing credentials") } if config.HTTPClient == nil { config.HTTPClient = http.DefaultClient } client := internal.NewClient(config.APIToken) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]string), }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Sequential All DNS challenges for this provider will be resolved sequentially. // Returns the interval between each iteration. func (d *DNSProvider) Sequential() time.Duration { return d.config.SequenceInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("variomedia: %w", err) } record := internal.DNSRecord{ RecordType: "TXT", Name: dns01.UnFqdn(strings.TrimSuffix(fqdn, authZone)), Domain: dns01.UnFqdn(authZone), Data: value, TTL: d.config.TTL, } cdrr, err := d.client.CreateDNSRecord(record) if err != nil { return fmt.Errorf("variomedia: %w", err) } err = d.waitJob(domain, cdrr.Data.ID) if err != nil { return fmt.Errorf("variomedia: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = strings.TrimPrefix(cdrr.Data.Links.DNSRecord, "https://api.variomedia.de/dns-records/") d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record previously created. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) // get the record's unique ID from when we created it d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("variomedia: unknown record ID for '%s'", fqdn) } ddrr, err := d.client.DeleteDNSRecord(recordID) if err != nil { return fmt.Errorf("variomedia: %w", err) } err = d.waitJob(domain, ddrr.Data.ID) if err != nil { return fmt.Errorf("variomedia: %w", err) } return nil } func (d *DNSProvider) waitJob(domain string, id string) error { return wait.For("variomedia: apply change on "+domain, d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) { result, err := d.client.GetJob(id) if err != nil { return false, err } log.Infof("variomedia: [%s] %s: %s %s", domain, result.Data.ID, result.Data.Attributes.JobType, result.Data.Attributes.Status) return result.Data.Attributes.Status == "done", nil }) } lego-4.9.1/providers/dns/variomedia/variomedia.toml000066400000000000000000000013671434020463500224210ustar00rootroot00000000000000Name = "Variomedia" Description = '''''' URL = "https://www.variomedia.de/" Code = "variomedia" Since = "v4.8.0" Example = ''' VARIOMEDIA_API_TOKEN=xxxx \ lego --email you@example.com --dns variomedia --domains my.example.org run ''' [Configuration] [Configuration.Credentials] VARIOMEDIA_API_TOKEN = "API token" [Configuration.Additional] VARIOMEDIA_POLLING_INTERVAL = "Time between DNS propagation check" VARIOMEDIA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" VARIOMEDIA_TTL = "The TTL of the TXT record used for the DNS challenge" DODE_SEQUENCE_INTERVAL = "Time between sequential requests" VARIOMEDIA_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.variomedia.de/docs/dns-records.html" lego-4.9.1/providers/dns/variomedia/variomedia_test.go000066400000000000000000000042641434020463500231110ustar00rootroot00000000000000package variomedia import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAPIToken).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIToken: "secret", }, }, { desc: "missing API token", expected: "variomedia: some credentials information are missing: VARIOMEDIA_API_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string expected string apiToken string }{ { desc: "success", apiToken: "secret", }, { desc: "missing api token", apiToken: "", expected: "variomedia: missing credentials", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIToken = test.apiToken p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/vegadns/000077500000000000000000000000001434020463500167045ustar00rootroot00000000000000lego-4.9.1/providers/dns/vegadns/vegadns.go000066400000000000000000000072721434020463500206720ustar00rootroot00000000000000// Package vegadns implements a DNS provider for solving the DNS-01 challenge using VegaDNS. package vegadns import ( "errors" "fmt" "strings" "time" vegaClient "github.com/OpenDNS/vegadns2client" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "VEGADNS_" EnvKey = "SECRET_VEGADNS_KEY" EnvSecret = "SECRET_VEGADNS_SECRET" EnvURL = envNamespace + "URL" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string APIKey string APISecret string PropagationTimeout time.Duration PollingInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 10), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 12*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 1*time.Minute), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client vegaClient.VegaDNSClient } // NewDNSProvider returns a DNSProvider instance configured for VegaDNS. // Credentials must be passed in the environment variables: // VEGADNS_URL, SECRET_VEGADNS_KEY, SECRET_VEGADNS_SECRET. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvURL) if err != nil { return nil, fmt.Errorf("vegadns: %w", err) } config := NewDefaultConfig() config.BaseURL = values[EnvURL] config.APIKey = env.GetOrFile(EnvKey) config.APISecret = env.GetOrFile(EnvSecret) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for VegaDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("vegadns: the configuration of the DNS provider is nil") } vega := vegaClient.NewVegaDNSClient(config.BaseURL) vega.APIKey = config.APIKey vega.APISecret = config.APISecret return &DNSProvider{client: vega, config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) _, domainID, err := d.client.GetAuthZone(fqdn) if err != nil { return fmt.Errorf("vegadns: can't find Authoritative Zone for %s in Present: %w", fqdn, err) } err = d.client.CreateTXT(domainID, fqdn, value, d.config.TTL) if err != nil { return fmt.Errorf("vegadns: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) _, domainID, err := d.client.GetAuthZone(fqdn) if err != nil { return fmt.Errorf("vegadns: can't find Authoritative Zone for %s in CleanUp: %w", fqdn, err) } txt := strings.TrimSuffix(fqdn, ".") recordID, err := d.client.GetRecordID(domainID, txt, "TXT") if err != nil { return fmt.Errorf("vegadns: couldn't get Record ID in CleanUp: %w", err) } err = d.client.DeleteRecord(recordID) if err != nil { return fmt.Errorf("vegadns: %w", err) } return nil } lego-4.9.1/providers/dns/vegadns/vegadns.toml000066400000000000000000000012201434020463500212230ustar00rootroot00000000000000Name = "VegaDNS" Description = '''''' URL = "https://github.com/shupp/VegaDNS-API" Code = "vegadns" Since = "v1.1.0" Example = '''''' [Configuration] [Configuration.Credentials] SECRET_VEGADNS_KEY = "API key" SECRET_VEGADNS_SECRET = "API secret" VEGADNS_URL = "API endpoint URL" [Configuration.Additional] VEGADNS_POLLING_INTERVAL = "Time between DNS propagation check" VEGADNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" VEGADNS_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://github.com/shupp/VegaDNS-API" GoClient = "https://github.com/OpenDNS/vegadns2client" lego-4.9.1/providers/dns/vegadns/vegadns_mock_test.go000066400000000000000000000027251434020463500227400ustar00rootroot00000000000000package vegadns const tokenResponseMock = ` { "access_token":"699dd4ff-e381-46b8-8bf8-5de49dd56c1f", "token_type":"bearer", "expires_in":3600 } ` const domainsResponseMock = ` { "domains":[ { "domain_id":1, "domain":"example.com", "status":"active", "owner_id":0 } ] } ` const recordsResponseMock = ` { "status":"ok", "total_records":2, "domain":{ "status":"active", "domain":"example.com", "owner_id":0, "domain_id":1 }, "records":[ { "retry":"2048", "minimum":"2560", "refresh":"16384", "email":"hostmaster.example.com", "record_type":"SOA", "expire":"1048576", "ttl":86400, "record_id":1, "nameserver":"ns1.example.com", "domain_id":1, "serial":"" }, { "name":"example.com", "value":"ns1.example.com", "record_type":"NS", "ttl":3600, "record_id":2, "location_id":null, "domain_id":1 }, { "name":"_acme-challenge.example.com", "value":"my_challenge", "record_type":"TXT", "ttl":3600, "record_id":3, "location_id":null, "domain_id":1 } ] } ` const recordCreatedResponseMock = ` { "status":"ok", "record":{ "name":"_acme-challenge.example.com", "value":"my_challenge", "record_type":"TXT", "ttl":3600, "record_id":3, "location_id":null, "domain_id":1 } } ` const recordDeletedResponseMock = `{"status": "ok"}` lego-4.9.1/providers/dns/vegadns/vegadns_test.go000066400000000000000000000153421434020463500217260ustar00rootroot00000000000000package vegadns import ( "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const testDomain = "example.com" var envTest = tester.NewEnvTest(EnvKey, EnvSecret, EnvURL) func TestNewDNSProvider_Fail(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() _, err := NewDNSProvider() assert.Error(t, err, "VEGADNS_URL env missing") } func TestDNSProvider_TimeoutSuccess(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() setupTest(t, muxSuccess()) provider, err := NewDNSProvider() require.NoError(t, err) timeout, interval := provider.Timeout() assert.Equal(t, timeout, 12*time.Minute) assert.Equal(t, interval, 1*time.Minute) } func TestDNSProvider_Present(t *testing.T) { testCases := []struct { desc string handler http.Handler expectedError string }{ { desc: "Success", handler: muxSuccess(), }, { desc: "FailToFindZone", handler: muxFailToFindZone(), expectedError: "vegadns: can't find Authoritative Zone for _acme-challenge.example.com. in Present: Unable to find auth zone for fqdn _acme-challenge.example.com", }, { desc: "FailToCreateTXT", handler: muxFailToCreateTXT(), expectedError: "vegadns: Got bad answer from VegaDNS on CreateTXT. Code: 400. Message: ", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() setupTest(t, test.handler) provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(testDomain, "token", "keyAuth") if test.expectedError == "" { assert.NoError(t, err) } else { assert.EqualError(t, err, test.expectedError) } }) } } func TestDNSProvider_CleanUp(t *testing.T) { testCases := []struct { desc string handler http.Handler expectedError string }{ { desc: "Success", handler: muxSuccess(), }, { desc: "FailToFindZone", handler: muxFailToFindZone(), expectedError: "vegadns: can't find Authoritative Zone for _acme-challenge.example.com. in CleanUp: Unable to find auth zone for fqdn _acme-challenge.example.com", }, { desc: "FailToGetRecordID", handler: muxFailToGetRecordID(), expectedError: "vegadns: couldn't get Record ID in CleanUp: Got bad answer from VegaDNS on GetRecordID. Code: 404. Message: ", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() setupTest(t, test.handler) provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(testDomain, "token", "keyAuth") if test.expectedError == "" { assert.NoError(t, err) } else { assert.EqualError(t, err, test.expectedError) } }) } } func muxSuccess() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { w.WriteHeader(http.StatusOK) fmt.Fprint(w, tokenResponseMock) return } w.WriteHeader(http.StatusBadRequest) }) mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("search") == "example.com" { w.WriteHeader(http.StatusOK) fmt.Fprint(w, domainsResponseMock) return } w.WriteHeader(http.StatusNotFound) }) mux.HandleFunc("/1.0/records", func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: if r.URL.Query().Get("domain_id") == "1" { w.WriteHeader(http.StatusOK) fmt.Fprint(w, recordsResponseMock) return } w.WriteHeader(http.StatusNotFound) return case http.MethodPost: w.WriteHeader(http.StatusCreated) fmt.Fprint(w, recordCreatedResponseMock) return } w.WriteHeader(http.StatusBadRequest) }) mux.HandleFunc("/1.0/records/3", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodDelete { w.WriteHeader(http.StatusOK) fmt.Fprint(w, recordDeletedResponseMock) return } w.WriteHeader(http.StatusBadRequest) }) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) fmt.Printf("Not Found for Request: (%+v)\n\n", r) }) return mux } func muxFailToFindZone() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { w.WriteHeader(http.StatusOK) fmt.Fprint(w, tokenResponseMock) return } w.WriteHeader(http.StatusBadRequest) }) mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) }) return mux } func muxFailToCreateTXT() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { w.WriteHeader(http.StatusOK) fmt.Fprint(w, tokenResponseMock) return } w.WriteHeader(http.StatusBadRequest) }) mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("search") == testDomain { w.WriteHeader(http.StatusOK) fmt.Fprint(w, domainsResponseMock) return } w.WriteHeader(http.StatusNotFound) }) mux.HandleFunc("/1.0/records", func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: if r.URL.Query().Get("domain_id") == "1" { w.WriteHeader(http.StatusOK) fmt.Fprint(w, recordsResponseMock) return } w.WriteHeader(http.StatusNotFound) return case http.MethodPost: w.WriteHeader(http.StatusBadRequest) return } w.WriteHeader(http.StatusBadRequest) }) return mux } func muxFailToGetRecordID() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { w.WriteHeader(http.StatusOK) fmt.Fprint(w, tokenResponseMock) return } w.WriteHeader(http.StatusBadRequest) }) mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("search") == testDomain { w.WriteHeader(http.StatusOK) fmt.Fprint(w, domainsResponseMock) return } w.WriteHeader(http.StatusNotFound) }) mux.HandleFunc("/1.0/records", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { w.WriteHeader(http.StatusNotFound) return } w.WriteHeader(http.StatusBadRequest) }) return mux } func setupTest(t *testing.T, mux http.Handler) { t.Helper() server := httptest.NewServer(mux) t.Cleanup(server.Close) envTest.Apply(map[string]string{ EnvKey: "key", EnvSecret: "secret", EnvURL: server.URL, }) } lego-4.9.1/providers/dns/vercel/000077500000000000000000000000001434020463500165355ustar00rootroot00000000000000lego-4.9.1/providers/dns/vercel/internal/000077500000000000000000000000001434020463500203515ustar00rootroot00000000000000lego-4.9.1/providers/dns/vercel/internal/client.go000066400000000000000000000066631434020463500221710ustar00rootroot00000000000000package internal import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "path" "time" "github.com/go-acme/lego/v4/challenge/dns01" ) const defaultBaseURL = "https://api.vercel.com" // Client Vercel client. type Client struct { authToken string teamID string baseURL *url.URL HTTPClient *http.Client } // NewClient creates a Client. func NewClient(authToken string, teamID string) *Client { baseURL, _ := url.Parse(defaultBaseURL) return &Client{ authToken: authToken, teamID: teamID, baseURL: baseURL, HTTPClient: &http.Client{Timeout: 10 * time.Second}, } } // CreateRecord creates a DNS record. // https://vercel.com/docs/rest-api#endpoints/dns/create-a-dns-record func (c *Client) CreateRecord(zone string, record Record) (*CreateRecordResponse, error) { endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "v2", "domains", dns01.UnFqdn(zone), "records")) if err != nil { return nil, err } body, err := json.Marshal(record) if err != nil { return nil, err } req, err := c.newRequest(http.MethodPost, endpoint.String(), bytes.NewReader(body)) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode >= http.StatusBadRequest { return nil, readError(req, resp) } content, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.New(toUnreadableBodyMessage(req, content)) } // Everything looks good; but we'll need the ID later to delete the record respData := &CreateRecordResponse{} err = json.Unmarshal(content, respData) if err != nil { return nil, fmt.Errorf("%w: %s", err, toUnreadableBodyMessage(req, content)) } return respData, nil } // DeleteRecord deletes a DNS record. // https://vercel.com/docs/rest-api#endpoints/dns/delete-a-dns-record func (c *Client) DeleteRecord(zone string, recordID string) error { endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "v2", "domains", dns01.UnFqdn(zone), "records", recordID)) if err != nil { return err } req, err := c.newRequest(http.MethodDelete, endpoint.String(), nil) if err != nil { return err } resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode >= http.StatusBadRequest { return readError(req, resp) } return nil } func (c *Client) newRequest(method, reqURL string, body io.Reader) (*http.Request, error) { req, err := http.NewRequest(method, reqURL, body) if err != nil { return nil, err } if c.teamID != "" { query := req.URL.Query() query.Add("teamId", c.teamID) req.URL.RawQuery = query.Encode() } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.authToken)) return req, nil } func readError(req *http.Request, resp *http.Response) error { content, err := io.ReadAll(resp.Body) if err != nil { return errors.New(toUnreadableBodyMessage(req, content)) } var errInfo APIErrorResponse err = json.Unmarshal(content, &errInfo) if err != nil { return fmt.Errorf("API Error unmarshaling error: %w: %s", err, toUnreadableBodyMessage(req, content)) } return fmt.Errorf("HTTP %d: %w", resp.StatusCode, errInfo.Error) } func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string { return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody)) } lego-4.9.1/providers/dns/vercel/internal/client_test.go000066400000000000000000000053741434020463500232260ustar00rootroot00000000000000package internal import ( "fmt" "io" "net/http" "net/http/httptest" "net/url" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setup(t *testing.T) (*Client, *http.ServeMux) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := NewClient("secret", "123") client.HTTPClient = server.Client() client.baseURL, _ = url.Parse(server.URL) return client, mux } func TestClient_CreateRecord(t *testing.T) { client, mux := setup(t) mux.HandleFunc("/v2/domains/example.com/records", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } auth := req.Header.Get("Authorization") if auth != "Bearer secret" { http.Error(rw, fmt.Sprintf("invalid API token: %s", auth), http.StatusUnauthorized) return } teamID := req.URL.Query().Get("teamId") if teamID != "123" { http.Error(rw, fmt.Sprintf("invalid team ID: %s", teamID), http.StatusUnauthorized) return } reqBody, err := io.ReadAll(req.Body) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } expectedReqBody := `{"name":"_acme-challenge.example.com.","type":"TXT","value":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":60}` assert.Equal(t, expectedReqBody, string(reqBody)) rw.WriteHeader(http.StatusOK) _, err = fmt.Fprintf(rw, `{ "uid": "9e2eab60-0ba5-4dff-b481-2999c9764b84", "updated": 1 }`) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) record := Record{ Name: "_acme-challenge.example.com.", Type: "TXT", Value: "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI", TTL: 60, } resp, err := client.CreateRecord("example.com.", record) require.NoError(t, err) expected := &CreateRecordResponse{ UID: "9e2eab60-0ba5-4dff-b481-2999c9764b84", Updated: 1, } assert.Equal(t, expected, resp) } func TestClient_DeleteRecord(t *testing.T) { client, mux := setup(t) mux.HandleFunc("/v2/domains/example.com/records/1234567", func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodDelete { http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest) return } auth := req.Header.Get("Authorization") if auth != "Bearer secret" { http.Error(rw, fmt.Sprintf("invalid API token: %s", auth), http.StatusUnauthorized) return } teamID := req.URL.Query().Get("teamId") if teamID != "123" { http.Error(rw, fmt.Sprintf("invalid team ID: %s", teamID), http.StatusUnauthorized) return } rw.WriteHeader(http.StatusOK) }) err := client.DeleteRecord("example.com.", "1234567") require.NoError(t, err) } lego-4.9.1/providers/dns/vercel/internal/types.go000066400000000000000000000013241434020463500220440ustar00rootroot00000000000000package internal import "fmt" type Record struct { ID string `json:"id,omitempty"` Slug string `json:"slug,omitempty"` Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` Value string `json:"value,omitempty"` TTL int `json:"ttl,omitempty"` } // CreateRecordResponse represents a response from Vercel's API after making a DNS record. type CreateRecordResponse struct { UID string `json:"uid"` Updated int `json:"updated,omitempty"` } type APIErrorResponse struct { Error *APIError `json:"error"` } type APIError struct { Code string `json:"code"` Message string `json:"message"` } func (a APIError) Error() string { return fmt.Sprintf("%s: %s", a.Code, a.Message) } lego-4.9.1/providers/dns/vercel/vercel.go000066400000000000000000000103751434020463500203520ustar00rootroot00000000000000// Package vercel implements a DNS provider for solving the DNS-01 challenge using Vercel DNS. package vercel import ( "errors" "fmt" "net/http" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/vercel/internal" ) // Environment variables names. const ( envNamespace = "VERCEL_" EnvAuthToken = envNamespace + "API_TOKEN" EnvTeamID = envNamespace + "TEAM_ID" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { AuthToken string TeamID string TTL int PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 60), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 60*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client recordIDs map[string]string recordIDsMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance configured for Vercel. // Credentials must be passed in the environment variables: VERCEL_API_TOKEN, VERCEL_TEAM_ID. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAuthToken) if err != nil { return nil, fmt.Errorf("vercel: %w", err) } config := NewDefaultConfig() config.AuthToken = values[EnvAuthToken] config.TeamID = env.GetOrDefaultString(EnvTeamID, "") return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Digital Ocean. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("vercel: the configuration of the DNS provider is nil") } if config.AuthToken == "" { return nil, errors.New("vercel: credentials missing") } client := internal.NewClient(config.AuthToken, config.TeamID) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]string), }, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("vercel: could not determine zone for domain %q: %w", domain, err) } record := internal.Record{ Name: fqdn, Type: "TXT", Value: value, TTL: d.config.TTL, } respData, err := d.client.CreateRecord(authZone, record) if err != nil { return fmt.Errorf("vercel: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = respData.UID d.recordIDsMu.Unlock() return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("vercel: %w", err) } // get the record's unique ID from when we created it d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("vercel: unknown record ID for '%s'", fqdn) } err = d.client.DeleteRecord(authZone, recordID) if err != nil { return fmt.Errorf("vercel: %w", err) } // Delete record ID from map d.recordIDsMu.Lock() delete(d.recordIDs, token) d.recordIDsMu.Unlock() return nil } lego-4.9.1/providers/dns/vercel/vercel.toml000066400000000000000000000013331434020463500207120ustar00rootroot00000000000000Name = "Vercel" Description = '''''' URL = "https://vercel.com" Code = "vercel" Since = "v4.7.0" Example = ''' VERCEL_API_TOKEN=xxxxxx \ lego --email you@example.com --dns vercel --domains my.example.org run ''' [Configuration] [Configuration.Credentials] VERCEL_API_TOKEN = "Authentication token" [Configuration.Additional] VERCEL_TEAM_ID = "Team ID (ex: team_xxxxxxxxxxxxxxxxxxxxxxxx)" VERCEL_POLLING_INTERVAL = "Time between DNS propagation check" VERCEL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" VERCEL_TTL = "The TTL of the TXT record used for the DNS challenge" VERCEL_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://vercel.com/docs/rest-api#endpoints/dns" lego-4.9.1/providers/dns/vercel/vercel_test.go000066400000000000000000000044271434020463500214120ustar00rootroot00000000000000package vercel import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAuthToken).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAuthToken: "123", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAuthToken: "", }, expected: "vercel: some credentials information are missing: VERCEL_API_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string authToken string expected string }{ { desc: "success", authToken: "123", }, { desc: "missing credentials", expected: "vercel: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.AuthToken = test.authToken p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.recordIDs) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/versio/000077500000000000000000000000001434020463500165645ustar00rootroot00000000000000lego-4.9.1/providers/dns/versio/client.go000066400000000000000000000054641434020463500204020ustar00rootroot00000000000000package versio import ( "bytes" "encoding/json" "fmt" "io" "net/http" "path" ) const defaultBaseURL = "https://www.versio.nl/api/v1/" type dnsRecordsResponse struct { Record dnsRecord `json:"domainInfo"` } type dnsRecord struct { DNSRecords []record `json:"dns_records"` } type record struct { Type string `json:"type,omitempty"` Name string `json:"name,omitempty"` Value string `json:"value,omitempty"` Priority int `json:"prio,omitempty"` TTL int `json:"ttl,omitempty"` } type dnsErrorResponse struct { Error errorMessage `json:"error"` } type errorMessage struct { Code int `json:"code,omitempty"` Message string `json:"message,omitempty"` } func (d *DNSProvider) postDNSRecords(domain string, msg interface{}) error { reqBody := &bytes.Buffer{} err := json.NewEncoder(reqBody).Encode(msg) if err != nil { return err } req, err := d.makeRequest(http.MethodPost, "domains/"+domain+"/update", reqBody) if err != nil { return err } return d.do(req, nil) } func (d *DNSProvider) getDNSRecords(domain string) (*dnsRecordsResponse, error) { req, err := d.makeRequest(http.MethodGet, "domains/"+domain+"?show_dns_records=true", nil) if err != nil { return nil, err } // we'll need all the dns_records to add the new TXT record respData := &dnsRecordsResponse{} err = d.do(req, respData) if err != nil { return nil, err } return respData, nil } func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (*http.Request, error) { endpoint, err := d.config.BaseURL.Parse(path.Join(d.config.BaseURL.EscapedPath(), uri)) if err != nil { return nil, err } req, err := http.NewRequest(method, endpoint.String(), body) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") if len(d.config.Username) > 0 && len(d.config.Password) > 0 { req.SetBasicAuth(d.config.Username, d.config.Password) } return req, nil } func (d *DNSProvider) do(req *http.Request, result interface{}) error { resp, err := d.config.HTTPClient.Do(req) if resp != nil { defer resp.Body.Close() } if err != nil { return err } if resp.StatusCode >= http.StatusBadRequest { var body []byte body, err = io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("%d: failed to read response body: %w", resp.StatusCode, err) } respError := &dnsErrorResponse{} err = json.Unmarshal(body, respError) if err != nil { return fmt.Errorf("%d: request failed: %s", resp.StatusCode, string(body)) } return fmt.Errorf("%d: request failed: %s", resp.StatusCode, respError.Error.Message) } if result != nil { content, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("request failed: %w", err) } if err = json.Unmarshal(content, result); err != nil { return fmt.Errorf("%w: %s", err, content) } } return nil } lego-4.9.1/providers/dns/versio/versio.go000066400000000000000000000114131434020463500204220ustar00rootroot00000000000000// Package versio implements a DNS provider for solving the DNS-01 challenge using versio DNS. package versio import ( "errors" "fmt" "net/http" "net/url" "sync" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "VERSIO_" EnvUsername = envNamespace + "USERNAME" EnvPassword = envNamespace + "PASSWORD" EnvEndpoint = envNamespace + "ENDPOINT" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL *url.URL TTL int Username string Password string PropagationTimeout time.Duration PollingInterval time.Duration SequenceInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { baseURL, err := url.Parse(env.GetOrDefaultString(EnvEndpoint, defaultBaseURL)) if err != nil { baseURL, _ = url.Parse(defaultBaseURL) } return &Config{ BaseURL: baseURL, TTL: env.GetOrDefaultInt(EnvTTL, 300), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 60*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config dnsEntriesMu sync.Mutex } // NewDNSProvider returns a DNSProvider instance. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvUsername, EnvPassword) if err != nil { return nil, fmt.Errorf("versio: %w", err) } config := NewDefaultConfig() config.Username = values[EnvUsername] config.Password = values[EnvPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Versio. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("versio: the configuration of the DNS provider is nil") } if config.Username == "" { return nil, errors.New("versio: the versio username is missing") } if config.Password == "" { return nil, errors.New("versio: the versio password is missing") } return &DNSProvider{config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("versio: %w", err) } // use mutex to prevent race condition from getDNSRecords until postDNSRecords d.dnsEntriesMu.Lock() defer d.dnsEntriesMu.Unlock() zoneName := dns01.UnFqdn(authZone) domains, err := d.getDNSRecords(zoneName) if err != nil { return fmt.Errorf("versio: %w", err) } txtRecord := record{ Type: "TXT", Name: fqdn, Value: `"` + value + `"`, TTL: d.config.TTL, } // Add new txtRercord to existing array of DNSRecords msg := &domains.Record msg.DNSRecords = append(msg.DNSRecords, txtRecord) err = d.postDNSRecords(zoneName, msg) if err != nil { return fmt.Errorf("versio: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("versio: %w", err) } // use mutex to prevent race condition from getDNSRecords until postDNSRecords d.dnsEntriesMu.Lock() defer d.dnsEntriesMu.Unlock() zoneName := dns01.UnFqdn(authZone) domains, err := d.getDNSRecords(zoneName) if err != nil { return fmt.Errorf("versio: %w", err) } // loop through the existing entries and remove the specific record msg := &dnsRecord{} for _, e := range domains.Record.DNSRecords { if e.Name != fqdn { msg.DNSRecords = append(msg.DNSRecords, e) } } err = d.postDNSRecords(zoneName, msg) if err != nil { return fmt.Errorf("versio: %w", err) } return nil } lego-4.9.1/providers/dns/versio/versio.toml000066400000000000000000000020131434020463500207640ustar00rootroot00000000000000Name = "Versio.[nl|eu|uk]" Description = '''''' URL = "https://www.versio.nl/domeinnamen" Code = "versio" Since = "v2.7.0" Example = ''' VERSIO_USERNAME= \ VERSIO_PASSWORD= \ lego --email you@example.com --dns versio --domains my.example.org run ''' Additional = ''' To test with the sandbox environment set ```VERSIO_ENDPOINT=https://www.versio.nl/testapi/v1/``` ''' [Configuration] [Configuration.Credentials] VERSIO_USERNAME = "Basic authentication username" VERSIO_PASSWORD = "Basic authentication password" [Configuration.Additional] VERSIO_ENDPOINT = "The endpoint URL of the API Server" VERSIO_POLLING_INTERVAL = "Time between DNS propagation check" VERSIO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" VERSIO_HTTP_TIMEOUT = "API request timeout" VERSIO_SEQUENCE_INTERVAL = "Time between sequential requests, default 60s" VERSIO_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://www.versio.nl/RESTapidoc/" lego-4.9.1/providers/dns/versio/versio_mock_test.go000066400000000000000000000006161434020463500224750ustar00rootroot00000000000000package versio const tokenResponseMock = ` { "access_token":"699dd4ff-e381-46b8-8bf8-5de49dd56c1f", "token_type":"bearer", "expires_in":3600 } ` const tokenFailToFindZoneMock = `{"error":{"code":401,"message":"ObjectDoesNotExist|Domain not found"}}` const tokenFailToCreateTXTMock = `{"error":{"code":400,"message":"ProcessError|DNS record invalid type _acme-challenge.example.eu. TST"}}` lego-4.9.1/providers/dns/versio/versio_test.go000066400000000000000000000161111434020463500214610ustar00rootroot00000000000000package versio import ( "fmt" "io" "net/http" "net/http/httptest" "testing" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const testDomain = "example.com" const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvUsername, EnvPassword, EnvEndpoint).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvUsername: "me@example.com", EnvPassword: "SECRET", }, }, { desc: "missing token", envVars: map[string]string{ EnvPassword: "me@example.com", }, expected: "versio: some credentials information are missing: VERSIO_USERNAME", }, { desc: "missing key", envVars: map[string]string{ EnvUsername: "TOKEN", }, expected: "versio: some credentials information are missing: VERSIO_PASSWORD", }, { desc: "missing credentials", envVars: map[string]string{}, expected: "versio: some credentials information are missing: VERSIO_USERNAME,VERSIO_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string config *Config expected string }{ { desc: "success", config: &Config{ Username: "me@example.com", Password: "PW", }, }, { desc: "nil config", config: nil, expected: "versio: the configuration of the DNS provider is nil", }, { desc: "missing username", config: &Config{ Password: "PW", }, expected: "versio: the versio username is missing", }, { desc: "missing password", config: &Config{ Username: "UN", }, expected: "versio: the versio password is missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { p, err := NewDNSProviderConfig(test.config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestDNSProvider_Present(t *testing.T) { testCases := []struct { desc string handler http.Handler expectedError string }{ { desc: "Success", handler: muxSuccess(), }, { desc: "FailToFindZone", handler: muxFailToFindZone(), expectedError: `versio: 401: request failed: ObjectDoesNotExist|Domain not found`, }, { desc: "FailToCreateTXT", handler: muxFailToCreateTXT(), expectedError: `versio: 400: request failed: ProcessError|DNS record invalid type _acme-challenge.example.eu. TST`, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() baseURL := setupTest(t, test.handler) envTest.Apply(map[string]string{ EnvUsername: "me@example.com", EnvPassword: "secret", EnvEndpoint: baseURL, }) provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(testDomain, "token", "keyAuth") if test.expectedError == "" { require.NoError(t, err) } else { assert.EqualError(t, err, test.expectedError) } }) } } func TestDNSProvider_CleanUp(t *testing.T) { testCases := []struct { desc string handler http.Handler expectedError string }{ { desc: "Success", handler: muxSuccess(), }, { desc: "FailToFindZone", handler: muxFailToFindZone(), expectedError: `versio: 401: request failed: ObjectDoesNotExist|Domain not found`, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() baseURL := setupTest(t, test.handler) envTest.Apply(map[string]string{ EnvUsername: "me@example.com", EnvPassword: "secret", EnvEndpoint: baseURL, }) provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(testDomain, "token", "keyAuth") if test.expectedError == "" { require.NoError(t, err) } else { require.EqualError(t, err, test.expectedError) } }) } } func muxSuccess() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/domains/example.com", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet && r.URL.Query().Get("show_dns_records") == "true" { fmt.Fprint(w, tokenResponseMock) return } w.WriteHeader(http.StatusBadRequest) }) mux.HandleFunc("/domains/example.com/update", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { fmt.Fprint(w, tokenResponseMock) return } w.WriteHeader(http.StatusBadRequest) }) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { log.Printf("unexpected request: %+v\n\n", r) data, _ := io.ReadAll(r.Body) defer func() { _ = r.Body.Close() }() log.Println(string(data)) http.NotFound(w, r) }) return mux } func muxFailToFindZone() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/domains/example.com", func(w http.ResponseWriter, _ *http.Request) { http.Error(w, tokenFailToFindZoneMock, http.StatusUnauthorized) }) return mux } func muxFailToCreateTXT() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/domains/example.com", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet && r.URL.Query().Get("show_dns_records") == "true" { fmt.Fprint(w, tokenResponseMock) return } w.WriteHeader(http.StatusBadRequest) }) mux.HandleFunc("/domains/example.com/update", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { http.Error(w, tokenFailToCreateTXTMock, http.StatusBadRequest) return } w.WriteHeader(http.StatusBadRequest) }) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { log.Printf("unexpected request: %+v\n\n", r) data, _ := io.ReadAll(r.Body) defer func() { _ = r.Body.Close() }() log.Println(string(data)) http.NotFound(w, r) }) return mux } func setupTest(t *testing.T, handler http.Handler) string { t.Helper() server := httptest.NewServer(handler) t.Cleanup(server.Close) return server.URL } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/vinyldns/000077500000000000000000000000001434020463500171235ustar00rootroot00000000000000lego-4.9.1/providers/dns/vinyldns/fixtures/000077500000000000000000000000001434020463500207745ustar00rootroot00000000000000lego-4.9.1/providers/dns/vinyldns/fixtures/recordSetChange-create.json000066400000000000000000000020651434020463500261730ustar00rootroot00000000000000{ "changeType": "Create", "created": "2021-03-04T00:49:00Z", "id": "27ba5c17-a217-4e8d-b662-b1dc8bee588f", "recordSet": { "account": "", "created": "2021-03-04T00:49:00Z", "id": "10000000-0000-0000-0000-000000000000", "name": "_acme-challenge.host", "records": [ { "text": "O2UTPYgIzRNt5N27EVcNKDxv6goSF7ru3zi3chZXKUw" } ], "status": "Active", "ttl": 30, "type": "TXT", "updated": "2021-03-04T00:49:00Z", "zoneId": "00000000-0000-0000-0000-000000000000" }, "singleBatchChangeIds": [], "status": "Complete", "userId": "50000000-0000-0000-0000-000000000000", "zone": { "account": "system", "acl": { "rules": [] }, "adminGroupId": "40000000-0000-0000-0000-000000000000", "created": "2020-07-15T21:15:36Z", "email": "Ops@company.invalid", "id": "00000000-0000-0000-0000-000000000000", "isTest": false, "latestSync": "2020-07-15T21:15:36Z", "name": "example.com.", "shared": false, "status": "Active", "updated": "2021-03-03T18:02:47Z" } } lego-4.9.1/providers/dns/vinyldns/fixtures/recordSetChange-delete.json000066400000000000000000000020651434020463500261720ustar00rootroot00000000000000{ "changeType": "Delete", "created": "2021-03-04T00:49:00Z", "id": "27ba5c17-a217-4e8d-b662-b1dc8bee588f", "recordSet": { "account": "", "created": "2021-03-04T00:49:00Z", "id": "10000000-0000-0000-0000-000000000000", "name": "_acme-challenge.host", "records": [ { "text": "O2UTPYgIzRNt5N27EVcNKDxv6goSF7ru3zi3chZXKUw" } ], "status": "Active", "ttl": 30, "type": "TXT", "updated": "2021-03-04T00:49:00Z", "zoneId": "00000000-0000-0000-0000-000000000000" }, "singleBatchChangeIds": [], "status": "Complete", "userId": "50000000-0000-0000-0000-000000000000", "zone": { "account": "system", "acl": { "rules": [] }, "adminGroupId": "40000000-0000-0000-0000-000000000000", "created": "2020-07-15T21:15:36Z", "email": "Ops@company.invalid", "id": "00000000-0000-0000-0000-000000000000", "isTest": false, "latestSync": "2020-07-15T21:15:36Z", "name": "example.com.", "shared": false, "status": "Active", "updated": "2021-03-03T18:02:47Z" } } lego-4.9.1/providers/dns/vinyldns/fixtures/recordSetDelete.json000066400000000000000000000020161434020463500247430ustar00rootroot00000000000000{ "changeType": "Delete", "created": "2021-03-04T16:21:54Z", "id": "20000000-0000-0000-0000-000000000000", "recordSet": { "account": "", "created": "2021-03-04T16:21:54Z", "id": "11000000-0000-0000-0000-000000000000", "name": "_acme-challenge.host", "records": [ { "text": "O2UTPYgIzRNt5N27EVcNKDxv6goSF7ru3zi3chZXKUw" } ], "status": "Pending", "ttl": 30, "type": "TXT", "zoneId": "00000000-0000-0000-0000-000000000000" }, "singleBatchChangeIds": [], "status": "Pending", "userId": "50000000-0000-0000-0000-000000000000", "zone": { "account": "system", "acl": { "rules": [] }, "adminGroupId": "40000000-0000-0000-0000-000000000000", "created": "2020-07-15T21:15:36Z", "email": "Ops@company.invalid", "id": "00000000-0000-0000-0000-000000000000", "isTest": false, "latestSync": "2020-07-15T21:15:36Z", "name": "example.com.", "shared": false, "status": "Active", "updated": "2021-03-03T18:02:47Z" } } lego-4.9.1/providers/dns/vinyldns/fixtures/recordSetUpdate-create.json000066400000000000000000000020161434020463500262240ustar00rootroot00000000000000{ "changeType": "Create", "created": "2021-03-04T16:21:54Z", "id": "20000000-0000-0000-0000-000000000000", "recordSet": { "account": "", "created": "2021-03-04T16:21:54Z", "id": "11000000-0000-0000-0000-000000000000", "name": "_acme-challenge.host", "records": [ { "text": "O2UTPYgIzRNt5N27EVcNKDxv6goSF7ru3zi3chZXKUw" } ], "status": "Pending", "ttl": 30, "type": "TXT", "zoneId": "00000000-0000-0000-0000-000000000000" }, "singleBatchChangeIds": [], "status": "Pending", "userId": "50000000-0000-0000-0000-000000000000", "zone": { "account": "system", "acl": { "rules": [] }, "adminGroupId": "40000000-0000-0000-0000-000000000000", "created": "2020-07-15T21:15:36Z", "email": "Ops@company.invalid", "id": "00000000-0000-0000-0000-000000000000", "isTest": false, "latestSync": "2020-07-15T21:15:36Z", "name": "example.com.", "shared": false, "status": "Active", "updated": "2021-03-03T18:02:47Z" } } lego-4.9.1/providers/dns/vinyldns/fixtures/recordSetsListAll-empty.json000066400000000000000000000001551434020463500264260ustar00rootroot00000000000000{ "maxItems": 100, "nameSort": "ASC", "recordNameFilter": "_acme-challenge.host", "recordSets": [] } lego-4.9.1/providers/dns/vinyldns/fixtures/recordSetsListAll.json000066400000000000000000000011661434020463500252750ustar00rootroot00000000000000{ "maxItems": 100, "nameSort": "ASC", "recordNameFilter": "_acme-challenge.host", "recordSets": [ { "accessLevel": "Delete", "account": "", "created": "2021-03-04T00:51:43Z", "fqdn": "_acme-challenge.host.example.com.", "id": "30000000-0000-0000-0000-000000000000", "name": "_acme-challenge.host", "records": [ { "text": "O2UTPYgIzRNt5N27EVcNKDxv6goSF7ru3zi3chZXKUw" } ], "status": "Active", "ttl": 30, "type": "TXT", "updated": "2021-03-04T00:51:43Z", "zoneId": "00000000-0000-0000-0000-000000000000" } ] } lego-4.9.1/providers/dns/vinyldns/fixtures/zoneByName.json000066400000000000000000000007431434020463500237420ustar00rootroot00000000000000{ "zone": { "accessLevel": "Delete", "account": "system", "acl": { "rules": [] }, "adminGroupId": "40000000-0000-0000-0000-000000000000", "adminGroupName": "OpsTeam", "created": "2020-07-15T21:15:36Z", "email": "Ops@company.invalid", "id": "00000000-0000-0000-0000-000000000000", "latestSync": "2020-07-15T21:15:36Z", "name": "example.com.", "shared": false, "status": "Active", "updated": "2021-03-03T18:02:47Z" } } lego-4.9.1/providers/dns/vinyldns/mock_test.go000066400000000000000000000043371434020463500214510ustar00rootroot00000000000000package vinyldns import ( "fmt" "net/http" "net/http/httptest" "os" "sync" "testing" "github.com/stretchr/testify/require" ) func setup(t *testing.T) (*http.ServeMux, *DNSProvider) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) config := NewDefaultConfig() config.AccessKey = "foo" config.SecretKey = "bar" config.Host = server.URL p, err := NewDNSProviderConfig(config) require.NoError(t, err) return mux, p } type mockRouter struct { debug bool mu sync.Mutex routes map[string]map[string]http.HandlerFunc } func newMockRouter() *mockRouter { routes := map[string]map[string]http.HandlerFunc{ http.MethodGet: {}, http.MethodPost: {}, http.MethodPut: {}, http.MethodDelete: {}, } return &mockRouter{ routes: routes, } } func (h *mockRouter) Debug() *mockRouter { h.debug = true return h } func (h *mockRouter) Get(path string, statusCode int, filename string) *mockRouter { h.add(http.MethodGet, path, statusCode, filename) return h } func (h *mockRouter) Post(path string, statusCode int, filename string) *mockRouter { h.add(http.MethodPost, path, statusCode, filename) return h } func (h *mockRouter) Put(path string, statusCode int, filename string) *mockRouter { h.add(http.MethodPut, path, statusCode, filename) return h } func (h *mockRouter) Delete(path string, statusCode int, filename string) *mockRouter { h.add(http.MethodDelete, path, statusCode, filename) return h } func (h *mockRouter) ServeHTTP(rw http.ResponseWriter, req *http.Request) { h.mu.Lock() defer h.mu.Unlock() if h.debug { fmt.Println(req) } rt := h.routes[req.Method] if rt == nil { http.NotFound(rw, req) return } hdl := rt[req.URL.Path] if hdl == nil { http.NotFound(rw, req) return } hdl(rw, req) } func (h *mockRouter) add(method, path string, statusCode int, filename string) { h.routes[method][path] = func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(statusCode) data, err := os.ReadFile(fmt.Sprintf("./fixtures/%s.json", filename)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } rw.Header().Set("Content-Type", "application/json") _, _ = rw.Write(data) } } lego-4.9.1/providers/dns/vinyldns/vinyldns.go000066400000000000000000000165201434020463500213240ustar00rootroot00000000000000// Package vinyldns implements a DNS provider for solving the DNS-01 challenge using VinylDNS. package vinyldns import ( "errors" "fmt" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/platform/wait" "github.com/vinyldns/go-vinyldns/vinyldns" ) // Environment variables names. const ( envNamespace = "VINYLDNS_" EnvAccessKey = envNamespace + "ACCESS_KEY" EnvSecretKey = envNamespace + "SECRET_KEY" EnvHost = envNamespace + "HOST" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { AccessKey string SecretKey string Host string TTL int PropagationTimeout time.Duration PollingInterval time.Duration } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 30), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *vinyldns.Client config *Config } // NewDNSProvider returns a DNSProvider instance configured for VinylDNS. // Credentials must be passed in the environment variables: // VINYLDNS_ACCESS_KEY, VINYLDNS_SECRET_KEY, VINYLDNS_HOST. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAccessKey, EnvSecretKey, EnvHost) if err != nil { return nil, fmt.Errorf("vinyldns: %w", err) } config := NewDefaultConfig() config.AccessKey = values[EnvAccessKey] config.SecretKey = values[EnvSecretKey] config.Host = values[EnvHost] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for VinylDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("vinyldns: the configuration of the VinylDNS DNS provider is nil") } if config.AccessKey == "" || config.SecretKey == "" { return nil, errors.New("vinyldns: credentials are missing") } if config.Host == "" { return nil, errors.New("vinyldns: host is missing") } client := vinyldns.NewClient(vinyldns.ClientConfiguration{ AccessKey: config.AccessKey, SecretKey: config.SecretKey, Host: config.Host, UserAgent: "go-acme/lego", }) client.HTTPClient.Timeout = 30 * time.Second return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) existingRecord, err := d.getRecordSet(fqdn) if err != nil { return fmt.Errorf("vinyldns: %w", err) } record := vinyldns.Record{Text: value} if existingRecord == nil || existingRecord.ID == "" { err = d.createRecordSet(fqdn, []vinyldns.Record{record}) if err != nil { return fmt.Errorf("vinyldns: %w", err) } return nil } for _, i := range existingRecord.Records { if i.Text == value { return nil } } records := existingRecord.Records records = append(records, record) err = d.updateRecordSet(existingRecord, records) if err != nil { return fmt.Errorf("vinyldns: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) existingRecord, err := d.getRecordSet(fqdn) if err != nil { return fmt.Errorf("vinyldns: %w", err) } if existingRecord == nil || existingRecord.ID == "" || len(existingRecord.Records) == 0 { return nil } var records []vinyldns.Record for _, i := range existingRecord.Records { if i.Text != value { records = append(records, i) } } if len(records) == 0 { err = d.deleteRecordSet(existingRecord) if err != nil { return fmt.Errorf("vinyldns: %w", err) } return nil } err = d.updateRecordSet(existingRecord, records) if err != nil { return fmt.Errorf("vinyldns: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func (d *DNSProvider) getRecordSet(fqdn string) (*vinyldns.RecordSet, error) { zoneName, hostName, err := splitDomain(fqdn) if err != nil { return nil, err } zone, err := d.client.ZoneByName(zoneName) if err != nil { return nil, err } allRecordSets, err := d.client.RecordSetsListAll(zone.ID, vinyldns.ListFilter{NameFilter: hostName}) if err != nil { return nil, err } var recordSets []vinyldns.RecordSet for _, i := range allRecordSets { if i.Type == "TXT" { recordSets = append(recordSets, i) } } switch { case len(recordSets) > 1: return nil, fmt.Errorf("ambiguous recordset definition of %s", fqdn) case len(recordSets) == 1: return &recordSets[0], nil default: return nil, nil } } func (d *DNSProvider) createRecordSet(fqdn string, records []vinyldns.Record) error { zoneName, hostName, err := splitDomain(fqdn) if err != nil { return err } zone, err := d.client.ZoneByName(zoneName) if err != nil { return err } recordSet := vinyldns.RecordSet{ Name: hostName, ZoneID: zone.ID, Type: "TXT", TTL: d.config.TTL, Records: records, } resp, err := d.client.RecordSetCreate(&recordSet) if err != nil { return err } return d.waitForChanges("CreateRS", resp) } func (d *DNSProvider) updateRecordSet(recordSet *vinyldns.RecordSet, newRecords []vinyldns.Record) error { operation := "delete" if len(recordSet.Records) < len(newRecords) { operation = "add" } recordSet.Records = newRecords recordSet.TTL = d.config.TTL resp, err := d.client.RecordSetUpdate(recordSet) if err != nil { return err } return d.waitForChanges("UpdateRS - "+operation, resp) } func (d *DNSProvider) deleteRecordSet(existingRecord *vinyldns.RecordSet) error { resp, err := d.client.RecordSetDelete(existingRecord.ZoneID, existingRecord.ID) if err != nil { return err } return d.waitForChanges("DeleteRS", resp) } func (d *DNSProvider) waitForChanges(operation string, resp *vinyldns.RecordSetUpdateResponse) error { return wait.For("vinyldns", d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) { change, err := d.client.RecordSetChange(resp.Zone.ID, resp.RecordSet.ID, resp.ChangeID) if err != nil { return false, fmt.Errorf("failed to query change status: %w", err) } if change.Status == "Complete" { return true, nil } return false, fmt.Errorf("waiting operation: %s, zoneID: %s, recordsetID: %s, changeID: %s", operation, resp.Zone.ID, resp.RecordSet.ID, resp.ChangeID) }, ) } // splitDomain splits the hostname from the authoritative zone, and returns both parts. func splitDomain(fqdn string) (string, string, error) { zone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", "", err } host := dns01.UnFqdn(strings.TrimSuffix(fqdn, zone)) return zone, host, nil } lego-4.9.1/providers/dns/vinyldns/vinyldns.toml000066400000000000000000000020741434020463500216710ustar00rootroot00000000000000Name = "VinylDNS" Description = '''''' URL = "https://www.vinyldns.io" Code = "vinyldns" Since = "v4.4.0" Example = ''' VINYLDNS_ACCESS_KEY=xxxxxx \ VINYLDNS_SECRET_KEY=yyyyy \ VINYLDNS_HOST=https://api.vinyldns.example.org:9443 \ lego --email you@example.com --dns vinyldns --domains my.example.org run ''' Additional = ''' The vinyldns integration makes use of dotted hostnames to ease permission management. Users are required to have DELETE ACL level or zone admin permissions on the VinylDNS zone containing the target host. ''' [Configuration] [Configuration.Credentials] VINYLDNS_ACCESS_KEY = "The VinylDNS API key" VINYLDNS_SECRET_KEY = "The VinylDNS API Secret key" VINYLDNS_HOST = "The VinylDNS API URL" [Configuration.Additional] VINYLDNS_POLLING_INTERVAL = "Time between DNS propagation check" VINYLDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" VINYLDNS_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://www.vinyldns.io/api/" GoClient = "https://github.com/vinyldns/go-vinyldns" lego-4.9.1/providers/dns/vinyldns/vinyldns_test.go000066400000000000000000000141521434020463500223620ustar00rootroot00000000000000package vinyldns import ( "net/http" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" const ( targetRootDomain = "example.com" targetDomain = "host." + targetRootDomain zoneID = "00000000-0000-0000-0000-000000000000" newRecordSetID = "11000000-0000-0000-0000-000000000000" newCreateChangeID = "20000000-0000-0000-0000-000000000000" recordID = "30000000-0000-0000-0000-000000000000" ) var envTest = tester.NewEnvTest( EnvAccessKey, EnvSecretKey, EnvHost). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAccessKey: "123", EnvSecretKey: "456", EnvHost: "https://example.org", }, }, { desc: "missing all credentials", envVars: map[string]string{ EnvHost: "https://example.org", }, expected: "vinyldns: some credentials information are missing: VINYLDNS_ACCESS_KEY,VINYLDNS_SECRET_KEY", }, { desc: "missing access key", envVars: map[string]string{ EnvSecretKey: "456", EnvHost: "https://example.org", }, expected: "vinyldns: some credentials information are missing: VINYLDNS_ACCESS_KEY", }, { desc: "missing secret key", envVars: map[string]string{ EnvAccessKey: "123", EnvHost: "https://example.org", }, expected: "vinyldns: some credentials information are missing: VINYLDNS_SECRET_KEY", }, { desc: "missing host", envVars: map[string]string{ EnvAccessKey: "123", EnvSecretKey: "456", }, expected: "vinyldns: some credentials information are missing: VINYLDNS_HOST", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string accessKey string secretKey string host string expected string }{ { desc: "success", accessKey: "123", secretKey: "456", host: "https://example.org", }, { desc: "missing all credentials", host: "https://example.org", expected: "vinyldns: credentials are missing", }, { desc: "missing access key", secretKey: "456", host: "https://example.org", expected: "vinyldns: credentials are missing", }, { desc: "missing secret key", accessKey: "123", host: "https://example.org", expected: "vinyldns: credentials are missing", }, { desc: "missing host", accessKey: "123", secretKey: "456", expected: "vinyldns: host is missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.AccessKey = test.accessKey config.SecretKey = test.secretKey config.Host = test.host p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestDNSProvider_Present(t *testing.T) { testCases := []struct { desc string keyAuth string handler http.Handler }{ { desc: "new record", keyAuth: "123456d==", handler: newMockRouter(). Get("/zones/name/"+targetRootDomain+".", http.StatusOK, "zoneByName"). Get("/zones/"+zoneID+"/recordsets", http.StatusOK, "recordSetsListAll-empty"). Post("/zones/"+zoneID+"/recordsets", http.StatusAccepted, "recordSetUpdate-create"). Get("/zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID, http.StatusOK, "recordSetChange-create"), }, { desc: "existing record", keyAuth: "123456d==", handler: newMockRouter(). Get("/zones/name/"+targetRootDomain+".", http.StatusOK, "zoneByName"). Get("/zones/"+zoneID+"/recordsets", http.StatusOK, "recordSetsListAll"), }, { desc: "duplicate key", keyAuth: "abc123!!", handler: newMockRouter(). Get("/zones/name/"+targetRootDomain+".", http.StatusOK, "zoneByName"). Get("/zones/"+zoneID+"/recordsets", http.StatusOK, "recordSetsListAll"). Put("/zones/"+zoneID+"/recordsets/"+recordID, http.StatusAccepted, "recordSetUpdate-create"). Get("/zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID, http.StatusOK, "recordSetChange-create"), }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() mux, p := setup(t) mux.Handle("/", test.handler) err := p.Present(targetDomain, "token"+test.keyAuth, test.keyAuth) require.NoError(t, err) }) } } func TestDNSProvider_CleanUp(t *testing.T) { mux, p := setup(t) mux.Handle("/", newMockRouter(). Get("/zones/name/"+targetRootDomain+".", http.StatusOK, "zoneByName"). Get("/zones/"+zoneID+"/recordsets", http.StatusOK, "recordSetsListAll"). Delete("/zones/"+zoneID+"/recordsets/"+recordID, http.StatusAccepted, "recordSetDelete"). Get("/zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID, http.StatusOK, "recordSetChange-delete"), ) err := p.CleanUp(targetDomain, "123456d==", "123456d==") require.NoError(t, err) } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/vkcloud/000077500000000000000000000000001434020463500167245ustar00rootroot00000000000000lego-4.9.1/providers/dns/vkcloud/internal/000077500000000000000000000000001434020463500205405ustar00rootroot00000000000000lego-4.9.1/providers/dns/vkcloud/internal/client.go000066400000000000000000000071661434020463500223570ustar00rootroot00000000000000package internal import ( "errors" "fmt" "net/http" "net/url" "path" "strings" "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" ) // Client VK client. type Client struct { baseURL *url.URL openstack *gophercloud.ProviderClient authOpts gophercloud.AuthOptions authenticated bool } // NewClient creates a Client. func NewClient(endpoint string, authOpts gophercloud.AuthOptions) (*Client, error) { err := validateAuthOptions(authOpts) if err != nil { return nil, err } openstackClient, err := openstack.NewClient(authOpts.IdentityEndpoint) if err != nil { return nil, fmt.Errorf("new client: %w", err) } baseURL, err := url.Parse(endpoint) if err != nil { return nil, fmt.Errorf("parse URL: %w", err) } return &Client{ baseURL: baseURL, openstack: openstackClient, authOpts: authOpts, }, nil } func (c *Client) ListZones() ([]DNSZone, error) { var zones []DNSZone opts := &gophercloud.RequestOpts{JSONResponse: &zones} // TODO(ldez): go1.19 => c.baseURL.JoinPath("/") endpoint := joinPath(c.baseURL, "/") err := c.request(http.MethodGet, endpoint, opts) if err != nil { return nil, err } return zones, nil } func (c *Client) ListTXTRecords(zoneUUID string) ([]DNSTXTRecord, error) { var records []DNSTXTRecord opts := &gophercloud.RequestOpts{JSONResponse: &records} // TODO(ldez): go1.19 => c.baseURL.JoinPath(zoneUUID, "txt", "/") endpoint := joinPath(c.baseURL, zoneUUID, "txt", "/") err := c.request(http.MethodGet, endpoint, opts) if err != nil { return nil, err } return records, nil } func (c *Client) CreateTXTRecord(zoneUUID string, record *DNSTXTRecord) error { opts := &gophercloud.RequestOpts{ JSONBody: record, JSONResponse: record, } // TODO(ldez): go1.19 => c.baseURL.JoinPath(zoneUUID, "txt", "/") endpoint := joinPath(c.baseURL, zoneUUID, "txt", "/") return c.request(http.MethodPost, endpoint, opts) } func (c *Client) DeleteTXTRecord(zoneUUID, recordUUID string) error { // TODO(ldez): go1.19 => c.baseURL.JoinPath(zoneUUID, "txt", recordUUID) endpoint := joinPath(c.baseURL, zoneUUID, "txt", recordUUID) return c.request(http.MethodDelete, endpoint, &gophercloud.RequestOpts{}) } func (c *Client) request(method string, endpoint *url.URL, options *gophercloud.RequestOpts) error { if err := c.lazyAuth(); err != nil { return fmt.Errorf("auth: %w", err) } _, err := c.openstack.Request(method, endpoint.String(), options) if err != nil { return fmt.Errorf("request: %w", err) } return nil } func (c *Client) lazyAuth() error { if c.authenticated { return nil } err := openstack.Authenticate(c.openstack, c.authOpts) if err != nil { return err } c.authenticated = true return nil } func validateAuthOptions(opts gophercloud.AuthOptions) error { if opts.TenantID == "" { return errors.New("project id is missing in credentials information") } if opts.Username == "" { return errors.New("username is missing in credentials information") } if opts.Password == "" { return errors.New("password is missing in credentials information") } if opts.IdentityEndpoint == "" { return errors.New("identity endpoint is missing in config") } if opts.DomainName == "" { return errors.New("domain name is missing in config") } return nil } // light version of go1.19 url.URL.JoinPath. // TODO(ldez): must be remove when we will update to go1.19. func joinPath(uri *url.URL, elem ...string) *url.URL { result := path.Join(elem...) result = path.Join(uri.Path, result) if len(elem) > 0 && strings.HasSuffix(elem[len(elem)-1], "/") { result += "/" } parse, _ := uri.Parse(result) return parse } lego-4.9.1/providers/dns/vkcloud/internal/types.go000066400000000000000000000015001434020463500222270ustar00rootroot00000000000000package internal type DNSZone struct { UUID string `json:"uuid,omitempty"` Tenant string `json:"tenant,omitempty"` SoaPrimaryDNS string `json:"soa_primary_dns,omitempty"` SoaAdminEmail string `json:"soa_admin_email,omitempty"` SoaSerial int `json:"soa_serial,omitempty"` SoaRefresh int `json:"soa_refresh,omitempty"` SoaRetry int `json:"soa_retry,omitempty"` SoaExpire int `json:"soa_expire,omitempty"` SoaTTL int `json:"soa_ttl,omitempty"` Zone string `json:"zone,omitempty"` Status string `json:"status,omitempty"` } type DNSTXTRecord struct { UUID string `json:"uuid,omitempty"` Name string `json:"name,omitempty"` DNS string `json:"dns,omitempty"` Content string `json:"content,omitempty"` TTL int `json:"ttl,omitempty"` } lego-4.9.1/providers/dns/vkcloud/vkcloud.go000066400000000000000000000140171434020463500207250ustar00rootroot00000000000000// Package vkcloud implements a DNS provider for solving the DNS-01 challenge using VK Cloud. package vkcloud import ( "errors" "fmt" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/vkcloud/internal" "github.com/gophercloud/gophercloud" ) const ( defaultIdentityEndpoint = "https://infra.mail.ru/identity/v3/" defaultDNSEndpoint = "https://mcs.mail.ru/public-dns/v2/dns" ) const defaultTTL = 60 const defaultDomainName = "users" // Environment variables names. const ( envNamespace = "VK_CLOUD_" EnvDNSEndpoint = envNamespace + "DNS_ENDPOINT" EnvIdentityEndpoint = envNamespace + "IDENTITY_ENDPOINT" EnvDomainName = envNamespace + "DOMAIN_NAME" EnvProjectID = envNamespace + "PROJECT_ID" EnvUsername = envNamespace + "USERNAME" EnvPassword = envNamespace + "PASSWORD" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { ProjectID string Username string Password string DNSEndpoint string IdentityEndpoint string DomainName string PropagationTimeout time.Duration PollingInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *internal.Client config *Config } // NewDNSProvider returns a DNSProvider instance configured for VK Cloud. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvProjectID, EnvUsername, EnvPassword) if err != nil { return nil, fmt.Errorf("vkcloud: %w", err) } config := NewDefaultConfig() config.ProjectID = values[EnvProjectID] config.Username = values[EnvUsername] config.Password = values[EnvPassword] config.IdentityEndpoint = env.GetOrDefaultString(EnvIdentityEndpoint, defaultIdentityEndpoint) config.DomainName = env.GetOrDefaultString(EnvDomainName, defaultDomainName) config.DNSEndpoint = env.GetOrDefaultString(EnvDNSEndpoint, defaultDNSEndpoint) return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for VK Cloud. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("vkcloud: the configuration of the DNS provider is nil") } if config.DNSEndpoint == "" { return nil, fmt.Errorf("vkcloud: DNS endpoint is missing in config") } authOpts := gophercloud.AuthOptions{ IdentityEndpoint: config.IdentityEndpoint, Username: config.Username, Password: config.Password, DomainName: config.DomainName, TenantID: config.ProjectID, } client, err := internal.NewClient(config.DNSEndpoint, authOpts) if err != nil { return nil, fmt.Errorf("vkcloud: unable to build VK Cloud client: %w", err) } return &DNSProvider{ client: client, config: config, }, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (r *DNSProvider) Present(domain, _, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("vkcloud: %w", err) } authZone = dns01.UnFqdn(authZone) zones, err := r.client.ListZones() if err != nil { return fmt.Errorf("vkcloud: unable to fetch dns zones: %w", err) } var zoneUUID string for _, zone := range zones { if zone.Zone == authZone { zoneUUID = zone.UUID } } if zoneUUID == "" { return fmt.Errorf("vkcloud: cant find dns zone %s in VK Cloud", authZone) } name := fqdn[:len(fqdn)-len(authZone)-1] err = r.upsertTXTRecord(zoneUUID, name, value) if err != nil { return fmt.Errorf("vkcloud: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (r *DNSProvider) CleanUp(domain, _, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("vkcloud: %w", err) } authZone = dns01.UnFqdn(authZone) zones, err := r.client.ListZones() if err != nil { return fmt.Errorf("vkcloud: unable to fetch dns zones: %w", err) } var zoneUUID string for _, zone := range zones { if zone.Zone == authZone { zoneUUID = zone.UUID } } if zoneUUID == "" { return nil } name := fqdn[:len(fqdn)-len(authZone)-1] err = r.removeTXTRecord(zoneUUID, name, value) if err != nil { return fmt.Errorf("vkcloud: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (r *DNSProvider) Timeout() (timeout, interval time.Duration) { return r.config.PropagationTimeout, r.config.PollingInterval } func (r *DNSProvider) upsertTXTRecord(zoneUUID, name, value string) error { records, err := r.client.ListTXTRecords(zoneUUID) if err != nil { return err } for _, record := range records { if record.Name == name && record.Content == value { // The DNSRecord is already present, nothing to do return nil } } return r.client.CreateTXTRecord(zoneUUID, &internal.DNSTXTRecord{ Name: name, Content: value, TTL: r.config.TTL, }) } func (r *DNSProvider) removeTXTRecord(zoneUUID, name, value string) error { records, err := r.client.ListTXTRecords(zoneUUID) if err != nil { return err } name = dns01.UnFqdn(name) for _, record := range records { if record.Name == name && record.Content == value { return r.client.DeleteTXTRecord(zoneUUID, record.UUID) } } // The DNSRecord is not present, nothing to do return nil } lego-4.9.1/providers/dns/vkcloud/vkcloud.toml000066400000000000000000000034041434020463500212710ustar00rootroot00000000000000Name = "VK Cloud" Description = '''''' URL = "https://mcs.mail.ru/" Code = "vkcloud" Since = "v4.9.0" Example = ''' VK_CLOUD_PROJECT_ID="" \ VK_CLOUD_USERNAME="" \ VK_CLOUD_PASSWORD="" \ lego --email you@example.com --dns vkcloud --domains "example.org" --domains "*.example.org" run ''' Additional = ''' ## Credential inforamtion You can find all required and additional information on ["Project/Keys" page](https://mcs.mail.ru/app/en/project/keys) of your cloud. | ENV Variable | Parameter from page | |----------------------------|---------------------| | VK_CLOUD_PROJECT_ID | Project ID | | VK_CLOUD_USERNAME | Username | | VK_CLOUD_DOMAIN_NAME | User Domain Name | | VK_CLOUD_IDENTITY_ENDPOINT | Identity endpoint | ''' [Configuration] [Configuration.Credentials] VK_CLOUD_PROJECT_ID = "String ID of project in VK Cloud" VK_CLOUD_USERNAME = "Email of VK Cloud account" VK_CLOUD_PASSWORD = "Password for VK Cloud account" [Configuration.Additional] VK_CLOUD_DNS_ENDPOINT="URL of DNS API. Defaults to https://mcs.mail.ru/public-dns but can be changed for usage with private clouds" VK_CLOUD_IDENTITY_ENDPOINT="URL of OpenStack Auth API, Defaults to https://infra.mail.ru:35357/v3/ but can be changed for usage with private clouds" VK_CLOUD_DOMAIN_NAME="Openstack users domain name. Defaults to `users` but can be changed for usage with private clouds" VK_CLOUD_POLLING_INTERVAL = "Time between DNS propagation check" VK_CLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" VK_CLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://mcs.mail.ru/docs/networks/vnet/networks/publicdns/api" lego-4.9.1/providers/dns/vkcloud/vkcloud_test.go000066400000000000000000000122331434020463500217620ustar00rootroot00000000000000package vkcloud import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" const ( fakeProjectID = "an_project_id_from_vk_cloud_ui" fakeUsername = "vkclouduser@email.address" fakePasswd = "vkcloudpasswd" ) var envTest = tester.NewEnvTest(EnvProjectID, EnvUsername, EnvPassword).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvProjectID: fakeProjectID, EnvUsername: fakeUsername, EnvPassword: fakePasswd, }, }, { desc: "missing project id", envVars: map[string]string{ EnvUsername: fakeUsername, EnvPassword: fakePasswd, }, expected: "vkcloud: some credentials information are missing: VK_CLOUD_PROJECT_ID", }, { desc: "missing username", envVars: map[string]string{ EnvProjectID: fakeProjectID, EnvPassword: fakePasswd, }, expected: "vkcloud: some credentials information are missing: VK_CLOUD_USERNAME", }, { desc: "missing password", envVars: map[string]string{ EnvProjectID: fakeProjectID, EnvUsername: fakeUsername, }, expected: "vkcloud: some credentials information are missing: VK_CLOUD_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string config *Config expected string }{ { desc: "success", config: &Config{ ProjectID: fakeProjectID, Username: fakeUsername, Password: fakePasswd, DNSEndpoint: defaultDNSEndpoint, IdentityEndpoint: defaultIdentityEndpoint, DomainName: defaultDomainName, }, }, { desc: "nil config", config: nil, expected: "vkcloud: the configuration of the DNS provider is nil", }, { desc: "missing project id", config: &Config{ Username: fakeUsername, Password: fakePasswd, DNSEndpoint: defaultDNSEndpoint, IdentityEndpoint: defaultIdentityEndpoint, DomainName: defaultDomainName, }, expected: "vkcloud: unable to build VK Cloud client: project id is missing in credentials information", }, { desc: "missing username", config: &Config{ ProjectID: fakeProjectID, Password: fakePasswd, DNSEndpoint: defaultDNSEndpoint, IdentityEndpoint: defaultIdentityEndpoint, DomainName: defaultDomainName, }, expected: "vkcloud: unable to build VK Cloud client: username is missing in credentials information", }, { desc: "missing password", config: &Config{ ProjectID: fakeProjectID, Username: fakeUsername, DNSEndpoint: defaultDNSEndpoint, IdentityEndpoint: defaultIdentityEndpoint, DomainName: defaultDomainName, }, expected: "vkcloud: unable to build VK Cloud client: password is missing in credentials information", }, { desc: "missing dns endpoint", config: &Config{ ProjectID: fakeProjectID, Username: fakeUsername, Password: fakePasswd, IdentityEndpoint: defaultIdentityEndpoint, DomainName: defaultDomainName, }, expected: "vkcloud: DNS endpoint is missing in config", }, { desc: "missing identity endpoint", config: &Config{ ProjectID: fakeProjectID, Username: fakeUsername, Password: fakePasswd, DNSEndpoint: defaultDNSEndpoint, DomainName: defaultDomainName, }, expected: "vkcloud: unable to build VK Cloud client: identity endpoint is missing in config", }, { desc: "missing domain name", config: &Config{ ProjectID: fakeProjectID, Username: fakeUsername, Password: fakePasswd, DNSEndpoint: defaultDNSEndpoint, IdentityEndpoint: defaultIdentityEndpoint, }, expected: "vkcloud: unable to build VK Cloud client: domain name is missing in config", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { p, err := NewDNSProviderConfig(test.config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/vscale/000077500000000000000000000000001434020463500165325ustar00rootroot00000000000000lego-4.9.1/providers/dns/vscale/vscale.go000066400000000000000000000106171434020463500203430ustar00rootroot00000000000000// Package vscale implements a DNS provider for solving the DNS-01 challenge using Vscale Domains API. // Vscale Domain API reference: https://developers.vscale.io/documentation/api/v1/#api-Domains // Token: https://vscale.io/panel/settings/tokens/ package vscale import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/internal/selectel" ) const minTTL = 60 // Environment variables names. const ( envNamespace = "VSCALE_" EnvBaseURL = envNamespace + "BASE_URL" EnvAPIToken = envNamespace + "API_TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { BaseURL string Token string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ BaseURL: env.GetOrDefaultString(EnvBaseURL, selectel.DefaultVScaleBaseURL), TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *selectel.Client } // NewDNSProvider returns a DNSProvider instance configured for Vscale Domains API. // API token must be passed in the environment variable VSCALE_API_TOKEN. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIToken) if err != nil { return nil, fmt.Errorf("vscale: %w", err) } config := NewDefaultConfig() config.Token = values[EnvAPIToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Vscale. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("vscale: the configuration of the DNS provider is nil") } if config.Token == "" { return nil, errors.New("vscale: credentials missing") } if config.TTL < minTTL { return nil, fmt.Errorf("vscale: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } client := selectel.NewClient(config.Token) client.BaseURL = config.BaseURL client.HTTPClient = config.HTTPClient return &DNSProvider{config: config, client: client}, nil } // Timeout returns the Timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill DNS-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) // TODO(ldez) replace domain by FQDN to follow CNAME. domainObj, err := d.client.GetDomainByName(domain) if err != nil { return fmt.Errorf("vscale: %w", err) } txtRecord := selectel.Record{ Type: "TXT", TTL: d.config.TTL, Name: fqdn, Content: value, } _, err = d.client.AddRecord(domainObj.ID, txtRecord) if err != nil { return fmt.Errorf("vscale: %w", err) } return nil } // CleanUp removes a TXT record used for DNS-01 challenge. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _ := dns01.GetRecord(domain, keyAuth) recordName := dns01.UnFqdn(fqdn) // TODO(ldez) replace domain by FQDN to follow CNAME. domainObj, err := d.client.GetDomainByName(domain) if err != nil { return fmt.Errorf("vscale: %w", err) } records, err := d.client.ListRecords(domainObj.ID) if err != nil { return fmt.Errorf("vscale: %w", err) } // Delete records with specific FQDN var lastErr error for _, record := range records { if record.Name == recordName { err = d.client.DeleteRecord(domainObj.ID, record.ID) if err != nil { lastErr = fmt.Errorf("vscale: %w", err) } } } return lastErr } lego-4.9.1/providers/dns/vscale/vscale.toml000066400000000000000000000013151434020463500207040ustar00rootroot00000000000000Name = "Vscale" Description = '''''' URL = "https://vscale.io/" Code = "vscale" Since = "v2.0.0" Example = ''' VSCALE_API_TOKEN=xxxxx \ lego --email you@example.com --dns vscale --domains my.example.org run ''' [Configuration] [Configuration.Credentials] VSCALE_API_TOKEN = "API token" [Configuration.Additional] VSCALE_BASE_URL = "API endpoint URL" VSCALE_POLLING_INTERVAL = "Time between DNS propagation check" VSCALE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" VSCALE_TTL = "The TTL of the TXT record used for the DNS challenge" VSCALE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://developers.vscale.io/documentation/api/v1/#api-Domains_Records" lego-4.9.1/providers/dns/vscale/vscale_test.go000066400000000000000000000047561434020463500214110ustar00rootroot00000000000000package vscale import ( "fmt" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var envTest = tester.NewEnvTest(EnvAPIToken, EnvTTL) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIToken: "123", }, }, { desc: "missing api key", envVars: map[string]string{ EnvAPIToken: "", }, expected: fmt.Sprintf("vscale: some credentials information are missing: %s", EnvAPIToken), }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string token string ttl int expected string }{ { desc: "success", token: "123", ttl: 60, }, { desc: "missing api key", token: "", ttl: 60, expected: "vscale: credentials missing", }, { desc: "bad TTL value", token: "123", ttl: 59, expected: fmt.Sprintf("vscale: invalid TTL, TTL (59) must be greater than %d", minTTL), }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.TTL = test.ttl config.Token = test.token p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) assert.NotNil(t, p.config) assert.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/vultr/000077500000000000000000000000001434020463500164315ustar00rootroot00000000000000lego-4.9.1/providers/dns/vultr/vultr.go000066400000000000000000000136701434020463500201430ustar00rootroot00000000000000// Package vultr implements a DNS provider for solving the DNS-01 challenge using the Vultr DNS. // See https://www.vultr.com/api/#dns package vultr import ( "context" "errors" "fmt" "net/http" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/vultr/govultr/v2" "golang.org/x/oauth2" ) // Environment variables names. const ( envNamespace = "VULTR_" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client HTTPTimeout time.Duration } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *govultr.Client } // NewDNSProvider returns a DNSProvider instance with a configured Vultr client. // Authentication uses the VULTR_API_KEY environment variable. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("vultr: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Vultr. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("vultr: the configuration of the DNS provider is nil") } if config.APIKey == "" { return nil, errors.New("vultr: credentials missing") } httpClient := config.HTTPClient if httpClient == nil { httpClient = &http.Client{ Timeout: config.HTTPTimeout, Transport: &oauth2.Transport{ Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: config.APIKey}), }, } } client := govultr.NewClient(httpClient) return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the DNS-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { ctx := context.Background() fqdn, value := dns01.GetRecord(domain, keyAuth) // TODO(ldez) replace domain by FQDN to follow CNAME. zoneDomain, err := d.getHostedZone(ctx, domain) if err != nil { return fmt.Errorf("vultr: %w", err) } name := extractRecordName(fqdn, zoneDomain) req := govultr.DomainRecordReq{ Name: name, Type: "TXT", Data: `"` + value + `"`, TTL: d.config.TTL, Priority: func(v int) *int { return &v }(0), } _, err = d.client.DomainRecord.Create(ctx, zoneDomain, &req) if err != nil { return fmt.Errorf("vultr: API call failed: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { ctx := context.Background() fqdn, _ := dns01.GetRecord(domain, keyAuth) // TODO(ldez) replace domain by FQDN to follow CNAME. zoneDomain, records, err := d.findTxtRecords(ctx, domain, fqdn) if err != nil { return fmt.Errorf("vultr: %w", err) } var allErr []string for _, rec := range records { err := d.client.DomainRecord.Delete(ctx, zoneDomain, rec.ID) if err != nil { allErr = append(allErr, err.Error()) } } if len(allErr) > 0 { return errors.New(strings.Join(allErr, ": ")) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func (d *DNSProvider) getHostedZone(ctx context.Context, domain string) (string, error) { listOptions := &govultr.ListOptions{PerPage: 25} var hostedDomain govultr.Domain for { domains, meta, err := d.client.Domain.List(ctx, listOptions) if err != nil { return "", fmt.Errorf("API call failed: %w", err) } for _, dom := range domains { if strings.HasSuffix(domain, dom.Domain) && len(dom.Domain) > len(hostedDomain.Domain) { hostedDomain = dom } } if domain == hostedDomain.Domain { break } if meta.Links.Next == "" { break } listOptions.Cursor = meta.Links.Next } if hostedDomain.Domain == "" { return "", fmt.Errorf("no matching domain found for domain %s", domain) } return hostedDomain.Domain, nil } func (d *DNSProvider) findTxtRecords(ctx context.Context, domain, fqdn string) (string, []govultr.DomainRecord, error) { zoneDomain, err := d.getHostedZone(ctx, domain) if err != nil { return "", nil, err } listOptions := &govultr.ListOptions{PerPage: 25} var records []govultr.DomainRecord for { result, meta, err := d.client.DomainRecord.List(ctx, zoneDomain, listOptions) if err != nil { return "", records, fmt.Errorf("API call has failed: %w", err) } recordName := extractRecordName(fqdn, zoneDomain) for _, record := range result { if record.Type == "TXT" && record.Name == recordName { records = append(records, record) } } if meta.Links.Next == "" { break } listOptions.Cursor = meta.Links.Next } return zoneDomain, records, nil } func extractRecordName(fqdn, zone string) string { name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+zone); idx != -1 { return name[:idx] } return name } lego-4.9.1/providers/dns/vultr/vultr.toml000066400000000000000000000012411434020463500205000ustar00rootroot00000000000000Name = "Vultr" Description = '''''' URL = "https://www.vultr.com/" Code = "vultr" Since = "v0.3.1" Example = ''' VULTR_API_KEY=xxxxx \ lego --email you@example.com --dns vultr --domains my.example.org run ''' [Configuration] [Configuration.Credentials] VULTR_API_KEY = "API key" [Configuration.Additional] VULTR_POLLING_INTERVAL = "Time between DNS propagation check" VULTR_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" VULTR_TTL = "The TTL of the TXT record used for the DNS challenge" VULTR_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://www.vultr.com/api/#dns" GoClient = "https://github.com/vultr/govultr" lego-4.9.1/providers/dns/vultr/vultr_test.go000066400000000000000000000126051434020463500211770ustar00rootroot00000000000000package vultr import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "strconv" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vultr/govultr/v2" ) const envDomain = envNamespace + "TEST_DOMAIN" var envTest = tester.NewEnvTest(EnvAPIKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", }, }, { desc: "missing api key", envVars: map[string]string{ EnvAPIKey: "", }, expected: "vultr: some credentials information are missing: VULTR_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiKey string expected string }{ { desc: "success", apiKey: "123", }, { desc: "missing credentials", expected: "vultr: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) require.NotNil(t, p.client) } else { require.EqualError(t, err, test.expected) } }) } } func TestDNSProvider_getHostedZone(t *testing.T) { testCases := []struct { desc string domain string expected string expectedPageCount int }{ { desc: "exact match, in latest page", domain: "test.my.example.com", expected: "test.my.example.com", expectedPageCount: 5, }, { desc: "exact match, in the middle", domain: "my.example.com", expected: "my.example.com", expectedPageCount: 3, }, { desc: "exact match, first page", domain: "example.com", expected: "example.com", expectedPageCount: 1, }, { desc: "match on apex", domain: "test.example.org", expected: "example.org", expectedPageCount: 5, }, { desc: "match on parent", domain: "test.my.example.net", expected: "my.example.net", expectedPageCount: 5, }, } domains := []govultr.Domain{{Domain: "example.com"}, {Domain: "example.org"}, {Domain: "example.net"}} for i := 0; i < 50; i++ { domains = append(domains, govultr.Domain{Domain: fmt.Sprintf("my%02d.example.com", i)}) } domains = append(domains, govultr.Domain{Domain: "my.example.com"}, govultr.Domain{Domain: "my.example.net"}) for i := 50; i < 100; i++ { domains = append(domains, govultr.Domain{Domain: fmt.Sprintf("my%02d.example.com", i)}) } domains = append(domains, govultr.Domain{Domain: "test.my.example.com"}) type domainsBase struct { Domains []govultr.Domain `json:"domains"` Meta *govultr.Meta `json:"meta"` } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client := govultr.NewClient(nil) err := client.SetBaseURL(server.URL) require.NoError(t, err) p := &DNSProvider{client: client} var pageCount int mux.HandleFunc("/v2/domains", func(rw http.ResponseWriter, req *http.Request) { pageCount++ query := req.URL.Query() cursor, _ := strconv.Atoi(query.Get("cursor")) perPage, _ := strconv.Atoi(query.Get("per_page")) var next string if len(domains)/perPage > cursor { next = strconv.Itoa(cursor + 1) } start := cursor * perPage if len(domains) < start { start = cursor * len(domains) } end := (cursor + 1) * perPage if len(domains) < end { end = len(domains) } db := domainsBase{ Domains: domains[start:end], Meta: &govultr.Meta{ Total: len(domains), Links: &govultr.Links{Next: next}, }, } err = json.NewEncoder(rw).Encode(db) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } }) zone, err := p.getHostedZone(context.Background(), test.domain) require.NoError(t, err) assert.Equal(t, test.expected, zone) assert.Equal(t, test.expectedPageCount, pageCount) }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/wedos/000077500000000000000000000000001434020463500163765ustar00rootroot00000000000000lego-4.9.1/providers/dns/wedos/internal/000077500000000000000000000000001434020463500202125ustar00rootroot00000000000000lego-4.9.1/providers/dns/wedos/internal/client.go000066400000000000000000000124641434020463500220260ustar00rootroot00000000000000package internal import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" ) const baseURL = "https://api.wedos.com/wapi/json" const codeOk = 1000 const ( commandPing = "ping" commandDNSDomainCommit = "dns-domain-commit" commandDNSRowsList = "dns-rows-list" commandDNSRowDelete = "dns-row-delete" commandDNSRowAdd = "dns-row-add" commandDNSRowUpdate = "dns-row-update" ) type ResponsePayload struct { Code int `json:"code,omitempty"` Result string `json:"result,omitempty"` Timestamp int `json:"timestamp,omitempty"` SvTRID string `json:"svTRID,omitempty"` Command string `json:"command,omitempty"` Data json.RawMessage `json:"data"` } type DNSRow struct { ID string `json:"ID,omitempty"` Name string `json:"name,omitempty"` TTL json.Number `json:"ttl,omitempty" type:"integer"` Type string `json:"rdtype,omitempty"` Data string `json:"rdata"` } type DNSRowRequest struct { ID string `json:"row_id,omitempty"` Domain string `json:"domain,omitempty"` Name string `json:"name,omitempty"` TTL json.Number `json:"ttl,omitempty" type:"integer"` Type string `json:"type,omitempty"` Data string `json:"rdata"` } type APIRequest struct { User string `json:"user,omitempty"` Auth string `json:"auth,omitempty"` Command string `json:"command,omitempty"` Data interface{} `json:"data,omitempty"` } type Client struct { username string password string baseURL string HTTPClient *http.Client } func NewClient(username string, password string) *Client { return &Client{ username: username, password: password, baseURL: baseURL, HTTPClient: &http.Client{Timeout: 10 * time.Second}, } } // GetRecords lists all the records in the zone. // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-rows-list/ func (c *Client) GetRecords(ctx context.Context, zone string) ([]DNSRow, error) { payload := map[string]interface{}{ "domain": dns01.UnFqdn(zone), } resp, err := c.do(ctx, commandDNSRowsList, payload) if err != nil { return nil, err } arrayWrapper := struct { Rows []DNSRow `json:"row"` }{} err = json.Unmarshal(resp.Data, &arrayWrapper) if err != nil { return nil, err } return arrayWrapper.Rows, err } // AddRecord adds a record in the zone, either by updating existing records or creating new ones. // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-add-row/ // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-row-update/ func (c *Client) AddRecord(ctx context.Context, zone string, record DNSRow) error { payload := DNSRowRequest{ Domain: dns01.UnFqdn(zone), TTL: record.TTL, Type: record.Type, Data: record.Data, } cmd := commandDNSRowAdd if record.ID == "" { payload.Name = record.Name } else { cmd = commandDNSRowUpdate payload.ID = record.ID } _, err := c.do(ctx, cmd, payload) if err != nil { return err } return nil } // DeleteRecord deletes a record from the zone. // If a record does not have an ID, it will be looked up. // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-row-delete/ func (c *Client) DeleteRecord(ctx context.Context, zone string, recordID string) error { payload := DNSRowRequest{ Domain: dns01.UnFqdn(zone), ID: recordID, } _, err := c.do(ctx, commandDNSRowDelete, payload) if err != nil { return err } return nil } // Commit not really required, all changes will be auto-committed after 5 minutes. // https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-domain-commit/ func (c *Client) Commit(ctx context.Context, zone string) error { payload := map[string]interface{}{ "name": dns01.UnFqdn(zone), } _, err := c.do(ctx, commandDNSDomainCommit, payload) if err != nil { return err } return nil } func (c *Client) Ping(ctx context.Context) error { _, err := c.do(ctx, commandPing, nil) if err != nil { return err } return nil } func (c *Client) do(ctx context.Context, command string, payload interface{}) (*ResponsePayload, error) { requestObject := map[string]interface{}{ "request": APIRequest{ User: c.username, Auth: authToken(c.username, c.password), Command: command, Data: payload, }, } jsonBytes, err := json.Marshal(requestObject) if err != nil { return nil, err } form := url.Values{} form.Add("request", string(jsonBytes)) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, strings.NewReader(form.Encode())) if err != nil { return nil, err } req.Header.Add("Content-Type", "application/x-www-form-urlencoded") resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode/100 != 2 { return nil, fmt.Errorf("API error, status code: %d", resp.StatusCode) } responseWrapper := struct { Response ResponsePayload `json:"response"` }{} err = json.Unmarshal(body, &responseWrapper) if err != nil { return nil, err } if responseWrapper.Response.Code != codeOk { return nil, fmt.Errorf("wedos responded with error code %d = %s", responseWrapper.Response.Code, responseWrapper.Response.Result) } return &responseWrapper.Response, err } lego-4.9.1/providers/dns/wedos/internal/client_test.go000066400000000000000000000070171434020463500230630ustar00rootroot00000000000000package internal import ( "context" "fmt" "net/http" "net/http/httptest" "os" "regexp" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupNew(t *testing.T, expectedForm string, filename string) *Client { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { err := req.ParseForm() if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } exp := regexp.MustCompile(`"auth":"\w+",`) form := req.PostForm.Get("request") form = exp.ReplaceAllString(form, `"auth":"xxx",`) if form != expectedForm { t.Logf("invalid form data: %s", req.PostForm.Get("request")) http.Error(rw, fmt.Sprintf("invalid form data: %s", req.PostForm.Get("request")), http.StatusBadRequest) return } data, err := os.ReadFile(fmt.Sprintf("./fixtures/%s.json", filename)) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } rw.Header().Set("Content-Type", "application/json") _, _ = rw.Write(data) }) client := NewClient("user", "secret") client.baseURL = server.URL return client } func TestClient_GetRecords(t *testing.T) { expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-rows-list","data":{"domain":"example.com"}}}` client := setupNew(t, expectedForm, commandDNSRowsList) records, err := client.GetRecords(context.Background(), "example.com.") require.NoError(t, err) assert.Len(t, records, 4) expected := []DNSRow{ { ID: "911", TTL: "1800", Type: "A", Data: "1.2.3.4", }, { ID: "913", TTL: "1800", Type: "MX", Data: "1 mail1.wedos.net", }, { ID: "914", TTL: "1800", Type: "MX", Data: "10 mailbackup.wedos.net", }, { ID: "912", Name: "*", TTL: "1800", Type: "A", Data: "1.2.3.4", }, } assert.Equal(t, expected, records) } func TestClient_AddRecord(t *testing.T) { expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-row-add","data":{"domain":"example.com","name":"foo","ttl":1800,"type":"TXT","rdata":"foobar"}}}` client := setupNew(t, expectedForm, commandDNSRowAdd) record := DNSRow{ ID: "", Name: "foo", TTL: "1800", Type: "TXT", Data: "foobar", } err := client.AddRecord(context.Background(), "example.com.", record) require.NoError(t, err) } func TestClient_AddRecord_update(t *testing.T) { expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-row-update","data":{"row_id":"1","domain":"example.com","ttl":1800,"type":"TXT","rdata":"foobar"}}}` client := setupNew(t, expectedForm, commandDNSRowUpdate) record := DNSRow{ ID: "1", Name: "foo", TTL: "1800", Type: "TXT", Data: "foobar", } err := client.AddRecord(context.Background(), "example.com.", record) require.NoError(t, err) } func TestClient_DeleteRecord(t *testing.T) { expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-row-delete","data":{"row_id":"1","domain":"example.com","rdata":""}}}` client := setupNew(t, expectedForm, commandDNSRowDelete) err := client.DeleteRecord(context.Background(), "example.com.", "1") require.NoError(t, err) } func TestClient_Commit(t *testing.T) { expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-domain-commit","data":{"name":"example.com"}}}` client := setupNew(t, expectedForm, commandDNSDomainCommit) err := client.Commit(context.Background(), "example.com.") require.NoError(t, err) } lego-4.9.1/providers/dns/wedos/internal/fixtures/000077500000000000000000000000001434020463500220635ustar00rootroot00000000000000lego-4.9.1/providers/dns/wedos/internal/fixtures/dns-domain-commit.json000066400000000000000000000002471434020463500263000ustar00rootroot00000000000000{ "response": { "code": 1000, "result": "OK", "timestamp": 1291192534, "svTRID": "1291192534.6326.32542.1", "command": "dns-domain-commit" } } lego-4.9.1/providers/dns/wedos/internal/fixtures/dns-row-add.json000066400000000000000000000002411434020463500250720ustar00rootroot00000000000000{ "response": { "code": 1000, "result": "OK", "timestamp": 1291210501, "svTRID": "1291210501.7672.19698.1", "command": "dns-row-add" } } lego-4.9.1/providers/dns/wedos/internal/fixtures/dns-row-delete.json000066400000000000000000000002431434020463500256060ustar00rootroot00000000000000{ "response": { "code": 1000, "result": "OK", "timestamp": 1291370821, "svTRID": "1291370821.1702.7371.1", "command": "dns-row-delete" } } lego-4.9.1/providers/dns/wedos/internal/fixtures/dns-row-update.json000066400000000000000000000002431434020463500256260ustar00rootroot00000000000000{ "response": { "code": 1000, "result": "OK", "timestamp": 1291370821, "svTRID": "1291370821.1702.7371.1", "command": "dns-row-update" } } lego-4.9.1/providers/dns/wedos/internal/fixtures/dns-rows-list.json000066400000000000000000000021621434020463500255040ustar00rootroot00000000000000{ "response": { "code": 1000, "result": "OK", "timestamp": 1291194425, "svTRID": "1291194425.9562.9881.1", "command": "dns-rows-list", "data": { "row": [ { "ID": "911", "name": "", "ttl": "1800", "rdtype": "A", "rdata": "1.2.3.4", "changed_date": "2010-12-01 09:54:41", "author_comment": "" }, { "ID": "913", "name": "", "ttl": "1800", "rdtype": "MX", "rdata": "1 mail1.wedos.net", "changed_date": "2010-12-01 09:54:54", "author_comment": "" }, { "ID": "914", "name": "", "ttl": "1800", "rdtype": "MX", "rdata": "10 mailbackup.wedos.net", "changed_date": "2010-12-01 09:55:07", "author_comment": "" }, { "ID": "912", "name": "*", "ttl": "1800", "rdtype": "A", "rdata": "1.2.3.4", "changed_date": "2010-12-01 09:54:46", "author_comment": "" } ] } } } lego-4.9.1/providers/dns/wedos/internal/token.go000066400000000000000000000035451434020463500216700ustar00rootroot00000000000000package internal import ( "crypto/sha1" "fmt" "io" "time" ) func authToken(userName string, wapiPass string) string { return sha1string(userName + sha1string(wapiPass) + czechHourString()) } func sha1string(txt string) string { h := sha1.New() _, _ = io.WriteString(h, txt) return fmt.Sprintf("%x", h.Sum(nil)) } func czechHourString() string { return formatHour(czechHour()) } func czechHour() int { tryZones := []string{"Europe/Prague", "Europe/Paris", "CET"} for _, zoneName := range tryZones { loc, err := time.LoadLocation(zoneName) if err == nil { return time.Now().In(loc).Hour() } } // hopefully this will never be used // this is fallback for containers without tzdata installed return utcToCet(time.Now().UTC()).Hour() } func utcToCet(utc time.Time) time.Time { // https://en.wikipedia.org/wiki/Central_European_Time // As of 2011, all member states of the European Union observe summer time (daylight saving time), // from the last Sunday in March to the last Sunday in October. // States within the CET area switch to Central European Summer Time (CEST -- UTC+02:00) for the summer.[1] utcMonth := utc.Month() if utcMonth < time.March || utcMonth > time.October { return utc.Add(time.Hour) } if utcMonth > time.March && utcMonth < time.October { return utc.Add(time.Hour * 2) } dayOff := 0 breaking := time.Date(utc.Year(), utcMonth+1, dayOff, 1, 0, 0, 0, time.UTC) for { if breaking.Weekday() == time.Sunday { break } dayOff-- breaking = time.Date(utc.Year(), utcMonth+1, dayOff, 1, 0, 0, 0, time.UTC) if dayOff < -7 { panic("safety exit to avoid infinite loop") } } if (utcMonth == time.March && utc.Before(breaking)) || (utcMonth == time.October && utc.After(breaking)) { return utc.Add(time.Hour) } return utc.Add(time.Hour * 2) } func formatHour(hour int) string { return fmt.Sprintf("%02d", hour) } lego-4.9.1/providers/dns/wedos/wedos.go000066400000000000000000000120401434020463500200430ustar00rootroot00000000000000package wedos import ( "context" "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/wedos/internal" ) // Environment variables names. const ( envNamespace = "WEDOS_" EnvUsername = envNamespace + "USERNAME" EnvPassword = envNamespace + "WAPI_PASSWORD" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) const minTTL = 5 * 60 // 5 minutes // Config is used to configure the creation of the DNSProvider. type Config struct { Username string Password string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), TTL: env.GetOrDefaultInt(EnvTTL, minTTL), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *internal.Client } // NewDNSProvider returns a DNSProvider instance. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvUsername, EnvPassword) if err != nil { return nil, fmt.Errorf("wedos: %w", err) } config := NewDefaultConfig() config.Username = values[EnvUsername] config.Password = values[EnvPassword] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("wedos: the configuration of the DNS provider is nil") } if config.Username == "" || config.Password == "" { return nil, errors.New("wedos: some credentials information are missing") } if config.TTL < minTTL { return nil, fmt.Errorf("wedos: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } client := internal.NewClient(config.Username, config.Password) if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { ctx := context.Background() fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("wedos: could not determine zone for domain %q: %w", domain, err) } subDomain := dns01.UnFqdn(strings.TrimSuffix(fqdn, authZone)) record := internal.DNSRow{ Name: subDomain, TTL: json.Number(strconv.Itoa(d.config.TTL)), Type: "TXT", Data: value, } records, err := d.client.GetRecords(ctx, authZone) if err != nil { return fmt.Errorf("wedos: could not get records for domain %q: %w", domain, err) } for _, candidate := range records { if candidate.Type == "TXT" && candidate.Name == subDomain && candidate.Data == value { record.ID = candidate.ID break } } err = d.client.AddRecord(ctx, authZone, record) if err != nil { return fmt.Errorf("wedos: could not add TXT record for domain %q: %w", domain, err) } err = d.client.Commit(ctx, authZone) if err != nil { return fmt.Errorf("wedos: could not commit TXT record for domain %q: %w", domain, err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { ctx := context.Background() fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("wedos: could not determine zone for domain %q: %w", domain, err) } subDomain := dns01.UnFqdn(strings.TrimSuffix(fqdn, authZone)) records, err := d.client.GetRecords(ctx, authZone) if err != nil { return fmt.Errorf("wedos: could not get records for domain %q: %w", domain, err) } for _, candidate := range records { if candidate.Type != "TXT" || candidate.Name != subDomain || candidate.Data != value { continue } err = d.client.DeleteRecord(ctx, authZone, candidate.ID) if err != nil { return fmt.Errorf("wedos: could not remove TXT record for domain %q: %w", domain, err) } err = d.client.Commit(ctx, authZone) if err != nil { return fmt.Errorf("wedos: could not commit TXT record for domain %q: %w", domain, err) } return nil } return nil } lego-4.9.1/providers/dns/wedos/wedos.toml000066400000000000000000000014721434020463500204200ustar00rootroot00000000000000Name = "WEDOS" Description = '''''' URL = "https://www.wedos.com" Code = "wedos" Since = "v4.4.0" Example = ''' WEDOS_USERNAME=xxxxxxxx \ WEDOS_WAPI_PASSWORD=xxxxxxxx \ lego --email you@example.com --dns wedos --domains my.example.org run ''' [Configuration] [Configuration.Credentials] WEDOS_USERNAME = "Username is the same as for the admin account" WEDOS_WAPI_PASSWORD = "Password needs to be generated and IP allowed in the admin interface" [Configuration.Additional] WEDOS_POLLING_INTERVAL = "Time between DNS propagation check" WEDOS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" WEDOS_HTTP_TIMEOUT = "API request timeout" WEDOS_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://kb.wedos.com/en/kategorie/wapi-api-interface/wdns-en/" lego-4.9.1/providers/dns/wedos/wedos_test.go000066400000000000000000000057261434020463500211170ustar00rootroot00000000000000package wedos import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvUsername, EnvPassword). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvUsername: "admin@example.com", EnvPassword: "secret", }, }, { desc: "missing credentials: username", envVars: map[string]string{ EnvUsername: "", EnvPassword: "secret", }, expected: "wedos: some credentials information are missing: WEDOS_USERNAME", }, { desc: "missing credentials: password", envVars: map[string]string{ EnvUsername: "admin@example.com", EnvPassword: "", }, expected: "wedos: some credentials information are missing: WEDOS_WAPI_PASSWORD", }, { desc: "missing credentials: all", envVars: map[string]string{ EnvUsername: "", EnvPassword: "", }, expected: "wedos: some credentials information are missing: WEDOS_USERNAME,WEDOS_WAPI_PASSWORD", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string username string password string expected string }{ { desc: "success", username: "admin@example.com", password: "secret", }, { desc: "missing username", password: "secret", expected: "wedos: some credentials information are missing", }, { desc: "missing WAPI password", username: "admin@example.com", expected: "wedos: some credentials information are missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.Username = test.username config.Password = test.password p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/yandex/000077500000000000000000000000001434020463500165455ustar00rootroot00000000000000lego-4.9.1/providers/dns/yandex/internal/000077500000000000000000000000001434020463500203615ustar00rootroot00000000000000lego-4.9.1/providers/dns/yandex/internal/client.go000066400000000000000000000053471434020463500221770ustar00rootroot00000000000000package internal import ( "encoding/json" "errors" "fmt" "net/http" "strings" "github.com/google/go-querystring/query" ) const defaultBaseURL = "https://pddimp.yandex.ru/api2/admin/dns" const successCode = "ok" const pddTokenHeader = "PddToken" type Client struct { HTTPClient *http.Client BaseURL string pddToken string } func NewClient(pddToken string) (*Client, error) { if pddToken == "" { return nil, errors.New("PDD token is required") } return &Client{ HTTPClient: &http.Client{}, BaseURL: defaultBaseURL, pddToken: pddToken, }, nil } func (c *Client) AddRecord(data Record) (*Record, error) { resp, err := c.postForm("/add", data) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API response error: %d", resp.StatusCode) } r := AddResponse{} err = json.NewDecoder(resp.Body).Decode(&r) if err != nil { return nil, err } if r.Success != successCode { return nil, fmt.Errorf("error during record addition: %s", r.Error) } return r.Record, nil } func (c *Client) RemoveRecord(data Record) (int, error) { resp, err := c.postForm("/del", data) if err != nil { return 0, err } defer func() { _ = resp.Body.Close() }() r := RemoveResponse{} err = json.NewDecoder(resp.Body).Decode(&r) if err != nil { return 0, err } if r.Success != successCode { return 0, fmt.Errorf("error during record addition: %s", r.Error) } return r.RecordID, nil } func (c *Client) GetRecords(domain string) ([]Record, error) { resp, err := c.get("/list", struct { Domain string `url:"domain"` }{Domain: domain}) if err != nil { return nil, err } defer func() { _ = resp.Body.Close() }() r := ListResponse{} err = json.NewDecoder(resp.Body).Decode(&r) if err != nil { return nil, err } if r.Success != successCode { return nil, fmt.Errorf("error during record addition: %s", r.Error) } return r.Records, nil } func (c *Client) postForm(uri string, data interface{}) (*http.Response, error) { values, err := query.Values(data) if err != nil { return nil, err } req, err := http.NewRequest(http.MethodPost, c.BaseURL+uri, strings.NewReader(values.Encode())) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set(pddTokenHeader, c.pddToken) return c.HTTPClient.Do(req) } func (c *Client) get(uri string, data interface{}) (*http.Response, error) { req, err := http.NewRequest(http.MethodGet, c.BaseURL+uri, nil) if err != nil { return nil, err } req.Header.Set(pddTokenHeader, c.pddToken) values, err := query.Values(data) if err != nil { return nil, err } req.URL.RawQuery = values.Encode() return c.HTTPClient.Do(req) } lego-4.9.1/providers/dns/yandex/internal/client_test.go000066400000000000000000000156061434020463500232350ustar00rootroot00000000000000package internal import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupTest(t *testing.T) (*http.ServeMux, *Client) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) client, err := NewClient("lego") require.NoError(t, err) client.HTTPClient = server.Client() client.BaseURL = server.URL return mux, client } func TestAddRecord(t *testing.T) { testCases := []struct { desc string handler http.HandlerFunc data Record expectError bool }{ { desc: "success", handler: func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "lego", r.Header.Get(pddTokenHeader)) err := r.ParseForm() if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } assert.Equal(t, `content=txtTXTtxtTXTtxtTXT&domain=example.com&subdomain=foo&ttl=300&type=TXT`, r.PostForm.Encode()) response := AddResponse{ Domain: "example.com", Record: &Record{ ID: 1, Type: "TXT", Domain: "example.com", SubDomain: "foo", FQDN: "foo.example.com.", Content: "txtTXTtxtTXTtxtTXT", TTL: 300, }, Success: "ok", } err = json.NewEncoder(w).Encode(response) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }, data: Record{ Domain: "example.com", Type: "TXT", Content: "txtTXTtxtTXTtxtTXT", SubDomain: "foo", TTL: 300, }, }, { desc: "error", handler: func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "lego", r.Header.Get(pddTokenHeader)) err := r.ParseForm() if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } assert.Equal(t, `content=txtTXTtxtTXTtxtTXT&domain=example.com&subdomain=foo&ttl=300&type=TXT`, r.PostForm.Encode()) response := AddResponse{ Domain: "example.com", Success: "error", Error: "bad things", } err = json.NewEncoder(w).Encode(response) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }, data: Record{ Domain: "example.com", Type: "TXT", Content: "txtTXTtxtTXTtxtTXT", SubDomain: "foo", TTL: 300, }, expectError: true, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/add", test.handler) record, err := client.AddRecord(test.data) if test.expectError { require.Error(t, err) require.Nil(t, record) } else { require.NoError(t, err) require.NotNil(t, record) } }) } } func TestRemoveRecord(t *testing.T) { testCases := []struct { desc string handler http.HandlerFunc data Record expectError bool }{ { desc: "success", handler: func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "lego", r.Header.Get(pddTokenHeader)) err := r.ParseForm() if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } assert.Equal(t, `domain=example.com&record_id=6`, r.PostForm.Encode()) response := RemoveResponse{ Domain: "example.com", RecordID: 6, Success: "ok", } err = json.NewEncoder(w).Encode(response) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }, data: Record{ ID: 6, Domain: "example.com", }, }, { desc: "error", handler: func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "lego", r.Header.Get(pddTokenHeader)) err := r.ParseForm() if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } assert.Equal(t, `domain=example.com&record_id=6`, r.PostForm.Encode()) response := RemoveResponse{ Domain: "example.com", RecordID: 6, Success: "error", Error: "bad things", } err = json.NewEncoder(w).Encode(response) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }, data: Record{ ID: 6, Domain: "example.com", }, expectError: true, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { mux, client := setupTest(t) mux.HandleFunc("/del", test.handler) id, err := client.RemoveRecord(test.data) if test.expectError { require.Error(t, err) require.Equal(t, 0, id) } else { require.NoError(t, err) require.Equal(t, 6, id) } }) } } func TestGetRecords(t *testing.T) { testCases := []struct { desc string handler http.HandlerFunc domain string expectError bool }{ { desc: "success", handler: func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodGet, r.Method) assert.Equal(t, "lego", r.Header.Get(pddTokenHeader)) assert.Equal(t, "domain=example.com", r.URL.RawQuery) response := ListResponse{ Domain: "example.com", Records: []Record{ { ID: 1, Type: "TXT", Domain: "example.com", SubDomain: "foo", FQDN: "foo.example.com.", Content: "txtTXTtxtTXTtxtTXT", TTL: 300, }, { ID: 2, Type: "NS", Domain: "example.com", SubDomain: "foo", FQDN: "foo.example.com.", Content: "bar", TTL: 300, }, }, Success: "ok", } err := json.NewEncoder(w).Encode(response) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }, domain: "example.com", }, { desc: "error", handler: func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodGet, r.Method) assert.Equal(t, "lego", r.Header.Get(pddTokenHeader)) assert.Equal(t, "domain=example.com", r.URL.RawQuery) response := ListResponse{ Domain: "example.com", Success: "error", Error: "bad things", } err := json.NewEncoder(w).Encode(response) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }, domain: "example.com", expectError: true, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() mux, client := setupTest(t) mux.HandleFunc("/list", test.handler) records, err := client.GetRecords(test.domain) if test.expectError { require.Error(t, err) require.Empty(t, records) } else { require.NoError(t, err) require.Len(t, records, 2) } }) } } lego-4.9.1/providers/dns/yandex/internal/types.go000066400000000000000000000021071434020463500220540ustar00rootroot00000000000000package internal type Record struct { ID int `json:"record_id,omitempty" url:"record_id,omitempty"` Domain string `json:"domain,omitempty" url:"domain,omitempty"` SubDomain string `json:"subdomain,omitempty" url:"subdomain,omitempty"` FQDN string `json:"fqdn,omitempty" url:"fqdn,omitempty"` TTL int `json:"ttl,omitempty" url:"ttl,omitempty"` Type string `json:"type,omitempty" url:"type,omitempty"` Content string `json:"content,omitempty" url:"content,omitempty"` } type AddResponse struct { Domain string `json:"domain,omitempty"` Record *Record `json:"record,omitempty"` Success string `json:"success"` Error string `json:"error,omitempty"` } type RemoveResponse struct { Domain string `json:"domain,omitempty"` RecordID int `json:"record_id,omitempty"` Success string `json:"success"` Error string `json:"error,omitempty"` } type ListResponse struct { Domain string `json:"domain,omitempty"` Records []Record `json:"records,omitempty"` Success string `json:"success"` Error string `json:"error,omitempty"` } lego-4.9.1/providers/dns/yandex/yandex.go000066400000000000000000000106421434020463500203670ustar00rootroot00000000000000// Package yandex implements a DNS provider for solving the DNS-01 challenge using Yandex. package yandex import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/yandex/internal" "github.com/miekg/dns" ) const defaultTTL = 21600 // Environment variables names. const ( envNamespace = "YANDEX_" EnvPddToken = envNamespace + "PDD_TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { PddToken string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *internal.Client config *Config } // NewDNSProvider returns a DNSProvider instance configured for Yandex. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvPddToken) if err != nil { return nil, fmt.Errorf("yandex: %w", err) } config := NewDefaultConfig() config.PddToken = values[EnvPddToken] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Yandex. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("yandex: the configuration of the DNS provider is nil") } if config.PddToken == "" { return nil, fmt.Errorf("yandex: credentials missing") } client, err := internal.NewClient(config.PddToken) if err != nil { return nil, fmt.Errorf("yandex: %w", err) } if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) rootDomain, subDomain, err := splitDomain(fqdn) if err != nil { return fmt.Errorf("yandex: %w", err) } data := internal.Record{ Domain: rootDomain, SubDomain: subDomain, Type: "TXT", TTL: d.config.TTL, Content: value, } _, err = d.client.AddRecord(data) if err != nil { return fmt.Errorf("yandex: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) rootDomain, subDomain, err := splitDomain(fqdn) if err != nil { return fmt.Errorf("yandex: %w", err) } records, err := d.client.GetRecords(rootDomain) if err != nil { return fmt.Errorf("yandex: %w", err) } var record *internal.Record for _, rcd := range records { rcd := rcd if rcd.Type == "TXT" && rcd.SubDomain == subDomain && rcd.Content == value { record = &rcd break } } if record == nil { return fmt.Errorf("yandex: TXT record not found for domain: %s", domain) } data := internal.Record{ ID: record.ID, Domain: rootDomain, } _, err = d.client.RemoveRecord(data) if err != nil { return fmt.Errorf("yandex: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func splitDomain(full string) (string, string, error) { split := dns.Split(full) if len(split) < 2 { return "", "", fmt.Errorf("unsupported domain: %s", full) } if len(split) == 2 { return full, "", nil } domain := full[split[len(split)-2]:] subDomain := full[:split[len(split)-2]-1] return domain, subDomain, nil } lego-4.9.1/providers/dns/yandex/yandex.toml000066400000000000000000000012751434020463500207370ustar00rootroot00000000000000Name = "Yandex PDD" Description = ''' ''' URL = "https://pdd.yandex.com" Code = "yandex" Since = "v3.7.0" Example = ''' YANDEX_PDD_TOKEN= \ lego --email you@example.com --dns yandex --domains my.example.org run ''' [Configuration] [Configuration.Credentials] YANDEX_PDD_TOKEN = "Basic authentication username" [Configuration.Additional] YANDEX_POLLING_INTERVAL = "Time between DNS propagation check" YANDEX_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" YANDEX_HTTP_TIMEOUT = "API request timeout" YANDEX_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://yandex.com/dev/domain/doc/concepts/api-dns.html" lego-4.9.1/providers/dns/yandex/yandex_test.go000066400000000000000000000043631434020463500214310ustar00rootroot00000000000000package yandex import ( "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvPddToken).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvPddToken: "SECRET", }, }, { desc: "missing token", envVars: map[string]string{}, expected: "yandex: some credentials information are missing: YANDEX_PDD_TOKEN", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string config *Config expected string }{ { desc: "success", config: &Config{ PddToken: "secret", }, }, { desc: "nil config", config: nil, expected: "yandex: the configuration of the DNS provider is nil", }, { desc: "missing token", config: &Config{}, expected: "yandex: credentials missing", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { p, err := NewDNSProviderConfig(test.config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/yandexcloud/000077500000000000000000000000001434020463500175745ustar00rootroot00000000000000lego-4.9.1/providers/dns/yandexcloud/yandexcloud.go000066400000000000000000000170131434020463500224440ustar00rootroot00000000000000// Package yandexcloud implements a DNS provider for solving the DNS-01 challenge using Yandex Cloud. package yandexcloud import ( "context" "encoding/base64" "encoding/json" "errors" "fmt" "strings" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ycdns "github.com/yandex-cloud/go-genproto/yandex/cloud/dns/v1" ycsdk "github.com/yandex-cloud/go-sdk" "github.com/yandex-cloud/go-sdk/iamkey" ) const defaultTTL = 60 // Environment variables names. const ( envNamespace = "YANDEX_CLOUD_" EnvIamToken = envNamespace + "IAM_TOKEN" EnvFolderID = envNamespace + "FOLDER_ID" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. type Config struct { IamToken string FolderID string PropagationTimeout time.Duration PollingInterval time.Duration TTL int } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { client *ycsdk.SDK config *Config } // NewDNSProvider returns a DNSProvider instance configured for Yandex Cloud. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvIamToken, EnvFolderID) if err != nil { return nil, fmt.Errorf("yandexcloud: %w", err) } config := NewDefaultConfig() config.IamToken = values[EnvIamToken] config.FolderID = values[EnvFolderID] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Yandex Cloud. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("yandexcloud: the configuration of the DNS provider is nil") } if config.IamToken == "" { return nil, fmt.Errorf("yandexcloud: some credentials information are missing IAM token") } if config.FolderID == "" { return nil, fmt.Errorf("yandexcloud: some credentials information are missing folder id") } creds, err := decodeCredentials(config.IamToken) if err != nil { return nil, fmt.Errorf("yandexcloud: iam token is malformed: %w", err) } client, err := ycsdk.Build(context.Background(), ycsdk.Config{Credentials: creds}) if err != nil { return nil, errors.New("yandexcloud: unable to build yandex cloud sdk") } return &DNSProvider{ client: client, config: config, }, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (r *DNSProvider) Present(domain, _, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("yandexcloud: %w", err) } ctx := context.Background() zones, err := r.getZones(ctx) if err != nil { return fmt.Errorf("yandexcloud: %w", err) } var zoneID string for _, zone := range zones { if zone.GetZone() == authZone { zoneID = zone.GetId() } } if zoneID == "" { return fmt.Errorf("yandexcloud: cant find dns zone %s in yandex cloud", authZone) } name := fqdn[:len(fqdn)-len(authZone)-1] err = r.upsertRecordSetData(ctx, zoneID, name, value) if err != nil { return fmt.Errorf("yandexcloud: %w", err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (r *DNSProvider) CleanUp(domain, _, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("yandexcloud: %w", err) } ctx := context.Background() zones, err := r.getZones(ctx) if err != nil { return fmt.Errorf("yandexcloud: %w", err) } var zoneID string for _, zone := range zones { if zone.GetZone() == authZone { zoneID = zone.GetId() } } if zoneID == "" { return nil } name := fqdn[:len(fqdn)-len(authZone)-1] err = r.removeRecordSetData(ctx, zoneID, name, value) if err != nil { return fmt.Errorf("yandexcloud: %w", err) } return nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (r *DNSProvider) Timeout() (timeout, interval time.Duration) { return r.config.PropagationTimeout, r.config.PollingInterval } // getZones retrieves available zones from yandex cloud. func (r *DNSProvider) getZones(ctx context.Context) ([]*ycdns.DnsZone, error) { list := &ycdns.ListDnsZonesRequest{ FolderId: r.config.FolderID, } response, err := r.client.DNS().DnsZone().List(ctx, list) if err != nil { return nil, errors.New("unable to fetch dns zones") } return response.DnsZones, nil } func (r *DNSProvider) upsertRecordSetData(ctx context.Context, zoneID, name, value string) error { get := &ycdns.GetDnsZoneRecordSetRequest{ DnsZoneId: zoneID, Name: name, Type: "TXT", } exist, err := r.client.DNS().DnsZone().GetRecordSet(ctx, get) if err != nil { if !strings.Contains(err.Error(), "RecordSet not found") { return err } } record := &ycdns.RecordSet{ Name: name, Type: "TXT", Ttl: int64(r.config.TTL), Data: []string{}, } var deletions []*ycdns.RecordSet if exist != nil { record.Data = append(record.Data, exist.Data...) deletions = append(deletions, exist) } appended := appendRecordSetData(record, value) if !appended { // The value already present in RecordSet, nothing to do return nil } update := &ycdns.UpdateRecordSetsRequest{ DnsZoneId: zoneID, Deletions: deletions, Additions: []*ycdns.RecordSet{record}, } _, err = r.client.DNS().DnsZone().UpdateRecordSets(ctx, update) return err } func (r *DNSProvider) removeRecordSetData(ctx context.Context, zoneID, name, value string) error { get := &ycdns.GetDnsZoneRecordSetRequest{ DnsZoneId: zoneID, Name: name, Type: "TXT", } previousRecord, err := r.client.DNS().DnsZone().GetRecordSet(ctx, get) if err != nil { if strings.Contains(err.Error(), "RecordSet not found") { // RecordSet is not present, nothing to do return nil } return err } var additions []*ycdns.RecordSet if len(previousRecord.Data) > 1 { // RecordSet is not empty we should update it record := &ycdns.RecordSet{ Name: name, Type: "TXT", Ttl: int64(r.config.TTL), Data: []string{}, } for _, data := range previousRecord.Data { if data != value { record.Data = append(record.Data, data) } } additions = append(additions, record) } update := &ycdns.UpdateRecordSetsRequest{ DnsZoneId: zoneID, Deletions: []*ycdns.RecordSet{previousRecord}, Additions: additions, } _, err = r.client.DNS().DnsZone().UpdateRecordSets(ctx, update) return err } // decodeCredentials converts base64 encoded json of iam token to struct. func decodeCredentials(accountB64 string) (ycsdk.Credentials, error) { account, err := base64.StdEncoding.DecodeString(accountB64) if err != nil { return nil, err } key := &iamkey.Key{} err = json.Unmarshal(account, key) if err != nil { return nil, err } return ycsdk.ServiceAccountKey(key) } func appendRecordSetData(record *ycdns.RecordSet, value string) bool { for _, data := range record.Data { if data == value { return false } } record.Data = append(record.Data, value) return true } lego-4.9.1/providers/dns/yandexcloud/yandexcloud.toml000066400000000000000000000033341434020463500230130ustar00rootroot00000000000000Name = "Yandex Cloud" Description = '''''' URL = "https://cloud.yandex.com" Code = "yandexcloud" Since = "v4.9.0" Example = ''' YANDEX_CLOUD_IAM_TOKEN= \ YANDEX_CLOUD_FOLDER_ID= \ lego --email you@example.com --dns yandexcloud --domains "example.org" --domains "*.example.org" run # --- YANDEX_CLOUD_IAM_TOKEN=$(echo '{ \ "id": "", \ "service_account_id": "", \ "created_at": "", \ "key_algorithm": "RSA_2048", \ "public_key": "-----BEGIN PUBLIC KEY----------END PUBLIC KEY-----", \ "private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----" \ }' | base64) \ YANDEX_CLOUD_FOLDER_ID= \ lego --email you@example.com --dns yandexcloud --domains "example.org" --domains "*.example.org" run ''' Additional = ''' ## IAM Token The simplest way to retrieve IAM access token is usage of yc-cli, follow [docs](https://cloud.yandex.ru/docs/iam/operations/iam-token/create-for-sa) to get it ```bash yc iam key create --service-account-name my-robot --output key.json cat key.json | base64 ``` ''' [Configuration] [Configuration.Credentials] YANDEX_CLOUD_IAM_TOKEN = "The base64 encoded json which contains inforamtion about iam token of serivce account with `dns.admin` permissions" YANDEX_CLOUD_FOLDER_ID = "The string id of folder (aka project) in Yandex Cloud" [Configuration.Additional] YANDEX_CLOUD_POLLING_INTERVAL = "Time between DNS propagation check" YANDEX_CLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" YANDEX_CLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" [Links] API = "https://cloud.yandex.com/en/docs/dns/quickstart" lego-4.9.1/providers/dns/yandexcloud/yandexcloud_test.go000066400000000000000000000135301434020463500235030ustar00rootroot00000000000000package yandexcloud import ( "encoding/base64" "testing" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" const fakeIAMToken = ` { "id": "abcdefghijklmnopqrst", "service_account_id": "abcdefghijklmnopqrst", "created_at": "2000-01-01T00:00:00.000000000Z", "key_algorithm": "RSA_2048", "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-----", "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-----" } ` var envTest = tester.NewEnvTest(EnvIamToken, EnvFolderID).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvIamToken: base64.StdEncoding.EncodeToString([]byte(fakeIAMToken)), EnvFolderID: "folder_id", }, }, { desc: "missing iam token", envVars: map[string]string{ EnvFolderID: "folder_id", }, expected: "yandexcloud: some credentials information are missing: YANDEX_CLOUD_IAM_TOKEN", }, { desc: "missing folder_id", envVars: map[string]string{ EnvIamToken: base64.StdEncoding.EncodeToString([]byte(fakeIAMToken)), }, expected: "yandexcloud: some credentials information are missing: YANDEX_CLOUD_FOLDER_ID", }, { desc: "malformed token (not base64)", envVars: map[string]string{ EnvIamToken: fakeIAMToken, EnvFolderID: "folder_id", }, expected: "yandexcloud: iam token is malformed: illegal base64 data at input byte 1", }, { desc: "malformed token (invalid json in bas64)", envVars: map[string]string{ EnvIamToken: "aW52YWxpZCBqc29u", EnvFolderID: "folder_id", }, expected: "yandexcloud: iam token is malformed: invalid character 'i' looking for beginning of value", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string config *Config expected string }{ { desc: "success", config: &Config{ IamToken: base64.StdEncoding.EncodeToString([]byte(fakeIAMToken)), FolderID: "folder_id", }, }, { desc: "nil config", config: nil, expected: "yandexcloud: the configuration of the DNS provider is nil", }, { desc: "missing token", config: &Config{ FolderID: "folder_id", }, expected: "yandexcloud: some credentials information are missing IAM token", }, { desc: "missing folder id", config: &Config{ IamToken: base64.StdEncoding.EncodeToString([]byte(fakeIAMToken)), }, expected: "yandexcloud: some credentials information are missing folder id", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { p, err := NewDNSProviderConfig(test.config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/dns/zoneee/000077500000000000000000000000001434020463500165425ustar00rootroot00000000000000lego-4.9.1/providers/dns/zoneee/client.go000066400000000000000000000055471434020463500203620ustar00rootroot00000000000000package zoneee import ( "bytes" "encoding/json" "fmt" "io" "net/http" "path" ) const defaultEndpoint = "https://api.zone.eu/v2/dns/" type txtRecord struct { // Identifier (identificator) ID string `json:"id,omitempty"` // Hostname Name string `json:"name"` // TXT content value Destination string `json:"destination"` // Can this record be deleted Delete bool `json:"delete,omitempty"` // Can this record be modified Modify bool `json:"modify,omitempty"` // API url to get this entity ResourceURL string `json:"resource_url,omitempty"` } func (d *DNSProvider) addTxtRecord(domain string, record txtRecord) ([]txtRecord, error) { reqBody := &bytes.Buffer{} if err := json.NewEncoder(reqBody).Encode(record); err != nil { return nil, err } req, err := d.makeRequest(http.MethodPost, path.Join(domain, "txt"), reqBody) if err != nil { return nil, err } var resp []txtRecord if err := d.sendRequest(req, &resp); err != nil { return nil, err } return resp, nil } func (d *DNSProvider) getTxtRecords(domain string) ([]txtRecord, error) { req, err := d.makeRequest(http.MethodGet, path.Join(domain, "txt"), nil) if err != nil { return nil, err } var resp []txtRecord if err := d.sendRequest(req, &resp); err != nil { return nil, err } return resp, nil } func (d *DNSProvider) removeTxtRecord(domain, id string) error { req, err := d.makeRequest(http.MethodDelete, path.Join(domain, "txt", id), nil) if err != nil { return err } return d.sendRequest(req, nil) } func (d *DNSProvider) makeRequest(method, resource string, body io.Reader) (*http.Request, error) { uri, err := d.config.Endpoint.Parse(resource) if err != nil { return nil, err } req, err := http.NewRequest(method, uri.String(), body) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.SetBasicAuth(d.config.Username, d.config.APIKey) return req, nil } func (d *DNSProvider) sendRequest(req *http.Request, result interface{}) error { resp, err := d.config.HTTPClient.Do(req) if err != nil { return err } if err = checkResponse(resp); err != nil { return err } defer resp.Body.Close() if result == nil { return nil } raw, err := io.ReadAll(resp.Body) if err != nil { return err } err = json.Unmarshal(raw, result) if err != nil { return fmt.Errorf("unmarshaling %T error [status code=%d]: %w: %s", result, resp.StatusCode, err, string(raw)) } return err } func checkResponse(resp *http.Response) error { if resp.StatusCode < http.StatusBadRequest { return nil } if resp.Body == nil { return fmt.Errorf("response body is nil, status code=%d", resp.StatusCode) } defer resp.Body.Close() raw, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("unable to read body: status code=%d, error=%w", resp.StatusCode, err) } return fmt.Errorf("status code=%d: %s", resp.StatusCode, string(raw)) } lego-4.9.1/providers/dns/zoneee/zoneee.go000066400000000000000000000103211434020463500203530ustar00rootroot00000000000000// Package zoneee implements a DNS provider for solving the DNS-01 challenge through zone.ee. package zoneee import ( "errors" "fmt" "net/http" "net/url" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) // Environment variables names. const ( envNamespace = "ZONEEE_" EnvEndpoint = envNamespace + "ENDPOINT" EnvAPIUser = envNamespace + "API_USER" EnvAPIKey = envNamespace + "API_KEY" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { Endpoint *url.URL Username string APIKey string PropagationTimeout time.Duration PollingInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { endpoint, _ := url.Parse(defaultEndpoint) return &Config{ Endpoint: endpoint, // zone.ee can take up to 5min to propagate according to the support PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config } // NewDNSProvider returns a DNSProvider instance. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIUser, EnvAPIKey) if err != nil { return nil, fmt.Errorf("zoneee: %w", err) } rawEndpoint := env.GetOrDefaultString(EnvEndpoint, defaultEndpoint) endpoint, err := url.Parse(rawEndpoint) if err != nil { return nil, fmt.Errorf("zoneee: %w", err) } config := NewDefaultConfig() config.Username = values[EnvAPIUser] config.APIKey = values[EnvAPIKey] config.Endpoint = endpoint return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Zone.ee. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("zoneee: the configuration of the DNS provider is nil") } if config.Username == "" { return nil, errors.New("zoneee: credentials missing: username") } if config.APIKey == "" { return nil, errors.New("zoneee: credentials missing: API key") } if config.Endpoint == nil { return nil, errors.New("zoneee: the endpoint is missing") } return &DNSProvider{config: config}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) record := txtRecord{ Name: fqdn[:len(fqdn)-1], Destination: value, } authZone, err := getHostedZone(fqdn) if err != nil { return fmt.Errorf("zoneee: %w", err) } _, err = d.addTxtRecord(authZone, record) if err != nil { return fmt.Errorf("zoneee: %w", err) } return nil } // CleanUp removes the TXT record previously created. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) authZone, err := getHostedZone(fqdn) if err != nil { return fmt.Errorf("zoneee: %w", err) } records, err := d.getTxtRecords(authZone) if err != nil { return fmt.Errorf("zoneee: %w", err) } var id string for _, record := range records { if record.Destination == value { id = record.ID } } if id == "" { return fmt.Errorf("zoneee: txt record does not exist for %s", value) } if err = d.removeTxtRecord(authZone, id); err != nil { return fmt.Errorf("zoneee: %w", err) } return nil } func getHostedZone(domain string) (string, error) { authZone, err := dns01.FindZoneByFqdn(domain) if err != nil { return "", err } return dns01.UnFqdn(authZone), nil } lego-4.9.1/providers/dns/zoneee/zoneee.toml000066400000000000000000000013231434020463500207230ustar00rootroot00000000000000Name = "Zone.ee" Description = '''''' URL = "https://www.zone.ee/" Code = "zoneee" Since = "v2.1.0" Example = ''' ZONEEE_API_USER=xxxxx \ ZONEEE_API_KEY=yyyyy \ lego --email you@example.com --dns zoneee --domains my.example.org run ''' [Configuration] [Configuration.Credentials] ZONEEE_API_USER = "API user" ZONEEE_API_KEY = "API key" [Configuration.Additional] ZONEEE_ENDPOINT = "API endpoint URL" ZONEEE_POLLING_INTERVAL = "Time between DNS propagation check" ZONEEE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" ZONEEE_TTL = "The TTL of the TXT record used for the DNS challenge" ZONEEE_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://api.zone.eu/v2" lego-4.9.1/providers/dns/zoneee/zoneee_test.go000066400000000000000000000235171434020463500214250ustar00rootroot00000000000000package zoneee import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvEndpoint, EnvAPIUser, EnvAPIKey). WithLiveTestRequirements(EnvAPIUser, EnvAPIKey). WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIUser: "123", EnvAPIKey: "456", }, }, { desc: "missing credentials", envVars: map[string]string{ EnvAPIUser: "", EnvAPIKey: "", }, expected: "zoneee: some credentials information are missing: ZONEEE_API_USER,ZONEEE_API_KEY", }, { desc: "missing username", envVars: map[string]string{ EnvAPIUser: "", EnvAPIKey: "456", }, expected: "zoneee: some credentials information are missing: ZONEEE_API_USER", }, { desc: "missing API key", envVars: map[string]string{ EnvAPIUser: "123", EnvAPIKey: "", }, expected: "zoneee: some credentials information are missing: ZONEEE_API_KEY", }, { desc: "invalid URL", envVars: map[string]string{ EnvAPIUser: "123", EnvAPIKey: "456", EnvEndpoint: ":", }, expected: `zoneee: parse ":": missing protocol scheme`, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string apiUser string apiKey string endpoint string expected string }{ { desc: "success", apiKey: "123", apiUser: "456", }, { desc: "missing credentials", expected: "zoneee: credentials missing: username", }, { desc: "missing api key", apiUser: "456", expected: "zoneee: credentials missing: API key", }, { desc: "missing username", apiKey: "123", expected: "zoneee: credentials missing: username", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey config.Username = test.apiUser if len(test.endpoint) > 0 { config.Endpoint = mustParse(test.endpoint) } p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestDNSProvider_Present(t *testing.T) { hostedZone := "example.com" domain := "prefix." + hostedZone testCases := []struct { desc string username string apiKey string handlers map[string]http.HandlerFunc expectedError string }{ { desc: "success", username: "bar", apiKey: "foo", handlers: map[string]http.HandlerFunc{ "/" + hostedZone + "/txt": mockHandlerCreateRecord, }, }, { desc: "invalid auth", username: "nope", apiKey: "foo", handlers: map[string]http.HandlerFunc{ "/" + hostedZone + "/txt": mockHandlerCreateRecord, }, expectedError: "zoneee: status code=401: Unauthorized\n", }, { desc: "error", username: "bar", apiKey: "foo", expectedError: "zoneee: status code=404: 404 page not found\n", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() mux := http.NewServeMux() for uri, handler := range test.handlers { mux.HandleFunc(uri, handler) } server := httptest.NewServer(mux) t.Cleanup(server.Close) config := NewDefaultConfig() config.Endpoint = mustParse(server.URL) config.Username = test.username config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) require.NoError(t, err) err = p.Present(domain, "token", "key") if test.expectedError == "" { require.NoError(t, err) } else { require.EqualError(t, err, test.expectedError) } }) } } func TestDNSProvider_Cleanup(t *testing.T) { hostedZone := "example.com" domain := "prefix." + hostedZone testCases := []struct { desc string username string apiKey string handlers map[string]http.HandlerFunc expectedError string }{ { desc: "success", username: "bar", apiKey: "foo", handlers: map[string]http.HandlerFunc{ "/" + hostedZone + "/txt": mockHandlerGetRecords([]txtRecord{{ ID: "1234", Name: domain, Destination: "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM", Delete: true, Modify: true, }}), "/" + hostedZone + "/txt/1234": mockHandlerDeleteRecord, }, }, { desc: "no txt records", username: "bar", apiKey: "foo", handlers: map[string]http.HandlerFunc{ "/" + hostedZone + "/txt": mockHandlerGetRecords([]txtRecord{}), "/" + hostedZone + "/txt/1234": mockHandlerDeleteRecord, }, expectedError: "zoneee: txt record does not exist for LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM", }, { desc: "invalid auth", username: "nope", apiKey: "foo", handlers: map[string]http.HandlerFunc{ "/" + hostedZone + "/txt": mockHandlerGetRecords([]txtRecord{{ ID: "1234", Name: domain, Destination: "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM", Delete: true, Modify: true, }}), "/" + hostedZone + "/txt/1234": mockHandlerDeleteRecord, }, expectedError: "zoneee: status code=401: Unauthorized\n", }, { desc: "error", username: "bar", apiKey: "foo", expectedError: "zoneee: status code=404: 404 page not found\n", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() mux := http.NewServeMux() for uri, handler := range test.handlers { mux.HandleFunc(uri, handler) } server := httptest.NewServer(mux) t.Cleanup(server.Close) config := NewDefaultConfig() config.Endpoint = mustParse(server.URL) config.Username = test.username config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) require.NoError(t, err) err = p.CleanUp(domain, "token", "key") if test.expectedError == "" { require.NoError(t, err) } else { require.EqualError(t, err, test.expectedError) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(2 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func mustParse(rawURL string) *url.URL { uri, err := url.Parse(rawURL) if err != nil { panic(err) } return uri } func mockHandlerCreateRecord(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } username, apiKey, ok := req.BasicAuth() if username != "bar" || apiKey != "foo" || !ok { rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and API key.")) http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } record := txtRecord{} err := json.NewDecoder(req.Body).Decode(&record) if err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } record.ID = "1234" record.Delete = true record.Modify = true record.ResourceURL = req.URL.String() + "/1234" bytes, err := json.Marshal([]txtRecord{record}) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } if _, err = rw.Write(bytes); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } } func mockHandlerGetRecords(records []txtRecord) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet { http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } username, apiKey, ok := req.BasicAuth() if username != "bar" || apiKey != "foo" || !ok { rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and API key.")) http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } for _, value := range records { if value.ResourceURL == "" { value.ResourceURL = req.URL.String() + "/" + value.ID } } bytes, err := json.Marshal(records) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } if _, err = rw.Write(bytes); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } } } func mockHandlerDeleteRecord(rw http.ResponseWriter, req *http.Request) { if req.Method != http.MethodDelete { http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } username, apiKey, ok := req.BasicAuth() if username != "bar" || apiKey != "foo" || !ok { rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and API key.")) http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } rw.WriteHeader(http.StatusNoContent) } lego-4.9.1/providers/dns/zonomi/000077500000000000000000000000001434020463500165705ustar00rootroot00000000000000lego-4.9.1/providers/dns/zonomi/zonomi.go000066400000000000000000000075001434020463500204340ustar00rootroot00000000000000// Package zonomi implements a DNS provider for solving the DNS-01 challenge using Zonomi DNS. package zonomi import ( "errors" "fmt" "net/http" "time" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/providers/dns/internal/rimuhosting" ) // Environment variables names. const ( envNamespace = "ZONOMI_" EnvAPIKey = envNamespace + "API_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) // Config is used to configure the creation of the DNSProvider. type Config struct { APIKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 3600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), }, } } // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config client *rimuhosting.Client } // NewDNSProvider returns a DNSProvider instance configured for Zonomi. // Credentials must be passed in the environment variables. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey) if err != nil { return nil, fmt.Errorf("zonomi: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] return NewDNSProviderConfig(config) } // NewDNSProviderConfig return a DNSProvider instance configured for Zonomi. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("zonomi: the configuration of the DNS provider is nil") } if config.APIKey == "" { return nil, errors.New("zonomi: incomplete credentials, missing API key") } client := rimuhosting.NewClient(config.APIKey) client.BaseURL = rimuhosting.DefaultZonomiBaseURL if config.HTTPClient != nil { client.HTTPClient = config.HTTPClient } return &DNSProvider{config: config, client: client}, nil } // Timeout returns the timeout and interval to use when checking for DNS propagation. // Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) records, err := d.client.FindTXTRecords(dns01.UnFqdn(fqdn)) if err != nil { return fmt.Errorf("zonomi: failed to find record(s) for %s: %w", domain, err) } actions := []rimuhosting.ActionParameter{ rimuhosting.AddRecord(dns01.UnFqdn(fqdn), value, d.config.TTL), } for _, record := range records { actions = append(actions, rimuhosting.AddRecord(record.Name, record.Content, d.config.TTL)) } _, err = d.client.DoActions(actions...) if err != nil { return fmt.Errorf("zonomi: failed to add record(s) for %s: %w", domain, err) } return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value := dns01.GetRecord(domain, keyAuth) action := rimuhosting.DeleteRecord(dns01.UnFqdn(fqdn), value) _, err := d.client.DoActions(action) if err != nil { return fmt.Errorf("zonomi: failed to delete record for %s: %w", domain, err) } return nil } lego-4.9.1/providers/dns/zonomi/zonomi.toml000066400000000000000000000012431434020463500210000ustar00rootroot00000000000000Name = "Zonomi" Description = '''''' URL = "https://zonomi.com" Code = "zonomi" Since = "v3.5.0" Example = ''' ZONOMI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ lego --email you@example.com --dns zonomi --domains my.example.org run ''' [Configuration] [Configuration.Credentials] ZONOMI_API_KEY = "User API key" [Configuration.Additional] ZONOMI_POLLING_INTERVAL = "Time between DNS propagation check" ZONOMI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" ZONOMI_TTL = "The TTL of the TXT record used for the DNS challenge" ZONOMI_HTTP_TIMEOUT = "API request timeout" [Links] API = "https://zonomi.com/app/dns/dyndns.jsp" lego-4.9.1/providers/dns/zonomi/zonomi_test.go000066400000000000000000000044571434020463500215030ustar00rootroot00000000000000package zonomi import ( "testing" "time" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/require" ) const envDomain = envNamespace + "DOMAIN" var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) func TestNewDNSProvider(t *testing.T) { testCases := []struct { desc string envVars map[string]string expected string }{ { desc: "success", envVars: map[string]string{ EnvAPIKey: "123", }, }, { desc: "missing api key", envVars: map[string]string{ EnvAPIKey: "", }, expected: "zonomi: some credentials information are missing: ZONOMI_API_KEY", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() envTest.Apply(test.envVars) p, err := NewDNSProvider() if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestNewDNSProviderConfig(t *testing.T) { testCases := []struct { desc string expected string apiKey string secretKey string }{ { desc: "success", apiKey: "api_key", secretKey: "api_secret", }, { desc: "missing api key", apiKey: "", secretKey: "api_secret", expected: "zonomi: incomplete credentials, missing API key", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { config := NewDefaultConfig() config.APIKey = test.apiKey p, err := NewDNSProviderConfig(config) if test.expected == "" { require.NoError(t, err) require.NotNil(t, p) require.NotNil(t, p.config) } else { require.EqualError(t, err, test.expected) } }) } } func TestLivePresent(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) err = provider.Present(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } func TestLiveCleanUp(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") } envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) time.Sleep(1 * time.Second) err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } lego-4.9.1/providers/http/000077500000000000000000000000001434020463500154505ustar00rootroot00000000000000lego-4.9.1/providers/http/memcached/000077500000000000000000000000001434020463500173565ustar00rootroot00000000000000lego-4.9.1/providers/http/memcached/README.md000066400000000000000000000006621434020463500206410ustar00rootroot00000000000000# Memcached http provider Publishes challenges into memcached where they can be retrieved by nginx. Allows specifying multiple memcached servers and the responses will be published to all of them, making it easier to verify when your domain is hosted on a cluster of servers. Example nginx config: ``` location /.well-known/acme-challenge/ { set $memcached_key "$uri"; memcached_pass 127.0.0.1:11211; } ``` lego-4.9.1/providers/http/memcached/memcached.go000066400000000000000000000027701434020463500216210ustar00rootroot00000000000000// Package memcached implements a HTTP provider for solving the HTTP-01 challenge using memcached // in combination with a webserver. package memcached import ( "errors" "fmt" "path" "github.com/go-acme/lego/v4/challenge/http01" "github.com/rainycape/memcache" ) // HTTPProvider implements HTTPProvider for `http-01` challenge. type HTTPProvider struct { hosts []string } // NewMemcachedProvider returns a HTTPProvider instance with a configured webroot path. func NewMemcachedProvider(hosts []string) (*HTTPProvider, error) { if len(hosts) == 0 { return nil, errors.New("no memcached hosts provided") } c := &HTTPProvider{ hosts: hosts, } return c, nil } // Present makes the token available at `HTTP01ChallengePath(token)` by creating a file in the given webroot path. func (w *HTTPProvider) Present(domain, token, keyAuth string) error { var errs []error challengePath := path.Join("/", http01.ChallengePath(token)) for _, host := range w.hosts { mc, err := memcache.New(host) if err != nil { errs = append(errs, err) continue } _ = mc.Add(&memcache.Item{ Key: challengePath, Value: []byte(keyAuth), Expiration: 60, }) } if len(errs) == len(w.hosts) { return fmt.Errorf("unable to store key in any of the memcache hosts: %v", errs) } return nil } // CleanUp removes the file created for the challenge. func (w *HTTPProvider) CleanUp(domain, token, keyAuth string) error { // Memcached will clean up itself, that's what expiration is for. return nil } lego-4.9.1/providers/http/memcached/memcached_test.go000066400000000000000000000053421434020463500226560ustar00rootroot00000000000000package memcached import ( "os" "path" "strings" "testing" "github.com/go-acme/lego/v4/challenge/http01" "github.com/rainycape/memcache" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( domain = "lego.test" token = "foo" keyAuth = "bar" ) var memcachedHosts = loadMemcachedHosts() func loadMemcachedHosts() []string { memcachedHostsStr := os.Getenv("MEMCACHED_HOSTS") if len(memcachedHostsStr) > 0 { return strings.Split(memcachedHostsStr, ",") } return nil } func TestNewMemcachedProviderEmpty(t *testing.T) { emptyHosts := make([]string, 0) _, err := NewMemcachedProvider(emptyHosts) assert.EqualError(t, err, "no memcached hosts provided") } func TestNewMemcachedProviderValid(t *testing.T) { if len(memcachedHosts) == 0 { t.Skip("Skipping memcached tests") } _, err := NewMemcachedProvider(memcachedHosts) require.NoError(t, err) } func TestMemcachedPresentSingleHost(t *testing.T) { if len(memcachedHosts) == 0 { t.Skip("Skipping memcached tests") } p, err := NewMemcachedProvider(memcachedHosts[0:1]) require.NoError(t, err) challengePath := path.Join("/", http01.ChallengePath(token)) err = p.Present(domain, token, keyAuth) require.NoError(t, err) mc, err := memcache.New(memcachedHosts[0]) require.NoError(t, err) i, err := mc.Get(challengePath) require.NoError(t, err) assert.Equal(t, i.Value, []byte(keyAuth)) } func TestMemcachedPresentMultiHost(t *testing.T) { if len(memcachedHosts) <= 1 { t.Skip("Skipping memcached multi-host tests") } p, err := NewMemcachedProvider(memcachedHosts) require.NoError(t, err) challengePath := path.Join("/", http01.ChallengePath(token)) err = p.Present(domain, token, keyAuth) require.NoError(t, err) for _, host := range memcachedHosts { mc, err := memcache.New(host) require.NoError(t, err) i, err := mc.Get(challengePath) require.NoError(t, err) assert.Equal(t, i.Value, []byte(keyAuth)) } } func TestMemcachedPresentPartialFailureMultiHost(t *testing.T) { if len(memcachedHosts) == 0 { t.Skip("Skipping memcached tests") } hosts := append(memcachedHosts, "5.5.5.5:11211") p, err := NewMemcachedProvider(hosts) require.NoError(t, err) challengePath := path.Join("/", http01.ChallengePath(token)) err = p.Present(domain, token, keyAuth) require.NoError(t, err) for _, host := range memcachedHosts { mc, err := memcache.New(host) require.NoError(t, err) i, err := mc.Get(challengePath) require.NoError(t, err) assert.Equal(t, i.Value, []byte(keyAuth)) } } func TestMemcachedCleanup(t *testing.T) { if len(memcachedHosts) == 0 { t.Skip("Skipping memcached tests") } p, err := NewMemcachedProvider(memcachedHosts) require.NoError(t, err) require.NoError(t, p.CleanUp(domain, token, keyAuth)) } lego-4.9.1/providers/http/webroot/000077500000000000000000000000001434020463500171315ustar00rootroot00000000000000lego-4.9.1/providers/http/webroot/webroot.go000066400000000000000000000030621434020463500211420ustar00rootroot00000000000000// Package webroot implements a HTTP provider for solving the HTTP-01 challenge using web server's root path. package webroot import ( "errors" "fmt" "os" "path/filepath" "github.com/go-acme/lego/v4/challenge/http01" ) // HTTPProvider implements ChallengeProvider for `http-01` challenge. type HTTPProvider struct { path string } // NewHTTPProvider returns a HTTPProvider instance with a configured webroot path. func NewHTTPProvider(path string) (*HTTPProvider, error) { if _, err := os.Stat(path); os.IsNotExist(err) { return nil, errors.New("webroot path does not exist") } return &HTTPProvider{path: path}, nil } // Present makes the token available at `HTTP01ChallengePath(token)` by creating a file in the given webroot path. func (w *HTTPProvider) Present(domain, token, keyAuth string) error { var err error challengeFilePath := filepath.Join(w.path, http01.ChallengePath(token)) err = os.MkdirAll(filepath.Dir(challengeFilePath), 0o755) if err != nil { return fmt.Errorf("could not create required directories in webroot for HTTP challenge: %w", err) } err = os.WriteFile(challengeFilePath, []byte(keyAuth), 0o644) if err != nil { return fmt.Errorf("could not write file in webroot for HTTP challenge: %w", err) } return nil } // CleanUp removes the file created for the challenge. func (w *HTTPProvider) CleanUp(domain, token, keyAuth string) error { err := os.Remove(filepath.Join(w.path, http01.ChallengePath(token))) if err != nil { return fmt.Errorf("could not remove file in webroot after HTTP challenge: %w", err) } return nil } lego-4.9.1/providers/http/webroot/webroot_test.go000066400000000000000000000016201434020463500221770ustar00rootroot00000000000000package webroot import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHTTPProvider(t *testing.T) { webroot := "webroot" domain := "domain" token := "token" keyAuth := "keyAuth" challengeFilePath := webroot + "/.well-known/acme-challenge/" + token require.NoError(t, os.MkdirAll(webroot+"/.well-known/acme-challenge", 0o777)) defer os.RemoveAll(webroot) provider, err := NewHTTPProvider(webroot) require.NoError(t, err) err = provider.Present(domain, token, keyAuth) require.NoError(t, err) if _, err = os.Stat(challengeFilePath); os.IsNotExist(err) { t.Error("Challenge file was not created in webroot") } var data []byte data, err = os.ReadFile(challengeFilePath) require.NoError(t, err) dataStr := string(data) assert.Equal(t, keyAuth, dataStr) err = provider.CleanUp(domain, token, keyAuth) require.NoError(t, err) } lego-4.9.1/registration/000077500000000000000000000000001434020463500151665ustar00rootroot00000000000000lego-4.9.1/registration/registar.go000066400000000000000000000114431434020463500173400ustar00rootroot00000000000000package registration import ( "errors" "net/http" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/log" ) // Resource represents all important information about a registration // of which the client needs to keep track itself. // WARNING: will be removed in the future (acme.ExtendedAccount), https://github.com/go-acme/lego/issues/855. type Resource struct { Body acme.Account `json:"body,omitempty"` URI string `json:"uri,omitempty"` } type RegisterOptions struct { TermsOfServiceAgreed bool } type RegisterEABOptions struct { TermsOfServiceAgreed bool Kid string HmacEncoded string } type Registrar struct { core *api.Core user User } func NewRegistrar(core *api.Core, user User) *Registrar { return &Registrar{ core: core, user: user, } } // Register the current account to the ACME server. func (r *Registrar) Register(options RegisterOptions) (*Resource, error) { if r == nil || r.user == nil { return nil, errors.New("acme: cannot register a nil client or user") } accMsg := acme.Account{ TermsOfServiceAgreed: options.TermsOfServiceAgreed, Contact: []string{}, } if r.user.GetEmail() != "" { log.Infof("acme: Registering account for %s", r.user.GetEmail()) accMsg.Contact = []string{"mailto:" + r.user.GetEmail()} } account, err := r.core.Accounts.New(accMsg) if err != nil { // seems impossible var errorDetails acme.ProblemDetails if !errors.As(err, &errorDetails) || errorDetails.HTTPStatus != http.StatusConflict { return nil, err } } return &Resource{URI: account.Location, Body: account.Account}, nil } // RegisterWithExternalAccountBinding Register the current account to the ACME server. func (r *Registrar) RegisterWithExternalAccountBinding(options RegisterEABOptions) (*Resource, error) { accMsg := acme.Account{ TermsOfServiceAgreed: options.TermsOfServiceAgreed, Contact: []string{}, } if r.user.GetEmail() != "" { log.Infof("acme: Registering account for %s", r.user.GetEmail()) accMsg.Contact = []string{"mailto:" + r.user.GetEmail()} } account, err := r.core.Accounts.NewEAB(accMsg, options.Kid, options.HmacEncoded) if err != nil { // seems impossible var errorDetails acme.ProblemDetails if !errors.As(err, &errorDetails) || errorDetails.HTTPStatus != http.StatusConflict { return nil, err } } return &Resource{URI: account.Location, Body: account.Account}, nil } // QueryRegistration runs a POST request on the client's registration and returns the result. // // This is similar to the Register function, // but acting on an existing registration link and resource. func (r *Registrar) QueryRegistration() (*Resource, error) { if r == nil || r.user == nil || r.user.GetRegistration() == nil { return nil, errors.New("acme: cannot query the registration of a nil client or user") } // Log the URL here instead of the email as the email may not be set log.Infof("acme: Querying account for %s", r.user.GetRegistration().URI) account, err := r.core.Accounts.Get(r.user.GetRegistration().URI) if err != nil { return nil, err } return &Resource{ Body: account, // Location: header is not returned so this needs to be populated off of existing URI URI: r.user.GetRegistration().URI, }, nil } // UpdateRegistration update the user registration on the ACME server. func (r *Registrar) UpdateRegistration(options RegisterOptions) (*Resource, error) { if r == nil || r.user == nil { return nil, errors.New("acme: cannot update a nil client or user") } accMsg := acme.Account{ TermsOfServiceAgreed: options.TermsOfServiceAgreed, Contact: []string{}, } if r.user.GetEmail() != "" { log.Infof("acme: Registering account for %s", r.user.GetEmail()) accMsg.Contact = []string{"mailto:" + r.user.GetEmail()} } accountURL := r.user.GetRegistration().URI account, err := r.core.Accounts.Update(accountURL, accMsg) if err != nil { return nil, err } return &Resource{URI: accountURL, Body: account}, nil } // DeleteRegistration deletes the client's user registration from the ACME server. func (r *Registrar) DeleteRegistration() error { if r == nil || r.user == nil { return errors.New("acme: cannot unregister a nil client or user") } log.Infof("acme: Deleting account for %s", r.user.GetEmail()) return r.core.Accounts.Deactivate(r.user.GetRegistration().URI) } // ResolveAccountByKey will attempt to look up an account using the given account key // and return its registration resource. func (r *Registrar) ResolveAccountByKey() (*Resource, error) { log.Infof("acme: Trying to resolve account by key") accMsg := acme.Account{OnlyReturnExisting: true} account, err := r.core.Accounts.New(accMsg) if err != nil { return nil, err } return &Resource{URI: account.Location, Body: account.Account}, nil } lego-4.9.1/registration/registar_test.go000066400000000000000000000023001434020463500203670ustar00rootroot00000000000000package registration import ( "crypto/rand" "crypto/rsa" "net/http" "testing" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRegistrar_ResolveAccountByKey(t *testing.T) { mux, apiURL := tester.SetupFakeAPI(t) mux.HandleFunc("/account", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Location", apiURL+"/account") err := tester.WriteJSONResponse(w, acme.Account{ Status: "valid", }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) key, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err, "Could not generate test key") user := mockUser{ email: "test@test.com", regres: &Resource{}, privatekey: key, } core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) require.NoError(t, err) registrar := NewRegistrar(core, user) res, err := registrar.ResolveAccountByKey() require.NoError(t, err, "Unexpected error resolving account by key") assert.Equal(t, "valid", res.Body.Status, "Unexpected account status") } lego-4.9.1/registration/user.go000066400000000000000000000004331434020463500164730ustar00rootroot00000000000000package registration import ( "crypto" ) // User interface is to be implemented by users of this library. // It is used by the client type to get user specific information. type User interface { GetEmail() string GetRegistration() *Resource GetPrivateKey() crypto.PrivateKey } lego-4.9.1/registration/user_test.go000066400000000000000000000005641434020463500175370ustar00rootroot00000000000000package registration import ( "crypto" "crypto/rsa" ) type mockUser struct { email string regres *Resource privatekey *rsa.PrivateKey } func (u mockUser) GetEmail() string { return u.email } func (u mockUser) GetRegistration() *Resource { return u.regres } func (u mockUser) GetPrivateKey() crypto.PrivateKey { return u.privatekey } lego-4.9.1/tmpl.Dockerfile000066400000000000000000000012101434020463500154130ustar00rootroot00000000000000# Dockerfile template used by Seihon to create multi-arch images. # https://github.com/ldez/seihon FROM golang:1-alpine as builder RUN apk --update upgrade \ && apk --no-cache --no-progress add git make ca-certificates tzdata WORKDIR /go/lego ENV GO111MODULE on # Download go modules COPY go.mod . COPY go.sum . RUN go mod download COPY . . RUN GOARCH={{ .GoARCH }} GOARM={{ .GoARM }} make build FROM {{ .RuntimeImage }} COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /go/lego/dist/lego /usr/bin/lego ENTRYPOINT [ "/usr/bin/lego" ]