diff --git a/.github/ISSUE_TEMPLATE/ISSUE.yml b/.github/ISSUE_TEMPLATE/ISSUE.yml new file mode 100644 index 000000000..199fb0c85 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ISSUE.yml @@ -0,0 +1,31 @@ +name: Issue +description: An actionable development item, like a bug report or feature request +body: + - type: markdown + attributes: + value: | + Thank you for opening an issue! This is for actionable development items like bug reports and feature requests. + If you have a question about using Caddy, please [post on our forums](https://caddy.community) instead. + - type: textarea + id: content + attributes: + label: Issue Details + placeholder: Describe the issue here. Be specific by providing complete logs and minimal instructions to reproduce, or a thoughtful proposal, etc. + validations: + required: true + - type: dropdown + id: assistance-disclosure + attributes: + label: Assistance Disclosure + description: "Our project allows assistance by AI/LLM tools as long as it is disclosed and described so we can better respond. Please certify whether you have used any such tooling related to this issue:" + options: + - + - AI used + - AI not used + validations: + required: true + - type: input + id: assistance-description + attributes: + label: If AI was used, describe the extent to which it was used. + description: 'Examples: "ChatGPT translated from my native language" or "Claude proposed this change/feature"' diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..81df4f1eb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Caddy forum + url: https://caddy.community + about: If you have questions (or answers!) about using Caddy, please use our forum \ No newline at end of file diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 44cc5b7cd..6f13031b9 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -5,11 +5,11 @@ The Caddy project would like to make sure that it stays on top of all practicall ## Supported Versions -| Version | Supported | -| ------- | ------------------ | -| 2.x | ✔️ | -| 1.x | :x: | -| < 1.x | :x: | +| Version | Supported | +| -------- | ----------| +| 2.latest | ✔️ | +| 1.x | :x: | +| < 1.x | :x: | ## Acceptable Scope @@ -48,9 +48,9 @@ We consider publicly-registered domain names to be public information. This nece It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, depending on available resources. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. (We get a lot of invalid reports.) Thank you for understanding. -When you are ready, please email Matt Holt (the author) directly: matt at dyanim dot com. +When you are ready, please submit a [new private vulnerability report](https://github.com/caddyserver/caddy/security/advisories/new). -Please don't encrypt the email body. It only makes the process more complicated. +Please don't encrypt the message. It only makes the process more complicated. Please also understand that due to our nature as an open source project, we do not have a budget to award security bounties. We can only thank you. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 64284b907..85a85f63a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,5 +3,20 @@ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" + open-pull-requests-limit: 1 + groups: + actions-deps: + patterns: + - "*" + schedule: + interval: "monthly" + + - package-ecosystem: "gomod" + directory: "/" + open-pull-requests-limit: 1 + groups: + all-updates: + patterns: + - "*" schedule: interval: "monthly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..d4ae5a3c2 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,29 @@ + + + +## Assistance Disclosure + + +_This PR is missing an assistance disclosure._ diff --git a/.github/workflows/ai.yml b/.github/workflows/ai.yml new file mode 100644 index 000000000..0008febba --- /dev/null +++ b/.github/workflows/ai.yml @@ -0,0 +1,30 @@ +name: AI Moderator +permissions: read-all +on: + issues: + types: [opened] + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] +jobs: + spam-detection: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + models: read + contents: read + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + - uses: github/ai-moderator@6bcdb2a79c2e564db8d76d7d4439d91a044c4eb6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + spam-label: 'spam' + ai-label: 'ai-generated' + minimize-detected-comments: true + # Built-in prompt configuration (all enabled by default) + enable-spam-detection: true + enable-link-spam-detection: true + enable-ai-detection: true + # custom-prompt-path: '.github/prompts/my-custom.prompt.yml' # Optional \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a74d8cc6..ca8fac298 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,28 +12,32 @@ on: - master - 2.* +env: + GOFLAGS: '-tags=nobadger,nomysql,nopgx' + # https://github.com/actions/setup-go/issues/491 + GOTOOLCHAIN: local + +permissions: + contents: read + jobs: test: strategy: # Default is true, cancels jobs for other platforms in the matrix if one fails fail-fast: false matrix: - os: + os: - linux - mac - windows - go: - - '1.22' - - '1.23' + go: + - '1.25' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.22' - GO_SEMVER: '~1.22.3' - - - go: '1.23' - GO_SEMVER: '~1.23.0' + - go: '1.25' + GO_SEMVER: '~1.25.0' # Set some variables per OS, usable via ${{ matrix.VAR }} # OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories) @@ -55,13 +59,21 @@ jobs: SUCCESS: 'True' runs-on: ${{ matrix.OS_LABEL }} - + permissions: + contents: read + pull-requests: read + actions: write # to allow uploading artifacts and cache steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + with: + egress-policy: audit + - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ matrix.GO_SEMVER }} check-latest: true @@ -99,7 +111,7 @@ jobs: env: CGO_ENABLED: 0 run: | - go build -tags nobadger -trimpath -ldflags="-w -s" -v + go build -trimpath -ldflags="-w -s" -v - name: Smoke test Caddy working-directory: ./cmd/caddy @@ -108,7 +120,7 @@ jobs: ./caddy stop - name: Publish Build Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }} path: ${{ matrix.CADDY_BIN_PATH }} @@ -122,7 +134,7 @@ jobs: # continue-on-error: true run: | # (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out - go test -tags nobadger -v -coverprofile="cover-profile.out" -short -race ./... + go test -v -coverprofile="cover-profile.out" -short -race ./... # echo "status=$?" >> $GITHUB_OUTPUT # Relevant step if we reinvestigate publishing test/coverage reports @@ -142,12 +154,21 @@ jobs: s390x-test: name: test (s390x on IBM Z) + permissions: + contents: read + pull-requests: read runs-on: ubuntu-latest - if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' + if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]' continue-on-error: true # August 2020: s390x VM is down due to weather and power issues steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + with: + egress-policy: audit + allowed-endpoints: ci-s390x.caddyserver.com:22 + - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Run Tests run: | set +e @@ -170,7 +191,7 @@ jobs: retries=3 exit_code=0 while ((retries > 0)); do - CGO_ENABLED=0 go test -p 1 -tags nobadger -v ./... + CGO_ENABLED=0 go test -p 1 -v ./... exit_code=$? if ((exit_code == 0)); then break @@ -194,11 +215,35 @@ jobs: goreleaser-check: runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]' steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + with: + egress-policy: audit + - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: goreleaser/goreleaser-action@v6 + - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 with: version: latest args: check + - name: Install Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: "~1.25" + check-latest: true + - name: Install xcaddy + run: | + go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest + xcaddy version + - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 + with: + version: latest + args: build --single-target --snapshot + env: + TAG: ${{ github.head_ref || github.ref_name }} diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml index e77e4e992..79dedb422 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -10,12 +10,21 @@ on: - master - 2.* +env: + GOFLAGS: '-tags=nobadger,nomysql,nopgx' + CGO_ENABLED: '0' + # https://github.com/actions/setup-go/issues/491 + GOTOOLCHAIN: local + +permissions: + contents: read + jobs: build: strategy: fail-fast: false matrix: - goos: + goos: - 'aix' - 'linux' - 'solaris' @@ -26,27 +35,31 @@ jobs: - 'windows' - 'darwin' - 'netbsd' - go: - - '1.22' - - '1.23' + go: + - '1.25' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.22' - GO_SEMVER: '~1.22.3' - - - go: '1.23' - GO_SEMVER: '~1.23.0' + - go: '1.25' + GO_SEMVER: '~1.25.0' runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read continue-on-error: true steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + with: + egress-policy: audit + - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ matrix.GO_SEMVER }} check-latest: true @@ -63,11 +76,9 @@ jobs: - name: Run Build env: - CGO_ENABLED: 0 GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }} shell: bash continue-on-error: true working-directory: ./cmd/caddy - run: | - GOOS=$GOOS GOARCH=$GOARCH go build -tags nobadger -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null + run: go build -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 22e13973f..4c3fd201b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,6 +13,10 @@ on: permissions: contents: read +env: + # https://github.com/actions/setup-go/issues/491 + GOTOOLCHAIN: local + jobs: # From https://github.com/golangci/golangci-lint-action golangci: @@ -40,14 +44,19 @@ jobs: runs-on: ${{ matrix.OS_LABEL }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: - go-version: '~1.23' + egress-policy: audit + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: '~1.25' check-latest: true - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 with: version: latest @@ -58,10 +67,39 @@ jobs: # only-new-issues: true govulncheck: + permissions: + contents: read + pull-requests: read runs-on: ubuntu-latest steps: - - name: govulncheck - uses: golang/govulncheck-action@v1 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: - go-version-input: '~1.23.0' + egress-policy: audit + + - name: govulncheck + uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4 + with: + go-version-input: '~1.25.0' check-latest: true + + dependency-review: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + with: + egress-policy: audit + + - name: 'Checkout Repository' + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: 'Dependency Review' + uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 + with: + comment-summary-in-pr: on-failure + # https://github.com/actions/dependency-review-action/issues/430#issuecomment-1468975566 + base-ref: ${{ github.event.pull_request.base.sha || 'master' }} + head-ref: ${{ github.event.pull_request.head.sha || github.ref }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1eb59e9d0..b6a0df5ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,13 @@ on: tags: - 'v*.*.*' +env: + # https://github.com/actions/setup-go/issues/491 + GOTOOLCHAIN: local + +permissions: + contents: read + jobs: release: name: Release @@ -13,13 +20,13 @@ jobs: os: - ubuntu-latest go: - - '1.23' + - '1.25' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.23' - GO_SEMVER: '~1.23.0' + - go: '1.25' + GO_SEMVER: '~1.25.0' runs-on: ${{ matrix.os }} # https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233 @@ -31,19 +38,24 @@ jobs: contents: write steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + with: + egress-policy: audit + - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ matrix.GO_SEMVER }} check-latest: true # Force fetch upstream tags -- because 65 minutes - # tl;dr: actions/checkout@v4 runs this line: + # tl;dr: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 runs this line: # git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/ # which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran: # git fetch --prune --unshallow @@ -97,16 +109,20 @@ jobs: git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1 - name: Install Cosign - uses: sigstore/cosign-installer@main + uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # main - name: Cosign version run: cosign version - name: Install Syft - uses: anchore/sbom-action/download-syft@main + uses: anchore/sbom-action/download-syft@7b36ad622f042cab6f59a75c2ac24ccb256e9b45 # main - name: Syft version run: syft version + - name: Install xcaddy + run: | + go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest + xcaddy version # GoReleaser will take care of publishing those artifacts into the release - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 with: version: latest args: release --clean --timeout 60m diff --git a/.github/workflows/release_published.yml b/.github/workflows/release_published.yml index 491dae75d..2ff313223 100644 --- a/.github/workflows/release_published.yml +++ b/.github/workflows/release_published.yml @@ -5,6 +5,9 @@ on: release: types: [published] +permissions: + contents: read + jobs: release: name: Release Published @@ -13,12 +16,20 @@ jobs: os: - ubuntu-latest runs-on: ${{ matrix.os }} - + permissions: + contents: read + pull-requests: read + actions: write steps: # See https://github.com/peter-evans/repository-dispatch + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + with: + egress-policy: audit + - name: Trigger event on caddyserver/dist - uses: peter-evans/repository-dispatch@v3 + uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 with: token: ${{ secrets.REPO_DISPATCH_TOKEN }} repository: caddyserver/dist @@ -26,7 +37,7 @@ jobs: client-payload: '{"tag": "${{ github.event.release.tag_name }}"}' - name: Trigger event on caddyserver/caddy-docker - uses: peter-evans/repository-dispatch@v3 + uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 with: token: ${{ secrets.REPO_DISPATCH_TOKEN }} repository: caddyserver/caddy-docker diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 000000000..a03c3bb5b --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,86 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: OpenSSF Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '20 2 * * 5' + push: + branches: [ "master", "2.*" ] + pull_request: + branches: [ "master", "2.*" ] + + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled. + if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request' + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + with: + egress-policy: audit + + - name: "Checkout code" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore + # file_mode: git + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5 + with: + sarif_file: results.sarif diff --git a/.golangci.yml b/.golangci.yml index aecff563e..4f4545054 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,27 +1,19 @@ -linters-settings: - errcheck: - exclude-functions: - - fmt.* - - (go.uber.org/zap/zapcore.ObjectEncoder).AddObject - - (go.uber.org/zap/zapcore.ObjectEncoder).AddArray - gci: - sections: - - standard # Standard section: captures all standard packages. - - default # Default section: contains all imports that could not be matched to another section type. - - prefix(github.com/caddyserver/caddy/v2/cmd) # ensure that this is always at the top and always has a line break. - - prefix(github.com/caddyserver/caddy) # Custom section: groups all imports with the specified Prefix. - # Skip generated files. - # Default: true - skip-generated: true - # Enable custom order of sections. - # If `true`, make the section order the same as the order of `sections`. - # Default: false - custom-order: true - exhaustive: - ignore-enum-types: reflect.Kind|svc.Cmd - +version: "2" +run: + issues-exit-code: 1 + tests: false + build-tags: + - nobadger + - nomysql + - nopgx +output: + formats: + text: + path: stdout + print-linter-name: true + print-issued-lines: true linters: - disable-all: true + default: none enable: - asasalint - asciicheck @@ -35,148 +27,96 @@ linters: - errcheck - errname - exhaustive - - gci - - gofmt - - goimports - - gofumpt - gosec - - gosimple - govet - - ineffassign - importas + - ineffassign - misspell - prealloc - promlinter - sloglint - sqlclosecheck - staticcheck - - tenv - testableexamples - testifylint - tparallel - - typecheck - unconvert - unused - wastedassign - whitespace - zerologlint - # these are implicitly disabled: - # - containedctx - # - contextcheck - # - cyclop - # - depguard - # - errchkjson - # - errorlint - # - exhaustruct - # - execinquery - # - exhaustruct - # - forbidigo - # - forcetypeassert - # - funlen - # - ginkgolinter - # - gocheckcompilerdirectives - # - gochecknoglobals - # - gochecknoinits - # - gochecksumtype - # - gocognit - # - goconst - # - gocritic - # - gocyclo - # - godot - # - godox - # - goerr113 - # - goheader - # - gomnd - # - gomoddirectives - # - gomodguard - # - goprintffuncname - # - gosmopolitan - # - grouper - # - inamedparam - # - interfacebloat - # - ireturn - # - lll - # - loggercheck - # - maintidx - # - makezero - # - mirror - # - musttag - # - nakedret - # - nestif - # - nilerr - # - nilnil - # - nlreturn - # - noctx - # - nolintlint - # - nonamedreturns - # - nosprintfhostport - # - paralleltest - # - perfsprint - # - predeclared - # - protogetter - # - reassign - # - revive - # - rowserrcheck - # - stylecheck - # - tagalign - # - tagliatelle - # - testpackage - # - thelper - # - unparam - # - usestdlibvars - # - varnamelen - # - wrapcheck - # - wsl - -run: - # default concurrency is a available CPU number. - # concurrency: 4 # explicitly omit this value to fully utilize available resources. - timeout: 5m - issues-exit-code: 1 - tests: false - -# output configuration options -output: - formats: - - format: 'colored-line-number' - print-issued-lines: true - print-linter-name: true - -issues: - exclude-rules: - - text: 'G115' # TODO: Either we should fix the issues or nuke the linter if it's bad - linters: - - gosec - # we aren't calling unknown URL - - text: 'G107' # G107: Url provided to HTTP request as taint input - linters: - - gosec - # as a web server that's expected to handle any template, this is totally in the hands of the user. - - text: 'G203' # G203: Use of unescaped data in HTML templates - linters: - - gosec - # we're shelling out to known commands, not relying on user-defined input. - - text: 'G204' # G204: Audit use of command execution - linters: - - gosec - # the choice of weakrand is deliberate, hence the named import "weakrand" - - path: modules/caddyhttp/reverseproxy/selectionpolicies.go - text: 'G404' # G404: Insecure random number source (rand) - linters: - - gosec - - path: modules/caddyhttp/reverseproxy/streaming.go - text: 'G404' # G404: Insecure random number source (rand) - linters: - - gosec - - path: modules/logging/filters.go - linters: - - dupl - - path: modules/caddyhttp/matchers.go - linters: - - dupl - - path: modules/caddyhttp/vars.go - linters: - - dupl - - path: _test\.go - linters: - - errcheck + settings: + staticcheck: + checks: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-QF1006", "-QF1008"] # default, and exclude 1 more undesired check + errcheck: + exclude-functions: + - fmt.* + - (go.uber.org/zap/zapcore.ObjectEncoder).AddObject + - (go.uber.org/zap/zapcore.ObjectEncoder).AddArray + exhaustive: + ignore-enum-types: reflect.Kind|svc.Cmd + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - gosec + text: G115 # TODO: Either we should fix the issues or nuke the linter if it's bad + - linters: + - gosec + text: G107 # we aren't calling unknown URL + - linters: + - gosec + text: G203 # as a web server that's expected to handle any template, this is totally in the hands of the user. + - linters: + - gosec + text: G204 # we're shelling out to known commands, not relying on user-defined input. + - linters: + - gosec + # the choice of weakrand is deliberate, hence the named import "weakrand" + path: modules/caddyhttp/reverseproxy/selectionpolicies.go + text: G404 + - linters: + - gosec + path: modules/caddyhttp/reverseproxy/streaming.go + text: G404 + - linters: + - dupl + path: modules/logging/filters.go + - linters: + - dupl + path: modules/caddyhttp/matchers.go + - linters: + - dupl + path: modules/caddyhttp/vars.go + - linters: + - errcheck + path: _test\.go + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + settings: + gci: + sections: + - standard # Standard section: captures all standard packages. + - default # Default section: contains all imports that could not be matched to another section type. + - prefix(github.com/caddyserver/caddy/v2/cmd) # ensure that this is always at the top and always has a line break. + - prefix(github.com/caddyserver/caddy) # Custom section: groups all imports with the specified Prefix. + custom-order: true + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.goreleaser.yml b/.goreleaser.yml index c7d01571e..c7ed4b365 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -13,8 +13,7 @@ before: - cp cmd/caddy/main.go caddy-build/main.go - /bin/sh -c 'cd ./caddy-build && go mod init caddy' # prepare syso files for windows embedding - - go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest - - /bin/sh -c 'for a in amd64 arm arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a $GOPATH/bin/xcaddy build {{.Env.TAG}}; done' + - /bin/sh -c 'for a in amd64 arm arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a xcaddy build {{.Env.TAG}}; done' - /bin/sh -c 'mv /tmp/buildenv_*/*.syso caddy-build' # GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env # so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate @@ -84,6 +83,8 @@ builds: - -s -w tags: - nobadger + - nomysql + - nopgx signs: - cmd: cosign @@ -110,7 +111,7 @@ archives: - id: default format_overrides: - goos: windows - format: zip + formats: zip name_template: >- {{ .ProjectName }}_ {{- .Version }}_ @@ -191,6 +192,9 @@ nfpms: preremove: ./caddy-dist/scripts/preremove.sh postremove: ./caddy-dist/scripts/postremove.sh + provides: + - httpd + release: github: owner: caddyserver diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..a38a13da3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: +- repo: https://github.com/gitleaks/gitleaks + rev: v8.16.3 + hooks: + - id: gitleaks +- repo: https://github.com/golangci/golangci-lint + rev: v1.52.2 + hooks: + - id: golangci-lint-config-verify + - id: golangci-lint + - id: golangci-lint-fmt +- repo: https://github.com/jumanjihouse/pre-commit-hooks + rev: 3.0.0 + hooks: + - id: shellcheck +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace diff --git a/README.md b/README.md index 2185eccd8..4c091f714 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,10 @@

Caddy is an extensible server platform that uses TLS by default.

+
- @caddyserver on Twitter + @caddyserver on Twitter Caddy Forum
Caddy on Sourcegraph @@ -67,6 +68,7 @@ - Fully-managed local CA for internal names & IPs - Can coordinate with other Caddy instances in a cluster - Multi-issuer fallback + - Encrypted ClientHello (ECH) support - **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues - **Production-ready** after serving trillions of requests and managing millions of TLS certificates - **Scales to hundreds of thousands of sites** as proven in production @@ -87,7 +89,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i Requirements: -- [Go 1.22.3 or newer](https://golang.org/dl/) +- [Go 1.25.0 or newer](https://golang.org/dl/) ### For development @@ -131,7 +133,7 @@ $ xcaddy build 4. Initialize a Go module: `go mod init caddy` 5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@version` replacing `version` with a git tag, commit, or branch name. 6. (Optional) Add plugins by adding their import: `_ "import/path/here"` -7. Compile: `go build` +7. Compile: `go build -tags=nobadger,nomysql,nopgx` @@ -176,7 +178,7 @@ The docs are also open source. You can contribute to them here: https://github.c ## Getting help -- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed. +- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com) before help is needed. - A [sponsorship](https://github.com/sponsors/mholt) goes a long way! We can offer private help to sponsors. If Caddy is benefitting your company, please consider a sponsorship. This not only helps fund full-time work to ensure the longevity of the project, it provides your company the resources, support, and discounts you need; along with being a great look for your company to your customers and potential customers! @@ -192,8 +194,8 @@ Matthew Holt began developing Caddy in 2014 while studying computer science at B **The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH. -- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_ -- _Author on Twitter: [@mholt6](https://twitter.com/mholt6)_ +- _Project on X: [@caddyserver](https://x.com/caddyserver)_ +- _Author on X: [@mholt6](https://x.com/mholt6)_ Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company. diff --git a/admin.go b/admin.go index 24c583235..e6895c39f 100644 --- a/admin.go +++ b/admin.go @@ -214,14 +214,15 @@ type AdminPermissions struct { // newAdminHandler reads admin's config and returns an http.Handler suitable // for use in an admin endpoint server, which will be listening on listenAddr. -func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler { +func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Context) adminHandler { muxWrap := adminHandler{mux: http.NewServeMux()} // secure the local or remote endpoint respectively if remote { muxWrap.remoteControl = admin.Remote } else { - muxWrap.enforceHost = !addr.isWildcardInterface() + // see comment in allowedOrigins() as to why we disable the host check for unix/fd networks + muxWrap.enforceHost = !addr.isWildcardInterface() && !addr.IsUnixNetwork() && !addr.IsFdNetwork() muxWrap.allowedOrigins = admin.allowedOrigins(addr) muxWrap.enforceOrigin = admin.EnforceOrigin } @@ -270,7 +271,6 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admi // register third-party module endpoints for _, m := range GetModules("admin.api") { router := m.New().(AdminRouter) - handlerLabel := m.ID.Name() for _, route := range router.Routes() { addRoute(route.Pattern, handlerLabel, route.Handler) } @@ -311,47 +311,43 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL { for _, o := range admin.Origins { uniqueOrigins[o] = struct{}{} } - if admin.Origins == nil { + // RFC 2616, Section 14.26: + // "A client MUST include a Host header field in all HTTP/1.1 request + // messages. If the requested URI does not include an Internet host + // name for the service being requested, then the Host header field MUST + // be given with an empty value." + // + // UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6. + // Understandable, but frustrating. See: + // https://github.com/golang/go/issues/60374 + // See also the discussion here: + // https://github.com/golang/go/issues/61431 + // + // We can no longer conform to RFC 2616 Section 14.26 from either Go or curl + // in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a + // bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin + // security checks, the infosec community assures me that it is secure to do + // so, because: + // + // 1) Browsers do not allow access to unix sockets + // 2) DNS is irrelevant to unix sockets + // + // If either of those two statements ever fail to hold true, it is not the + // fault of Caddy. + // + // Thus, we do not fill out allowed origins and do not enforce Host + // requirements for unix sockets. Enforcing it leads to confusion and + // frustration, when UDS have their own permissions from the OS. + // Enforcing host requirements here is effectively security theater, + // and a false sense of security. + // + // See also the discussion in #6832. + if admin.Origins == nil && !addr.IsUnixNetwork() && !addr.IsFdNetwork() { if addr.isLoopback() { - if addr.IsUnixNetwork() || addr.IsFdNetwork() { - // RFC 2616, Section 14.26: - // "A client MUST include a Host header field in all HTTP/1.1 request - // messages. If the requested URI does not include an Internet host - // name for the service being requested, then the Host header field MUST - // be given with an empty value." - // - // UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6. - // Understandable, but frustrating. See: - // https://github.com/golang/go/issues/60374 - // See also the discussion here: - // https://github.com/golang/go/issues/61431 - // - // We can no longer conform to RFC 2616 Section 14.26 from either Go or curl - // in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a - // bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin - // security checks, the infosec community assures me that it is secure to do - // so, because: - // 1) Browsers do not allow access to unix sockets - // 2) DNS is irrelevant to unix sockets - // - // I am not quite ready to trust either of those external factors, so instead - // of disabling Host/Origin checks, we now allow specific Host values when - // accessing the admin endpoint over unix sockets. I definitely don't trust - // DNS (e.g. I don't trust 'localhost' to always resolve to the local host), - // and IP shouldn't even be used, but if it is for some reason, I think we can - // at least be reasonably assured that 127.0.0.1 and ::1 route to the local - // machine, meaning that a hypothetical browser origin would have to be on the - // local machine as well. - uniqueOrigins[""] = struct{}{} - uniqueOrigins["127.0.0.1"] = struct{}{} - uniqueOrigins["::1"] = struct{}{} - } else { - uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{} - uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{} - uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{} - } - } - if !addr.IsUnixNetwork() && !addr.IsFdNetwork() { + uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{} + uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{} + uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{} + } else { uniqueOrigins[addr.JoinHostPort(0)] = struct{}{} } } @@ -382,7 +378,9 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL { // for the admin endpoint exists in cfg, a default one is used, so // that there is always an admin server (unless it is explicitly // configured to be disabled). -func replaceLocalAdminServer(cfg *Config) error { +// Critically note that some elements and functionality of the context +// may not be ready, e.g. storage. Tread carefully. +func replaceLocalAdminServer(cfg *Config, ctx Context) error { // always* be sure to close down the old admin endpoint // as gracefully as possible, even if the new one is // disabled -- careful to use reference to the current @@ -424,7 +422,14 @@ func replaceLocalAdminServer(cfg *Config) error { return err } - handler := cfg.Admin.newAdminHandler(addr, false) + handler := cfg.Admin.newAdminHandler(addr, false, ctx) + + // run the provisioners for loaded modules to make sure local + // state is properly re-initialized in the new admin server + err = cfg.Admin.provisionAdminRouters(ctx) + if err != nil { + return err + } ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{}) if err != nil { @@ -545,7 +550,14 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error { // make the HTTP handler but disable Host/Origin enforcement // because we are using TLS authentication instead - handler := cfg.Admin.newAdminHandler(addr, true) + handler := cfg.Admin.newAdminHandler(addr, true, ctx) + + // run the provisioners for loaded modules to make sure local + // state is properly re-initialized in the new admin server + err = cfg.Admin.provisionAdminRouters(ctx) + if err != nil { + return err + } // create client certificate pool for TLS mutual auth, and extract public keys // so that we can enforce access controls at the application layer @@ -934,7 +946,7 @@ func (h adminHandler) originAllowed(origin *url.URL) bool { return false } -// etagHasher returns a the hasher we used on the config to both +// etagHasher returns the hasher we used on the config to both // produce and verify ETags. func etagHasher() hash.Hash { return xxhash.New() } @@ -1138,7 +1150,7 @@ traverseLoop: return fmt.Errorf("[%s] invalid array index '%s': %v", path, idxStr, err) } - if idx < 0 || idx >= len(arr) { + if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) { return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr) } } diff --git a/admin_test.go b/admin_test.go index 9137a8881..99b4c2e47 100644 --- a/admin_test.go +++ b/admin_test.go @@ -15,12 +15,20 @@ package caddy import ( + "context" + "crypto/x509" "encoding/json" "fmt" + "maps" "net/http" + "net/http/httptest" "reflect" "sync" "testing" + + "github.com/caddyserver/certmagic" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" ) var testCfg = []byte(`{ @@ -199,7 +207,723 @@ func TestETags(t *testing.T) { } func BenchmarkLoad(b *testing.B) { - for i := 0; i < b.N; i++ { + for b.Loop() { Load(testCfg, true) } } + +func TestAdminHandlerErrorHandling(t *testing.T) { + initAdminMetrics() + + handler := adminHandler{ + mux: http.NewServeMux(), + } + + handler.mux.Handle("/error", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := fmt.Errorf("test error") + handler.handleError(w, r, err) + })) + + req := httptest.NewRequest(http.MethodGet, "/error", nil) + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + + if rr.Code == http.StatusOK { + t.Error("expected error response, got success") + } + + var apiErr APIError + if err := json.NewDecoder(rr.Body).Decode(&apiErr); err != nil { + t.Fatalf("decoding response: %v", err) + } + if apiErr.Message != "test error" { + t.Errorf("expected error message 'test error', got '%s'", apiErr.Message) + } +} + +func initAdminMetrics() { + if adminMetrics.requestErrors != nil { + prometheus.Unregister(adminMetrics.requestErrors) + } + if adminMetrics.requestCount != nil { + prometheus.Unregister(adminMetrics.requestCount) + } + + adminMetrics.requestErrors = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "caddy", + Subsystem: "admin_http", + Name: "request_errors_total", + Help: "Number of errors that occurred handling admin endpoint requests", + }, []string{"handler", "path", "method"}) + + adminMetrics.requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "caddy", + Subsystem: "admin_http", + Name: "requests_total", + Help: "Count of requests to the admin endpoint", + }, []string{"handler", "path", "code", "method"}) // Added code and method labels + + prometheus.MustRegister(adminMetrics.requestErrors) + prometheus.MustRegister(adminMetrics.requestCount) +} + +func TestAdminHandlerBuiltinRouteErrors(t *testing.T) { + initAdminMetrics() + + cfg := &Config{ + Admin: &AdminConfig{ + Listen: "localhost:2019", + }, + } + + err := replaceLocalAdminServer(cfg, Context{}) + if err != nil { + t.Fatalf("setting up admin server: %v", err) + } + defer func() { + stopAdminServer(localAdminServer) + }() + + tests := []struct { + name string + path string + method string + expectedStatus int + }{ + { + name: "stop endpoint wrong method", + path: "/stop", + method: http.MethodGet, + expectedStatus: http.StatusMethodNotAllowed, + }, + { + name: "config endpoint wrong content-type", + path: "/config/", + method: http.MethodPost, + expectedStatus: http.StatusBadRequest, + }, + { + name: "config ID missing ID", + path: "/id/", + method: http.MethodGet, + expectedStatus: http.StatusBadRequest, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil) + rr := httptest.NewRecorder() + + localAdminServer.Handler.ServeHTTP(rr, req) + + if rr.Code != test.expectedStatus { + t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code) + } + + metricValue := testGetMetricValue(map[string]string{ + "path": test.path, + "handler": "admin", + "method": test.method, + }) + if metricValue != 1 { + t.Errorf("expected error metric to be incremented once, got %v", metricValue) + } + }) + } +} + +func testGetMetricValue(labels map[string]string) float64 { + promLabels := prometheus.Labels{} + maps.Copy(promLabels, labels) + + metric, err := adminMetrics.requestErrors.GetMetricWith(promLabels) + if err != nil { + return 0 + } + + pb := &dto.Metric{} + metric.Write(pb) + return pb.GetCounter().GetValue() +} + +type mockRouter struct { + routes []AdminRoute +} + +func (m mockRouter) Routes() []AdminRoute { + return m.routes +} + +type mockModule struct { + mockRouter +} + +func (m *mockModule) CaddyModule() ModuleInfo { + return ModuleInfo{ + ID: "admin.api.mock", + New: func() Module { + mm := &mockModule{ + mockRouter: mockRouter{ + routes: m.routes, + }, + } + return mm + }, + } +} + +func TestNewAdminHandlerRouterRegistration(t *testing.T) { + originalModules := make(map[string]ModuleInfo) + maps.Copy(originalModules, modules) + defer func() { + modules = originalModules + }() + + mockRoute := AdminRoute{ + Pattern: "/mock", + Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + w.WriteHeader(http.StatusOK) + return nil + }), + } + + mock := &mockModule{ + mockRouter: mockRouter{ + routes: []AdminRoute{mockRoute}, + }, + } + RegisterModule(mock) + + addr, err := ParseNetworkAddress("localhost:2019") + if err != nil { + t.Fatalf("Failed to parse address: %v", err) + } + + admin := &AdminConfig{ + EnforceOrigin: false, + } + handler := admin.newAdminHandler(addr, false, Context{}) + + req := httptest.NewRequest("GET", "/mock", nil) + req.Host = "localhost:2019" + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code) + t.Logf("Response body: %s", rr.Body.String()) + } + + if len(admin.routers) != 1 { + t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers)) + } +} + +type mockProvisionableRouter struct { + mockRouter + provisionErr error + provisioned bool +} + +func (m *mockProvisionableRouter) Provision(Context) error { + m.provisioned = true + return m.provisionErr +} + +type mockProvisionableModule struct { + *mockProvisionableRouter +} + +func (m *mockProvisionableModule) CaddyModule() ModuleInfo { + return ModuleInfo{ + ID: "admin.api.mock_provision", + New: func() Module { + mm := &mockProvisionableModule{ + mockProvisionableRouter: &mockProvisionableRouter{ + mockRouter: m.mockRouter, + provisionErr: m.provisionErr, + }, + } + return mm + }, + } +} + +func TestAdminRouterProvisioning(t *testing.T) { + tests := []struct { + name string + provisionErr error + wantErr bool + routersAfter int // expected number of routers after provisioning + }{ + { + name: "successful provisioning", + provisionErr: nil, + wantErr: false, + routersAfter: 0, + }, + { + name: "provisioning error", + provisionErr: fmt.Errorf("provision failed"), + wantErr: true, + routersAfter: 1, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + originalModules := make(map[string]ModuleInfo) + maps.Copy(originalModules, modules) + defer func() { + modules = originalModules + }() + + mockRoute := AdminRoute{ + Pattern: "/mock", + Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return nil + }), + } + + // Create provisionable module + mock := &mockProvisionableModule{ + mockProvisionableRouter: &mockProvisionableRouter{ + mockRouter: mockRouter{ + routes: []AdminRoute{mockRoute}, + }, + provisionErr: test.provisionErr, + }, + } + RegisterModule(mock) + + admin := &AdminConfig{} + addr, err := ParseNetworkAddress("localhost:2019") + if err != nil { + t.Fatalf("Failed to parse address: %v", err) + } + + _ = admin.newAdminHandler(addr, false, Context{}) + err = admin.provisionAdminRouters(Context{}) + + if test.wantErr { + if err == nil { + t.Error("Expected error but got nil") + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + } + } + + if len(admin.routers) != test.routersAfter { + t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers)) + } + }) + } +} + +func TestAllowedOriginsUnixSocket(t *testing.T) { + // see comment in allowedOrigins() as to why we do not fill out allowed origins for UDS + tests := []struct { + name string + addr NetworkAddress + origins []string + expectOrigins []string + }{ + { + name: "unix socket with default origins", + addr: NetworkAddress{ + Network: "unix", + Host: "/tmp/caddy.sock", + }, + origins: nil, // default origins + expectOrigins: []string{}, + }, + { + name: "unix socket with custom origins", + addr: NetworkAddress{ + Network: "unix", + Host: "/tmp/caddy.sock", + }, + origins: []string{"example.com"}, + expectOrigins: []string{ + "example.com", + }, + }, + { + name: "tcp socket on localhost gets all loopback addresses", + addr: NetworkAddress{ + Network: "tcp", + Host: "localhost", + StartPort: 2019, + EndPort: 2019, + }, + origins: nil, + expectOrigins: []string{ + "localhost:2019", + "[::1]:2019", + "127.0.0.1:2019", + }, + }, + } + + for i, test := range tests { + t.Run(test.name, func(t *testing.T) { + admin := AdminConfig{ + Origins: test.origins, + } + + got := admin.allowedOrigins(test.addr) + + var gotOrigins []string + for _, u := range got { + gotOrigins = append(gotOrigins, u.Host) + } + + if len(gotOrigins) != len(test.expectOrigins) { + t.Errorf("%d: Expected %d origins but got %d", i, len(test.expectOrigins), len(gotOrigins)) + return + } + + expectMap := make(map[string]struct{}) + for _, origin := range test.expectOrigins { + expectMap[origin] = struct{}{} + } + + gotMap := make(map[string]struct{}) + for _, origin := range gotOrigins { + gotMap[origin] = struct{}{} + } + + if !reflect.DeepEqual(expectMap, gotMap) { + t.Errorf("%d: Origins mismatch.\nExpected: %v\nGot: %v", i, test.expectOrigins, gotOrigins) + } + }) + } +} + +func TestReplaceRemoteAdminServer(t *testing.T) { + const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw +MTAwMDAwMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA4O4S6BSoYcoxvRqI+h7yPOjF6KjntjzVVm9M+uHK4lzX +F1L3pSxJ2nDD4wZEV3FJ5yFOHVFqkG2vXG3BIczOlYG7UeNmKbQnKc5kZj3HGUrS +VGEktA4OJbeZhhWP15gcXN5eDM2eH3g9BFXVX6AURxLiUXzhNBUEZuj/OEyH9yEF +/qPCE+EjzVvWxvBXwgz/io4r4yok/Vq/bxJ6FlV6R7DX5oJSXyO0VEHZPi9DIyNU +kK3F/r4U1sWiJGWOs8i3YQWZ2ejh1C0aLFZpPcCGGgMNpoF31gyYP6ZuPDUyCXsE +g36UUw1JHNtIXYcLhnXuqj4A8TybTDpgXLqvwA9DBQIDAQABo1MwUTAdBgNVHQ4E +FgQUc13z30pFC63rr/HGKOE7E82vjXwwHwYDVR0jBBgwFoAUc13z30pFC63rr/HG +KOE7E82vjXwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAHO3j +oeiUXXJ7xD4P8Wj5t9d+E8lE1Xv1Dk3Z+EdG5+dan+RcToE42JJp9zB7FIh5Qz8g +W77LAjqh5oyqz3A2VJcyVgfE3uJP1R1mJM7JfGHf84QH4TZF2Q1RZY4SZs0VQ6+q +5wSlIZ4NXDy4Q4XkIJBGS61wT8IzYFXYBpx4PCP1Qj0PIE4sevEGwjsBIgxK307o +BxF8AWe6N6e4YZmQLGjQ+SeH0iwZb6vpkHyAY8Kj2hvK+cq2P7vU3VGi0t3r1F8L +IvrXHCvO2BMNJ/1UK1M4YNX8LYJqQhg9hEsIROe1OE/m3VhxIYMJI+qZXk9yHfgJ +vq+SH04xKhtFudVBAQ==` + + tests := []struct { + name string + cfg *Config + wantErr bool + }{ + { + name: "nil config", + cfg: nil, + wantErr: false, + }, + { + name: "nil admin config", + cfg: &Config{ + Admin: nil, + }, + wantErr: false, + }, + { + name: "nil remote config", + cfg: &Config{ + Admin: &AdminConfig{}, + }, + wantErr: false, + }, + { + name: "invalid listen address", + cfg: &Config{ + Admin: &AdminConfig{ + Remote: &RemoteAdmin{ + Listen: "invalid:address", + }, + }, + }, + wantErr: true, + }, + { + name: "valid config", + cfg: &Config{ + Admin: &AdminConfig{ + Identity: &IdentityConfig{}, + Remote: &RemoteAdmin{ + Listen: "localhost:2021", + AccessControl: []*AdminAccess{ + { + PublicKeys: []string{testCert}, + Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}}, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "invalid certificate", + cfg: &Config{ + Admin: &AdminConfig{ + Identity: &IdentityConfig{}, + Remote: &RemoteAdmin{ + Listen: "localhost:2021", + AccessControl: []*AdminAccess{ + { + PublicKeys: []string{"invalid-cert-data"}, + Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}}, + }, + }, + }, + }, + }, + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := Context{ + Context: context.Background(), + cfg: test.cfg, + } + + if test.cfg != nil { + test.cfg.storage = &certmagic.FileStorage{Path: t.TempDir()} + } + + if test.cfg != nil && test.cfg.Admin != nil && test.cfg.Admin.Identity != nil { + identityCertCache = certmagic.NewCache(certmagic.CacheOptions{ + GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) { + return &certmagic.Config{}, nil + }, + }) + } + + err := replaceRemoteAdminServer(ctx, test.cfg) + + if test.wantErr { + if err == nil { + t.Error("Expected error but got nil") + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + } + } + + // Clean up + if remoteAdminServer != nil { + _ = stopAdminServer(remoteAdminServer) + } + }) + } +} + +type mockIssuer struct { + configSet *certmagic.Config +} + +func (m *mockIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { + return &certmagic.IssuedCertificate{ + Certificate: []byte(csr.Raw), + }, nil +} + +func (m *mockIssuer) SetConfig(cfg *certmagic.Config) { + m.configSet = cfg +} + +func (m *mockIssuer) IssuerKey() string { + return "mock" +} + +type mockIssuerModule struct { + *mockIssuer +} + +func (m *mockIssuerModule) CaddyModule() ModuleInfo { + return ModuleInfo{ + ID: "tls.issuance.acme", + New: func() Module { + return &mockIssuerModule{mockIssuer: new(mockIssuer)} + }, + } +} + +func TestManageIdentity(t *testing.T) { + originalModules := make(map[string]ModuleInfo) + maps.Copy(originalModules, modules) + defer func() { + modules = originalModules + }() + + RegisterModule(&mockIssuerModule{}) + + certPEM := []byte(`-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE +BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl +cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw +WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN +TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp +bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3lcub2pUwkjC +5GJQA2ZZfJJi6d1QHhEmkX9VxKYGp6gagZuRqJWy9TXP6++1ZzQQxqZLD0TkuxZ9 +8i9Nz00000CCBjCCAQQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGgG +CCsGAQUFBwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29t +L0dJQUcyLmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5j +b20vb2NzcDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/ +BAIwADAfBgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHREEEDAO +ggxtYWlsLmdvb2dsZTANBgkqhkiG9w0BAQUFAAOCAQEAMP6IWgNGZE8wP9TjFjSZ +3mmW3A1eIr0CuPwNZ2LJ5ZD1i70ojzcj4I9IdP5yPg9CAEV4hNASbM1LzfC7GmJE +tPzW5tRmpKVWZGRgTgZI8Hp/xZXMwLh9ZmXV4kESFAGj5G5FNvJyUV7R5Eh+7OZX +7G4jJ4ZGJh+5jzN9HdJJHQHGYNIYOzC7+HH9UMwCjX9vhQ4RjwFZJThS2Yb+y7pb +9yxTJZoXC6J0H5JpnZb7kZEJ+Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +-----END CERTIFICATE-----`) + + keyPEM := []byte(`-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP +... +-----END PRIVATE KEY-----`) + + testStorage := certmagic.FileStorage{Path: t.TempDir()} + err := testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM) + if err != nil { + t.Fatal(err) + } + err = testStorage.Store(context.Background(), "localhost/localhost.key", keyPEM) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + cfg *Config + wantErr bool + checkState func(*testing.T, *Config) + }{ + { + name: "nil config", + cfg: nil, + }, + { + name: "nil admin config", + cfg: &Config{ + Admin: nil, + }, + }, + { + name: "nil identity config", + cfg: &Config{ + Admin: &AdminConfig{}, + }, + }, + { + name: "default issuer when none specified", + cfg: &Config{ + Admin: &AdminConfig{ + Identity: &IdentityConfig{ + Identifiers: []string{"localhost"}, + }, + }, + storage: &testStorage, + }, + checkState: func(t *testing.T, cfg *Config) { + if len(cfg.Admin.Identity.issuers) == 0 { + t.Error("Expected at least 1 issuer to be configured") + return + } + if _, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule); !ok { + t.Error("Expected mock issuer to be configured") + } + }, + }, + { + name: "custom issuer", + cfg: &Config{ + Admin: &AdminConfig{ + Identity: &IdentityConfig{ + Identifiers: []string{"localhost"}, + IssuersRaw: []json.RawMessage{ + json.RawMessage(`{"module": "acme"}`), + }, + }, + }, + storage: &certmagic.FileStorage{Path: "testdata"}, + }, + checkState: func(t *testing.T, cfg *Config) { + if len(cfg.Admin.Identity.issuers) != 1 { + t.Fatalf("Expected 1 issuer, got %d", len(cfg.Admin.Identity.issuers)) + } + mockIss, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule) + if !ok { + t.Fatal("Expected mock issuer") + } + if mockIss.configSet == nil { + t.Error("Issuer config was not set") + } + }, + }, + { + name: "invalid issuer module", + cfg: &Config{ + Admin: &AdminConfig{ + Identity: &IdentityConfig{ + Identifiers: []string{"localhost"}, + IssuersRaw: []json.RawMessage{ + json.RawMessage(`{"module": "doesnt_exist"}`), + }, + }, + }, + }, + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if identityCertCache != nil { + // Reset the cert cache before each test + identityCertCache.Stop() + identityCertCache = nil + } + + ctx := Context{ + Context: context.Background(), + cfg: test.cfg, + moduleInstances: make(map[string][]Module), + } + + err := manageIdentity(ctx, test.cfg) + + if test.wantErr { + if err == nil { + t.Error("Expected error but got nil") + } + return + } + if err != nil { + t.Fatalf("Expected no error but got: %v", err) + } + + if test.checkState != nil { + test.checkState(t, test.cfg) + } + }) + } +} diff --git a/caddy.go b/caddy.go index 7dd989c9e..abbd6d6f0 100644 --- a/caddy.go +++ b/caddy.go @@ -81,13 +81,17 @@ type Config struct { // associated value. AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="` - apps map[string]App - storage certmagic.Storage + apps map[string]App + + // failedApps is a map of apps that failed to provision with their underlying error. + failedApps map[string]error + storage certmagic.Storage + eventEmitter eventEmitter cancelFunc context.CancelFunc - // filesystems is a dict of filesystems that will later be loaded from and added to. - filesystems FileSystems + // fileSystems is a dict of fileSystems that will later be loaded from and added to. + fileSystems FileSystems } // App is a thing that Caddy runs. @@ -399,6 +403,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error { func run(newCfg *Config, start bool) (Context, error) { ctx, err := provisionContext(newCfg, start) if err != nil { + globalMetrics.configSuccess.Set(0) return ctx, err } @@ -406,6 +411,19 @@ func run(newCfg *Config, start bool) (Context, error) { return ctx, nil } + defer func() { + // if newCfg fails to start completely, clean up the already provisioned modules + // partially copied from provisionContext + if err != nil { + globalMetrics.configSuccess.Set(0) + ctx.cfg.cancelFunc() + + if currentCtx.cfg != nil { + certmagic.Default.Storage = currentCtx.cfg.storage + } + } + }() + // Provision any admin routers which may need to access // some of the other apps at runtime err = ctx.cfg.Admin.provisionAdminRouters(ctx) @@ -437,10 +455,16 @@ func run(newCfg *Config, start bool) (Context, error) { if err != nil { return ctx, err } + globalMetrics.configSuccess.Set(1) + globalMetrics.configSuccessTime.SetToCurrentTime() + + // TODO: This event is experimental and subject to change. + ctx.emitEvent("started", nil) // now that the user's config is running, finish setting up anything else, // such as remote admin endpoint, config loader, etc. - return ctx, finishSettingUp(ctx, ctx.cfg) + err = finishSettingUp(ctx, ctx.cfg) + return ctx, err } // provisionContext creates a new context from the given configuration and provisions @@ -471,6 +495,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg}) defer func() { if err != nil { + globalMetrics.configSuccess.Set(0) // if there were any errors during startup, // we should cancel the new context we created // since the associated config won't be used; @@ -495,19 +520,12 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) return ctx, err } - // start the admin endpoint (and stop any prior one) - if replaceAdminServer { - err = replaceLocalAdminServer(newCfg) - if err != nil { - return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err) - } - } - // create the new filesystem map - newCfg.filesystems = &filesystems.FilesystemMap{} + newCfg.fileSystems = &filesystems.FileSystemMap{} // prepare the new config for use newCfg.apps = make(map[string]App) + newCfg.failedApps = make(map[string]error) // set up global storage and make it CertMagic's default storage, too err = func() error { @@ -534,6 +552,14 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) return ctx, err } + // start the admin endpoint (and stop any prior one) + if replaceAdminServer { + err = replaceLocalAdminServer(newCfg, ctx) + if err != nil { + return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err) + } + } + // Load and Provision each app and their submodules err = func() error { for appName := range newCfg.AppsRaw { @@ -691,6 +717,9 @@ func unsyncedStop(ctx Context) { return } + // TODO: This event is experimental and subject to change. + ctx.emitEvent("stopping", nil) + // stop each app for name, a := range ctx.cfg.apps { err := a.Stop() @@ -720,8 +749,10 @@ func Validate(cfg *Config) error { // Errors are logged along the way, and an appropriate exit // code is emitted. func exitProcess(ctx context.Context, logger *zap.Logger) { - // let the rest of the program know we're quitting - atomic.StoreInt32(exiting, 1) + // let the rest of the program know we're quitting; only do it once + if !atomic.CompareAndSwapInt32(exiting, 0, 1) { + return + } // give the OS or service/process manager our 2 weeks' notice: we quit if err := notify.Stopping(); err != nil { @@ -1031,6 +1062,98 @@ func Version() (simple, full string) { return } +// Event represents something that has happened or is happening. +// An Event value is not synchronized, so it should be copied if +// being used in goroutines. +// +// EXPERIMENTAL: Events are subject to change. +type Event struct { + // If non-nil, the event has been aborted, meaning + // propagation has stopped to other handlers and + // the code should stop what it was doing. Emitters + // may choose to use this as a signal to adjust their + // code path appropriately. + Aborted error + + // The data associated with the event. Usually the + // original emitter will be the only one to set or + // change these values, but the field is exported + // so handlers can have full access if needed. + // However, this map is not synchronized, so + // handlers must not use this map directly in new + // goroutines; instead, copy the map to use it in a + // goroutine. Data may be nil. + Data map[string]any + + id uuid.UUID + ts time.Time + name string + origin Module +} + +// NewEvent creates a new event, but does not emit the event. To emit an +// event, call Emit() on the current instance of the caddyevents app insteaad. +// +// EXPERIMENTAL: Subject to change. +func NewEvent(ctx Context, name string, data map[string]any) (Event, error) { + id, err := uuid.NewRandom() + if err != nil { + return Event{}, fmt.Errorf("generating new event ID: %v", err) + } + name = strings.ToLower(name) + return Event{ + Data: data, + id: id, + ts: time.Now(), + name: name, + origin: ctx.Module(), + }, nil +} + +func (e Event) ID() uuid.UUID { return e.id } +func (e Event) Timestamp() time.Time { return e.ts } +func (e Event) Name() string { return e.name } +func (e Event) Origin() Module { return e.origin } // Returns the module that originated the event. May be nil, usually if caddy core emits the event. + +// CloudEvent exports event e as a structure that, when +// serialized as JSON, is compatible with the +// CloudEvents spec. +func (e Event) CloudEvent() CloudEvent { + dataJSON, _ := json.Marshal(e.Data) + var source string + if e.Origin() == nil { + source = "caddy" + } else { + source = string(e.Origin().CaddyModule().ID) + } + return CloudEvent{ + ID: e.id.String(), + Source: source, + SpecVersion: "1.0", + Type: e.name, + Time: e.ts, + DataContentType: "application/json", + Data: dataJSON, + } +} + +// CloudEvent is a JSON-serializable structure that +// is compatible with the CloudEvents specification. +// See https://cloudevents.io. +// EXPERIMENTAL: Subject to change. +type CloudEvent struct { + ID string `json:"id"` + Source string `json:"source"` + SpecVersion string `json:"specversion"` + Type string `json:"type"` + Time time.Time `json:"time"` + DataContentType string `json:"datacontenttype,omitempty"` + Data json.RawMessage `json:"data,omitempty"` +} + +// ErrEventAborted cancels an event. +var ErrEventAborted = errors.New("event aborted") + // ActiveContext returns the currently-active context. // This function is experimental and might be changed // or removed in the future. diff --git a/caddy_test.go b/caddy_test.go index adf14350e..08fa5c0d0 100644 --- a/caddy_test.go +++ b/caddy_test.go @@ -15,6 +15,7 @@ package caddy import ( + "context" "testing" "time" ) @@ -72,3 +73,21 @@ func TestParseDuration(t *testing.T) { } } } + +func TestEvent_CloudEvent_NilOrigin(t *testing.T) { + ctx, _ := NewContext(Context{Context: context.Background()}) // module will be nil by default + event, err := NewEvent(ctx, "started", nil) + if err != nil { + t.Fatalf("NewEvent() error = %v", err) + } + + // This should not panic + ce := event.CloudEvent() + + if ce.Source != "caddy" { + t.Errorf("Expected CloudEvent Source to be 'caddy', got '%s'", ce.Source) + } + if ce.Type != "started" { + t.Errorf("Expected CloudEvent Type to be 'started', got '%s'", ce.Type) + } +} diff --git a/caddyconfig/caddyfile/adapter.go b/caddyconfig/caddyfile/adapter.go index da4f98337..449370dc6 100644 --- a/caddyconfig/caddyfile/adapter.go +++ b/caddyconfig/caddyfile/adapter.go @@ -68,7 +68,7 @@ func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconf // TODO: also perform this check on imported files func FormattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) { // replace windows-style newlines to normalize comparison - normalizedBody := bytes.Replace(body, []byte("\r\n"), []byte("\n"), -1) + normalizedBody := bytes.ReplaceAll(body, []byte("\r\n"), []byte("\n")) formatted := Format(normalizedBody) if bytes.Equal(formatted, normalizedBody) { diff --git a/caddyconfig/caddyfile/dispenser.go b/caddyconfig/caddyfile/dispenser.go index 325bb54d3..d95196e48 100644 --- a/caddyconfig/caddyfile/dispenser.go +++ b/caddyconfig/caddyfile/dispenser.go @@ -308,9 +308,9 @@ func (d *Dispenser) CountRemainingArgs() int { } // RemainingArgs loads any more arguments (tokens on the same line) -// into a slice and returns them. Open curly brace tokens also indicate -// the end of arguments, and the curly brace is not included in -// the return value nor is it loaded. +// into a slice of strings and returns them. Open curly brace tokens +// also indicate the end of arguments, and the curly brace is not +// included in the return value nor is it loaded. func (d *Dispenser) RemainingArgs() []string { var args []string for d.NextArg() { @@ -320,9 +320,9 @@ func (d *Dispenser) RemainingArgs() []string { } // RemainingArgsRaw loads any more arguments (tokens on the same line, -// retaining quotes) into a slice and returns them. Open curly brace -// tokens also indicate the end of arguments, and the curly brace is -// not included in the return value nor is it loaded. +// retaining quotes) into a slice of strings and returns them. +// Open curly brace tokens also indicate the end of arguments, +// and the curly brace is not included in the return value nor is it loaded. func (d *Dispenser) RemainingArgsRaw() []string { var args []string for d.NextArg() { @@ -331,6 +331,18 @@ func (d *Dispenser) RemainingArgsRaw() []string { return args } +// RemainingArgsAsTokens loads any more arguments (tokens on the same line) +// into a slice of Token-structs and returns them. Open curly brace tokens +// also indicate the end of arguments, and the curly brace is not included +// in the return value nor is it loaded. +func (d *Dispenser) RemainingArgsAsTokens() []Token { + var args []Token + for d.NextArg() { + args = append(args, d.Token()) + } + return args +} + // NewFromNextSegment returns a new dispenser with a copy of // the tokens from the current token until the end of the // "directive" whether that be to the end of the line or diff --git a/caddyconfig/caddyfile/dispenser_test.go b/caddyconfig/caddyfile/dispenser_test.go index 0f6ee5043..f5d226005 100644 --- a/caddyconfig/caddyfile/dispenser_test.go +++ b/caddyconfig/caddyfile/dispenser_test.go @@ -274,6 +274,66 @@ func TestDispenser_RemainingArgs(t *testing.T) { } } +func TestDispenser_RemainingArgsAsTokens(t *testing.T) { + input := `dir1 arg1 arg2 arg3 + dir2 arg4 arg5 + dir3 arg6 { arg7 + dir4` + d := NewTestDispenser(input) + + d.Next() // dir1 + + args := d.RemainingArgsAsTokens() + + tokenTexts := make([]string, 0, len(args)) + for _, arg := range args { + tokenTexts = append(tokenTexts, arg.Text) + } + + if expected := []string{"arg1", "arg2", "arg3"}; !reflect.DeepEqual(tokenTexts, expected) { + t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts) + } + + d.Next() // dir2 + + args = d.RemainingArgsAsTokens() + + tokenTexts = tokenTexts[:0] + for _, arg := range args { + tokenTexts = append(tokenTexts, arg.Text) + } + + if expected := []string{"arg4", "arg5"}; !reflect.DeepEqual(tokenTexts, expected) { + t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts) + } + + d.Next() // dir3 + + args = d.RemainingArgsAsTokens() + tokenTexts = tokenTexts[:0] + for _, arg := range args { + tokenTexts = append(tokenTexts, arg.Text) + } + + if expected := []string{"arg6"}; !reflect.DeepEqual(tokenTexts, expected) { + t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts) + } + + d.Next() // { + d.Next() // arg7 + d.Next() // dir4 + + args = d.RemainingArgsAsTokens() + tokenTexts = tokenTexts[:0] + for _, arg := range args { + tokenTexts = append(tokenTexts, arg.Text) + } + + if len(args) != 0 { + t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", []string{}, tokenTexts) + } +} + func TestDispenser_ArgErr_Err(t *testing.T) { input := `dir1 { } diff --git a/caddyconfig/caddyfile/formatter.go b/caddyconfig/caddyfile/formatter.go index d35f0ac6b..8a757ea58 100644 --- a/caddyconfig/caddyfile/formatter.go +++ b/caddyconfig/caddyfile/formatter.go @@ -61,7 +61,8 @@ func Format(input []byte) []byte { heredocMarker []rune heredocClosingMarker []rune - nesting int // indentation level + nesting int // indentation level + withinBackquote bool ) write := func(ch rune) { @@ -88,9 +89,12 @@ func Format(input []byte) []byte { } panic(err) } + if ch == '`' { + withinBackquote = !withinBackquote + } // detect whether we have the start of a heredoc - if !quoted && !(heredoc != heredocClosed || heredocEscaped) && + if !quoted && (heredoc == heredocClosed && !heredocEscaped) && space && last == '<' && ch == '<' { write(ch) heredoc = heredocOpening @@ -220,7 +224,7 @@ func Format(input []byte) []byte { openBrace = false if beginningOfLine { indent() - } else if !openBraceSpace { + } else if !openBraceSpace || !unicode.IsSpace(last) { write(' ') } write('{') @@ -236,14 +240,23 @@ func Format(input []byte) []byte { switch { case ch == '{': openBrace = true - openBraceWritten = false openBraceSpace = spacePrior && !beginningOfLine - if openBraceSpace { + if openBraceSpace && newLines == 0 { write(' ') } + openBraceWritten = false + if withinBackquote { + write('{') + openBraceWritten = true + continue + } continue case ch == '}' && (spacePrior || !openBrace): + if withinBackquote { + write('}') + continue + } if last != '\n' { nextLine() } diff --git a/caddyconfig/caddyfile/formatter_test.go b/caddyconfig/caddyfile/formatter_test.go index 6eec822fe..29b910ff1 100644 --- a/caddyconfig/caddyfile/formatter_test.go +++ b/caddyconfig/caddyfile/formatter_test.go @@ -432,6 +432,31 @@ block2 { heredoc \< 1 && string(val[:2]) == "<<" { // a space means it's just a regular token and not a heredoc if ch == ' ' { @@ -323,7 +323,8 @@ func (l *lexer) finalizeHeredoc(val []rune, marker string) ([]rune, error) { // if the padding doesn't match exactly at the start then we can't safely strip if index != 0 { - return nil, fmt.Errorf("mismatched leading whitespace in heredoc <<%s on line #%d [%s], expected whitespace [%s] to match the closing marker", marker, l.line+lineNum+1, lineText, paddingToStrip) + cleanLineText := strings.TrimRight(lineText, "\r\n") + return nil, fmt.Errorf("mismatched leading whitespace in heredoc <<%s on line #%d [%s], expected whitespace [%s] to match the closing marker", marker, l.line+lineNum+1, cleanLineText, paddingToStrip) } // strip, then append the line, with the newline, to the output. diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go index 17d824efd..2bc52aded 100644 --- a/caddyconfig/caddyfile/parse.go +++ b/caddyconfig/caddyfile/parse.go @@ -264,8 +264,13 @@ func (p *parser) addresses() error { return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", value) } - token.Text = value - p.block.Keys = append(p.block.Keys, token) + // After the above, a comma surrounded by spaces would result + // in an empty token which we should ignore + if value != "" { + // Add the token as a site address + token.Text = value + p.block.Keys = append(p.block.Keys, token) + } } // Advance token and possibly break out of loop or return error @@ -374,28 +379,23 @@ func (p *parser) doImport(nesting int) error { if len(blockTokens) > 0 { // use such tokens to create a new dispenser, and then use it to parse each block bd := NewDispenser(blockTokens) + + // one iteration processes one sub-block inside the import for bd.Next() { - // see if we can grab a key - var currentMappingKey string - if bd.Val() == "{" { + currentMappingKey := bd.Val() + + if currentMappingKey == "{" { return p.Err("anonymous blocks are not supported") } - currentMappingKey = bd.Val() - currentMappingTokens := []Token{} - // read all args until end of line / { - if bd.NextArg() { + + // load up all arguments (if there even are any) + currentMappingTokens := bd.RemainingArgsAsTokens() + + // load up the entire block + for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); { currentMappingTokens = append(currentMappingTokens, bd.Token()) - for bd.NextArg() { - currentMappingTokens = append(currentMappingTokens, bd.Token()) - } - // TODO(elee1766): we don't enter another mapping here because it's annoying to extract the { and } properly. - // maybe someone can do that in the future - } else { - // attempt to enter a block and add tokens to the currentMappingTokens - for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); { - currentMappingTokens = append(currentMappingTokens, bd.Token()) - } } + blockMapping[currentMappingKey] = currentMappingTokens } } @@ -418,7 +418,7 @@ func (p *parser) doImport(nesting int) error { // make path relative to the file of the _token_ being processed rather // than current working directory (issue #867) and then use glob to get // list of matching filenames - absFile, err := filepath.Abs(p.Dispenser.File()) + absFile, err := caddy.FastAbs(p.Dispenser.File()) if err != nil { return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.File(), err) } @@ -617,7 +617,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) { // Tack the file path onto these tokens so errors show the imported file's name // (we use full, absolute path to avoid bugs: issue #1892) - filename, err := filepath.Abs(importFile) + filename, err := caddy.FastAbs(importFile) if err != nil { return nil, p.Errf("Failed to get absolute path of file: %s: %v", importFile, err) } diff --git a/caddyconfig/caddyfile/parse_test.go b/caddyconfig/caddyfile/parse_test.go index 7157b2b5f..bf149e635 100644 --- a/caddyconfig/caddyfile/parse_test.go +++ b/caddyconfig/caddyfile/parse_test.go @@ -18,6 +18,7 @@ import ( "bytes" "os" "path/filepath" + "strings" "testing" ) @@ -555,6 +556,10 @@ func TestParseAll(t *testing.T) { {"localhost:1234", "http://host2"}, }}, + {`foo.example.com , example.com`, false, [][]string{ + {"foo.example.com", "example.com"}, + }}, + {`localhost:1234, http://host2,`, true, [][]string{}}, {`http://host1.com, http://host2.com { @@ -614,8 +619,8 @@ func TestParseAll(t *testing.T) { } for j, block := range blocks { if len(block.Keys) != len(test.keys[j]) { - t.Errorf("Test %d: Expected %d keys in block %d, got %d", - i, len(test.keys[j]), j, len(block.Keys)) + t.Errorf("Test %d: Expected %d keys in block %d, got %d: %v", + i, len(test.keys[j]), j, len(block.Keys), block.Keys) continue } for k, addr := range block.GetKeysText() { @@ -880,6 +885,51 @@ func TestRejectsGlobalMatcher(t *testing.T) { } } +func TestRejectAnonymousImportBlock(t *testing.T) { + p := testParser(` + (site) { + http://{args[0]} https://{args[0]} { + {block} + } + } + + import site test.domain { + { + header_up Host {host} + header_up X-Real-IP {remote_host} + } + } + `) + _, err := p.parseAll() + if err == nil { + t.Fatal("Expected an error, but got nil") + } + expected := "anonymous blocks are not supported" + if !strings.HasPrefix(err.Error(), "anonymous blocks are not supported") { + t.Errorf("Expected error to start with '%s' but got '%v'", expected, err) + } +} + +func TestAcceptSiteImportWithBraces(t *testing.T) { + p := testParser(` + (site) { + http://{args[0]} https://{args[0]} { + {block} + } + } + + import site test.domain { + reverse_proxy http://192.168.1.1:8080 { + header_up Host {host} + } + } + `) + _, err := p.parseAll() + if err != nil { + t.Errorf("Expected error to be nil but got '%v'", err) + } +} + func testParser(input string) parser { return parser{Dispenser: NewTestDispenser(input)} } diff --git a/caddyconfig/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go index 1c331eadc..1121776d9 100644 --- a/caddyconfig/httpcaddyfile/addresses.go +++ b/caddyconfig/httpcaddyfile/addresses.go @@ -31,7 +31,7 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) -// mapAddressToServerBlocks returns a map of listener address to list of server +// mapAddressToProtocolToServerBlocks returns a map of listener address to list of server // blocks that will be served on that address. To do this, each server block is // expanded so that each one is considered individually, although keys of a // server block that share the same address stay grouped together so the config @@ -329,8 +329,12 @@ func (st *ServerType) listenersForServerBlockAddress(sblock serverBlock, addr Ad // use a map to prevent duplication listeners := map[string]map[string]struct{}{} for _, lnCfgVal := range lnCfgVals { - for _, lnHost := range lnCfgVal.addresses { - networkAddr, err := caddy.ParseNetworkAddressFromHostPort(lnHost, lnPort) + for _, lnAddr := range lnCfgVal.addresses { + lnNetw, lnHost, _, err := caddy.SplitNetworkAddress(lnAddr) + if err != nil { + return nil, fmt.Errorf("splitting listener address: %v", err) + } + networkAddr, err := caddy.ParseNetworkAddress(caddy.JoinNetworkAddress(lnNetw, lnHost, lnPort)) if err != nil { return nil, fmt.Errorf("parsing network address: %v", err) } diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 165c66b25..71aa0c2b8 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -15,6 +15,7 @@ package httpcaddyfile import ( + "encoding/json" "fmt" "html" "net/http" @@ -24,7 +25,7 @@ import ( "time" "github.com/caddyserver/certmagic" - "github.com/mholt/acmez/v2/acme" + "github.com/mholt/acmez/v3/acme" "go.uber.org/zap/zapcore" "github.com/caddyserver/caddy/v2" @@ -84,7 +85,7 @@ func parseBind(h Helper) ([]ConfigValue, error) { // parseTLS parses the tls directive. Syntax: // -// tls [|internal]|[ ] { +// tls [|internal|force_automate]|[ ] { // protocols [] // ciphers // curves @@ -99,7 +100,7 @@ func parseBind(h Helper) ([]ConfigValue, error) { // ca // ca_root // key_type [ed25519|p256|p384|rsa2048|rsa4096] -// dns [...] +// dns [ [...]] (required, though, if DNS is not configured as global option) // propagation_delay // propagation_timeout // resolvers @@ -107,6 +108,7 @@ func parseBind(h Helper) ([]ConfigValue, error) { // dns_challenge_override_domain // on_demand // reuse_private_keys +// force_automate // eab // issuer [...] // get_certificate [...] @@ -126,6 +128,10 @@ func parseTLS(h Helper) ([]ConfigValue, error) { var certManagers []certmagic.Manager var onDemand bool var reusePrivateKeys bool + var forceAutomate bool + + // Track which DNS challenge options are set + var dnsOptionsSet []string firstLine := h.RemainingArgs() switch len(firstLine) { @@ -133,8 +139,10 @@ func parseTLS(h Helper) ([]ConfigValue, error) { case 1: if firstLine[0] == "internal" { internalIssuer = new(caddytls.InternalIssuer) + } else if firstLine[0] == "force_automate" { + forceAutomate = true } else if !strings.Contains(firstLine[0], "@") { - return nil, h.Err("single argument must either be 'internal' or an email address") + return nil, h.Err("single argument must either be 'internal', 'force_automate', or an email address") } else { acmeIssuer = &caddytls.ACMEIssuer{ Email: firstLine[0], @@ -308,10 +316,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) { certManagers = append(certManagers, certManager) case "dns": - if !h.NextArg() { - return nil, h.ArgErr() - } - provName := h.Val() if acmeIssuer == nil { acmeIssuer = new(caddytls.ACMEIssuer) } @@ -321,12 +325,19 @@ func parseTLS(h Helper) ([]ConfigValue, error) { if acmeIssuer.Challenges.DNS == nil { acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) } - modID := "dns.providers." + provName - unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) - if err != nil { - return nil, err + // DNS provider configuration optional, since it may be configured globally via the TLS app with global options + if h.NextArg() { + provName := h.Val() + modID := "dns.providers." + provName + unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) + if err != nil { + return nil, err + } + acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings) + } else if h.Option("dns") == nil { + // if DNS is omitted locally, it needs to be configured globally + return nil, h.ArgErr() } - acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings) case "resolvers": args := h.RemainingArgs() @@ -342,6 +353,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) { if acmeIssuer.Challenges.DNS == nil { acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) } + dnsOptionsSet = append(dnsOptionsSet, "resolvers") acmeIssuer.Challenges.DNS.Resolvers = args case "propagation_delay": @@ -363,6 +375,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) { if acmeIssuer.Challenges.DNS == nil { acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) } + dnsOptionsSet = append(dnsOptionsSet, "propagation_delay") acmeIssuer.Challenges.DNS.PropagationDelay = caddy.Duration(delay) case "propagation_timeout": @@ -390,6 +403,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) { if acmeIssuer.Challenges.DNS == nil { acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) } + dnsOptionsSet = append(dnsOptionsSet, "propagation_timeout") acmeIssuer.Challenges.DNS.PropagationTimeout = caddy.Duration(timeout) case "dns_ttl": @@ -411,6 +425,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) { if acmeIssuer.Challenges.DNS == nil { acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) } + dnsOptionsSet = append(dnsOptionsSet, "dns_ttl") acmeIssuer.Challenges.DNS.TTL = caddy.Duration(ttl) case "dns_challenge_override_domain": @@ -427,6 +442,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) { if acmeIssuer.Challenges.DNS == nil { acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) } + dnsOptionsSet = append(dnsOptionsSet, "dns_challenge_override_domain") acmeIssuer.Challenges.DNS.OverrideDomain = arg[0] case "ca_root": @@ -462,6 +478,18 @@ func parseTLS(h Helper) ([]ConfigValue, error) { } } + // Validate DNS challenge config: any DNS challenge option except "dns" requires a DNS provider + if acmeIssuer != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil { + dnsCfg := acmeIssuer.Challenges.DNS + providerSet := dnsCfg.ProviderRaw != nil || h.Option("dns") != nil + if len(dnsOptionsSet) > 0 && !providerSet { + return nil, h.Errf( + "setting DNS challenge options [%s] requires a DNS provider (set with the 'dns' subdirective or 'acme_dns' global option)", + strings.Join(dnsOptionsSet, ", "), + ) + } + } + // a naked tls directive is not allowed if len(firstLine) == 0 && !hasBlock { return nil, h.ArgErr() @@ -569,6 +597,15 @@ func parseTLS(h Helper) ([]ConfigValue, error) { }) } + // if enabled, the names in the site addresses will be + // added to the automation policies + if forceAutomate { + configVals = append(configVals, ConfigValue{ + Class: "tls.force_automate", + Value: true, + }) + } + // custom certificate selection if len(certSelector.AnyTag) > 0 { cp.CertSelection = &certSelector @@ -827,13 +864,18 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) { return nil, h.Errf("segment was not parsed as a subroute") } + // wrap the subroutes + wrappingRoute := caddyhttp.Route{ + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)}, + } + subroute = &caddyhttp.Subroute{ + Routes: []caddyhttp.Route{wrappingRoute}, + } if expression != "" { statusMatcher := caddy.ModuleMap{ "expression": h.JSON(caddyhttp.MatchExpression{Expr: expression}), } - for i := range subroute.Routes { - subroute.Routes[i].MatcherSetsRaw = []caddy.ModuleMap{statusMatcher} - } + subroute.Routes[0].MatcherSetsRaw = []caddy.ModuleMap{statusMatcher} } return []ConfigValue{ { @@ -981,6 +1023,50 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue } cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings) + case "sampling": + d := h.Dispenser.NewFromNextSegment() + for d.NextArg() { + // consume any tokens on the same line, if any. + } + + sampling := &caddy.LogSampling{} + for nesting := d.Nesting(); d.NextBlock(nesting); { + subdir := d.Val() + switch subdir { + case "interval": + if !d.NextArg() { + return nil, d.ArgErr() + } + interval, err := time.ParseDuration(d.Val() + "ns") + if err != nil { + return nil, d.Errf("failed to parse interval: %v", err) + } + sampling.Interval = interval + case "first": + if !d.NextArg() { + return nil, d.ArgErr() + } + first, err := strconv.Atoi(d.Val()) + if err != nil { + return nil, d.Errf("failed to parse first: %v", err) + } + sampling.First = first + case "thereafter": + if !d.NextArg() { + return nil, d.ArgErr() + } + thereafter, err := strconv.Atoi(d.Val()) + if err != nil { + return nil, d.Errf("failed to parse thereafter: %v", err) + } + sampling.Thereafter = thereafter + default: + return nil, d.Errf("unrecognized subdirective: %s", subdir) + } + } + + cl.Sampling = sampling + case "core": if !h.NextArg() { return nil, h.ArgErr() @@ -1100,6 +1186,11 @@ func parseLogSkip(h Helper) (caddyhttp.MiddlewareHandler, error) { if h.NextArg() { return nil, h.ArgErr() } + + if h.NextBlock(0) { + return nil, h.Err("log_skip directive does not accept blocks") + } + return caddyhttp.VarsMiddleware{"log_skip": true}, nil } diff --git a/caddyconfig/httpcaddyfile/builtins_test.go b/caddyconfig/httpcaddyfile/builtins_test.go index cf7463484..c23531f22 100644 --- a/caddyconfig/httpcaddyfile/builtins_test.go +++ b/caddyconfig/httpcaddyfile/builtins_test.go @@ -62,6 +62,20 @@ func TestLogDirectiveSyntax(t *testing.T) { output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`, expectError: false, }, + { + input: `:8080 { + log { + sampling { + interval 2 + first 3 + thereafter 4 + } + } + } + `, + output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`, + expectError: false, + }, } { adapter := caddyfile.Adapter{ diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go index f0687a7e9..eac7f5dc2 100644 --- a/caddyconfig/httpcaddyfile/directives.go +++ b/caddyconfig/httpcaddyfile/directives.go @@ -16,6 +16,7 @@ package httpcaddyfile import ( "encoding/json" + "maps" "net" "slices" "sort" @@ -173,10 +174,12 @@ func RegisterDirectiveOrder(dir string, position Positional, standardDir string) if d != standardDir { continue } - if position == Before { + switch position { + case Before: newOrder = append(newOrder[:i], append([]string{dir}, newOrder[i:]...)...) - } else if position == After { + case After: newOrder = append(newOrder[:i+1], append([]string{dir}, newOrder[i+1:]...)...) + case First, Last: } break } @@ -365,9 +368,7 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) { // copy existing matcher definitions so we can augment // new ones that are defined only in this scope matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs)) - for key, val := range h.matcherDefs { - matcherDefs[key] = val - } + maps.Copy(matcherDefs, h.matcherDefs) // find and extract any embedded matcher definitions in this scope for i := 0; i < len(segments); i++ { @@ -483,12 +484,29 @@ func sortRoutes(routes []ConfigValue) { // we can only confidently compare path lengths if both // directives have a single path to match (issue #5037) if iPathLen > 0 && jPathLen > 0 { + // trim the trailing wildcard if there is one + iPathTrimmed := strings.TrimSuffix(iPM[0], "*") + jPathTrimmed := strings.TrimSuffix(jPM[0], "*") + // if both paths are the same except for a trailing wildcard, // sort by the shorter path first (which is more specific) - if strings.TrimSuffix(iPM[0], "*") == strings.TrimSuffix(jPM[0], "*") { + if iPathTrimmed == jPathTrimmed { return iPathLen < jPathLen } + // we use the trimmed length to compare the paths + // https://github.com/caddyserver/caddy/issues/7012#issuecomment-2870142195 + // credit to https://github.com/Hellio404 + // for sorts with many items, mixing matchers w/ and w/o wildcards will confuse the sort and result in incorrect orders + iPathLen = len(iPathTrimmed) + jPathLen = len(jPathTrimmed) + + // if both paths have the same length, sort lexically + // https://github.com/caddyserver/caddy/pull/7015#issuecomment-2871993588 + if iPathLen == jPathLen { + return iPathTrimmed < jPathTrimmed + } + // sort most-specific (longest) path first return iPathLen > jPathLen } diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 4dacd9058..3dcd3ea5b 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -15,6 +15,7 @@ package httpcaddyfile import ( + "cmp" "encoding/json" "fmt" "net" @@ -186,12 +187,25 @@ func (st ServerType) Setup( return nil, warnings, err } + // hoist the metrics config from per-server to global + metrics, _ := options["metrics"].(*caddyhttp.Metrics) + for _, s := range servers { + if s.Metrics != nil { + metrics = cmp.Or(metrics, &caddyhttp.Metrics{}) + metrics = &caddyhttp.Metrics{ + PerHost: metrics.PerHost || s.Metrics.PerHost, + } + s.Metrics = nil // we don't need it anymore + } + } + // now that each server is configured, make the HTTP app httpApp := caddyhttp.App{ HTTPPort: tryInt(options["http_port"], &warnings), HTTPSPort: tryInt(options["https_port"], &warnings), GracePeriod: tryDuration(options["grace_period"], &warnings), ShutdownDelay: tryDuration(options["shutdown_delay"], &warnings), + Metrics: metrics, Servers: servers, } @@ -336,7 +350,7 @@ func (st ServerType) Setup( // avoid duplicates by sorting + compacting sort.Strings(defaultLog.Exclude) - defaultLog.Exclude = slices.Compact[[]string, string](defaultLog.Exclude) + defaultLog.Exclude = slices.Compact(defaultLog.Exclude) } } // we may have not actually added anything, so remove if empty @@ -619,12 +633,6 @@ func (st *ServerType) serversFromPairings( srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) } srv.AutoHTTPS.IgnoreLoadedCerts = true - - case "prefer_wildcard": - if srv.AutoHTTPS == nil { - srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) - } - srv.AutoHTTPS.PreferWildcard = true } } @@ -739,6 +747,14 @@ func (st *ServerType) serversFromPairings( } } + // collect hosts that are forced to be automated + forceAutomatedNames := make(map[string]struct{}) + if _, ok := sblock.pile["tls.force_automate"]; ok { + for _, host := range hosts { + forceAutomatedNames[host] = struct{}{} + } + } + // tls: connection policies if cpVals, ok := sblock.pile["tls.connection_policy"]; ok { // tls connection policies @@ -769,21 +785,20 @@ func (st *ServerType) serversFromPairings( cp.FallbackSNI = fallbackSNI } - // only append this policy if it actually changes something - if !cp.SettingsEmpty() { + // only append this policy if it actually changes something, + // or if the configuration explicitly automates certs for + // these names (this is necessary to hoist a connection policy + // above one that may manually load a wildcard cert that would + // otherwise clobber the automated one; the code that appends + // policies that manually load certs comes later, so they're + // lower in the list) + if !cp.SettingsEmpty() || mapContains(forceAutomatedNames, hosts) { srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp) hasCatchAllTLSConnPolicy = len(hosts) == 0 } } } - wildcardHosts := []string{} - for _, addr := range sblock.parsedKeys { - if strings.HasPrefix(addr.Host, "*.") { - wildcardHosts = append(wildcardHosts, addr.Host[2:]) - } - } - for _, addr := range sblock.parsedKeys { // if server only uses HTTP port, auto-HTTPS will not apply if listenersUseAnyPortOtherThan(srv.Listen, httpPort) { @@ -799,18 +814,6 @@ func (st *ServerType) serversFromPairings( } } - // If prefer wildcard is enabled, then we add hosts that are - // already covered by the wildcard to the skip list - if srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard && addr.Scheme == "https" { - baseDomain := addr.Host - if idx := strings.Index(baseDomain, "."); idx != -1 { - baseDomain = baseDomain[idx+1:] - } - if !strings.HasPrefix(addr.Host, "*.") && slices.Contains(wildcardHosts, baseDomain) { - srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host) - } - } - // If TLS is specified as directive, it will also result in 1 or more connection policy being created // Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without // specifying prefix "https://" @@ -827,6 +830,7 @@ func (st *ServerType) serversFromPairings( (addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) { addressQualifiesForTLS = true } + // predict whether auto-HTTPS will add the conn policy for us; if so, we // may not need to add one for this server autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy && @@ -1057,11 +1061,40 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti // if they're exactly equal in every way, just keep one of them if reflect.DeepEqual(cps[i], cps[j]) { - cps = append(cps[:j], cps[j+1:]...) + cps = slices.Delete(cps, j, j+1) i-- break } + // as a special case, if there are adjacent TLS conn policies that are identical except + // by their matchers, and the matchers are specifically just ServerName ("sni") matchers + // (by far the most common), we can combine them into a single policy + if i == j-1 && len(cps[i].MatchersRaw) == 1 && len(cps[j].MatchersRaw) == 1 { + if iSNIMatcherJSON, ok := cps[i].MatchersRaw["sni"]; ok { + if jSNIMatcherJSON, ok := cps[j].MatchersRaw["sni"]; ok { + // position of policies and the matcher criteria check out; if settings are + // the same, then we can combine the policies; we have to unmarshal and + // remarshal the matchers though + if cps[i].SettingsEqual(*cps[j]) { + var iSNIMatcher caddytls.MatchServerName + if err := json.Unmarshal(iSNIMatcherJSON, &iSNIMatcher); err == nil { + var jSNIMatcher caddytls.MatchServerName + if err := json.Unmarshal(jSNIMatcherJSON, &jSNIMatcher); err == nil { + iSNIMatcher = append(iSNIMatcher, jSNIMatcher...) + cps[i].MatchersRaw["sni"], err = json.Marshal(iSNIMatcher) + if err != nil { + return nil, fmt.Errorf("recombining SNI matchers: %v", err) + } + cps = slices.Delete(cps, j, j+1) + i-- + break + } + } + } + } + } + } + // if they have the same matcher, try to reconcile each field: either they must // be identical, or we have to be able to combine them safely if reflect.DeepEqual(cps[i].MatchersRaw, cps[j].MatchersRaw) { @@ -1095,6 +1128,12 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti return nil, fmt.Errorf("two policies with same match criteria have conflicting default SNI: %s vs. %s", cps[i].DefaultSNI, cps[j].DefaultSNI) } + if cps[i].FallbackSNI != "" && + cps[j].FallbackSNI != "" && + cps[i].FallbackSNI != cps[j].FallbackSNI { + return nil, fmt.Errorf("two policies with same match criteria have conflicting fallback SNI: %s vs. %s", + cps[i].FallbackSNI, cps[j].FallbackSNI) + } if cps[i].ProtocolMin != "" && cps[j].ProtocolMin != "" && cps[i].ProtocolMin != cps[j].ProtocolMin { @@ -1135,6 +1174,9 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti if cps[i].DefaultSNI == "" && cps[j].DefaultSNI != "" { cps[i].DefaultSNI = cps[j].DefaultSNI } + if cps[i].FallbackSNI == "" && cps[j].FallbackSNI != "" { + cps[i].FallbackSNI = cps[j].FallbackSNI + } if cps[i].ProtocolMin == "" && cps[j].ProtocolMin != "" { cps[i].ProtocolMin = cps[j].ProtocolMin } @@ -1154,12 +1196,13 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti } } - cps = append(cps[:j], cps[j+1:]...) + cps = slices.Delete(cps, j, j+1) i-- break } } } + return cps, nil } @@ -1448,9 +1491,9 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod // iterate each pairing of host and path matchers and // put them into a map for JSON encoding - var matcherSets []map[string]caddyhttp.RequestMatcher + var matcherSets []map[string]caddyhttp.RequestMatcherWithError for _, mp := range matcherPairs { - matcherSet := make(map[string]caddyhttp.RequestMatcher) + matcherSet := make(map[string]caddyhttp.RequestMatcherWithError) if len(mp.hostm) > 0 { matcherSet["host"] = mp.hostm } @@ -1509,12 +1552,17 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M if err != nil { return err } - rm, ok := unm.(caddyhttp.RequestMatcher) - if !ok { - return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) + + if rm, ok := unm.(caddyhttp.RequestMatcherWithError); ok { + matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil) + return nil } - matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil) - return nil + // nolint:staticcheck + if rm, ok := unm.(caddyhttp.RequestMatcher); ok { + matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil) + return nil + } + return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) } // if the next token is quoted, we can assume it's not a matcher name @@ -1558,7 +1606,7 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M return nil } -func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.ModuleMap, error) { +func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcherWithError) (caddy.ModuleMap, error) { msEncoded := make(caddy.ModuleMap) for matcherName, val := range matchers { jsonBytes, err := json.Marshal(val) @@ -1638,6 +1686,18 @@ func listenersUseAnyPortOtherThan(addresses []string, otherPort string) bool { return false } +func mapContains[K comparable, V any](m map[K]V, keys []K) bool { + if len(m) == 0 || len(keys) == 0 { + return false + } + for _, key := range keys { + if _, ok := m[key]; ok { + return true + } + } + return false +} + // specificity returns len(s) minus any wildcards (*) and // placeholders ({...}). Basically, it's a length count // that penalizes the use of wildcards and placeholders. diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index abbe8f418..336c6999f 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -19,11 +19,13 @@ import ( "strconv" "github.com/caddyserver/certmagic" - "github.com/mholt/acmez/v2/acme" + "github.com/libdns/libdns" + "github.com/mholt/acmez/v3/acme" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddytls" ) @@ -38,12 +40,13 @@ func init() { RegisterGlobalOption("fallback_sni", parseOptSingleString) RegisterGlobalOption("order", parseOptOrder) RegisterGlobalOption("storage", parseOptStorage) - RegisterGlobalOption("storage_clean_interval", parseOptDuration) + RegisterGlobalOption("storage_check", parseStorageCheck) + RegisterGlobalOption("storage_clean_interval", parseStorageCleanInterval) RegisterGlobalOption("renew_interval", parseOptDuration) RegisterGlobalOption("ocsp_interval", parseOptDuration) RegisterGlobalOption("acme_ca", parseOptSingleString) RegisterGlobalOption("acme_ca_root", parseOptSingleString) - RegisterGlobalOption("acme_dns", parseOptACMEDNS) + RegisterGlobalOption("acme_dns", parseOptDNS) RegisterGlobalOption("acme_eab", parseOptACMEEAB) RegisterGlobalOption("cert_issuer", parseOptCertIssuer) RegisterGlobalOption("skip_install_trust", parseOptTrue) @@ -53,12 +56,15 @@ func init() { RegisterGlobalOption("local_certs", parseOptTrue) RegisterGlobalOption("key_type", parseOptSingleString) RegisterGlobalOption("auto_https", parseOptAutoHTTPS) + RegisterGlobalOption("metrics", parseMetricsOptions) RegisterGlobalOption("servers", parseServerOptions) RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions) RegisterGlobalOption("cert_lifetime", parseOptDuration) RegisterGlobalOption("log", parseLogOptions) RegisterGlobalOption("preferred_chains", parseOptPreferredChains) RegisterGlobalOption("persist_config", parseOptPersistConfig) + RegisterGlobalOption("dns", parseOptDNS) + RegisterGlobalOption("ech", parseOptECH) } func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil } @@ -187,6 +193,40 @@ func parseOptStorage(d *caddyfile.Dispenser, _ any) (any, error) { return storage, nil } +func parseStorageCheck(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name + if !d.Next() { + return "", d.ArgErr() + } + val := d.Val() + if d.Next() { + return "", d.ArgErr() + } + if val != "off" { + return "", d.Errf("storage_check must be 'off'") + } + return val, nil +} + +func parseStorageCleanInterval(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name + if !d.Next() { + return "", d.ArgErr() + } + val := d.Val() + if d.Next() { + return "", d.ArgErr() + } + if val == "off" { + return false, nil + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return nil, d.Errf("failed to parse storage_clean_interval, must be a duration or 'off' %w", err) + } + return caddy.Duration(dur), nil +} + func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) { if !d.Next() { // consume option name return nil, d.ArgErr() @@ -201,25 +241,6 @@ func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) { return caddy.Duration(dur), nil } -func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) { - if !d.Next() { // consume option name - return nil, d.ArgErr() - } - if !d.Next() { // get DNS module name - return nil, d.ArgErr() - } - modID := "dns.providers." + d.Val() - unm, err := caddyfile.UnmarshalModule(d, modID) - if err != nil { - return nil, err - } - prov, ok := unm.(certmagic.DNSProvider) - if !ok { - return nil, d.Errf("module %s (%T) is not a certmagic.DNSProvider", modID, unm) - } - return prov, nil -} - func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) { eab := new(acme.EAB) d.Next() // consume option name @@ -394,36 +415,10 @@ func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) { ond.PermissionRaw = caddyconfig.JSONModuleObject(perm, "module", modName, nil) case "interval": - if !d.NextArg() { - return nil, d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return nil, err - } - if ond == nil { - ond = new(caddytls.OnDemandConfig) - } - if ond.RateLimit == nil { - ond.RateLimit = new(caddytls.RateLimit) - } - ond.RateLimit.Interval = caddy.Duration(dur) + return nil, d.Errf("the on_demand_tls 'interval' option is no longer supported, remove it from your config") case "burst": - if !d.NextArg() { - return nil, d.ArgErr() - } - burst, err := strconv.Atoi(d.Val()) - if err != nil { - return nil, err - } - if ond == nil { - ond = new(caddytls.OnDemandConfig) - } - if ond.RateLimit == nil { - ond.RateLimit = new(caddytls.RateLimit) - } - ond.RateLimit.Burst = burst + return nil, d.Errf("the on_demand_tls 'burst' option is no longer supported, remove it from your config") default: return nil, d.Errf("unrecognized parameter '%s'", d.Val()) @@ -463,8 +458,6 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) { case "disable_certs": case "ignore_loaded_certs": case "prefer_wildcard": - break - default: return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', 'ignore_loaded_certs', or 'prefer_wildcard'") } @@ -472,6 +465,24 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) { return val, nil } +func unmarshalCaddyfileMetricsOptions(d *caddyfile.Dispenser) (any, error) { + d.Next() // consume option name + metrics := new(caddyhttp.Metrics) + for d.NextBlock(0) { + switch d.Val() { + case "per_host": + metrics.PerHost = true + default: + return nil, d.Errf("unrecognized servers option '%s'", d.Val()) + } + } + return metrics, nil +} + +func parseMetricsOptions(d *caddyfile.Dispenser, _ any) (any, error) { + return unmarshalCaddyfileMetricsOptions(d) +} + func parseServerOptions(d *caddyfile.Dispenser, _ any) (any, error) { return unmarshalCaddyfileServerOptions(d) } @@ -541,3 +552,74 @@ func parseOptPreferredChains(d *caddyfile.Dispenser, _ any) (any, error) { d.Next() return caddytls.ParseCaddyfilePreferredChainsOptions(d) } + +func parseOptDNS(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name + optName := d.Val() + + // get DNS module name + if !d.Next() { + // this is allowed if this is the "acme_dns" option since it may refer to the globally-configured "dns" option's value + if optName == "acme_dns" { + return nil, nil + } + return nil, d.ArgErr() + } + modID := "dns.providers." + d.Val() + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return nil, err + } + switch unm.(type) { + case libdns.RecordGetter, + libdns.RecordSetter, + libdns.RecordAppender, + libdns.RecordDeleter: + default: + return nil, d.Errf("module %s (%T) is not a libdns provider", modID, unm) + } + return unm, nil +} + +func parseOptECH(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name + + ech := new(caddytls.ECH) + + publicNames := d.RemainingArgs() + for _, publicName := range publicNames { + ech.Configs = append(ech.Configs, caddytls.ECHConfiguration{ + PublicName: publicName, + }) + } + if len(ech.Configs) == 0 { + return nil, d.ArgErr() + } + + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "dns": + if !d.Next() { + return nil, d.ArgErr() + } + providerName := d.Val() + modID := "dns.providers." + providerName + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return nil, err + } + ech.Publication = append(ech.Publication, &caddytls.ECHPublication{ + Configs: publicNames, + PublishersRaw: caddy.ModuleMap{ + "dns": caddyconfig.JSON(caddytls.ECHDNSPublisher{ + ProviderRaw: caddyconfig.JSONModuleObject(unm, "name", providerName, nil), + }, nil), + }, + }) + default: + return nil, d.Errf("ech: unrecognized subdirective '%s'", d.Val()) + } + } + + return ech, nil +} diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go index 7087cdba5..d60ce51a9 100644 --- a/caddyconfig/httpcaddyfile/serveroptions.go +++ b/caddyconfig/httpcaddyfile/serveroptions.go @@ -240,13 +240,16 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { } case "metrics": - if d.NextArg() { - return nil, d.ArgErr() - } - if nesting := d.Nesting(); d.NextBlock(nesting) { - return nil, d.ArgErr() - } + caddy.Log().Warn("The nested 'metrics' option inside `servers` is deprecated and will be removed in the next major version. Use the global 'metrics' option instead.") serverOpts.Metrics = new(caddyhttp.Metrics) + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "per_host": + serverOpts.Metrics.PerHost = true + default: + return nil, d.Errf("unrecognized metrics option '%s'", d.Val()) + } + } case "trace": if d.NextArg() { diff --git a/caddyconfig/httpcaddyfile/shorthands.go b/caddyconfig/httpcaddyfile/shorthands.go index 5d9ef31eb..bf612e092 100644 --- a/caddyconfig/httpcaddyfile/shorthands.go +++ b/caddyconfig/httpcaddyfile/shorthands.go @@ -52,19 +52,30 @@ func NewShorthandReplacer() ShorthandReplacer { // be used in the Caddyfile, and the right is the replacement. func placeholderShorthands() []string { return []string{ - "{dir}", "{http.request.uri.path.dir}", - "{file}", "{http.request.uri.path.file}", "{host}", "{http.request.host}", "{hostport}", "{http.request.hostport}", "{port}", "{http.request.port}", + "{orig_method}", "{http.request.orig_method}", + "{orig_uri}", "{http.request.orig_uri}", + "{orig_path}", "{http.request.orig_uri.path}", + "{orig_dir}", "{http.request.orig_uri.path.dir}", + "{orig_file}", "{http.request.orig_uri.path.file}", + "{orig_query}", "{http.request.orig_uri.query}", + "{orig_?query}", "{http.request.orig_uri.prefixed_query}", "{method}", "{http.request.method}", + "{uri}", "{http.request.uri}", + "{%uri}", "{http.request.uri_escaped}", "{path}", "{http.request.uri.path}", + "{%path}", "{http.request.uri.path_escaped}", + "{dir}", "{http.request.uri.path.dir}", + "{file}", "{http.request.uri.path.file}", "{query}", "{http.request.uri.query}", + "{%query}", "{http.request.uri.query_escaped}", + "{?query}", "{http.request.uri.prefixed_query}", "{remote}", "{http.request.remote}", "{remote_host}", "{http.request.remote.host}", "{remote_port}", "{http.request.remote.port}", "{scheme}", "{http.request.scheme}", - "{uri}", "{http.request.uri}", "{uuid}", "{http.request.uuid}", "{tls_cipher}", "{http.request.tls.cipher_suite}", "{tls_version}", "{http.request.tls.version}", diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index ed708524d..1023a9d21 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -25,7 +25,7 @@ import ( "strings" "github.com/caddyserver/certmagic" - "github.com/mholt/acmez/v2/acme" + "github.com/mholt/acmez/v3/acme" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -92,6 +92,26 @@ func (st ServerType) buildTLSApp( tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP) } + var wildcardHosts []string // collect all hosts that have a wildcard in them, and aren't HTTP + forcedAutomatedNames := make(map[string]struct{}) // explicitly configured to be automated, even if covered by a wildcard + + for _, p := range pairings { + var addresses []string + for _, addressWithProtocols := range p.addressesWithProtocols { + addresses = append(addresses, addressWithProtocols.address) + } + if !listenersUseAnyPortOtherThan(addresses, httpPort) { + continue + } + for _, sblock := range p.serverBlocks { + for _, addr := range sblock.parsedKeys { + if strings.HasPrefix(addr.Host, "*.") { + wildcardHosts = append(wildcardHosts, addr.Host[2:]) + } + } + } + } + for _, p := range pairings { // avoid setting up TLS automation policies for a server that is HTTP-only var addresses []string @@ -115,6 +135,12 @@ func (st ServerType) buildTLSApp( return nil, warnings, err } + // make a plain copy so we can compare whether we made any changes + apCopy, err := newBaseAutomationPolicy(options, warnings, true) + if err != nil { + return nil, warnings, err + } + sblockHosts := sblock.hostsFromKeys(false) if len(sblockHosts) == 0 && catchAllAP != nil { ap = catchAllAP @@ -125,6 +151,13 @@ func (st ServerType) buildTLSApp( ap.OnDemand = true } + // collect hosts that are forced to have certs automated for their specific name + if _, ok := sblock.pile["tls.force_automate"]; ok { + for _, host := range sblockHosts { + forcedAutomatedNames[host] = struct{}{} + } + } + // reuse private keys tls if _, ok := sblock.pile["tls.reuse_private_keys"]; ok { ap.ReusePrivateKeys = true @@ -217,9 +250,21 @@ func (st ServerType) buildTLSApp( catchAllAP = ap } + hostsNotHTTP := sblock.hostsFromKeysNotHTTP(httpPort) + sort.Strings(hostsNotHTTP) // solely for deterministic test results + + // if the we prefer wildcards and the AP is unchanged, + // then we can skip this AP because it should be covered + // by an AP with a wildcard + if slices.Contains(autoHTTPS, "prefer_wildcard") { + if hostsCoveredByWildcard(hostsNotHTTP, wildcardHosts) && + reflect.DeepEqual(ap, apCopy) { + continue + } + } + // associate our new automation policy with this server block's hosts - ap.SubjectsRaw = sblock.hostsFromKeysNotHTTP(httpPort) - sort.Strings(ap.SubjectsRaw) // solely for deterministic test results + ap.SubjectsRaw = hostsNotHTTP // if a combination of public and internal names were given // for this same server block and no issuer was specified, we @@ -258,6 +303,7 @@ func (st ServerType) buildTLSApp( ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)} } } + if tlsApp.Automation == nil { tlsApp.Automation = new(caddytls.AutomationConfig) } @@ -292,7 +338,7 @@ func (st ServerType) buildTLSApp( combined = reflect.New(reflect.TypeOf(cl)).Elem() } clVal := reflect.ValueOf(cl) - for i := 0; i < clVal.Len(); i++ { + for i := range clVal.Len() { combined = reflect.Append(combined, clVal.Index(i)) } loadersByName[name] = combined.Interface().(caddytls.CertificateLoader) @@ -311,6 +357,42 @@ func (st ServerType) buildTLSApp( tlsApp.Automation.OnDemand = onDemand } + // set up "global" (to the TLS app) DNS provider config + if globalDNS, ok := options["dns"]; ok && globalDNS != nil { + tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil) + } + + // set up ECH from Caddyfile options + if ech, ok := options["ech"].(*caddytls.ECH); ok { + tlsApp.EncryptedClientHello = ech + + // outer server names will need certificates, so make sure they're included + // in an automation policy for them that applies any global options + ap, err := newBaseAutomationPolicy(options, warnings, true) + if err != nil { + return nil, warnings, err + } + for _, cfg := range ech.Configs { + if cfg.PublicName != "" { + ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName) + } + } + if tlsApp.Automation == nil { + tlsApp.Automation = new(caddytls.AutomationConfig) + } + tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap) + } + + // if the storage clean interval is a boolean, then it's "off" to disable cleaning + if sc, ok := options["storage_check"].(string); ok && sc == "off" { + tlsApp.DisableStorageCheck = true + } + + // if the storage clean interval is a boolean, then it's "off" to disable cleaning + if sci, ok := options["storage_clean_interval"].(bool); ok && !sci { + tlsApp.DisableStorageClean = true + } + // set the storage clean interval if configured if storageCleanInterval, ok := options["storage_clean_interval"].(caddy.Duration); ok { if tlsApp.Automation == nil { @@ -359,6 +441,13 @@ func (st ServerType) buildTLSApp( } } } + for name := range forcedAutomatedNames { + if slices.Contains(al, name) { + continue + } + al = append(al, name) + } + slices.Sort(al) // to stabilize the adapt output if len(al) > 0 { tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings) } @@ -375,12 +464,12 @@ func (st ServerType) buildTLSApp( globalEmail := options["email"] globalACMECA := options["acme_ca"] globalACMECARoot := options["acme_ca_root"] - globalACMEDNS := options["acme_dns"] + _, globalACMEDNS := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set globalACMEEAB := options["acme_eab"] globalPreferredChains := options["preferred_chains"] - hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil + hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS || globalACMEEAB != nil || globalPreferredChains != nil if hasGlobalACMEDefaults { - for i := 0; i < len(tlsApp.Automation.Policies); i++ { + for i := range tlsApp.Automation.Policies { ap := tlsApp.Automation.Policies[i] if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) { // for public names, create default issuers which will later be filled in with configured global defaults @@ -418,10 +507,7 @@ func (st ServerType) buildTLSApp( } // consolidate automation policies that are the exact same - tlsApp.Automation.Policies = consolidateAutomationPolicies( - tlsApp.Automation.Policies, - slices.Contains(autoHTTPS, "prefer_wildcard"), - ) + tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies) // ensure automation policies don't overlap subjects (this should be // an error at provision-time as well, but catch it in the adapt phase @@ -463,7 +549,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e globalEmail := options["email"] globalACMECA := options["acme_ca"] globalACMECARoot := options["acme_ca_root"] - globalACMEDNS := options["acme_dns"] + globalACMEDNS, globalACMEDNSok := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set globalACMEEAB := options["acme_eab"] globalPreferredChains := options["preferred_chains"] globalCertLifetime := options["cert_lifetime"] @@ -478,11 +564,22 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e if globalACMECARoot != nil && !slices.Contains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) { acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) } - if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) { - acmeIssuer.Challenges = &caddytls.ChallengesConfig{ - DNS: &caddytls.DNSChallengeConfig{ - ProviderRaw: caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil), - }, + if globalACMEDNSok { + globalDNS := options["dns"] + if globalDNS != nil { + // If global `dns` is set, do NOT set provider in issuer, just set empty dns config + acmeIssuer.Challenges = &caddytls.ChallengesConfig{ + DNS: &caddytls.DNSChallengeConfig{}, + } + } else if globalACMEDNS != nil { + // Set a global DNS provider if `acme_dns` is set and `dns` is NOT set + acmeIssuer.Challenges = &caddytls.ChallengesConfig{ + DNS: &caddytls.DNSChallengeConfig{ + ProviderRaw: caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil), + }, + } + } else { + return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option") } } if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil { @@ -491,7 +588,8 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil { acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference) } - if globalHTTPPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.HTTP == nil || acmeIssuer.Challenges.HTTP.AlternatePort == 0) { + // only configure alt HTTP and TLS-ALPN ports if the DNS challenge is not enabled (wouldn't hurt, but isn't necessary since the DNS challenge is exclusive of others) + if globalHTTPPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.HTTP == nil || acmeIssuer.Challenges.HTTP.AlternatePort == 0) { if acmeIssuer.Challenges == nil { acmeIssuer.Challenges = new(caddytls.ChallengesConfig) } @@ -500,7 +598,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e } acmeIssuer.Challenges.HTTP.AlternatePort = globalHTTPPort.(int) } - if globalHTTPSPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.TLSALPN == nil || acmeIssuer.Challenges.TLSALPN.AlternatePort == 0) { + if globalHTTPSPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.TLSALPN == nil || acmeIssuer.Challenges.TLSALPN.AlternatePort == 0) { if acmeIssuer.Challenges == nil { acmeIssuer.Challenges = new(caddytls.ChallengesConfig) } @@ -529,12 +627,18 @@ func newBaseAutomationPolicy( _, hasLocalCerts := options["local_certs"] keyType, hasKeyType := options["key_type"] ocspStapling, hasOCSPStapling := options["ocsp_stapling"] - hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling + globalACMECA := options["acme_ca"] + globalACMECARoot := options["acme_ca_root"] + _, globalACMEDNS := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set + globalACMEEAB := options["acme_eab"] + globalPreferredChains := options["preferred_chains"] + hasGlobalACMEDefaults := globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS || globalACMEEAB != nil || globalPreferredChains != nil + // if there are no global options related to automation policies // set, then we can just return right away - if !hasGlobalAutomationOpts { + if !hasGlobalAutomationOpts && !hasGlobalACMEDefaults { if always { return new(caddytls.AutomationPolicy), nil } @@ -556,6 +660,14 @@ func newBaseAutomationPolicy( ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)} } + if hasGlobalACMEDefaults { + for i := range ap.Issuers { + if err := fillInGlobalACMEDefaults(ap.Issuers[i], options); err != nil { + return nil, fmt.Errorf("filling in global issuer defaults for issuer %d: %v", i, err) + } + } + } + if hasOCSPStapling { ocspConfig := ocspStapling.(certmagic.OCSPConfig) ap.DisableOCSPStapling = ocspConfig.DisableStapling @@ -567,7 +679,7 @@ func newBaseAutomationPolicy( // consolidateAutomationPolicies combines automation policies that are the same, // for a cleaner overall output. -func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy, preferWildcard bool) []*caddytls.AutomationPolicy { +func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy { // sort from most specific to least specific; we depend on this ordering sort.SliceStable(aps, func(i, j int) bool { if automationPolicyIsSubset(aps[i], aps[j]) { @@ -652,31 +764,6 @@ outer: j-- } } - - if preferWildcard { - // remove subjects from i if they're covered by a wildcard in j - iSubjs := aps[i].SubjectsRaw - for iSubj := 0; iSubj < len(iSubjs); iSubj++ { - for jSubj := range aps[j].SubjectsRaw { - if !strings.HasPrefix(aps[j].SubjectsRaw[jSubj], "*.") { - continue - } - if certmagic.MatchWildcard(aps[i].SubjectsRaw[iSubj], aps[j].SubjectsRaw[jSubj]) { - iSubjs = slices.Delete(iSubjs, iSubj, iSubj+1) - iSubj-- - break - } - } - } - aps[i].SubjectsRaw = iSubjs - - // remove i if it has no subjects left - if len(aps[i].SubjectsRaw) == 0 { - aps = slices.Delete(aps, i, i+1) - i-- - continue outer - } - } } } @@ -748,3 +835,20 @@ func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool { func isTailscaleDomain(name string) bool { return strings.HasSuffix(strings.ToLower(name), ".ts.net") } + +func hostsCoveredByWildcard(hosts []string, wildcards []string) bool { + if len(hosts) == 0 || len(wildcards) == 0 { + return false + } + for _, host := range hosts { + for _, wildcard := range wildcards { + if strings.HasPrefix(host, "*.") { + continue + } + if certmagic.MatchWildcard(host, "*."+wildcard) { + return true + } + } + } + return false +} diff --git a/caddyconfig/httploader.go b/caddyconfig/httploader.go index 55fadf65b..a25041a34 100644 --- a/caddyconfig/httploader.go +++ b/caddyconfig/httploader.go @@ -35,7 +35,7 @@ func init() { // If the response is not a JSON config, a config adapter must be specified // either in the loader config (`adapter`), or in the Content-Type HTTP header // returned in the HTTP response from the server. The Content-Type header is -// read just like the admin API's `/load` endpoint. Uf you don't have control +// read just like the admin API's `/load` endpoint. If you don't have control // over the HTTP server (but can still trust its response), you can override // the Content-Type header by setting the `adapter` property in this config. type HTTPLoader struct { diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 05aa1e3f5..7b56bb281 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -31,8 +31,8 @@ import ( _ "github.com/caddyserver/caddy/v2/modules/standard" ) -// Defaults store any configuration required to make the tests run -type Defaults struct { +// Config store any configuration required to make the tests run +type Config struct { // Port we expect caddy to listening on AdminPort int // Certificates we expect to be loaded before attempting to run the tests @@ -44,7 +44,7 @@ type Defaults struct { } // Default testing values -var Default = Defaults{ +var Default = Config{ AdminPort: 2999, // different from what a real server also running on a developer's machine might be Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"}, TestRequestTimeout: 5 * time.Second, @@ -61,6 +61,7 @@ type Tester struct { Client *http.Client configLoaded bool t testing.TB + config Config } // NewTester will create a new testing client with an attached cookie jar @@ -78,9 +79,29 @@ func NewTester(t testing.TB) *Tester { }, configLoaded: false, t: t, + config: Default, } } +// WithDefaultOverrides this will override the default test configuration with the provided values. +func (tc *Tester) WithDefaultOverrides(overrides Config) *Tester { + if overrides.AdminPort != 0 { + tc.config.AdminPort = overrides.AdminPort + } + if len(overrides.Certificates) > 0 { + tc.config.Certificates = overrides.Certificates + } + if overrides.TestRequestTimeout != 0 { + tc.config.TestRequestTimeout = overrides.TestRequestTimeout + tc.Client.Timeout = overrides.TestRequestTimeout + } + if overrides.LoadRequestTimeout != 0 { + tc.config.LoadRequestTimeout = overrides.LoadRequestTimeout + } + + return tc +} + type configLoadError struct { Response string } @@ -113,7 +134,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error { return nil } - err := validateTestPrerequisites(tc.t) + err := validateTestPrerequisites(tc) if err != nil { tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err) return nil @@ -121,7 +142,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error { tc.t.Cleanup(func() { if tc.t.Failed() && tc.configLoaded { - res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) + res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort)) if err != nil { tc.t.Log("unable to read the current config") return @@ -151,10 +172,10 @@ func (tc *Tester) initServer(rawConfig string, configType string) error { tc.t.Logf("After: %s", rawConfig) } client := &http.Client{ - Timeout: Default.LoadRequestTimeout, + Timeout: tc.config.LoadRequestTimeout, } start := time.Now() - req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig)) + req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", tc.config.AdminPort), strings.NewReader(rawConfig)) if err != nil { tc.t.Errorf("failed to create request. %s", err) return err @@ -205,11 +226,11 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error } client := &http.Client{ - Timeout: Default.LoadRequestTimeout, + Timeout: tc.config.LoadRequestTimeout, } fetchConfig := func(client *http.Client) any { - resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) + resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort)) if err != nil { return nil } @@ -237,30 +258,30 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error } const initConfig = `{ - admin localhost:2999 + admin localhost:%d } ` // validateTestPrerequisites ensures the certificates are available in the // designated path and Caddy sub-process is running. -func validateTestPrerequisites(t testing.TB) error { +func validateTestPrerequisites(tc *Tester) error { // check certificates are found - for _, certName := range Default.Certificates { + for _, certName := range tc.config.Certificates { if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("caddy integration test certificates (%s) not found", certName) } } - if isCaddyAdminRunning() != nil { + if isCaddyAdminRunning(tc) != nil { // setup the init config file, and set the cleanup afterwards f, err := os.CreateTemp("", "") if err != nil { return err } - t.Cleanup(func() { + tc.t.Cleanup(func() { os.Remove(f.Name()) }) - if _, err := f.WriteString(initConfig); err != nil { + if _, err := fmt.Fprintf(f, initConfig, tc.config.AdminPort); err != nil { return err } @@ -271,23 +292,23 @@ func validateTestPrerequisites(t testing.TB) error { }() // wait for caddy to start serving the initial config - for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- { + for retries := 10; retries > 0 && isCaddyAdminRunning(tc) != nil; retries-- { time.Sleep(1 * time.Second) } } // one more time to return the error - return isCaddyAdminRunning() + return isCaddyAdminRunning(tc) } -func isCaddyAdminRunning() error { +func isCaddyAdminRunning(tc *Tester) error { // assert that caddy is running client := &http.Client{ - Timeout: Default.LoadRequestTimeout, + Timeout: tc.config.LoadRequestTimeout, } - resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) + resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort)) if err != nil { - return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", Default.AdminPort) + return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", tc.config.AdminPort) } resp.Body.Close() diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go index ceacd1db0..f10aef6a8 100644 --- a/caddytest/integration/acme_test.go +++ b/caddytest/integration/acme_test.go @@ -6,17 +6,20 @@ import ( "crypto/elliptic" "crypto/rand" "fmt" + "log/slog" "net" "net/http" "strings" "testing" - "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/caddytest" - "github.com/mholt/acmez/v2" - "github.com/mholt/acmez/v2/acme" + "github.com/mholt/acmez/v3" + "github.com/mholt/acmez/v3/acme" smallstepacme "github.com/smallstep/certificates/acme" "go.uber.org/zap" + "go.uber.org/zap/exp/zapslog" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddytest" ) const acmeChallengePort = 9081 @@ -48,7 +51,7 @@ func TestACMEServerWithDefaults(t *testing.T) { Client: &acme.Client{ Directory: "https://acme.localhost:9443/acme/local/directory", HTTPClient: tester.Client, - Logger: logger, + Logger: slog.New(zapslog.NewHandler(logger.Core())), }, ChallengeSolvers: map[string]acmez.Solver{ acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger}, @@ -117,7 +120,7 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) { Client: &acme.Client{ Directory: "https://acme.localhost:9443/acme/local/directory", HTTPClient: tester.Client, - Logger: logger, + Logger: slog.New(zapslog.NewHandler(logger.Core())), }, ChallengeSolvers: map[string]acmez.Solver{ acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger}, diff --git a/caddytest/integration/acmeserver_test.go b/caddytest/integration/acmeserver_test.go index 22b716f84..d6a9ba005 100644 --- a/caddytest/integration/acmeserver_test.go +++ b/caddytest/integration/acmeserver_test.go @@ -5,13 +5,16 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "log/slog" "strings" "testing" - "github.com/caddyserver/caddy/v2/caddytest" - "github.com/mholt/acmez/v2" - "github.com/mholt/acmez/v2/acme" + "github.com/mholt/acmez/v3" + "github.com/mholt/acmez/v3/acme" "go.uber.org/zap" + "go.uber.org/zap/exp/zapslog" + + "github.com/caddyserver/caddy/v2/caddytest" ) func TestACMEServerDirectory(t *testing.T) { @@ -76,7 +79,7 @@ func TestACMEServerAllowPolicy(t *testing.T) { Client: &acme.Client{ Directory: "https://acme.localhost:9443/acme/local/directory", HTTPClient: tester.Client, - Logger: logger, + Logger: slog.New(zapslog.NewHandler(logger.Core())), }, ChallengeSolvers: map[string]acmez.Solver{ acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger}, @@ -165,7 +168,7 @@ func TestACMEServerDenyPolicy(t *testing.T) { Client: &acme.Client{ Directory: "https://acme.localhost:9443/acme/local/directory", HTTPClient: tester.Client, - Logger: logger, + Logger: slog.New(zapslog.NewHandler(logger.Core())), }, ChallengeSolvers: map[string]acmez.Solver{ acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger}, diff --git a/caddytest/integration/caddyfile_adapt/acme_dns_configured.caddyfiletest b/caddytest/integration/caddyfile_adapt/acme_dns_configured.caddyfiletest new file mode 100644 index 000000000..eaf8d0623 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/acme_dns_configured.caddyfiletest @@ -0,0 +1,68 @@ +{ + acme_dns mock foo +} + +example.com { + respond "Hello World" +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Hello World", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "issuers": [ + { + "challenges": { + "dns": { + "provider": { + "name": "mock" + } + } + }, + "module": "acme" + } + ] + } + ] + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/acme_dns_naked_use_dns_defaults.caddyfiletest b/caddytest/integration/caddyfile_adapt/acme_dns_naked_use_dns_defaults.caddyfiletest new file mode 100644 index 000000000..750ba22a4 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/acme_dns_naked_use_dns_defaults.caddyfiletest @@ -0,0 +1,53 @@ +{ + dns mock + acme_dns +} + +example.com { + +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "issuers": [ + { + "challenges": { + "dns": {} + }, + "module": "acme" + } + ] + } + ] + }, + "dns": { + "name": "mock" + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/acme_dns_naked_without_dns.caddyfiletest b/caddytest/integration/caddyfile_adapt/acme_dns_naked_without_dns.caddyfiletest new file mode 100644 index 000000000..e171c5493 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/acme_dns_naked_without_dns.caddyfiletest @@ -0,0 +1,9 @@ +{ + acme_dns +} + +example.com { + respond "Hello World" +} +---------- +acme_dns specified without DNS provider config, but no provider specified with 'dns' global option \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/acme_server_policy-allow.caddyfiletest b/caddytest/integration/caddyfile_adapt/acme_server_policy-allow.caddyfiletest new file mode 100644 index 000000000..5d1d8a3bc --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/acme_server_policy-allow.caddyfiletest @@ -0,0 +1,72 @@ +{ + pki { + ca custom-ca { + name "Custom CA" + } + } +} + +acme.example.com { + acme_server { + ca custom-ca + allow { + domains host-1.internal.example.com host-2.internal.example.com + } + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "acme.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "ca": "custom-ca", + "handler": "acme_server", + "policy": { + "allow": { + "domains": [ + "host-1.internal.example.com", + "host-2.internal.example.com" + ] + } + } + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + }, + "pki": { + "certificate_authorities": { + "custom-ca": { + "name": "Custom CA" + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/acme_server_policy-both.caddyfiletest b/caddytest/integration/caddyfile_adapt/acme_server_policy-both.caddyfiletest new file mode 100644 index 000000000..15cdbba90 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/acme_server_policy-both.caddyfiletest @@ -0,0 +1,80 @@ +{ + pki { + ca custom-ca { + name "Custom CA" + } + } +} + +acme.example.com { + acme_server { + ca custom-ca + allow { + domains host-1.internal.example.com host-2.internal.example.com + } + deny { + domains dc.internal.example.com + } + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "acme.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "ca": "custom-ca", + "handler": "acme_server", + "policy": { + "allow": { + "domains": [ + "host-1.internal.example.com", + "host-2.internal.example.com" + ] + }, + "deny": { + "domains": [ + "dc.internal.example.com" + ] + } + } + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + }, + "pki": { + "certificate_authorities": { + "custom-ca": { + "name": "Custom CA" + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/acme_server_policy-deny.caddyfiletest b/caddytest/integration/caddyfile_adapt/acme_server_policy-deny.caddyfiletest new file mode 100644 index 000000000..0478088c9 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/acme_server_policy-deny.caddyfiletest @@ -0,0 +1,71 @@ +{ + pki { + ca custom-ca { + name "Custom CA" + } + } +} + +acme.example.com { + acme_server { + ca custom-ca + deny { + domains dc.internal.example.com + } + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "acme.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "ca": "custom-ca", + "handler": "acme_server", + "policy": { + "deny": { + "domains": [ + "dc.internal.example.com" + ] + } + } + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + }, + "pki": { + "certificate_authorities": { + "custom-ca": { + "name": "Custom CA" + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/acme_server_sign_with_root.caddyfiletest b/caddytest/integration/caddyfile_adapt/acme_server_sign_with_root.caddyfiletest index 9880f2821..5b5040107 100644 --- a/caddytest/integration/caddyfile_adapt/acme_server_sign_with_root.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/acme_server_sign_with_root.caddyfiletest @@ -5,15 +5,15 @@ root_cn "Internal Root Cert" intermediate_cn "Internal Intermediate Cert" } - } + } } - acme.example.com { acme_server { ca internal sign_with_root } } + ---------- { "apps": { diff --git a/caddytest/integration/caddyfile_adapt/ambiguous_site_definition.caddyfiletest b/caddytest/integration/caddyfile_adapt/ambiguous_site_definition.caddyfiletest new file mode 100644 index 000000000..bd62d3c4e --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/ambiguous_site_definition.caddyfiletest @@ -0,0 +1,12 @@ +example.com +handle { + respond "one" +} + +example.com +handle { + respond "two" +} +---------- +Caddyfile:6: unrecognized directive: example.com +Did you mean to define a second site? If so, you must use curly braces around each site to separate their configurations. \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/ambiguous_site_definition_duplicate_key.caddyfiletest b/caddytest/integration/caddyfile_adapt/ambiguous_site_definition_duplicate_key.caddyfiletest new file mode 100644 index 000000000..d182b8ffd --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/ambiguous_site_definition_duplicate_key.caddyfiletest @@ -0,0 +1,9 @@ +:8080 { + respond "one" +} + +:8080 { + respond "two" +} +---------- +ambiguous site definition: :8080 \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/directive_as_site_address.caddyfiletest b/caddytest/integration/caddyfile_adapt/directive_as_site_address.caddyfiletest new file mode 100644 index 000000000..7245fd984 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/directive_as_site_address.caddyfiletest @@ -0,0 +1,5 @@ +handle + +respond "should not work" +---------- +Caddyfile:1: parsed 'handle' as a site address, but it is a known directive; directives must appear in a site block \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/duplicate_listener_address_global.caddyfiletest b/caddytest/integration/caddyfile_adapt/duplicate_listener_address_global.caddyfiletest new file mode 100644 index 000000000..5287557b3 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/duplicate_listener_address_global.caddyfiletest @@ -0,0 +1,12 @@ +{ + servers { + srv0 { + listen :8080 + } + srv1 { + listen :8080 + } + } +} +---------- +parsing caddyfile tokens for 'servers': unrecognized servers option 'srv0', at Caddyfile:3 \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest b/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest index 181bc2264..ea9038ef8 100644 --- a/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest @@ -21,6 +21,8 @@ encode { zstd gzip 5 } + +encode ---------- { "apps": { @@ -76,6 +78,17 @@ encode { "zstd", "gzip" ] + }, + { + "encodings": { + "gzip": {}, + "zstd": {} + }, + "handler": "encode", + "prefer": [ + "zstd", + "gzip" + ] } ] } diff --git a/caddytest/integration/caddyfile_adapt/error_example.caddyfiletest b/caddytest/integration/caddyfile_adapt/error_example.caddyfiletest index bd42aee55..6f3059ab2 100644 --- a/caddytest/integration/caddyfile_adapt/error_example.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/error_example.caddyfiletest @@ -106,20 +106,29 @@ example.com { "handler": "subroute", "routes": [ { - "group": "group0", "handle": [ { - "handler": "rewrite", - "uri": "/{http.error.status_code}.html" - } - ] - }, - { - "handle": [ - { - "handler": "file_server", - "hide": [ - "./Caddyfile" + "handler": "subroute", + "routes": [ + { + "group": "group0", + "handle": [ + { + "handler": "rewrite", + "uri": "/{http.error.status_code}.html" + } + ] + }, + { + "handle": [ + { + "handler": "file_server", + "hide": [ + "./Caddyfile" + ] + } + ] + } ] } ] diff --git a/caddytest/integration/caddyfile_adapt/error_multi_site_blocks.caddyfiletest b/caddytest/integration/caddyfile_adapt/error_multi_site_blocks.caddyfiletest index 0e84a13c2..1bec4b3e8 100644 --- a/caddytest/integration/caddyfile_adapt/error_multi_site_blocks.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/error_multi_site_blocks.caddyfiletest @@ -165,8 +165,17 @@ bar.localhost { { "handle": [ { - "body": "404 or 410 error", - "handler": "static_response" + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "404 or 410 error", + "handler": "static_response" + } + ] + } + ] } ], "match": [ @@ -178,8 +187,17 @@ bar.localhost { { "handle": [ { - "body": "Error In range [500 .. 599]", - "handler": "static_response" + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Error In range [500 .. 599]", + "handler": "static_response" + } + ] + } + ] } ], "match": [ @@ -208,8 +226,17 @@ bar.localhost { { "handle": [ { - "body": "404 or 410 error from second site", - "handler": "static_response" + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "404 or 410 error from second site", + "handler": "static_response" + } + ] + } + ] } ], "match": [ @@ -221,8 +248,17 @@ bar.localhost { { "handle": [ { - "body": "Error In range [500 .. 599] from second site", - "handler": "static_response" + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Error In range [500 .. 599] from second site", + "handler": "static_response" + } + ] + } + ] } ], "match": [ diff --git a/caddytest/integration/caddyfile_adapt/error_range_codes.caddyfiletest b/caddytest/integration/caddyfile_adapt/error_range_codes.caddyfiletest index 46b70c8e3..299abc5f1 100644 --- a/caddytest/integration/caddyfile_adapt/error_range_codes.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/error_range_codes.caddyfiletest @@ -96,8 +96,17 @@ localhost:3010 { { "handle": [ { - "body": "Error in the [400 .. 499] range", - "handler": "static_response" + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Error in the [400 .. 499] range", + "handler": "static_response" + } + ] + } + ] } ], "match": [ diff --git a/caddytest/integration/caddyfile_adapt/error_range_simple_codes.caddyfiletest b/caddytest/integration/caddyfile_adapt/error_range_simple_codes.caddyfiletest index 70158830c..9d4d2645c 100644 --- a/caddytest/integration/caddyfile_adapt/error_range_simple_codes.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/error_range_simple_codes.caddyfiletest @@ -116,8 +116,17 @@ localhost:2099 { { "handle": [ { - "body": "Error in the [400 .. 499] range", - "handler": "static_response" + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Error in the [400 .. 499] range", + "handler": "static_response" + } + ] + } + ] } ], "match": [ @@ -129,8 +138,17 @@ localhost:2099 { { "handle": [ { - "body": "Error code is equal to 500 or in the [300..399] range", - "handler": "static_response" + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Error code is equal to 500 or in the [300..399] range", + "handler": "static_response" + } + ] + } + ] } ], "match": [ diff --git a/caddytest/integration/caddyfile_adapt/error_simple_codes.caddyfiletest b/caddytest/integration/caddyfile_adapt/error_simple_codes.caddyfiletest index 5ac5863e3..29a51ffad 100644 --- a/caddytest/integration/caddyfile_adapt/error_simple_codes.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/error_simple_codes.caddyfiletest @@ -96,8 +96,17 @@ localhost:3010 { { "handle": [ { - "body": "404 or 410 error", - "handler": "static_response" + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "404 or 410 error", + "handler": "static_response" + } + ] + } + ] } ], "match": [ diff --git a/caddytest/integration/caddyfile_adapt/error_sort.caddyfiletest b/caddytest/integration/caddyfile_adapt/error_sort.caddyfiletest index 63701cccb..1faf9b3bd 100644 --- a/caddytest/integration/caddyfile_adapt/error_sort.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/error_sort.caddyfiletest @@ -116,8 +116,17 @@ localhost:2099 { { "handle": [ { - "body": "Error in the [400 .. 499] range", - "handler": "static_response" + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Error in the [400 .. 499] range", + "handler": "static_response" + } + ] + } + ] } ], "match": [ @@ -129,8 +138,17 @@ localhost:2099 { { "handle": [ { - "body": "Fallback route: code outside the [400..499] range", - "handler": "static_response" + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Fallback route: code outside the [400..499] range", + "handler": "static_response" + } + ] + } + ] } ] } diff --git a/caddytest/integration/caddyfile_adapt/error_subhandlers.caddyfiletest b/caddytest/integration/caddyfile_adapt/error_subhandlers.caddyfiletest new file mode 100644 index 000000000..54429a73e --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/error_subhandlers.caddyfiletest @@ -0,0 +1,260 @@ +{ + http_port 2099 +} +localhost:2099 { + root * /var/www/ + file_server + + handle_errors 404 { + handle /en/* { + respond "not found" 404 + } + handle /es/* { + respond "no encontrado" + } + handle { + respond "default not found" + } + } + handle_errors { + handle /en/* { + respond "English error" + } + handle /es/* { + respond "Spanish error" + } + handle { + respond "Default error" + } + } +} +---------- +{ + "apps": { + "http": { + "http_port": 2099, + "servers": { + "srv0": { + "listen": [ + ":2099" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "vars", + "root": "/var/www/" + }, + { + "handler": "file_server", + "hide": [ + "./Caddyfile" + ] + } + ] + } + ] + } + ], + "terminal": true + } + ], + "errors": { + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "group": "group3", + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "not found", + "handler": "static_response", + "status_code": 404 + } + ] + } + ] + } + ], + "match": [ + { + "path": [ + "/en/*" + ] + } + ] + }, + { + "group": "group3", + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "no encontrado", + "handler": "static_response" + } + ] + } + ] + } + ], + "match": [ + { + "path": [ + "/es/*" + ] + } + ] + }, + { + "group": "group3", + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "default not found", + "handler": "static_response" + } + ] + } + ] + } + ] + } + ] + } + ], + "match": [ + { + "expression": "{http.error.status_code} in [404]" + } + ] + }, + { + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "group": "group8", + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "English error", + "handler": "static_response" + } + ] + } + ] + } + ], + "match": [ + { + "path": [ + "/en/*" + ] + } + ] + }, + { + "group": "group8", + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Spanish error", + "handler": "static_response" + } + ] + } + ] + } + ], + "match": [ + { + "path": [ + "/es/*" + ] + } + ] + }, + { + "group": "group8", + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Default error", + "handler": "static_response" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } + } +} + diff --git a/caddytest/integration/caddyfile_adapt/file_server_file_limit.caddyfiletest b/caddytest/integration/caddyfile_adapt/file_server_file_limit.caddyfiletest new file mode 100644 index 000000000..cd73fbff6 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/file_server_file_limit.caddyfiletest @@ -0,0 +1,36 @@ +:80 + +file_server { + browse { + file_limit 4000 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "routes": [ + { + "handle": [ + { + "browse": { + "file_limit": 4000 + }, + "handler": "file_server", + "hide": [ + "./Caddyfile" + ] + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/file_server_precompressed.caddyfiletest b/caddytest/integration/caddyfile_adapt/file_server_precompressed.caddyfiletest index ac7d7eddd..3154de96e 100644 --- a/caddytest/integration/caddyfile_adapt/file_server_precompressed.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/file_server_precompressed.caddyfiletest @@ -3,6 +3,10 @@ file_server { precompressed zstd br gzip } + +file_server { + precompressed +} ---------- { "apps": { @@ -30,6 +34,22 @@ file_server { "br", "gzip" ] + }, + { + "handler": "file_server", + "hide": [ + "./Caddyfile" + ], + "precompressed": { + "br": {}, + "gzip": {}, + "zstd": {} + }, + "precompressed_order": [ + "br", + "zstd", + "gzip" + ] } ] } diff --git a/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest b/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest index a05703280..240bdc62f 100644 --- a/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest @@ -1,6 +1,6 @@ app.example.com { forward_auth authelia:9091 { - uri /api/verify?rd=https://authelia.example.com + uri /api/authz/forward-auth copy_headers Remote-User Remote-Groups Remote-Name Remote-Email } @@ -39,6 +39,13 @@ app.example.com { ] }, "routes": [ + { + "handle": [ + { + "handler": "vars" + } + ] + }, { "handle": [ { @@ -47,19 +54,104 @@ app.example.com { "set": { "Remote-Email": [ "{http.reverse_proxy.header.Remote-Email}" - ], + ] + } + } + } + ], + "match": [ + { + "not": [ + { + "vars": { + "{http.reverse_proxy.header.Remote-Email}": [ + "" + ] + } + } + ] + } + ] + }, + { + "handle": [ + { + "handler": "headers", + "request": { + "set": { "Remote-Groups": [ "{http.reverse_proxy.header.Remote-Groups}" - ], + ] + } + } + } + ], + "match": [ + { + "not": [ + { + "vars": { + "{http.reverse_proxy.header.Remote-Groups}": [ + "" + ] + } + } + ] + } + ] + }, + { + "handle": [ + { + "handler": "headers", + "request": { + "set": { "Remote-Name": [ "{http.reverse_proxy.header.Remote-Name}" - ], + ] + } + } + } + ], + "match": [ + { + "not": [ + { + "vars": { + "{http.reverse_proxy.header.Remote-Name}": [ + "" + ] + } + } + ] + } + ] + }, + { + "handle": [ + { + "handler": "headers", + "request": { + "set": { "Remote-User": [ "{http.reverse_proxy.header.Remote-User}" ] } } } + ], + "match": [ + { + "not": [ + { + "vars": { + "{http.reverse_proxy.header.Remote-User}": [ + "" + ] + } + } + ] + } ] } ] @@ -80,7 +172,7 @@ app.example.com { }, "rewrite": { "method": "GET", - "uri": "/api/verify?rd=https://authelia.example.com" + "uri": "/api/authz/forward-auth" }, "upstreams": [ { diff --git a/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest b/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest index 65228174d..c2be2ed43 100644 --- a/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest @@ -28,6 +28,13 @@ forward_auth localhost:9000 { ] }, "routes": [ + { + "handle": [ + { + "handler": "vars" + } + ] + }, { "handle": [ { @@ -36,22 +43,131 @@ forward_auth localhost:9000 { "set": { "1": [ "{http.reverse_proxy.header.A}" - ], - "3": [ - "{http.reverse_proxy.header.C}" - ], - "5": [ - "{http.reverse_proxy.header.E}" - ], + ] + } + } + } + ], + "match": [ + { + "not": [ + { + "vars": { + "{http.reverse_proxy.header.A}": [ + "" + ] + } + } + ] + } + ] + }, + { + "handle": [ + { + "handler": "headers", + "request": { + "set": { "B": [ "{http.reverse_proxy.header.B}" - ], + ] + } + } + } + ], + "match": [ + { + "not": [ + { + "vars": { + "{http.reverse_proxy.header.B}": [ + "" + ] + } + } + ] + } + ] + }, + { + "handle": [ + { + "handler": "headers", + "request": { + "set": { + "3": [ + "{http.reverse_proxy.header.C}" + ] + } + } + } + ], + "match": [ + { + "not": [ + { + "vars": { + "{http.reverse_proxy.header.C}": [ + "" + ] + } + } + ] + } + ] + }, + { + "handle": [ + { + "handler": "headers", + "request": { + "set": { "D": [ "{http.reverse_proxy.header.D}" ] } } } + ], + "match": [ + { + "not": [ + { + "vars": { + "{http.reverse_proxy.header.D}": [ + "" + ] + } + } + ] + } + ] + }, + { + "handle": [ + { + "handler": "headers", + "request": { + "set": { + "5": [ + "{http.reverse_proxy.header.E}" + ] + } + } + } + ], + "match": [ + { + "not": [ + { + "vars": { + "{http.reverse_proxy.header.E}": [ + "" + ] + } + } + ] + } ] } ] diff --git a/caddytest/integration/caddyfile_adapt/global_options.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options.caddyfiletest index 88729c512..99f45cdd5 100644 --- a/caddytest/integration/caddyfile_adapt/global_options.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/global_options.caddyfiletest @@ -9,6 +9,8 @@ storage file_system { root /data } + storage_check off + storage_clean_interval off acme_ca https://example.com acme_ca_root /path/to/ca.crt ocsp_stapling off @@ -17,8 +19,6 @@ admin off on_demand_tls { ask https://example.com - interval 30s - burst 20 } local_certs key_type ed25519 @@ -72,14 +72,12 @@ "permission": { "endpoint": "https://example.com", "module": "http" - }, - "rate_limit": { - "interval": 30000000000, - "burst": 20 } } }, - "disable_ocsp_stapling": true + "disable_ocsp_stapling": true, + "disable_storage_check": true, + "disable_storage_clean": true } } } diff --git a/caddytest/integration/caddyfile_adapt/global_options_acme.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_acme.caddyfiletest index bc4b6dcaf..004a3a32e 100644 --- a/caddytest/integration/caddyfile_adapt/global_options_acme.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/global_options_acme.caddyfiletest @@ -17,8 +17,6 @@ admin off on_demand_tls { ask https://example.com - interval 30s - burst 20 } storage_clean_interval 7d renew_interval 1d @@ -89,10 +87,6 @@ "permission": { "endpoint": "https://example.com", "module": "http" - }, - "rate_limit": { - "interval": 30000000000, - "burst": 20 } }, "ocsp_interval": 172800000000000, diff --git a/caddytest/integration/caddyfile_adapt/global_options_admin.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_admin.caddyfiletest index cfc578826..be309eaa3 100644 --- a/caddytest/integration/caddyfile_adapt/global_options_admin.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/global_options_admin.caddyfiletest @@ -16,8 +16,6 @@ } on_demand_tls { ask https://example.com - interval 30s - burst 20 } local_certs key_type ed25519 @@ -74,10 +72,6 @@ "permission": { "endpoint": "https://example.com", "module": "http" - }, - "rate_limit": { - "interval": 30000000000, - "burst": 20 } } } diff --git a/caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest new file mode 100644 index 000000000..12b73b2b7 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest @@ -0,0 +1,23 @@ +{ + log { + sampling { + interval 300 + first 50 + thereafter 40 + } + } +} +---------- +{ + "logging": { + "logs": { + "default": { + "sampling": { + "interval": 300, + "first": 50, + "thereafter": 40 + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/global_options_preferred_chains.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_preferred_chains.caddyfiletest index 1f5d0093e..e910a3d7e 100644 --- a/caddytest/integration/caddyfile_adapt/global_options_preferred_chains.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/global_options_preferred_chains.caddyfiletest @@ -31,9 +31,6 @@ example.com "automation": { "policies": [ { - "subjects": [ - "example.com" - ], "issuers": [ { "module": "acme", diff --git a/caddytest/integration/caddyfile_adapt/header.caddyfiletest b/caddytest/integration/caddyfile_adapt/header.caddyfiletest index ec2a842a3..a0af24ff6 100644 --- a/caddytest/integration/caddyfile_adapt/header.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/header.caddyfiletest @@ -12,10 +12,14 @@ @images path /images/* header @images { Cache-Control "public, max-age=3600, stale-while-revalidate=86400" + match { + status 200 + } } header { +Link "Foo" +Link "Bar" + match status 200 } header >Set Defer header >Replace Deferred Replacement @@ -42,6 +46,11 @@ { "handler": "headers", "response": { + "require": { + "status_code": [ + 200 + ] + }, "set": { "Cache-Control": [ "public, max-age=3600, stale-while-revalidate=86400" @@ -136,6 +145,11 @@ "Foo", "Bar" ] + }, + "require": { + "status_code": [ + 200 + ] } } }, diff --git a/caddytest/integration/caddyfile_adapt/header_placeholder_search.caddyfiletest b/caddytest/integration/caddyfile_adapt/header_placeholder_search.caddyfiletest new file mode 100644 index 000000000..9a9e46b62 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/header_placeholder_search.caddyfiletest @@ -0,0 +1,64 @@ +:80 { + header Test-Static ":443" "STATIC-WORKS" + header Test-Dynamic ":{http.request.local.port}" "DYNAMIC-WORKS" + header Test-Complex "port-{http.request.local.port}-end" "COMPLEX-{http.request.method}" +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "routes": [ + { + "handle": [ + { + "handler": "headers", + "response": { + "replace": { + "Test-Static": [ + { + "replace": "STATIC-WORKS", + "search_regexp": ":443" + } + ] + } + } + }, + { + "handler": "headers", + "response": { + "replace": { + "Test-Dynamic": [ + { + "replace": "DYNAMIC-WORKS", + "search_regexp": ":{http.request.local.port}" + } + ] + } + } + }, + { + "handler": "headers", + "response": { + "replace": { + "Test-Complex": [ + { + "replace": "COMPLEX-{http.request.method}", + "search_regexp": "port-{http.request.local.port}-end" + } + ] + } + } + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/heredoc.caddyfiletest b/caddytest/integration/caddyfile_adapt/heredoc.caddyfiletest index cc1174d6d..f50d2b7f9 100644 --- a/caddytest/integration/caddyfile_adapt/heredoc.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/heredoc.caddyfiletest @@ -1,11 +1,12 @@ example.com { - respond < Foo Foo EOF 200 } + ---------- { "apps": { diff --git a/caddytest/integration/caddyfile_adapt/heredoc_extra_indentation.caddyfiletest b/caddytest/integration/caddyfile_adapt/heredoc_extra_indentation.caddyfiletest new file mode 100644 index 000000000..1b71b91fc --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/heredoc_extra_indentation.caddyfiletest @@ -0,0 +1,41 @@ +:80 + +handle { + respond < 0 && expected[0] == '{' { + ok := caddytest.CompareAdapt(t, filename, caddyfile, "caddyfile", expected) + if !ok { + t.Errorf("failed to adapt %s", filename) + } + return + } + + // otherwise, adapt the Caddyfile and check for errors + cfgAdapter := caddyconfig.GetAdapter("caddyfile") + _, _, err = cfgAdapter.Adapt([]byte(caddyfile), nil) + if err == nil { + t.Errorf("expected error for %s but got none", filename) + } else { + normalizedErr := winNewlines.ReplaceAllString(err.Error(), "\n") + if !strings.Contains(normalizedErr, expected) { + t.Errorf("expected error for %s to contain:\n%s\nbut got:\n%s", filename, expected, normalizedErr) + } + } + }) } } diff --git a/caddytest/integration/caddyfile_test.go b/caddytest/integration/caddyfile_test.go index 11ffc08ae..d45d5a5e9 100644 --- a/caddytest/integration/caddyfile_test.go +++ b/caddytest/integration/caddyfile_test.go @@ -615,7 +615,6 @@ func TestReplaceWithReplacementPlaceholder(t *testing.T) { respond "{query}"`, "caddyfile") tester.AssertGetResponse("http://localhost:9080/endpoint?placeholder=baz&foo=bar", 200, "foo=baz&placeholder=baz") - } func TestReplaceWithKeyPlaceholder(t *testing.T) { @@ -783,6 +782,46 @@ func TestHandleErrorRangeAndCodes(t *testing.T) { tester.AssertGetResponse("http://localhost:9080/private", 410, "Error in the [400 .. 499] range") } +func TestHandleErrorSubHandlers(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(`{ + admin localhost:2999 + http_port 9080 + } + localhost:9080 { + root * /srv + file_server + error /*/internalerr* "Internal Server Error" 500 + + handle_errors 404 { + handle /en/* { + respond "not found" 404 + } + handle /es/* { + respond "no encontrado" 404 + } + handle { + respond "default not found" + } + } + handle_errors { + handle { + respond "Default error" + } + handle /en/* { + respond "English error" + } + } + } + `, "caddyfile") + // act and assert + tester.AssertGetResponse("http://localhost:9080/en/notfound", 404, "not found") + tester.AssertGetResponse("http://localhost:9080/es/notfound", 404, "no encontrado") + tester.AssertGetResponse("http://localhost:9080/notfound", 404, "default not found") + tester.AssertGetResponse("http://localhost:9080/es/internalerr", 500, "Default error") + tester.AssertGetResponse("http://localhost:9080/en/internalerr", 500, "English error") +} + func TestInvalidSiteAddressesAsDirectives(t *testing.T) { type testCase struct { config, expectedError string diff --git a/caddytest/integration/mockdns_test.go b/caddytest/integration/mockdns_test.go index 1b2efb653..31dc4be7b 100644 --- a/caddytest/integration/mockdns_test.go +++ b/caddytest/integration/mockdns_test.go @@ -3,10 +3,11 @@ package integration import ( "context" - "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/certmagic" "github.com/libdns/libdns" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { @@ -34,7 +35,7 @@ func (MockDNSProvider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } -// AppendsRecords appends DNS records to the zone. +// AppendRecords appends DNS records to the zone. func (MockDNSProvider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) { return nil, nil } @@ -55,7 +56,9 @@ func (MockDNSProvider) SetRecords(ctx context.Context, zone string, recs []libdn } // Interface guard -var _ caddyfile.Unmarshaler = (*MockDNSProvider)(nil) -var _ certmagic.DNSProvider = (*MockDNSProvider)(nil) -var _ caddy.Provisioner = (*MockDNSProvider)(nil) -var _ caddy.Module = (*MockDNSProvider)(nil) +var ( + _ caddyfile.Unmarshaler = (*MockDNSProvider)(nil) + _ certmagic.DNSProvider = (*MockDNSProvider)(nil) + _ caddy.Provisioner = (*MockDNSProvider)(nil) + _ caddy.Module = (*MockDNSProvider)(nil) +) diff --git a/caddytest/integration/stream_test.go b/caddytest/integration/stream_test.go index d2f2fd79b..57231a527 100644 --- a/caddytest/integration/stream_test.go +++ b/caddytest/integration/stream_test.go @@ -13,9 +13,10 @@ import ( "testing" "time" - "github.com/caddyserver/caddy/v2/caddytest" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" + + "github.com/caddyserver/caddy/v2/caddytest" ) // (see https://github.com/caddyserver/caddy/issues/3556 for use case) diff --git a/cmd/caddy/main.go b/cmd/caddy/main.go index f1aeda0a4..48fa149aa 100644 --- a/cmd/caddy/main.go +++ b/cmd/caddy/main.go @@ -1,8 +1,3 @@ -// The below line is required to enable post-quantum key agreement in Go 1.23 -// by default without insisting on setting a minimum version of 1.23 in go.mod. -// See https://github.com/caddyserver/caddy/issues/6540#issuecomment-2313094905 -//go:debug tlskyber=1 - // Copyright 2015 Matthew Holt and The Caddy Authors // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index a5c357cd7..028ce5bd9 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -24,6 +24,7 @@ import ( "io" "io/fs" "log" + "maps" "net" "net/http" "os" @@ -171,6 +172,10 @@ func cmdStart(fl Flags) (int, error) { func cmdRun(fl Flags) (int, error) { caddy.TrapSignals() + logger := caddy.Log() + undoMaxProcs := setResourceLimits(logger) + defer undoMaxProcs() + configFlag := fl.String("config") configAdapterFlag := fl.String("adapter") resumeFlag := fl.Bool("resume") @@ -196,18 +201,18 @@ func cmdRun(fl Flags) (int, error) { config, err = os.ReadFile(caddy.ConfigAutosavePath) if errors.Is(err, fs.ErrNotExist) { // not a bad error; just can't resume if autosave file doesn't exist - caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath)) + logger.Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath)) resumeFlag = false } else if err != nil { return caddy.ExitCodeFailedStartup, err } else { if configFlag == "" { - caddy.Log().Info("resuming from last configuration", + logger.Info("resuming from last configuration", zap.String("autosave_file", caddy.ConfigAutosavePath)) } else { // if they also specified a config file, user should be aware that we're not // using it (doing so could lead to data/config loss by overwriting!) - caddy.Log().Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration", + logger.Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration", zap.String("autosave_file", caddy.ConfigAutosavePath)) } } @@ -225,7 +230,7 @@ func cmdRun(fl Flags) (int, error) { if pidfileFlag != "" { err := caddy.PIDFile(pidfileFlag) if err != nil { - caddy.Log().Error("unable to write PID file", + logger.Error("unable to write PID file", zap.String("pidfile", pidfileFlag), zap.Error(err)) } @@ -236,7 +241,7 @@ func cmdRun(fl Flags) (int, error) { if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err) } - caddy.Log().Info("serving initial configuration") + logger.Info("serving initial configuration") // if we are to report to another process the successful start // of the server, do so now by echoing back contents of stdin @@ -272,15 +277,15 @@ func cmdRun(fl Flags) (int, error) { switch runtime.GOOS { case "windows": if os.Getenv("HOME") == "" && os.Getenv("USERPROFILE") == "" && !hasXDG { - caddy.Log().Warn("neither HOME nor USERPROFILE environment variables are set - please fix; some assets might be stored in ./caddy") + logger.Warn("neither HOME nor USERPROFILE environment variables are set - please fix; some assets might be stored in ./caddy") } case "plan9": if os.Getenv("home") == "" && !hasXDG { - caddy.Log().Warn("$home environment variable is empty - please fix; some assets might be stored in ./caddy") + logger.Warn("$home environment variable is empty - please fix; some assets might be stored in ./caddy") } default: if os.Getenv("HOME") == "" && !hasXDG { - caddy.Log().Warn("$HOME environment variable is empty - please fix; some assets might be stored in ./caddy") + logger.Warn("$HOME environment variable is empty - please fix; some assets might be stored in ./caddy") } } @@ -436,16 +441,20 @@ func cmdEnviron(fl Flags) (int, error) { } func cmdAdaptConfig(fl Flags) (int, error) { - inputFlag := fl.String("config") + configFlag := fl.String("config") adapterFlag := fl.String("adapter") prettyFlag := fl.Bool("pretty") validateFlag := fl.Bool("validate") var err error - inputFlag, err = configFileWithRespectToDefault(caddy.Log(), inputFlag) + configFlag, err = configFileWithRespectToDefault(caddy.Log(), configFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } + if configFlag == "" { + return caddy.ExitCodeFailedStartup, + fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)") + } // load all additional envs as soon as possible err = handleEnvFileFlag(fl) @@ -464,13 +473,19 @@ func cmdAdaptConfig(fl Flags) (int, error) { fmt.Errorf("unrecognized config adapter: %s", adapterFlag) } - input, err := os.ReadFile(inputFlag) + var input []byte + // read from stdin if the file name is "-" + if configFlag == "-" { + input, err = io.ReadAll(os.Stdin) + } else { + input, err = os.ReadFile(configFlag) + } if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("reading input file: %v", err) } - opts := map[string]any{"filename": inputFlag} + opts := map[string]any{"filename": configFlag} adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts) if err != nil { @@ -560,10 +575,15 @@ func cmdValidateConfig(fl Flags) (int, error) { func cmdFmt(fl Flags) (int, error) { configFile := fl.Arg(0) - if configFile == "" { - configFile = "Caddyfile" + configFlag := fl.String("config") + if (len(fl.Args()) > 1) || (configFlag != "" && configFile != "") { + return caddy.ExitCodeFailedStartup, fmt.Errorf("fmt does not support multiple files %s %s", configFlag, strings.Join(fl.Args(), " ")) + } + if configFile == "" && configFlag == "" { + configFile = "Caddyfile" + } else if configFile == "" { + configFile = configFlag } - // as a special case, read from stdin if the file name is "-" if configFile == "-" { input, err := io.ReadAll(os.Stdin) @@ -694,9 +714,7 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io if body != nil { req.Header.Set("Content-Type", "application/json") } - for k, v := range headers { - req.Header[k] = v - } + maps.Copy(req.Header, headers) // make an HTTP client that dials our network type, since admin // endpoints aren't always TCP, which is what the default transport diff --git a/cmd/commands.go b/cmd/commands.go index 0853ebf83..c9ea636b9 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -20,6 +20,7 @@ import ( "os" "regexp" "strings" + "sync" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" @@ -80,10 +81,16 @@ type CommandFunc func(Flags) (int, error) // Commands returns a list of commands initialised by // RegisterCommand func Commands() map[string]Command { + commandsMu.RLock() + defer commandsMu.RUnlock() + return commands } -var commands = make(map[string]Command) +var ( + commandsMu sync.RWMutex + commands = make(map[string]Command) +) func init() { RegisterCommand(Command{ @@ -286,6 +293,8 @@ zero exit status will be returned. If --envfile is specified, an environment file with environment variables in the KEY=VALUE format will be loaded into the Caddy process. + +If you wish to use stdin instead of a regular file, use - as the path. `, CobraFunc: func(cmd *cobra.Command) { cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)") @@ -383,11 +392,12 @@ lines will be prefixed with '-' and '+' where they differ. Note that unchanged lines are prefixed with two spaces for alignment, and that this is not a valid patch format. -If you wish you use stdin instead of a regular file, use - as the path. +If you wish to use stdin instead of a regular file, use - as the path. When reading from stdin, the --overwrite flag has no effect: the result is always printed to stdout. `, CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().StringP("config", "c", "", "Configuration file") cmd.Flags().BoolP("overwrite", "w", false, "Overwrite the input file with the results") cmd.Flags().BoolP("diff", "d", false, "Print the differences between the input file and the formatted output") cmd.RunE = WrapCommandFuncForCobra(cmdFmt) @@ -409,12 +419,13 @@ latest versions. EXPERIMENTAL: May be changed or removed. RegisterCommand(Command{ Name: "add-package", - Usage: "", + Usage: "", Short: "Adds Caddy packages (EXPERIMENTAL)", Long: ` Downloads an updated Caddy binary with the specified packages (module/plugin) -added. Retains existing packages. Returns an error if the any of packages are -already included. EXPERIMENTAL: May be changed or removed. +added, with an optional version specified (e.g., "package@version"). Retains +existing packages. Returns an error if any of the specified packages are already +included. EXPERIMENTAL: May be changed or removed. `, CobraFunc: func(cmd *cobra.Command) { cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it") @@ -439,7 +450,7 @@ EXPERIMENTAL: May be changed or removed. }) defaultFactory.Use(func(rootCmd *cobra.Command) { - RegisterCommand(Command{ + manpageCommand := Command{ Name: "manpage", Usage: "--directory ", Short: "Generates the manual pages for Caddy commands", @@ -469,11 +480,12 @@ argument of --directory. If the directory does not exist, it will be created. return caddy.ExitCodeSuccess, nil }) }, - }) + } - // source: https://github.com/spf13/cobra/blob/main/shell_completions.md - rootCmd.AddCommand(&cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", + // source: https://github.com/spf13/cobra/blob/6dec1ae26659a130bdb4c985768d1853b0e1bc06/site/content/completions/_index.md + completionCommand := Command{ + Name: "completion", + Usage: "[bash|zsh|fish|powershell]", Short: "Generate completion script", Long: fmt.Sprintf(`To load completions: @@ -514,24 +526,37 @@ argument of --directory. If the directory does not exist, it will be created. PS> %[1]s completion powershell > %[1]s.ps1 # and source this file from your PowerShell profile. `, rootCmd.Root().Name()), - DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), - RunE: func(cmd *cobra.Command, args []string) error { - switch args[0] { - case "bash": - return cmd.Root().GenBashCompletion(os.Stdout) - case "zsh": - return cmd.Root().GenZshCompletion(os.Stdout) - case "fish": - return cmd.Root().GenFishCompletion(os.Stdout, true) - case "powershell": - return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) - default: - return fmt.Errorf("unrecognized shell: %s", args[0]) + CobraFunc: func(cmd *cobra.Command) { + cmd.DisableFlagsInUseLine = true + cmd.ValidArgs = []string{"bash", "zsh", "fish", "powershell"} + cmd.Args = cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs) + cmd.RunE = func(cmd *cobra.Command, args []string) error { + switch args[0] { + case "bash": + return cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + return cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + return cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + default: + return fmt.Errorf("unrecognized shell: %s", args[0]) + } } }, - }) + } + + rootCmd.AddCommand(caddyCmdToCobra(manpageCommand)) + rootCmd.AddCommand(caddyCmdToCobra(completionCommand)) + + // add manpage and completion commands to the map of + // available commands, because they're not registered + // through RegisterCommand. + commandsMu.Lock() + commands[manpageCommand.Name] = manpageCommand + commands[completionCommand.Name] = completionCommand + commandsMu.Unlock() }) } @@ -550,6 +575,9 @@ argument of --directory. If the directory does not exist, it will be created. // // This function should be used in init(). func RegisterCommand(cmd Command) { + commandsMu.Lock() + defer commandsMu.Unlock() + if cmd.Name == "" { panic("command name is required") } @@ -568,6 +596,7 @@ func RegisterCommand(cmd Command) { defaultFactory.Use(func(rootCmd *cobra.Command) { rootCmd.AddCommand(caddyCmdToCobra(cmd)) }) + commands[cmd.Name] = cmd } var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`) diff --git a/cmd/commands_test.go b/cmd/commands_test.go new file mode 100644 index 000000000..085a9d789 --- /dev/null +++ b/cmd/commands_test.go @@ -0,0 +1,39 @@ +package caddycmd + +import ( + "maps" + "reflect" + "slices" + "testing" +) + +func TestCommandsAreAvailable(t *testing.T) { + // trigger init, and build the default factory, so that + // all commands from this package are available + cmd := defaultFactory.Build() + if cmd == nil { + t.Fatal("default factory failed to build") + } + + // check that the default factory has 17 commands; it doesn't + // include the commands registered through calls to init in + // other packages + cmds := Commands() + if len(cmds) != 17 { + t.Errorf("expected 17 commands, got %d", len(cmds)) + } + + commandNames := slices.Collect(maps.Keys(cmds)) + slices.Sort(commandNames) + + expectedCommandNames := []string{ + "adapt", "add-package", "build-info", "completion", + "environ", "fmt", "list-modules", "manpage", + "reload", "remove-package", "run", "start", + "stop", "storage", "upgrade", "validate", "version", + } + + if !reflect.DeepEqual(expectedCommandNames, commandNames) { + t.Errorf("expected %v, got %v", expectedCommandNames, commandNames) + } +} diff --git a/cmd/main.go b/cmd/main.go index 655c0084b..47d702ca7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -24,6 +24,7 @@ import ( "io" "io/fs" "log" + "log/slog" "net" "os" "path/filepath" @@ -33,10 +34,12 @@ import ( "strings" "time" + "github.com/KimMachineGun/automemlimit/memlimit" "github.com/caddyserver/certmagic" "github.com/spf13/pflag" "go.uber.org/automaxprocs/maxprocs" "go.uber.org/zap" + "go.uber.org/zap/exp/zapslog" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -66,12 +69,6 @@ func Main() { os.Exit(caddy.ExitCodeFailedStartup) } - undo, err := maxprocs.Set() - defer undo() - if err != nil { - caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err)) - } - if err := defaultFactory.Build().Execute(); err != nil { var exitError *exitError if errors.As(err, &exitError) { @@ -421,7 +418,7 @@ func parseEnvFile(envInput io.Reader) (map[string]string, error) { // quoted value: support newlines if strings.HasPrefix(val, `"`) || strings.HasPrefix(val, "'") { quote := string(val[0]) - for !(strings.HasSuffix(line, quote) && !strings.HasSuffix(line, `\`+quote)) { + for !strings.HasSuffix(line, quote) || strings.HasSuffix(line, `\`+quote) { val = strings.ReplaceAll(val, `\`+quote, quote) if !scanner.Scan() { break @@ -467,6 +464,31 @@ func printEnvironment() { } } +func setResourceLimits(logger *zap.Logger) func() { + // Configure the maximum number of CPUs to use to match the Linux container quota (if any) + // See https://pkg.go.dev/runtime#GOMAXPROCS + undo, err := maxprocs.Set(maxprocs.Logger(logger.Sugar().Infof)) + if err != nil { + logger.Warn("failed to set GOMAXPROCS", zap.Error(err)) + } + + // Configure the maximum memory to use to match the Linux container quota (if any) or system memory + // See https://pkg.go.dev/runtime/debug#SetMemoryLimit + _, _ = memlimit.SetGoMemLimitWithOpts( + memlimit.WithLogger( + slog.New(zapslog.NewHandler(logger.Core())), + ), + memlimit.WithProvider( + memlimit.ApplyFallback( + memlimit.FromCgroup, + memlimit.FromSystem, + ), + ), + ) + + return undo +} + // StringSlice is a flag.Value that enables repeated use of a string flag. type StringSlice []string diff --git a/cmd/main_test.go b/cmd/main_test.go index 3b2412c57..bff34f443 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -235,7 +235,6 @@ func Test_isCaddyfile(t *testing.T) { wantErr: false, }, { - name: "json is not caddyfile but not error", args: args{ configFile: "./Caddyfile.json", @@ -245,7 +244,6 @@ func Test_isCaddyfile(t *testing.T) { wantErr: false, }, { - name: "prefix of Caddyfile and ./ with any extension is Caddyfile", args: args{ configFile: "./Caddyfile.prd", @@ -255,7 +253,6 @@ func Test_isCaddyfile(t *testing.T) { wantErr: false, }, { - name: "prefix of Caddyfile without ./ with any extension is Caddyfile", args: args{ configFile: "Caddyfile.prd", diff --git a/cmd/packagesfuncs.go b/cmd/packagesfuncs.go index 0784f7ee6..cda6f31f6 100644 --- a/cmd/packagesfuncs.go +++ b/cmd/packagesfuncs.go @@ -46,6 +46,25 @@ func cmdUpgrade(fl Flags) (int, error) { return upgradeBuild(pluginPkgs, fl) } +func splitModule(arg string) (module, version string, err error) { + const versionSplit = "@" + + // accommodate module paths that have @ in them, but we can only tolerate that if there's also + // a version, otherwise we don't know if it's a version separator or part of the file path + lastVersionSplit := strings.LastIndex(arg, versionSplit) + if lastVersionSplit < 0 { + module = arg + } else { + module, version = arg[:lastVersionSplit], arg[lastVersionSplit+1:] + } + + if module == "" { + err = fmt.Errorf("module name is required") + } + + return +} + func cmdAddPackage(fl Flags) (int, error) { if len(fl.Args()) == 0 { return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified") @@ -60,10 +79,15 @@ func cmdAddPackage(fl Flags) (int, error) { } for _, arg := range fl.Args() { - if _, ok := pluginPkgs[arg]; ok { + module, version, err := splitModule(arg) + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid module name: %v", err) + } + // only allow a version to be specified if it's different from the existing version + if _, ok := pluginPkgs[module]; ok && (version == "" || pluginPkgs[module].Version == version) { return caddy.ExitCodeFailedStartup, fmt.Errorf("package is already added") } - pluginPkgs[arg] = struct{}{} + pluginPkgs[module] = pluginPackage{Version: version, Path: module} } return upgradeBuild(pluginPkgs, fl) @@ -83,7 +107,11 @@ func cmdRemovePackage(fl Flags) (int, error) { } for _, arg := range fl.Args() { - if _, ok := pluginPkgs[arg]; !ok { + module, _, err := splitModule(arg) + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid module name: %v", err) + } + if _, ok := pluginPkgs[module]; !ok { // package does not exist return caddy.ExitCodeFailedStartup, fmt.Errorf("package is not added") } @@ -93,7 +121,7 @@ func cmdRemovePackage(fl Flags) (int, error) { return upgradeBuild(pluginPkgs, fl) } -func upgradeBuild(pluginPkgs map[string]struct{}, fl Flags) (int, error) { +func upgradeBuild(pluginPkgs map[string]pluginPackage, fl Flags) (int, error) { l := caddy.Log() thisExecPath, err := os.Executable() @@ -120,8 +148,8 @@ func upgradeBuild(pluginPkgs map[string]struct{}, fl Flags) (int, error) { "os": {runtime.GOOS}, "arch": {runtime.GOARCH}, } - for pkg := range pluginPkgs { - qs.Add("p", pkg) + for _, pkgInfo := range pluginPkgs { + qs.Add("p", pkgInfo.String()) } // initiate the build @@ -276,14 +304,14 @@ func downloadBuild(qs url.Values) (*http.Response, error) { return resp, nil } -func getPluginPackages(modules []moduleInfo) (map[string]struct{}, error) { - pluginPkgs := make(map[string]struct{}) +func getPluginPackages(modules []moduleInfo) (map[string]pluginPackage, error) { + pluginPkgs := make(map[string]pluginPackage) for _, mod := range modules { if mod.goModule.Replace != nil { return nil, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s", mod.goModule.Path, mod.goModule.Replace.Path) } - pluginPkgs[mod.goModule.Path] = struct{}{} + pluginPkgs[mod.goModule.Path] = pluginPackage{Version: mod.goModule.Version, Path: mod.goModule.Path} } return pluginPkgs, nil } @@ -312,3 +340,15 @@ func writeCaddyBinary(path string, body *io.ReadCloser, fileInfo os.FileInfo) er } const downloadPath = "https://caddyserver.com/api/download" + +type pluginPackage struct { + Version string + Path string +} + +func (p pluginPackage) String() string { + if p.Version == "" { + return p.Path + } + return p.Path + "@" + p.Version +} diff --git a/cmd/storagefuncs.go b/cmd/storagefuncs.go index 60e25485d..3c4219719 100644 --- a/cmd/storagefuncs.go +++ b/cmd/storagefuncs.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "io" + "io/fs" "os" "github.com/caddyserver/certmagic" @@ -190,12 +191,20 @@ func cmdExportStorage(fl Flags) (int, error) { for _, k := range keys { info, err := stor.Stat(ctx, k) if err != nil { + if errors.Is(err, fs.ErrNotExist) { + caddy.Log().Warn(fmt.Sprintf("key: %s removed while export is in-progress", k)) + continue + } return caddy.ExitCodeFailedQuit, err } if info.IsTerminal { v, err := stor.Load(ctx, k) if err != nil { + if errors.Is(err, fs.ErrNotExist) { + caddy.Log().Warn(fmt.Sprintf("key: %s removed while export is in-progress", k)) + continue + } return caddy.ExitCodeFailedQuit, err } diff --git a/context.go b/context.go index 5b8c10703..4c1139936 100644 --- a/context.go +++ b/context.go @@ -23,6 +23,8 @@ import ( "reflect" "github.com/caddyserver/certmagic" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" "go.uber.org/zap" "go.uber.org/zap/exp/zapslog" @@ -47,6 +49,7 @@ type Context struct { ancestry []Module cleanupFuncs []func() // invoked at every config unload exitFuncs []func(context.Context) // invoked at config unload ONLY IF the process is exiting (EXPERIMENTAL) + metricsRegistry *prometheus.Registry } // NewContext provides a new context derived from the given @@ -58,7 +61,7 @@ type Context struct { // modules which are loaded will be properly unloaded. // See standard library context package's documentation. func NewContext(ctx Context) (Context, context.CancelFunc) { - newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg} + newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg, metricsRegistry: prometheus.NewPedanticRegistry()} c, cancel := context.WithCancel(ctx.Context) wrappedCancel := func() { cancel() @@ -79,6 +82,7 @@ func NewContext(ctx Context) (Context, context.CancelFunc) { } } newCtx.Context = c + newCtx.initMetrics() return newCtx, wrappedCancel } @@ -87,14 +91,32 @@ func (ctx *Context) OnCancel(f func()) { ctx.cleanupFuncs = append(ctx.cleanupFuncs, f) } -// Filesystems returns a ref to the FilesystemMap. +// FileSystems returns a ref to the FilesystemMap. // EXPERIMENTAL: This API is subject to change. -func (ctx *Context) Filesystems() FileSystems { +func (ctx *Context) FileSystems() FileSystems { // if no config is loaded, we use a default filesystemmap, which includes the osfs if ctx.cfg == nil { - return &filesystems.FilesystemMap{} + return &filesystems.FileSystemMap{} } - return ctx.cfg.filesystems + return ctx.cfg.fileSystems +} + +// Returns the active metrics registry for the context +// EXPERIMENTAL: This API is subject to change. +func (ctx *Context) GetMetricsRegistry() *prometheus.Registry { + return ctx.metricsRegistry +} + +func (ctx *Context) initMetrics() { + ctx.metricsRegistry.MustRegister( + collectors.NewBuildInfoCollector(), + collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), + collectors.NewGoCollector(), + adminMetrics.requestCount, + adminMetrics.requestErrors, + globalMetrics.configSuccess, + globalMetrics.configSuccessTime, + ) } // OnExit executes f when the process exits gracefully. @@ -255,6 +277,14 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error) return result, nil } +// emitEvent is a small convenience method so the caddy core can emit events, if the event app is configured. +func (ctx Context) emitEvent(name string, data map[string]any) Event { + if ctx.cfg == nil || ctx.cfg.eventEmitter == nil { + return Event{} + } + return ctx.cfg.eventEmitter.Emit(ctx, name, data) +} + // loadModulesFromSomeMap loads modules from val, which must be a type of map[string]any. // Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module // name) or as a regular map (key is not the module name, and module name is defined inline). @@ -363,10 +393,28 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error return nil, fmt.Errorf("module value cannot be null") } + var err error + + // if this is an app module, keep a reference to it, + // since submodules may need to reference it during + // provisioning (even though the parent app module + // may not be fully provisioned yet; this is the case + // with the tls app's automation policies, which may + // refer to the tls app to check if a global DNS + // module has been configured for DNS challenges) + if appModule, ok := val.(App); ok { + ctx.cfg.apps[id] = appModule + defer func() { + if err != nil { + ctx.cfg.failedApps[id] = err + } + }() + } + ctx.ancestry = append(ctx.ancestry, val) if prov, ok := val.(Provisioner); ok { - err := prov.Provision(ctx) + err = prov.Provision(ctx) if err != nil { // incomplete provisioning could have left state // dangling, so make sure it gets cleaned up @@ -381,7 +429,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error } if validator, ok := val.(Validator); ok { - err := validator.Validate() + err = validator.Validate() if err != nil { // since the module was already provisioned, make sure we clean up if cleanerUpper, ok := val.(CleanerUpper); ok { @@ -396,6 +444,14 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error ctx.moduleInstances[id] = append(ctx.moduleInstances[id], val) + // if the loaded module happens to be an app that can emit events, store it so the + // core can have access to emit events without an import cycle + if ee, ok := val.(eventEmitter); ok { + if _, ok := ee.(App); ok { + ctx.cfg.eventEmitter = ee + } + } + return val, nil } @@ -438,6 +494,10 @@ func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json. // or stop App modules. The caller is expected to assert to the // concrete type. func (ctx Context) App(name string) (any, error) { + // if the app failed to load before, return the cached error + if err, ok := ctx.cfg.failedApps[name]; ok { + return nil, fmt.Errorf("loading %s app module: %v", name, err) + } if app, ok := ctx.cfg.apps[name]; ok { return app, nil } @@ -449,7 +509,6 @@ func (ctx Context) App(name string) (any, error) { if appRaw != nil { ctx.cfg.AppsRaw[name] = nil // allow GC to deallocate } - ctx.cfg.apps[name] = modVal.(App) return modVal, nil } @@ -463,6 +522,10 @@ func (ctx Context) AppIfConfigured(name string) (any, error) { if ctx.cfg == nil { return nil, fmt.Errorf("app module %s: %w", name, ErrNotConfigured) } + // if the app failed to load before, return the cached error + if err, ok := ctx.cfg.failedApps[name]; ok { + return nil, fmt.Errorf("loading %s app module: %v", name, err) + } if app, ok := ctx.cfg.apps[name]; ok { return app, nil } @@ -529,18 +592,14 @@ func (ctx Context) Slogger() *slog.Logger { if err != nil { panic("config missing, unable to create dev logger: " + err.Error()) } - return slog.New(zapslog.NewHandler(l.Core(), nil)) + return slog.New(zapslog.NewHandler(l.Core())) } mod := ctx.Module() if mod == nil { - return slog.New(zapslog.NewHandler(Log().Core(), nil)) + return slog.New(zapslog.NewHandler(Log().Core())) } - - return slog.New(zapslog.NewHandler( - ctx.cfg.Logging.Logger(mod).Core(), - &zapslog.HandlerOptions{ - LoggerName: string(mod.CaddyModule().ID), - }, + return slog.New(zapslog.NewHandler(ctx.cfg.Logging.Logger(mod).Core(), + zapslog.WithName(string(mod.CaddyModule().ID)), )) } @@ -572,3 +631,11 @@ func (ctx *Context) WithValue(key, value any) Context { exitFuncs: ctx.exitFuncs, } } + +// eventEmitter is a small interface that inverts dependencies for +// the caddyevents package, so the core can emit events without an +// import cycle (i.e. the caddy package doesn't have to import +// the caddyevents package, which imports the caddy package). +type eventEmitter interface { + Emit(ctx Context, eventName string, data map[string]any) Event +} diff --git a/filepath.go b/filepath.go new file mode 100644 index 000000000..aad907799 --- /dev/null +++ b/filepath.go @@ -0,0 +1,39 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows + +package caddy + +import ( + "os" + "path/filepath" +) + +// FastAbs is an optimized version of filepath.Abs for Unix systems, +// since we don't expect the working directory to ever change once +// Caddy is running. Avoid the os.Getwd() syscall overhead. +// It's overall the same as stdlib's implementation, the difference +// being cached working directory. +func FastAbs(path string) (string, error) { + if filepath.IsAbs(path) { + return filepath.Clean(path), nil + } + if wderr != nil { + return "", wderr + } + return filepath.Join(wd, path), nil +} + +var wd, wderr = os.Getwd() diff --git a/filepath_windows.go b/filepath_windows.go new file mode 100644 index 000000000..aa70955e9 --- /dev/null +++ b/filepath_windows.go @@ -0,0 +1,27 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddy + +import ( + "path/filepath" +) + +// FastAbs can't be optimized on Windows because there +// are special file paths that require the use of syscall.FullPath +// to handle correctly. +// Just call stdlib's implementation which uses that function. +func FastAbs(path string) (string, error) { + return filepath.Abs(path) +} diff --git a/filesystem.go b/filesystem.go index 9785f57d4..d6679e90b 100644 --- a/filesystem.go +++ b/filesystem.go @@ -1,3 +1,17 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package caddy import "io/fs" diff --git a/go.mod b/go.mod index 62fe30dee..370747a1d 100644 --- a/go.mod +++ b/go.mod @@ -1,157 +1,163 @@ module github.com/caddyserver/caddy/v2 -go 1.22.3 - -toolchain go1.23.0 +go 1.25 require ( - github.com/BurntSushi/toml v1.3.2 - github.com/Masterminds/sprig/v3 v3.2.3 - github.com/alecthomas/chroma/v2 v2.13.0 + github.com/BurntSushi/toml v1.5.0 + github.com/KimMachineGun/automemlimit v0.7.4 + github.com/Masterminds/sprig/v3 v3.3.0 + github.com/alecthomas/chroma/v2 v2.20.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.21.3 + github.com/caddyserver/certmagic v0.24.0 github.com/caddyserver/zerossl v0.1.3 + github.com/cloudflare/circl v1.6.1 github.com/dustin/go-humanize v1.0.1 - github.com/go-chi/chi/v5 v5.0.12 - github.com/google/cel-go v0.21.0 + github.com/go-chi/chi/v5 v5.2.2 + github.com/google/cel-go v0.26.0 github.com/google/uuid v1.6.0 - github.com/klauspost/compress v1.17.8 - github.com/klauspost/cpuid/v2 v2.2.7 - github.com/mholt/acmez/v2 v2.0.1 - github.com/prometheus/client_golang v1.19.1 - github.com/quic-go/quic-go v0.47.0 - github.com/smallstep/certificates v0.26.1 - github.com/smallstep/nosql v0.6.1 + github.com/klauspost/compress v1.18.0 + github.com/klauspost/cpuid/v2 v2.3.0 + github.com/mholt/acmez/v3 v3.1.2 + github.com/prometheus/client_golang v1.23.0 + github.com/quic-go/quic-go v0.54.0 + github.com/smallstep/certificates v0.28.4 + github.com/smallstep/nosql v0.7.0 github.com/smallstep/truststore v0.13.0 - github.com/spf13/cobra v1.8.0 - github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.9.0 + github.com/spf13/cobra v1.9.1 + github.com/spf13/pflag v1.0.7 + github.com/stretchr/testify v1.10.0 github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 - github.com/yuin/goldmark v1.7.1 + github.com/yuin/goldmark v1.7.13 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 - go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 - go.opentelemetry.io/otel v1.24.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 - go.opentelemetry.io/otel/sdk v1.21.0 - go.uber.org/automaxprocs v1.5.3 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 + go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 + go.opentelemetry.io/otel v1.37.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 + go.opentelemetry.io/otel/sdk v1.37.0 + go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.0 - go.uber.org/zap/exp v0.2.0 - golang.org/x/crypto v0.26.0 - golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 - golang.org/x/net v0.28.0 - golang.org/x/sync v0.8.0 - golang.org/x/term v0.23.0 - golang.org/x/time v0.5.0 + go.uber.org/zap/exp v0.3.0 + golang.org/x/crypto v0.40.0 + golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 + golang.org/x/net v0.42.0 + golang.org/x/sync v0.16.0 + golang.org/x/term v0.33.0 + golang.org/x/time v0.12.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 ) require ( + cel.dev/expr v0.24.0 // indirect + cloud.google.com/go/auth v0.16.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect + dario.cat/mergo v1.0.1 // indirect github.com/Microsoft/go-winio v0.6.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/ccoveille/go-safecast v1.6.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/coreos/go-oidc/v3 v3.14.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/francoispqt/gojay v1.2.13 // indirect - github.com/fxamacker/cbor/v2 v2.6.0 // indirect - github.com/go-jose/go-jose/v3 v3.0.3 // indirect - github.com/go-kit/log v0.2.1 // indirect - github.com/golang/glog v1.2.0 // indirect + github.com/fxamacker/cbor/v2 v2.8.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect - github.com/google/go-tpm v0.9.0 // indirect + github.com/google/go-tpm v0.9.5 // indirect github.com/google/go-tspi v0.3.0 // indirect - github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect - github.com/onsi/ginkgo/v2 v2.13.2 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect - github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 // indirect - github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect - github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect + github.com/smallstep/cli-utils v0.12.1 // indirect + github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect + github.com/smallstep/linkedca v0.23.0 // indirect + github.com/smallstep/pkcs7 v0.2.1 // indirect + github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/zeebo/blake3 v0.2.3 // indirect - go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect - go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect - go.uber.org/mock v0.4.0 // indirect - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/propagators/aws v1.37.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.37.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 // indirect + go.opentelemetry.io/contrib/propagators/ot v1.37.0 // indirect + go.uber.org/mock v0.5.2 // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + google.golang.org/api v0.240.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect ) require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 + github.com/cespare/xxhash/v2 v2.3.0 github.com/chzyer/readline v1.5.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/dgraph-io/badger v1.6.2 // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect - github.com/dgraph-io/ristretto v0.1.0 // indirect + github.com/dgraph-io/ristretto v0.2.0 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-kit/kit v0.13.0 // indirect - github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-sql-driver/mysql v1.7.1 // indirect - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/huandu/xstrings v1.3.3 // indirect - github.com/imdario/mergo v0.3.12 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.14.3 // indirect - github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgtype v1.14.0 // indirect - github.com/jackc/pgx/v4 v4.18.3 // indirect - github.com/libdns/libdns v0.2.2 // indirect + github.com/libdns/libdns v1.1.0 github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/miekg/dns v1.1.59 // indirect + github.com/miekg/dns v1.1.63 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964 + github.com/pires/go-proxyproto v0.8.1 github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/rs/xid v1.5.0 // indirect + github.com/prometheus/client_model v0.6.2 + github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/shopspring/decimal v1.2.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/slackhq/nebula v1.6.1 // indirect - github.com/spf13/cast v1.4.1 // indirect + github.com/slackhq/nebula v1.9.5 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect - github.com/urfave/cli v1.22.14 // indirect - go.etcd.io/bbolt v1.3.9 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 - go.opentelemetry.io/proto/otlp v1.0.0 // indirect - go.step.sm/cli-utils v0.9.0 // indirect - go.step.sm/crypto v0.45.0 - go.step.sm/linkedca v0.20.1 // indirect + github.com/urfave/cli v1.22.17 // indirect + go.etcd.io/bbolt v1.3.10 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 + go.opentelemetry.io/proto/otlp v1.7.0 // indirect + go.step.sm/crypto v0.67.0 go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/sys v0.23.0 - golang.org/x/text v0.17.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect - google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.34.1 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/sys v0.34.0 + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/grpc v1.73.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect howett.net/plist v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 1f741c104..2d3d94c75 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,25 @@ +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.31.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.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= -cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= -cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg= -cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro= -cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= -cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= -cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= -cloud.google.com/go/kms v1.16.0 h1:1yZsRPhmargZOmY+fVAh8IKiR9HzCb0U1zsxb5g2nRY= -cloud.google.com/go/kms v1.16.0/go.mod h1:olQUXy2Xud+1GzYfiBO9N0RhjsJk5IJLU6n/ethLXVc= -cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= -cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= +cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= +cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= +cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= +cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk= +cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= @@ -26,77 +30,79 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk= +github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= -github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= -github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= -github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI= -github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= +github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= +github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg= +github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= -github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= -github.com/aws/aws-sdk-go-v2/config v1.27.13 h1:WbKW8hOzrWoOA/+35S5okqO/2Ap8hkkFUzoW8Hzq24A= -github.com/aws/aws-sdk-go-v2/config v1.27.13/go.mod h1:XLiyiTMnguytjRER7u5RIkhIqS8Nyz41SwAWb4xEjxs= -github.com/aws/aws-sdk-go-v2/credentials v1.17.13 h1:XDCJDzk/u5cN7Aple7D/MiAhx1Rjo/0nueJ0La8mRuE= -github.com/aws/aws-sdk-go-v2/credentials v1.17.13/go.mod h1:FMNcjQrmuBYvOTZDtOLCIu0esmxjF7RuA/89iSXWzQI= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= -github.com/aws/aws-sdk-go-v2/service/kms v1.31.1 h1:5wtyAwuUiJiM3DHYeGZmP5iMonM7DFBWAEaaVPHYZA0= -github.com/aws/aws-sdk-go-v2/service/kms v1.31.1/go.mod h1:2snWQJQUKsbN66vAawJuOGX7dr37pfOq9hb0tZDGIqQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 h1:Qe0r0lVURDDeBQJ4yP+BOrJkvkiCo/3FH/t+wY11dmw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw= -github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= -github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/aws/aws-sdk-go-v2 v1.36.4 h1:GySzjhVvx0ERP6eyfAbAuAXLtAda5TEy19E5q5W8I9E= +github.com/aws/aws-sdk-go-v2 v1.36.4/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/config v1.29.16 h1:XkruGnXX1nEZ+Nyo9v84TzsX+nj86icbFAeust6uo8A= +github.com/aws/aws-sdk-go-v2/config v1.29.16/go.mod h1:uCW7PNjGwZ5cOGZ5jr8vCWrYkGIhPoTNV23Q/tpHKzg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.69 h1:8B8ZQboRc3uaIKjshve/XlvJ570R7BKNy3gftSbS178= +github.com/aws/aws-sdk-go-v2/credentials v1.17.69/go.mod h1:gPME6I8grR1jCqBFEGthULiolzf/Sexq/Wy42ibKK9c= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 h1:oQWSGexYasNpYp4epLGZxxjsDo8BMBh6iNWkTXQvkwk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31/go.mod h1:nc332eGUU+djP3vrMI6blS0woaCfHTe3KiSQUVTMRq0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 h1:o1v1VFfPcDVlK3ll1L5xHsaQAFdNtZ5GXnNR7SwueC4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35/go.mod h1:rZUQNYMNG+8uZxz9FOerQJ+FceCiodXvixpeRtdESrU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 h1:R5b82ubO2NntENm3SAm0ADME+H630HomNJdgv+yZ3xw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35/go.mod h1:FuA+nmgMRfkzVKYDNEqQadvEMxtxl9+RLT9ribCwEMs= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 h1:/ldKrPPXTC421bTNWrUIpq3CxwHwRI/kpc+jPUTJocM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16/go.mod h1:5vkf/Ws0/wgIMJDQbjI4p2op86hNW6Hie5QtebrDgT8= +github.com/aws/aws-sdk-go-v2/service/kms v1.41.0 h1:2jKyib9msVrAVn+lngwlSplG13RpUZmzVte2yDao5nc= +github.com/aws/aws-sdk-go-v2/service/kms v1.41.0/go.mod h1:RyhzxkWGcfixlkieewzpO3D4P4fTMxhIDqDZWsh0u/4= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 h1:EU58LP8ozQDVroOEyAfcq0cGc5R/FTZjVoYJ6tvby3w= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.4/go.mod h1:CrtOgCcysxMvrCoHnvNAD7PHWclmoFG78Q2xLK0KKcs= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 h1:XB4z0hbQtpmBnb1FQYvKaCM7UsS6Y/u8jVBwIUGeCTk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2/go.mod h1:hwRpqkRxnQ58J9blRDrB4IanlXCpcKmsC83EhG77upg= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 h1:nyLjs8sYJShFYj6aiyjCBI3EcLn1udWrQTjEF+SOXB0= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.21/go.mod h1:EhdxtZ+g84MSGrSrHzZiUm9PYiZkrADNja15wtRJSJo= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/caddyserver/certmagic v0.21.3 h1:pqRRry3yuB4CWBVq9+cUqu+Y6E2z8TswbhNx1AZeYm0= -github.com/caddyserver/certmagic v0.21.3/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI= +github.com/caddyserver/certmagic v0.24.0 h1:EfXTWpxHAUKgDfOj6MHImJN8Jm4AMFfMT6ITuKhrDF0= +github.com/caddyserver/certmagic v0.24.0/go.mod h1:xPT7dC1DuHHnS2yuEQCEyks+b89sUkMENh8dJF+InLE= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q= +github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= @@ -107,19 +113,18 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= +github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/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= @@ -129,15 +134,15 @@ github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdw github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= -github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= -github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= +github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= @@ -146,45 +151,29 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= -github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= -github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= -github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= -github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= +github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= -github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 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= @@ -198,181 +187,120 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI= -github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= -github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= -github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98= -github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY= +github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= +github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm-tools v0.4.5 h1:3fhthtyMDbIZFR5/0y1hvUoZ1Kf4i1eZ7C73R4Pvd+k= +github.com/google/go-tpm-tools v0.4.5/go.mod h1:ktjTNq8yZFD6TzdBFefUfen96rF3NpYwpSb2d8bc+Y8= github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= 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-20231212022811-ec68065c825e h1:bwOy7hAFd0C91URzMIEBfr6BAz29yk7Qj0cy6S7DJlU= -github.com/google/pprof v0.0.0-20231212022811-ec68065c825e/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= -github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= -github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= -github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= -github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= -github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= -github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= -github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 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/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= -github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU= +github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/mholt/acmez/v2 v2.0.1 h1:3/3N0u1pLjMK4sNEAFSI+bcvzbPhRpY383sy1kLHJ6k= -github.com/mholt/acmez/v2 v2.0.1/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U= +github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= +github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= -github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= -github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= +github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs= -github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= -github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= -github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= -github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964 h1:ct/vxNBgHpASQ4sT8NaBX9LtsEtluZqaUJydLG50U3E= -github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY= +github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= +github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= 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= @@ -381,39 +309,33 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y= -github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 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/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E= github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= @@ -438,25 +360,28 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/slackhq/nebula v1.6.1 h1:/OCTR3abj0Sbf2nGoLUrdDXImrCv0ZVFpVPP5qa0DsM= -github.com/slackhq/nebula v1.6.1/go.mod h1:UmkqnXe4O53QwToSl/gG7sM4BroQwAB7dd4hUaT6MlI= +github.com/slackhq/nebula v1.9.5 h1:ZrxcvP/lxwFglaijmiwXLuCSkybZMJnqSYI1S8DtGnY= +github.com/slackhq/nebula v1.9.5/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= -github.com/smallstep/certificates v0.26.1 h1:FIUliEBcExSfJJDhRFA/s8aZgMIFuorexnRSKQd884o= -github.com/smallstep/certificates v0.26.1/go.mod h1:OQMrW39IrGKDViKSHrKcgSQArMZ8c7EcjhYKK7mYqis= -github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA= -github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= -github.com/smallstep/nosql v0.6.1 h1:X8IBZFTRIp1gmuf23ne/jlD/BWKJtDQbtatxEn7Et1Y= -github.com/smallstep/nosql v0.6.1/go.mod h1:vrN+CftYYNnDM+DQqd863ATynvYFm/6FuY9D4TeAm2Y= -github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 h1:B6cED3iLJTgxpdh4tuqByDjRRKan2EvtnOfHr2zHJVg= -github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y= -github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d h1:06LUHn4Ia2X6syjIaCMNaXXDNdU+1N/oOHynJbWgpXw= -github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d/go.mod h1:4d0ub42ut1mMtvGyMensjuHYEUpRrASvkzLEJvoRQcU= +github.com/smallstep/certificates v0.28.4 h1:JTU6/A5Xes6m+OsR6fw1RACSA362vJc9SOFVG7poBEw= +github.com/smallstep/certificates v0.28.4/go.mod h1:LUqo+7mKZE7FZldlTb0zhU4A0bq4G4+akieFMcTaWvA= +github.com/smallstep/cli-utils v0.12.1 h1:D9QvfbFqiKq3snGZ2xDcXEFrdFJ1mQfPHZMq/leerpE= +github.com/smallstep/cli-utils v0.12.1/go.mod h1:skV2Neg8qjiKPu2fphM89H9bIxNpKiiRTnX9Q6Lc+20= +github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4= +github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= +github.com/smallstep/linkedca v0.23.0 h1:5W/7EudlK1HcCIdZM68dJlZ7orqCCCyv6bm2l/0JmLU= +github.com/smallstep/linkedca v0.23.0/go.mod h1:7cyRM9soAYySg9ag65QwytcgGOM+4gOlkJ/YA58A9E8= +github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE= +github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU= +github.com/smallstep/pkcs7 v0.0.0-20240911091500-b1cae6277023/go.mod h1:CM5KrX7rxWgwDdMj9yef/pJB2OPgy/56z4IEx2UIbpc= +github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= +github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= +github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 h1:LyZqn24/ZiVg8v9Hq07K6mx6RqPtpDeK+De5vf4QEY4= +github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101/go.mod h1:EuKQjYGQwhUa1mgD21zxIgOgUYLsqikJmvxNscxpS/Y= github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4= github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= @@ -466,41 +391,39 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ= github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= -github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= +github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= +github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -508,113 +431,94 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U= -github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= -github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= -go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= +go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= +go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 h1:s2RzYOAqHVgG23q8fPWYChobUoZM6rJZ98EnylJr66w= -go.opentelemetry.io/contrib/propagators/autoprop v0.42.0/go.mod h1:Mv/tWNtZn+NbALDb2XcItP0OM3lWWZjAfSroINxfW+Y= -go.opentelemetry.io/contrib/propagators/aws v1.17.0 h1:IX8d7l2uRw61BlmZBOTQFaK+y22j6vytMVTs9wFrO+c= -go.opentelemetry.io/contrib/propagators/aws v1.17.0/go.mod h1:pAlCYRWff4uGqRXOVn3WP8pDZ5E0K56bEoG7a1VSL4k= -go.opentelemetry.io/contrib/propagators/b3 v1.17.0 h1:ImOVvHnku8jijXqkwCSyYKRDt2YrnGXD4BbhcpfbfJo= -go.opentelemetry.io/contrib/propagators/b3 v1.17.0/go.mod h1:IkfUfMpKWmynvvE0264trz0sf32NRTZL4nuAN9AbWRc= -go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 h1:Zbpbmwav32Ea5jSotpmkWEl3a6Xvd4tw/3xxGO1i05Y= -go.opentelemetry.io/contrib/propagators/jaeger v1.17.0/go.mod h1:tcTUAlmO8nuInPDSBVfG+CP6Mzjy5+gNV4mPxMbL0IA= -go.opentelemetry.io/contrib/propagators/ot v1.17.0 h1:ufo2Vsz8l76eI47jFjuVyjyB3Ae2DmfiCV/o6Vc8ii0= -go.opentelemetry.io/contrib/propagators/ot v1.17.0/go.mod h1:SbKPj5XGp8K/sGm05XblaIABgMgw2jDczP8gGeuaVLk= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= -go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= -go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ= -go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8= -go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc= -go.step.sm/crypto v0.45.0/go.mod h1:6IYlT0L2jfj81nVyCPpvA5cORy0EVHPhieSgQyuwHIY= -go.step.sm/linkedca v0.20.1 h1:bHDn1+UG1NgRrERkWbbCiAIvv4lD5NOFaswPDTyO5vU= -go.step.sm/linkedca v0.20.1/go.mod h1:Vaq4+Umtjh7DLFI1KuIxeo598vfBzgSYZUjgVJ7Syxw= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= -go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 h1:1+EHlhAe/tukctfePZRrDruB9vn7MdwyC+rf36nUSPM= +go.opentelemetry.io/contrib/propagators/autoprop v0.62.0/go.mod h1:skzESZBY3IYcqJgImc+fwXQWflvVe+jZxoA/uw60NaI= +go.opentelemetry.io/contrib/propagators/aws v1.37.0 h1:cp8AFiM/qjBm10C/ATIRnEDXpD5MBknrA0ANw4T2/ss= +go.opentelemetry.io/contrib/propagators/aws v1.37.0/go.mod h1:Cy8Hk2E2iSGEbsLnPUdeigrexaAOAGIAmBFK919EQs0= +go.opentelemetry.io/contrib/propagators/b3 v1.37.0 h1:0aGKdIuVhy5l4GClAjl72ntkZJhijf2wg1S7b5oLoYA= +go.opentelemetry.io/contrib/propagators/b3 v1.37.0/go.mod h1:nhyrxEJEOQdwR15zXrCKI6+cJK60PXAkJ/jRyfhr2mg= +go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 h1:pW+qDVo0jB0rLsNeaP85xLuz20cvsECUcN7TE+D8YTM= +go.opentelemetry.io/contrib/propagators/jaeger v1.37.0/go.mod h1:x7bd+t034hxLTve1hF9Yn9qQJlO/pP8H5pWIt7+gsFM= +go.opentelemetry.io/contrib/propagators/ot v1.37.0 h1:tVjnBF6EiTDMXoq2Xuc2vK0I7MTbEs05II/0j9mMK+E= +go.opentelemetry.io/contrib/propagators/ot v1.37.0/go.mod h1:MQjyNXtxAC8PGN9gzPtO4GY5zuP+RI3XX53uWbCTvEQ= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.step.sm/crypto v0.67.0 h1:1km9LmxMKG/p+mKa1R4luPN04vlJYnRLlLQrWv7egGU= +go.step.sm/crypto v0.67.0/go.mod h1:+AoDpB0mZxbW/PmOXuwkPSpXRgaUaoIK+/Wx/HGgtAU= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= -go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/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-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/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.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= -golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw= -golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 h1:V5+zy0jmgNYmK1uW/sPpBw8ioFvalrhaUrYWmu1Fpe4= +golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 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-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 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= @@ -622,24 +526,23 @@ golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20181106065722-10aee1819953/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-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 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= @@ -648,26 +551,22 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/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-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -675,66 +574,63 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= 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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/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-20181030000716-a0a13e073c7b/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-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/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-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= 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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= -google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4= -google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE= +google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU= +google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -744,35 +640,32 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw= -google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw= -google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae h1:AH34z6WAGVNkllnKs5raNq3yRq93VnjBG6rpfub/jYk= -google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 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-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/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= 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.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -781,7 +674,6 @@ grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJd honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 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.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= diff --git a/internal/filesystems/map.go b/internal/filesystems/map.go index e795ed1fe..3ecb34e40 100644 --- a/internal/filesystems/map.go +++ b/internal/filesystems/map.go @@ -7,10 +7,10 @@ import ( ) const ( - DefaultFilesystemKey = "default" + DefaultFileSystemKey = "default" ) -var DefaultFilesystem = &wrapperFs{key: DefaultFilesystemKey, FS: OsFS{}} +var DefaultFileSystem = &wrapperFs{key: DefaultFileSystemKey, FS: OsFS{}} // wrapperFs exists so can easily add to wrapperFs down the line type wrapperFs struct { @@ -18,24 +18,24 @@ type wrapperFs struct { fs.FS } -// FilesystemMap stores a map of filesystems +// FileSystemMap stores a map of filesystems // the empty key will be overwritten to be the default key // it includes a default filesystem, based off the os fs -type FilesystemMap struct { +type FileSystemMap struct { m sync.Map } // note that the first invocation of key cannot be called in a racy context. -func (f *FilesystemMap) key(k string) string { +func (f *FileSystemMap) key(k string) string { if k == "" { - k = DefaultFilesystemKey + k = DefaultFileSystemKey } return k } // Register will add the filesystem with key to later be retrieved // A call with a nil fs will call unregister, ensuring that a call to Default() will never be nil -func (f *FilesystemMap) Register(k string, v fs.FS) { +func (f *FileSystemMap) Register(k string, v fs.FS) { k = f.key(k) if v == nil { f.Unregister(k) @@ -47,23 +47,23 @@ func (f *FilesystemMap) Register(k string, v fs.FS) { // Unregister will remove the filesystem with key from the filesystem map // if the key is the default key, it will set the default to the osFS instead of deleting it // modules should call this on cleanup to be safe -func (f *FilesystemMap) Unregister(k string) { +func (f *FileSystemMap) Unregister(k string) { k = f.key(k) - if k == DefaultFilesystemKey { - f.m.Store(k, DefaultFilesystem) + if k == DefaultFileSystemKey { + f.m.Store(k, DefaultFileSystem) } else { f.m.Delete(k) } } // Get will get a filesystem with a given key -func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) { +func (f *FileSystemMap) Get(k string) (v fs.FS, ok bool) { k = f.key(k) c, ok := f.m.Load(strings.TrimSpace(k)) if !ok { - if k == DefaultFilesystemKey { - f.m.Store(k, DefaultFilesystem) - return DefaultFilesystem, true + if k == DefaultFileSystemKey { + f.m.Store(k, DefaultFileSystem) + return DefaultFileSystem, true } return nil, ok } @@ -71,7 +71,7 @@ func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) { } // Default will get the default filesystem in the filesystem map -func (f *FilesystemMap) Default() fs.FS { - val, _ := f.Get(DefaultFilesystemKey) +func (f *FileSystemMap) Default() fs.FS { + val, _ := f.Get(DefaultFileSystemKey) return val } diff --git a/internal/logs.go b/internal/logs.go new file mode 100644 index 000000000..4ed4a572e --- /dev/null +++ b/internal/logs.go @@ -0,0 +1,22 @@ +package internal + +import "fmt" + +// MaxSizeSubjectsListForLog returns the keys in the map as a slice of maximum length +// maxToDisplay. It is useful for logging domains being managed, for example, since a +// map is typically needed for quick lookup, but a slice is needed for logging, and this +// can be quite a doozy since there may be a huge amount (hundreds of thousands). +func MaxSizeSubjectsListForLog(subjects map[string]struct{}, maxToDisplay int) []string { + numberOfNamesToDisplay := min(len(subjects), maxToDisplay) + domainsToDisplay := make([]string, 0, numberOfNamesToDisplay) + for domain := range subjects { + domainsToDisplay = append(domainsToDisplay, domain) + if len(domainsToDisplay) >= numberOfNamesToDisplay { + break + } + } + if len(subjects) > maxToDisplay { + domainsToDisplay = append(domainsToDisplay, fmt.Sprintf("(and %d more...)", len(subjects)-maxToDisplay)) + } + return domainsToDisplay +} diff --git a/listen.go b/listen.go index f5c2086a6..e84a197ac 100644 --- a/listen.go +++ b/listen.go @@ -30,7 +30,7 @@ import ( "go.uber.org/zap" ) -func reuseUnixSocket(network, addr string) (any, error) { +func reuseUnixSocket(_, _ string) (any, error) { return nil, nil } @@ -107,7 +107,8 @@ func listenReusable(ctx context.Context, lnKey string, network, address string, if err != nil { return nil, err } - return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil + + return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAliveConfig: config.KeepAliveConfig}, nil } // fakeCloseListener is a private wrapper over a listener that @@ -121,12 +122,11 @@ func listenReusable(ctx context.Context, lnKey string, network, address string, type fakeCloseListener struct { closed int32 // accessed atomically; belongs to this struct only *sharedListener // embedded, so we also become a net.Listener - keepAlivePeriod time.Duration + keepAliveConfig net.KeepAliveConfig } -type canSetKeepAlive interface { - SetKeepAlivePeriod(d time.Duration) error - SetKeepAlive(bool) error +type canSetKeepAliveConfig interface { + SetKeepAliveConfig(config net.KeepAliveConfig) error } func (fcl *fakeCloseListener) Accept() (net.Conn, error) { @@ -140,12 +140,8 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) { if err == nil { // if 0, do nothing, Go's default is already set // and if the connection allows setting KeepAlive, set it - if tconn, ok := conn.(canSetKeepAlive); ok && fcl.keepAlivePeriod != 0 { - if fcl.keepAlivePeriod > 0 { - err = tconn.SetKeepAlivePeriod(fcl.keepAlivePeriod) - } else { // negative - err = tconn.SetKeepAlive(false) - } + if tconn, ok := conn.(canSetKeepAliveConfig); ok && fcl.keepAliveConfig.Enable { + err = tconn.SetKeepAliveConfig(fcl.keepAliveConfig) if err != nil { Log().With(zap.String("server", fcl.sharedListener.key)).Warn("unable to set keepalive for new connection:", zap.Error(err)) } diff --git a/listeners.go b/listeners.go index 3a2a5180f..01adc615d 100644 --- a/listeners.go +++ b/listeners.go @@ -139,7 +139,7 @@ func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net } // check to see if plugin provides listener - if ln, err := getListenerFromPlugin(ctx, na.Network, na.JoinHostPort(portOffset), config); ln != nil || err != nil { + if ln, err := getListenerFromPlugin(ctx, na.Network, na.Host, na.port(), portOffset, config); ln != nil || err != nil { return ln, err } @@ -210,7 +210,7 @@ func (na NetworkAddress) IsUnixNetwork() bool { return IsUnixNetwork(na.Network) } -// IsUnixNetwork returns true if na.Network is +// IsFdNetwork returns true if na.Network is // fd or fdgram. func (na NetworkAddress) IsFdNetwork() bool { return IsFdNetwork(na.Network) @@ -305,25 +305,6 @@ func IsFdNetwork(netw string) bool { return strings.HasPrefix(netw, "fd") } -// normally we would simply append the port, -// but if host is IPv6, we need to ensure it -// is enclosed in [ ]; net.JoinHostPort does -// this for us, but host might also have a -// network type in front (e.g. "tcp/") leading -// to "[tcp/::1]" which causes parsing failures -// later; what we need is "tcp/[::1]", so we have -// to split the network and host, then re-combine -func ParseNetworkAddressFromHostPort(host, port string) (NetworkAddress, error) { - network, addr, ok := strings.Cut(host, "/") - if !ok { - addr = network - network = "" - } - addr = strings.Trim(addr, "[]") // IPv6 - networkAddr := JoinNetworkAddress(network, addr, port) - return ParseNetworkAddress(networkAddr) -} - // ParseNetworkAddress parses addr into its individual // components. The input string is expected to be of // the form "network/host:port-range" where any part is @@ -399,25 +380,28 @@ func SplitNetworkAddress(a string) (network, host, port string, err error) { if slashFound { network = strings.ToLower(strings.TrimSpace(beforeSlash)) a = afterSlash + if IsUnixNetwork(network) || IsFdNetwork(network) { + host = a + return + } } - if IsUnixNetwork(network) || IsFdNetwork(network) { - host = a - return - } + host, port, err = net.SplitHostPort(a) - if err == nil || a == "" { - return - } - // in general, if there was an error, it was likely "missing port", - // so try adding a bogus port to take advantage of standard library's - // robust parser, then strip the artificial port before returning - // (don't overwrite original error though; might still be relevant) - var err2 error - host, port, err2 = net.SplitHostPort(a + ":0") - if err2 == nil { - err = nil + firstErr := err + + if err != nil { + // in general, if there was an error, it was likely "missing port", + // so try removing square brackets around an IPv6 host, adding a bogus + // port to take advantage of standard library's robust parser, then + // strip the artificial port. + host, _, err = net.SplitHostPort(net.JoinHostPort(strings.Trim(a, "[]"), "0")) port = "" } + + if err != nil { + err = errors.Join(firstErr, err) + } + return } @@ -446,7 +430,8 @@ func JoinNetworkAddress(network, host, port string) string { // address instead. // // NOTE: This API is EXPERIMENTAL and may be changed or removed. -func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config) (http3.QUICEarlyListener, error) { +// NOTE: user should close the returned listener twice, once to stop accepting new connections, the second time to free up the packet conn. +func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config) (http3.QUICListener, error) { lnKey := listenerKey("quic"+na.Network, na.JoinHostPort(portOffset)) sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { @@ -626,7 +611,7 @@ type fakeCloseQuicListener struct { // server on which Accept would be called with non-empty contexts // (mind that the default net listeners' Accept doesn't take a context argument) // sounds way too rare for us to sacrifice efficiency here. -func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (quic.EarlyConnection, error) { +func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error) { conn, err := fcql.sharedQuicListener.Accept(fcql.context) if err == nil { return conn, nil @@ -642,6 +627,7 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (quic.EarlyConnecti func (fcql *fakeCloseQuicListener) Close() error { if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) { fcql.contextCancel() + } else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) { _, _ = listenerPool.Delete(fcql.sharedQuicListener.key) } return nil @@ -657,7 +643,7 @@ func RegisterNetwork(network string, getListener ListenerFunc) { if network == "tcp" || network == "tcp4" || network == "tcp6" || network == "udp" || network == "udp4" || network == "udp6" || network == "unix" || network == "unixpacket" || network == "unixgram" || - strings.HasPrefix("ip:", network) || strings.HasPrefix("ip4:", network) || strings.HasPrefix("ip6:", network) || + strings.HasPrefix(network, "ip:") || strings.HasPrefix(network, "ip4:") || strings.HasPrefix(network, "ip6:") || network == "fd" || network == "fdgram" { panic("network type " + network + " is reserved") } @@ -674,11 +660,11 @@ var unixSocketsMu sync.Mutex // getListenerFromPlugin returns a listener on the given network and address // if a plugin has registered the network name. It may return (nil, nil) if // no plugin can provide a listener. -func getListenerFromPlugin(ctx context.Context, network, addr string, config net.ListenConfig) (any, error) { +func getListenerFromPlugin(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) { // get listener from plugin if network type is registered if getListener, ok := networkTypes[network]; ok { Log().Debug("getting listener from plugin", zap.String("network", network)) - return getListener(ctx, network, addr, config) + return getListener(ctx, network, host, port, portOffset, config) } return nil, nil @@ -692,7 +678,7 @@ func listenerKey(network, addr string) string { // The listeners must be capable of overlapping: with Caddy, new configs are loaded // before old ones are unloaded, so listeners may overlap briefly if the configs // both need the same listener. EXPERIMENTAL and subject to change. -type ListenerFunc func(ctx context.Context, network, addr string, cfg net.ListenConfig) (any, error) +type ListenerFunc func(ctx context.Context, network, host, portRange string, portOffset uint, cfg net.ListenConfig) (any, error) var networkTypes = map[string]ListenerFunc{} diff --git a/listeners_test.go b/listeners_test.go index f8a13cafc..a4cadd3aa 100644 --- a/listeners_test.go +++ b/listeners_test.go @@ -30,8 +30,8 @@ func TestSplitNetworkAddress(t *testing.T) { expectErr bool }{ { - input: "", - expectErr: true, + input: "", + expectHost: "", }, { input: "foo", @@ -41,8 +41,8 @@ func TestSplitNetworkAddress(t *testing.T) { input: ":", // empty host & empty port }, { - input: "::", - expectErr: true, + input: "::", + expectHost: "::", }, { input: "[::]", @@ -77,7 +77,7 @@ func TestSplitNetworkAddress(t *testing.T) { { input: "udp/", expectNetwork: "udp", - expectErr: true, + expectHost: "", }, { input: "unix//foo/bar", @@ -184,8 +184,8 @@ func TestParseNetworkAddress(t *testing.T) { expectErr bool }{ { - input: "", - expectErr: true, + input: "", + expectAddr: NetworkAddress{}, }, { input: ":", @@ -310,8 +310,8 @@ func TestParseNetworkAddressWithDefaults(t *testing.T) { expectErr bool }{ { - input: "", - expectErr: true, + input: "", + expectAddr: NetworkAddress{}, }, { input: ":", diff --git a/logging.go b/logging.go index ca10beeed..1a7b0ce29 100644 --- a/logging.go +++ b/logging.go @@ -20,6 +20,7 @@ import ( "io" "log" "os" + "slices" "strings" "sync" "time" @@ -161,7 +162,9 @@ func (logging *Logging) setupNewDefault(ctx Context) error { if err != nil { return fmt.Errorf("setting up default log: %v", err) } - newDefault.logger = zap.New(newDefault.CustomLog.core, options...) + + filteringCore := &filteringCore{newDefault.CustomLog.core, newDefault.CustomLog} + newDefault.logger = zap.New(filteringCore, options...) // redirect the default caddy logs defaultLoggerMu.Lock() @@ -490,10 +493,8 @@ func (cl *CustomLog) provision(ctx Context, logging *Logging) error { if len(cl.Include) > 0 && len(cl.Exclude) > 0 { // prevent intersections for _, allow := range cl.Include { - for _, deny := range cl.Exclude { - if allow == deny { - return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow) - } + if slices.Contains(cl.Exclude, allow) { + return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow) } } diff --git a/logging_test.go b/logging_test.go new file mode 100644 index 000000000..293591fbb --- /dev/null +++ b/logging_test.go @@ -0,0 +1,106 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddy + +import "testing" + +func TestCustomLog_loggerAllowed(t *testing.T) { + type fields struct { + BaseLog BaseLog + Include []string + Exclude []string + } + type args struct { + name string + isModule bool + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "include", + fields: fields{ + Include: []string{"foo"}, + }, + args: args{ + name: "foo", + isModule: true, + }, + want: true, + }, + { + name: "exclude", + fields: fields{ + Exclude: []string{"foo"}, + }, + args: args{ + name: "foo", + isModule: true, + }, + want: false, + }, + { + name: "include and exclude", + fields: fields{ + Include: []string{"foo"}, + Exclude: []string{"foo"}, + }, + args: args{ + name: "foo", + isModule: true, + }, + want: false, + }, + { + name: "include and exclude (longer namespace)", + fields: fields{ + Include: []string{"foo.bar"}, + Exclude: []string{"foo"}, + }, + args: args{ + name: "foo.bar", + isModule: true, + }, + want: true, + }, + { + name: "excluded module is not printed", + fields: fields{ + Include: []string{"admin.api.load"}, + Exclude: []string{"admin.api"}, + }, + args: args{ + name: "admin.api", + isModule: false, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cl := &CustomLog{ + BaseLog: tt.fields.BaseLog, + Include: tt.fields.Include, + Exclude: tt.fields.Exclude, + } + if got := cl.loggerAllowed(tt.args.name, tt.args.isModule); got != tt.want { + t.Errorf("CustomLog.loggerAllowed() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/metrics.go b/metrics.go index 0f8ea03cb..0ee3853eb 100644 --- a/metrics.go +++ b/metrics.go @@ -4,30 +4,33 @@ import ( "net/http" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" - "github.com/prometheus/client_golang/prometheus/promauto" "github.com/caddyserver/caddy/v2/internal/metrics" ) // define and register the metrics used in this package. func init() { - prometheus.MustRegister(collectors.NewBuildInfoCollector()) - const ns, sub = "caddy", "admin" - - adminMetrics.requestCount = promauto.NewCounterVec(prometheus.CounterOpts{ + adminMetrics.requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: ns, Subsystem: sub, Name: "http_requests_total", Help: "Counter of requests made to the Admin API's HTTP endpoints.", }, []string{"handler", "path", "code", "method"}) - adminMetrics.requestErrors = promauto.NewCounterVec(prometheus.CounterOpts{ + adminMetrics.requestErrors = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: ns, Subsystem: sub, Name: "http_request_errors_total", Help: "Number of requests resulting in middleware errors.", }, []string{"handler", "path", "method"}) + globalMetrics.configSuccess = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "caddy_config_last_reload_successful", + Help: "Whether the last configuration reload attempt was successful.", + }) + globalMetrics.configSuccessTime = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "caddy_config_last_reload_success_timestamp_seconds", + Help: "Timestamp of the last successful configuration reload.", + }) } // adminMetrics is a collection of metrics that can be tracked for the admin API. @@ -36,6 +39,12 @@ var adminMetrics = struct { requestErrors *prometheus.CounterVec }{} +// globalMetrics is a collection of metrics that can be tracked for Caddy global state +var globalMetrics = struct { + configSuccess prometheus.Gauge + configSuccessTime prometheus.Gauge +}{} + // Similar to promhttp.InstrumentHandlerCounter, but upper-cases method names // instead of lower-casing them. // diff --git a/modules.go b/modules.go index 470c25e37..37b56a988 100644 --- a/modules.go +++ b/modules.go @@ -18,6 +18,8 @@ import ( "bytes" "encoding/json" "fmt" + "net/http" + "net/url" "reflect" "sort" "strings" @@ -360,6 +362,14 @@ func isModuleMapType(typ reflect.Type) bool { isJSONRawMessage(typ.Elem()) } +// ProxyFuncProducer is implemented by modules which produce a +// function that returns a URL to use as network proxy. Modules +// in the namespace `caddy.network_proxy` must implement this +// interface. +type ProxyFuncProducer interface { + ProxyFunc() func(*http.Request) (*url.URL, error) +} + var ( modules = make(map[string]ModuleInfo) modulesMu sync.RWMutex diff --git a/modules/caddyevents/app.go b/modules/caddyevents/app.go index 2e4a6f2c0..6c2abbf7c 100644 --- a/modules/caddyevents/app.go +++ b/modules/caddyevents/app.go @@ -20,9 +20,7 @@ import ( "errors" "fmt" "strings" - "time" - "github.com/google/uuid" "go.uber.org/zap" "github.com/caddyserver/caddy/v2" @@ -206,27 +204,26 @@ func (app *App) On(eventName string, handler Handler) error { // // Note that the data map is not copied, for efficiency. After Emit() is called, the // data passed in should not be changed in other goroutines. -func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) Event { +func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) caddy.Event { logger := app.logger.With(zap.String("name", eventName)) - id, err := uuid.NewRandom() + e, err := caddy.NewEvent(ctx, eventName, data) if err != nil { - logger.Error("failed generating new event ID", zap.Error(err)) + logger.Error("failed to create event", zap.Error(err)) } - eventName = strings.ToLower(eventName) - - e := Event{ - Data: data, - id: id, - ts: time.Now(), - name: eventName, - origin: ctx.Module(), + var originModule caddy.ModuleInfo + var originModuleID caddy.ModuleID + var originModuleName string + if origin := e.Origin(); origin != nil { + originModule = origin.CaddyModule() + originModuleID = originModule.ID + originModuleName = originModule.String() } logger = logger.With( - zap.String("id", e.id.String()), - zap.String("origin", e.origin.CaddyModule().String())) + zap.String("id", e.ID().String()), + zap.String("origin", originModuleName)) // add event info to replacer, make sure it's in the context repl, ok := ctx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) @@ -239,21 +236,21 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E case "event": return e, true case "event.id": - return e.id, true + return e.ID(), true case "event.name": - return e.name, true + return e.Name(), true case "event.time": - return e.ts, true + return e.Timestamp(), true case "event.time_unix": - return e.ts.UnixMilli(), true + return e.Timestamp().UnixMilli(), true case "event.module": - return e.origin.CaddyModule().ID, true + return originModuleID, true case "event.data": return e.Data, true } - if strings.HasPrefix(key, "event.data.") { - key = strings.TrimPrefix(key, "event.data.") + if after, ok0 := strings.CutPrefix(key, "event.data."); ok0 { + key = after if val, ok := e.Data[key]; ok { return val, true } @@ -262,14 +259,14 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E return nil, false }) - logger = logger.With(zap.Any("data", e.Data)) + logger = logger.WithLazy(zap.Any("data", e.Data)) logger.Debug("event") // invoke handlers bound to the event by name and also all events; this for loop // iterates twice at most: once for the event name, once for "" (all events) for { - moduleID := e.origin.CaddyModule().ID + moduleID := originModuleID // implement propagation up the module tree (i.e. start with "a.b.c" then "a.b" then "a" then "") for { @@ -292,7 +289,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E zap.Any("handler", handler)) if err := handler.Handle(ctx, e); err != nil { - aborted := errors.Is(err, ErrAborted) + aborted := errors.Is(err, caddy.ErrEventAborted) logger.Error("handler error", zap.Error(err), @@ -326,76 +323,9 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E return e } -// Event represents something that has happened or is happening. -// An Event value is not synchronized, so it should be copied if -// being used in goroutines. -// -// EXPERIMENTAL: As with the rest of this package, events are -// subject to change. -type Event struct { - // If non-nil, the event has been aborted, meaning - // propagation has stopped to other handlers and - // the code should stop what it was doing. Emitters - // may choose to use this as a signal to adjust their - // code path appropriately. - Aborted error - - // The data associated with the event. Usually the - // original emitter will be the only one to set or - // change these values, but the field is exported - // so handlers can have full access if needed. - // However, this map is not synchronized, so - // handlers must not use this map directly in new - // goroutines; instead, copy the map to use it in a - // goroutine. - Data map[string]any - - id uuid.UUID - ts time.Time - name string - origin caddy.Module -} - -func (e Event) ID() uuid.UUID { return e.id } -func (e Event) Timestamp() time.Time { return e.ts } -func (e Event) Name() string { return e.name } -func (e Event) Origin() caddy.Module { return e.origin } - -// CloudEvent exports event e as a structure that, when -// serialized as JSON, is compatible with the -// CloudEvents spec. -func (e Event) CloudEvent() CloudEvent { - dataJSON, _ := json.Marshal(e.Data) - return CloudEvent{ - ID: e.id.String(), - Source: e.origin.CaddyModule().String(), - SpecVersion: "1.0", - Type: e.name, - Time: e.ts, - DataContentType: "application/json", - Data: dataJSON, - } -} - -// CloudEvent is a JSON-serializable structure that -// is compatible with the CloudEvents specification. -// See https://cloudevents.io. -type CloudEvent struct { - ID string `json:"id"` - Source string `json:"source"` - SpecVersion string `json:"specversion"` - Type string `json:"type"` - Time time.Time `json:"time"` - DataContentType string `json:"datacontenttype,omitempty"` - Data json.RawMessage `json:"data,omitempty"` -} - -// ErrAborted cancels an event. -var ErrAborted = errors.New("event aborted") - // Handler is a type that can handle events. type Handler interface { - Handle(context.Context, Event) error + Handle(context.Context, caddy.Event) error } // Interface guards diff --git a/modules/caddyfs/filesystem.go b/modules/caddyfs/filesystem.go index b2fdcf7a2..2ec43079a 100644 --- a/modules/caddyfs/filesystem.go +++ b/modules/caddyfs/filesystem.go @@ -69,11 +69,11 @@ func (xs *Filesystems) Provision(ctx caddy.Context) error { } // register that module ctx.Logger().Debug("registering fs", zap.String("fs", f.Key)) - ctx.Filesystems().Register(f.Key, f.fileSystem) + ctx.FileSystems().Register(f.Key, f.fileSystem) // remember to unregister the module when we are done xs.defers = append(xs.defers, func() { ctx.Logger().Debug("unregistering fs", zap.String("fs", f.Key)) - ctx.Filesystems().Unregister(f.Key) + ctx.FileSystems().Unregister(f.Key) }) } return nil diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 7a5c10623..45f99c4d7 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -15,6 +15,7 @@ package caddyhttp import ( + "cmp" "context" "crypto/tls" "fmt" @@ -27,7 +28,6 @@ import ( "go.uber.org/zap" "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyevents" @@ -72,7 +72,7 @@ func init() { // `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on // `{http.request.local.port}` | The port part of the local address the connection arrived on // `{http.request.local}` | The local address the connection arrived on -// `{http.request.remote.host}` | The host (IP) part of the remote client's address +// `{http.request.remote.host}` | The host (IP) part of the remote client's address, if available (not known with HTTP/3 early data) // `{http.request.remote.port}` | The port part of the remote client's address // `{http.request.remote}` | The address of the remote client // `{http.request.scheme}` | The request scheme, typically `http` or `https` @@ -142,12 +142,21 @@ type App struct { // affect functionality. Servers map[string]*Server `json:"servers,omitempty"` + // If set, metrics observations will be enabled. + // This setting is EXPERIMENTAL and subject to change. + Metrics *Metrics `json:"metrics,omitempty"` + ctx caddy.Context logger *zap.Logger tlsApp *caddytls.TLS + // stopped indicates whether the app has stopped + // It can only happen if it has started successfully in the first place. + // Otherwise, Cleanup will call Stop to clean up resources. + stopped bool + // used temporarily between phases 1 and 2 of auto HTTPS - allCertDomains []string + allCertDomains map[string]struct{} } // CaddyModule returns the Caddy module information. @@ -161,13 +170,15 @@ func (App) CaddyModule() caddy.ModuleInfo { // Provision sets up the app. func (app *App) Provision(ctx caddy.Context) error { // store some references + app.logger = ctx.Logger() + app.ctx = ctx + + // provision TLS and events apps tlsAppIface, err := ctx.App("tls") if err != nil { return fmt.Errorf("getting tls app: %v", err) } app.tlsApp = tlsAppIface.(*caddytls.TLS) - app.ctx = ctx - app.logger = ctx.Logger() eventsAppIface, err := ctx.App("events") if err != nil { @@ -184,6 +195,10 @@ func (app *App) Provision(ctx caddy.Context) error { return err } + if app.Metrics != nil { + app.Metrics.init = sync.Once{} + app.Metrics.httpMetrics = &httpMetrics{} + } // prepare each server oldContext := ctx.Context for srvName, srv := range app.Servers { @@ -196,6 +211,15 @@ func (app *App) Provision(ctx caddy.Context) error { srv.errorLogger = app.logger.Named("log.error") srv.shutdownAtMu = new(sync.RWMutex) + if srv.Metrics != nil { + srv.logger.Warn("per-server 'metrics' is deprecated; use 'metrics' in the root 'http' app instead") + app.Metrics = cmp.Or(app.Metrics, &Metrics{ + init: sync.Once{}, + httpMetrics: &httpMetrics{}, + }) + app.Metrics.PerHost = app.Metrics.PerHost || srv.Metrics.PerHost + } + // only enable access logs if configured if srv.Logs != nil { srv.accessLogger = app.logger.Named("log.access") @@ -213,15 +237,6 @@ func (app *App) Provision(ctx caddy.Context) error { for _, srvProtocol := range srv.Protocols { srvProtocolsUnique[srvProtocol] = struct{}{} } - _, h1ok := srvProtocolsUnique["h1"] - _, h2ok := srvProtocolsUnique["h2"] - _, h2cok := srvProtocolsUnique["h2c"] - - // the Go standard library does not let us serve only HTTP/2 using - // http.Server; we would probably need to write our own server - if !h1ok && (h2ok || h2cok) { - return fmt.Errorf("server %s: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName) - } if srv.ListenProtocols != nil { if len(srv.ListenProtocols) != len(srv.Listen) { @@ -255,19 +270,6 @@ func (app *App) Provision(ctx caddy.Context) error { } } - lnProtocolsIncludeUnique := map[string]struct{}{} - for _, lnProtocol := range lnProtocolsInclude { - lnProtocolsIncludeUnique[lnProtocol] = struct{}{} - } - _, h1ok := lnProtocolsIncludeUnique["h1"] - _, h2ok := lnProtocolsIncludeUnique["h2"] - _, h2cok := lnProtocolsIncludeUnique["h2c"] - - // check if any listener protocols contain h2 or h2c without h1 - if !h1ok && (h2ok || h2cok) { - return fmt.Errorf("server %s, listener %d: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName, i) - } - srv.ListenProtocols[i] = lnProtocolsInclude } } @@ -342,12 +344,11 @@ func (app *App) Provision(ctx caddy.Context) error { srv.listenerWrappers = append([]caddy.ListenerWrapper{new(tlsPlaceholderWrapper)}, srv.listenerWrappers...) } } - // pre-compile the primary handler chain, and be sure to wrap it in our // route handler so that important security checks are done, etc. primaryRoute := emptyHandler if srv.Routes != nil { - err := srv.Routes.ProvisionHandlers(ctx, srv.Metrics) + err := srv.Routes.ProvisionHandlers(ctx, app.Metrics) if err != nil { return fmt.Errorf("server %s: setting up route handlers: %v", srvName, err) } @@ -366,7 +367,7 @@ func (app *App) Provision(ctx caddy.Context) error { // provision the named routes (they get compiled at runtime) for name, route := range srv.NamedRoutes { - err := route.Provision(ctx, srv.Metrics) + err := route.Provision(ctx, app.Metrics) if err != nil { return fmt.Errorf("server %s: setting up named route '%s' handlers: %v", name, srvName, err) } @@ -384,6 +385,9 @@ func (app *App) Provision(ctx caddy.Context) error { if srv.IdleTimeout == 0 { srv.IdleTimeout = defaultIdleTimeout } + if srv.ReadHeaderTimeout == 0 { + srv.ReadHeaderTimeout = defaultReadHeaderTimeout // see #6663 + } } ctx.Context = oldContext return nil @@ -423,6 +427,25 @@ func (app *App) Validate() error { return nil } +func removeTLSALPN(srv *Server, target string) { + for _, cp := range srv.TLSConnPolicies { + // the TLSConfig was already provisioned, so... manually remove it + for i, np := range cp.TLSConfig.NextProtos { + if np == target { + cp.TLSConfig.NextProtos = append(cp.TLSConfig.NextProtos[:i], cp.TLSConfig.NextProtos[i+1:]...) + break + } + } + // remove it from the parent connection policy too, just to keep things tidy + for i, alpn := range cp.ALPN { + if alpn == target { + cp.ALPN = append(cp.ALPN[:i], cp.ALPN[i+1:]...) + break + } + } + } +} + // Start runs the app. It finishes automatic HTTPS if enabled, // including management of certificates. func (app *App) Start() error { @@ -441,32 +464,37 @@ func (app *App) Start() error { MaxHeaderBytes: srv.MaxHeaderBytes, Handler: srv, ErrorLog: serverLogger, + Protocols: new(http.Protocols), ConnContext: func(ctx context.Context, c net.Conn) context.Context { return context.WithValue(ctx, ConnCtxKey, c) }, } - h2server := new(http2.Server) // disable HTTP/2, which we enabled by default during provisioning if !srv.protocol("h2") { srv.server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) - for _, cp := range srv.TLSConnPolicies { - // the TLSConfig was already provisioned, so... manually remove it - for i, np := range cp.TLSConfig.NextProtos { - if np == "h2" { - cp.TLSConfig.NextProtos = append(cp.TLSConfig.NextProtos[:i], cp.TLSConfig.NextProtos[i+1:]...) - break - } - } - // remove it from the parent connection policy too, just to keep things tidy - for i, alpn := range cp.ALPN { - if alpn == "h2" { - cp.ALPN = append(cp.ALPN[:i], cp.ALPN[i+1:]...) - break - } - } - } - } else { + removeTLSALPN(srv, "h2") + } + if !srv.protocol("h1") { + removeTLSALPN(srv, "http/1.1") + } + + // configure the http versions the server will serve + if srv.protocol("h1") { + srv.server.Protocols.SetHTTP1(true) + } + + if srv.protocol("h2") || srv.protocol("h2c") { + // skip setting h2 because if NextProtos is present, it's list of alpn versions will take precedence. + // it will always be present because http2.ConfigureServer will populate that field + // enabling h2c because some listener wrapper will wrap the connection that is no longer *tls.Conn + // However, we need to handle the case that if the connection is h2c but h2c is not enabled. We identify + // this type of connection by checking if it's behind a TLS listener wrapper or if it implements tls.ConnectionState. + srv.server.Protocols.SetUnencryptedHTTP2(true) + // when h2c is enabled but h2 disabled, we already removed h2 from NextProtos + // the handshake will never succeed with h2 + // http2.ConfigureServer will enable the server to handle both h2 and h2c + h2server := new(http2.Server) //nolint:errcheck http2.ConfigureServer(srv.server, h2server) } @@ -476,11 +504,6 @@ func (app *App) Start() error { tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx) srv.configureServer(srv.server) - // enable H2C if configured - if srv.protocol("h2c") { - srv.server.Handler = h2c.NewHandler(srv, h2server) - } - for lnIndex, lnAddr := range srv.Listen { listenAddr, err := caddy.ParseNetworkAddress(lnAddr) if err != nil { @@ -509,24 +532,14 @@ func (app *App) Start() error { // enable TLS if there is a policy and if this is not the HTTP port useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort() - // enable HTTP/3 if configured - if h3ok && useTLS { - app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport)) - if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil { - return err - } - } - - if h3ok && !useTLS { - // Can only serve h3 with TLS enabled - app.logger.Warn("HTTP/3 skipped because it requires TLS", - zap.String("network", listenAddr.Network), - zap.String("addr", hostport)) - } - if h1ok || h2ok && useTLS || h2cok { // create the listener for this socket - lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{KeepAlive: time.Duration(srv.KeepAliveInterval)}) + lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{ + KeepAliveConfig: net.KeepAliveConfig{ + Enable: srv.KeepAliveInterval >= 0, + Interval: time.Duration(srv.KeepAliveInterval), + }, + }) if err != nil { return fmt.Errorf("listening on %s: %v", listenAddr.At(portOffset), err) } @@ -555,15 +568,13 @@ func (app *App) Start() error { ln = srv.listenerWrappers[i].WrapListener(ln) } - // handle http2 if use tls listener wrapper - if h2ok { - http2lnWrapper := &http2Listener{ - Listener: ln, - server: srv.server, - h2server: h2server, - } - srv.h2listeners = append(srv.h2listeners, http2lnWrapper) - ln = http2lnWrapper + // check if the connection is h2c + ln = &http2Listener{ + useTLS: useTLS, + useH1: h1ok, + useH2: h2ok || h2cok, + Listener: ln, + logger: app.logger, } // if binding to port 0, the OS chooses a port for us; @@ -581,11 +592,8 @@ func (app *App) Start() error { srv.listeners = append(srv.listeners, ln) - // enable HTTP/1 if configured - if h1ok { - //nolint:errcheck - go srv.server.Serve(ln) - } + //nolint:errcheck + go srv.server.Serve(ln) } if h2ok && !useTLS { @@ -594,6 +602,33 @@ func (app *App) Start() error { zap.String("network", listenAddr.Network), zap.String("addr", hostport)) } + + if h3ok { + // Can't serve HTTP/3 on the same socket as HTTP/1 and 2 because it uses + // a different transport mechanism... which is fine, but the OS doesn't + // differentiate between a SOCK_STREAM file and a SOCK_DGRAM file; they + // are still one file on the system. So even though "unixpacket" and + // "unixgram" are different network types just as "tcp" and "udp" are, + // the OS will not let us use the same file as both STREAM and DGRAM. + if listenAddr.IsUnixNetwork() { + app.logger.Warn("HTTP/3 disabled because Unix can't multiplex STREAM and DGRAM on same socket", + zap.String("file", hostport)) + continue + } + + if useTLS { + // enable HTTP/3 if configured + app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport)) + if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil { + return err + } + } else { + // Can only serve h3 with TLS enabled + app.logger.Warn("HTTP/3 skipped because it requires TLS", + zap.String("network", listenAddr.Network), + zap.String("addr", hostport)) + } + } } } @@ -671,6 +706,11 @@ func (app *App) Stop() error { defer finishedShutdown.Done() startedShutdown.Done() + // possible if server failed to Start + if server.server == nil { + return + } + if err := server.server.Shutdown(ctx); err != nil { app.logger.Error("server shutdown", zap.Error(err), @@ -685,40 +725,36 @@ func (app *App) Stop() error { return } - // First close h3server then close listeners unlike stdlib for several reasons: - // 1, udp has only a single socket, once closed, no more data can be read and - // written. In contrast, closing tcp listeners won't affect established connections. - // This have something to do with graceful shutdown when upstream implements it. - // 2, h3server will only close listeners it's registered (quic listeners). Closing - // listener first and these listeners maybe unregistered thus won't be closed. caddy - // distinguishes quic-listener and underlying datagram sockets. + // closing quic listeners won't affect accepted connections now + // so like stdlib, close listeners first, but keep the net.PacketConns open + for _, h3ln := range server.quicListeners { + if err := h3ln.Close(); err != nil { + app.logger.Error("http3 listener close", + zap.Error(err)) + } + } - // TODO: CloseGracefully, once implemented upstream (see https://github.com/quic-go/quic-go/issues/2103) - if err := server.h3server.Close(); err != nil { + if err := server.h3server.Shutdown(ctx); err != nil { app.logger.Error("HTTP/3 server shutdown", zap.Error(err), zap.Strings("addresses", server.Listen)) } - } - stopH2Listener := func(server *Server) { - defer finishedShutdown.Done() - startedShutdown.Done() - for i, s := range server.h2listeners { - if err := s.Shutdown(ctx); err != nil { - app.logger.Error("http2 listener shutdown", - zap.Error(err), - zap.Int("index", i)) + // close the underlying net.PacketConns now + // see the comment for ListenQUIC + for _, h3ln := range server.quicListeners { + if err := h3ln.Close(); err != nil { + app.logger.Error("http3 listener close socket", + zap.Error(err)) } } } for _, server := range app.Servers { - startedShutdown.Add(3) - finishedShutdown.Add(3) + startedShutdown.Add(2) + finishedShutdown.Add(2) go stopServer(server) go stopH3Server(server) - go stopH2Listener(server) } // block until all the goroutines have been run by the scheduler; @@ -745,9 +781,20 @@ func (app *App) Stop() error { } } + app.stopped = true return nil } +// Cleanup will close remaining listeners if they still remain +// because some of the servers fail to start. +// It simply calls Stop because Stop won't be called when Start fails. +func (app *App) Cleanup() error { + if app.stopped { + return nil + } + return app.Stop() +} + func (app *App) httpPort() int { if app.HTTPPort == 0 { return DefaultHTTPPort @@ -762,11 +809,20 @@ func (app *App) httpsPort() int { return app.HTTPSPort } -// defaultIdleTimeout is the default HTTP server timeout -// for closing idle connections; useful to avoid resource -// exhaustion behind hungry CDNs, for example (we've had -// several complaints without this). -const defaultIdleTimeout = caddy.Duration(5 * time.Minute) +const ( + // defaultIdleTimeout is the default HTTP server timeout + // for closing idle connections; useful to avoid resource + // exhaustion behind hungry CDNs, for example (we've had + // several complaints without this). + defaultIdleTimeout = caddy.Duration(5 * time.Minute) + + // defaultReadHeaderTimeout is the default timeout for + // reading HTTP headers from clients. Headers are generally + // small, often less than 1 KB, so it shouldn't take a + // long time even on legitimately slow connections or + // busy servers to read it. + defaultReadHeaderTimeout = caddy.Duration(time.Minute) +) // Interface guards var ( diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index ccb610327..c34954f92 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -25,6 +25,7 @@ import ( "go.uber.org/zap" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/internal" "github.com/caddyserver/caddy/v2/modules/caddytls" ) @@ -65,12 +66,6 @@ type AutoHTTPSConfig struct { // enabled. To force automated certificate management // regardless of loaded certificates, set this to true. IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"` - - // If true, automatic HTTPS will prefer wildcard names - // and ignore non-wildcard names if both are available. - // This allows for writing a config with top-level host - // matchers without having those names produce certificates. - PreferWildcard bool `json:"prefer_wildcard,omitempty"` } // automaticHTTPSPhase1 provisions all route matchers, determines @@ -163,26 +158,13 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er } } - if srv.AutoHTTPS.PreferWildcard { - wildcards := make(map[string]struct{}) - for d := range serverDomainSet { - if strings.HasPrefix(d, "*.") { - wildcards[d[2:]] = struct{}{} - } - } - for d := range serverDomainSet { - if strings.HasPrefix(d, "*.") { - continue - } - base := d - if idx := strings.Index(d, "."); idx != -1 { - base = d[idx+1:] - } - if _, ok := wildcards[base]; ok { - delete(serverDomainSet, d) - } - } + // build the list of domains that could be used with ECH (if enabled) + // so the TLS app can know to publish ECH configs for them + echDomains := make([]string, 0, len(serverDomainSet)) + for d := range serverDomainSet { + echDomains = append(echDomains, d) } + app.tlsApp.RegisterServerNames(echDomains) // nothing more to do here if there are no domains that qualify for // automatic HTTPS and there are no explicit TLS connection policies: @@ -283,19 +265,10 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er } } - // we now have a list of all the unique names for which we need certs; - // turn the set into a slice so that phase 2 can use it - app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts)) + // we now have a list of all the unique names for which we need certs var internal, tailscale []string uniqueDomainsLoop: for d := range uniqueDomainsForCerts { - if !isTailscaleDomain(d) { - // whether or not there is already an automation policy for this - // name, we should add it to the list to manage a cert for it, - // unless it's a Tailscale domain, because we don't manage those - app.allCertDomains = append(app.allCertDomains, d) - } - // some names we've found might already have automation policies // explicitly specified for them; we should exclude those from // our hidden/implicit policy, since applying a name to more than @@ -320,11 +293,22 @@ uniqueDomainsLoop: } } - // if no automation policy exists for the name yet, we - // will associate it with an implicit one + // if no automation policy exists for the name yet, we will associate it with an implicit one; + // we handle tailscale domains specially, and we also separate out identifiers that need the + // internal issuer (self-signed certs); certmagic does not consider public IP addresses to be + // disqualified for public certs, because there are public CAs that will issue certs for IPs. + // However, with auto-HTTPS, many times there is no issuer explicitly defined, and the default + // issuers do not (currently, as of 2024) issue IP certificates; so assign all IP subjects to + // the internal issuer when there are no explicit automation policies + shouldUseInternal := func(ident string) bool { + usingDefaultIssuersAndIsIP := certmagic.SubjectIsIP(ident) && + (app.tlsApp == nil || app.tlsApp.Automation == nil || len(app.tlsApp.Automation.Policies) == 0) + return !certmagic.SubjectQualifiesForPublicCert(d) || usingDefaultIssuersAndIsIP + } if isTailscaleDomain(d) { tailscale = append(tailscale, d) - } else if !certmagic.SubjectQualifiesForPublicCert(d) { + delete(uniqueDomainsForCerts, d) // not managed by us; handled separately + } else if shouldUseInternal(d) { internal = append(internal, d) } } @@ -359,7 +343,7 @@ uniqueDomainsLoop: // match on known domain names, unless it's our special case of a // catch-all which is an empty string (common among catch-all sites // that enable on-demand TLS for yet-unknown domain names) - if !(len(domains) == 1 && domains[0] == "") { + if len(domains) != 1 || domains[0] != "" { matcherSet = append(matcherSet, MatchHost(domains)) } @@ -453,6 +437,9 @@ redirServersLoop: } } + // persist the domains/IPs we're managing certs for through provisioning/startup + app.allCertDomains = uniqueDomainsForCerts + logger.Debug("adjusted config", zap.Reflect("tls", app.tlsApp), zap.Reflect("http", app)) @@ -755,7 +742,7 @@ func (app *App) automaticHTTPSPhase2() error { return nil } app.logger.Info("enabling automatic TLS certificate management", - zap.Strings("domains", app.allCertDomains), + zap.Strings("domains", internal.MaxSizeSubjectsListForLog(app.allCertDomains, 1000)), ) err := app.tlsApp.Manage(app.allCertDomains) if err != nil { diff --git a/modules/caddyhttp/caddyauth/basicauth.go b/modules/caddyhttp/caddyauth/basicauth.go index 52a5a08c1..5a9e167e1 100644 --- a/modules/caddyhttp/caddyauth/basicauth.go +++ b/modules/caddyhttp/caddyauth/basicauth.go @@ -236,10 +236,7 @@ func (c *Cache) makeRoom() { // the cache is on a long tail, we can save a lot of CPU // time by doing a whole bunch of deletions now and then // we won't have to do them again for a while - numToDelete := len(c.cache) / 10 - if numToDelete < 1 { - numToDelete = 1 - } + numToDelete := max(len(c.cache)/10, 1) for deleted := 0; deleted <= numToDelete; deleted++ { // Go maps are "nondeterministic" not actually random, // so although we could just chop off the "front" of the diff --git a/modules/caddyhttp/caddyauth/caddyauth.go b/modules/caddyhttp/caddyauth/caddyauth.go index f799d7a0c..792c198ee 100644 --- a/modules/caddyhttp/caddyauth/caddyauth.go +++ b/modules/caddyhttp/caddyauth/caddyauth.go @@ -37,6 +37,10 @@ func init() { // `{http.auth.user.*}` placeholders may be set for any authentication // modules that provide user metadata. // +// In case of an error, the placeholder `{http.auth..error}` +// will be set to the error message returned by the authentication +// provider. +// // Its API is still experimental and may be subject to change. type Authentication struct { // A set of authentication providers. If none are specified, @@ -56,7 +60,8 @@ func (Authentication) CaddyModule() caddy.ModuleInfo { } } -// Provision sets up a. +// Provision sets up an Authentication module by initializing its logger, +// loading and registering all configured authentication providers. func (a *Authentication) Provision(ctx caddy.Context) error { a.logger = ctx.Logger() a.Providers = make(map[string]Authenticator) @@ -71,6 +76,7 @@ func (a *Authentication) Provision(ctx caddy.Context) error { } func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { + repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) var user User var authed bool var err error @@ -80,6 +86,9 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c if c := a.logger.Check(zapcore.ErrorLevel, "auth provider returned error"); c != nil { c.Write(zap.String("provider", provName), zap.Error(err)) } + // Set the error from the authentication provider in a placeholder, + // so it can be used in the handle_errors directive. + repl.Set("http.auth."+provName+".error", err.Error()) continue } if authed { @@ -90,7 +99,6 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated")) } - repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl.Set("http.auth.user.id", user.ID) for k, v := range user.Metadata { repl.Set("http.auth.user."+k, v) diff --git a/modules/caddyhttp/caddyauth/command.go b/modules/caddyhttp/caddyauth/command.go index c9f440060..2acaee6c4 100644 --- a/modules/caddyhttp/caddyauth/command.go +++ b/modules/caddyhttp/caddyauth/command.go @@ -32,7 +32,7 @@ import ( func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "hash-password", - Usage: "[--plaintext ] [--algorithm ]", + Usage: "[--plaintext ] [--algorithm ] [--bcrypt-cost ]", Short: "Hashes a password and writes base64", Long: ` Convenient way to hash a plaintext password. The resulting @@ -43,10 +43,17 @@ Caddy is attached to a controlling tty, the plaintext will not be echoed. --algorithm currently only supports 'bcrypt', and is the default. + +--bcrypt-cost sets the bcrypt hashing difficulty. +Higher values increase security by making the hash computation slower and more CPU-intensive. +If the provided cost is not within the valid range [bcrypt.MinCost, bcrypt.MaxCost], +the default value (defaultBcryptCost) will be used instead. +Note: Higher cost values can significantly degrade performance on slower systems. `, CobraFunc: func(cmd *cobra.Command) { cmd.Flags().StringP("plaintext", "p", "", "The plaintext password") cmd.Flags().StringP("algorithm", "a", "bcrypt", "Name of the hash algorithm") + cmd.Flags().Int("bcrypt-cost", defaultBcryptCost, "Bcrypt hashing cost (only used with 'bcrypt' algorithm)") cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdHashPassword) }, }) @@ -57,6 +64,7 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) { algorithm := fs.String("algorithm") plaintext := []byte(fs.String("plaintext")) + bcryptCost := fs.Int("bcrypt-cost") if len(plaintext) == 0 { fd := int(os.Stdin.Fd()) @@ -108,7 +116,7 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) { var hashString string switch algorithm { case "bcrypt": - hash, err = BcryptHash{}.Hash(plaintext) + hash, err = BcryptHash{cost: bcryptCost}.Hash(plaintext) hashString = string(hash) default: return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm) diff --git a/modules/caddyhttp/caddyauth/hashes.go b/modules/caddyhttp/caddyauth/hashes.go index ce3df901e..ea91f24e2 100644 --- a/modules/caddyhttp/caddyauth/hashes.go +++ b/modules/caddyhttp/caddyauth/hashes.go @@ -15,6 +15,8 @@ package caddyauth import ( + "errors" + "golang.org/x/crypto/bcrypt" "github.com/caddyserver/caddy/v2" @@ -24,8 +26,15 @@ func init() { caddy.RegisterModule(BcryptHash{}) } +// defaultBcryptCost cost 14 strikes a solid balance between security, usability, and hardware performance +const defaultBcryptCost = 14 + // BcryptHash implements the bcrypt hash. -type BcryptHash struct{} +type BcryptHash struct { + // cost is the bcrypt hashing difficulty factor (work factor). + // Higher values increase computation time and security. + cost int +} // CaddyModule returns the Caddy module information. func (BcryptHash) CaddyModule() caddy.ModuleInfo { @@ -38,7 +47,7 @@ func (BcryptHash) CaddyModule() caddy.ModuleInfo { // Compare compares passwords. func (BcryptHash) Compare(hashed, plaintext []byte) (bool, error) { err := bcrypt.CompareHashAndPassword(hashed, plaintext) - if err == bcrypt.ErrMismatchedHashAndPassword { + if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { return false, nil } if err != nil { @@ -48,8 +57,13 @@ func (BcryptHash) Compare(hashed, plaintext []byte) (bool, error) { } // Hash hashes plaintext using a random salt. -func (BcryptHash) Hash(plaintext []byte) ([]byte, error) { - return bcrypt.GenerateFromPassword(plaintext, 14) +func (b BcryptHash) Hash(plaintext []byte) ([]byte, error) { + cost := b.cost + if cost < bcrypt.MinCost || cost > bcrypt.MaxCost { + cost = defaultBcryptCost + } + + return bcrypt.GenerateFromPassword(plaintext, cost) } // FakeHash returns a fake hash. diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index e1e71f4a0..aacafc92e 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -36,10 +36,26 @@ func init() { // RequestMatcher is a type that can match to a request. // A route matcher MUST NOT modify the request, with the // only exception being its context. +// +// Deprecated: Matchers should now implement RequestMatcherWithError. +// You may remove any interface guards for RequestMatcher +// but keep your Match() methods for backwards compatibility. type RequestMatcher interface { Match(*http.Request) bool } +// RequestMatcherWithError is like RequestMatcher but can return an error. +// An error during matching will abort the request middleware chain and +// invoke the error middleware chain. +// +// This will eventually replace RequestMatcher. Matcher modules +// should implement both interfaces, and once all modules have +// been updated to use RequestMatcherWithError, the RequestMatcher +// interface may eventually be dropped. +type RequestMatcherWithError interface { + MatchWithError(*http.Request) (bool, error) +} + // Handler is like http.Handler except ServeHTTP may return an error. // // If any handler encounters an error, it should be returned for proper diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go index 2a03ebba7..3d118ea79 100644 --- a/modules/caddyhttp/celmatcher.go +++ b/modules/caddyhttp/celmatcher.go @@ -202,17 +202,25 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error { // Match returns true if r matches m. func (m MatchExpression) Match(r *http.Request) bool { + match, err := m.MatchWithError(r) + if err != nil { + SetVar(r.Context(), MatcherErrorVarKey, err) + } + return match +} + +// MatchWithError returns true if r matches m. +func (m MatchExpression) MatchWithError(r *http.Request) (bool, error) { celReq := celHTTPRequest{r} out, _, err := m.prg.Eval(celReq) if err != nil { m.log.Error("evaluating expression", zap.Error(err)) - SetVar(r.Context(), MatcherErrorVarKey, err) - return false + return false, err } if outBool, ok := out.Value().(bool); ok { - return outBool + return outBool, nil } - return false + return false, nil } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. @@ -380,7 +388,7 @@ type CELLibraryProducer interface { // limited set of function signatures. For strong type validation you may need // to provide a custom macro which does a more detailed analysis of the CEL // literal provided to the macro as an argument. -func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fac CELMatcherFactory) (cel.Library, error) { +func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fac any) (cel.Library, error) { requestType := cel.ObjectType("http.Request") var macro parser.Macro switch len(matcherDataTypes) { @@ -424,7 +432,11 @@ func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fa } // CELMatcherFactory converts a constant CEL value into a RequestMatcher. -type CELMatcherFactory func(data ref.Val) (RequestMatcher, error) +// Deprecated: Use CELMatcherWithErrorFactory instead. +type CELMatcherFactory = func(data ref.Val) (RequestMatcher, error) + +// CELMatcherWithErrorFactory converts a constant CEL value into a RequestMatcherWithError. +type CELMatcherWithErrorFactory = func(data ref.Val) (RequestMatcherWithError, error) // matcherCELLibrary is a simplistic configurable cel.Library implementation. type matcherCELLibrary struct { @@ -452,7 +464,7 @@ func (lib *matcherCELLibrary) ProgramOptions() []cel.ProgramOption { // that takes a single argument, and optimizes the implementation to precompile // the matcher and return a function that references the precompiled and // provisioned matcher. -func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.InterpretableDecorator { +func CELMatcherDecorator(funcName string, fac any) interpreter.InterpretableDecorator { return func(i interpreter.Interpretable) (interpreter.Interpretable, error) { call, ok := i.(interpreter.InterpretableCall) if !ok { @@ -481,35 +493,92 @@ func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.Int // and matcher provisioning should be handled at dynamically. return i, nil } - matcher, err := fac(matcherData.Value()) - if err != nil { - return nil, err + + if factory, ok := fac.(CELMatcherWithErrorFactory); ok { + matcher, err := factory(matcherData.Value()) + if err != nil { + return nil, err + } + return interpreter.NewCall( + i.ID(), funcName, funcName+"_opt", + []interpreter.Interpretable{reqAttr}, + func(args ...ref.Val) ref.Val { + // The request value, guaranteed to be of type celHTTPRequest + celReq := args[0] + // If needed this call could be changed to convert the value + // to a *http.Request using CEL's ConvertToNative method. + httpReq := celReq.Value().(celHTTPRequest) + match, err := matcher.MatchWithError(httpReq.Request) + if err != nil { + return types.WrapErr(err) + } + return types.Bool(match) + }, + ), nil } - return interpreter.NewCall( - i.ID(), funcName, funcName+"_opt", - []interpreter.Interpretable{reqAttr}, - func(args ...ref.Val) ref.Val { - // The request value, guaranteed to be of type celHTTPRequest - celReq := args[0] - // If needed this call could be changed to convert the value - // to a *http.Request using CEL's ConvertToNative method. - httpReq := celReq.Value().(celHTTPRequest) - return types.Bool(matcher.Match(httpReq.Request)) - }, - ), nil + + if factory, ok := fac.(CELMatcherFactory); ok { + matcher, err := factory(matcherData.Value()) + if err != nil { + return nil, err + } + return interpreter.NewCall( + i.ID(), funcName, funcName+"_opt", + []interpreter.Interpretable{reqAttr}, + func(args ...ref.Val) ref.Val { + // The request value, guaranteed to be of type celHTTPRequest + celReq := args[0] + // If needed this call could be changed to convert the value + // to a *http.Request using CEL's ConvertToNative method. + httpReq := celReq.Value().(celHTTPRequest) + if m, ok := matcher.(RequestMatcherWithError); ok { + match, err := m.MatchWithError(httpReq.Request) + if err != nil { + return types.WrapErr(err) + } + return types.Bool(match) + } + return types.Bool(matcher.Match(httpReq.Request)) + }, + ), nil + } + + return nil, fmt.Errorf("invalid matcher factory, must be CELMatcherFactory or CELMatcherWithErrorFactory: %T", fac) } } // CELMatcherRuntimeFunction creates a function binding for when the input to the matcher // is dynamically resolved rather than a set of static constant values. -func CELMatcherRuntimeFunction(funcName string, fac CELMatcherFactory) functions.BinaryOp { +func CELMatcherRuntimeFunction(funcName string, fac any) functions.BinaryOp { return func(celReq, matcherData ref.Val) ref.Val { - matcher, err := fac(matcherData) - if err != nil { - return types.WrapErr(err) + if factory, ok := fac.(CELMatcherWithErrorFactory); ok { + matcher, err := factory(matcherData) + if err != nil { + return types.WrapErr(err) + } + httpReq := celReq.Value().(celHTTPRequest) + match, err := matcher.MatchWithError(httpReq.Request) + if err != nil { + return types.WrapErr(err) + } + return types.Bool(match) } - httpReq := celReq.Value().(celHTTPRequest) - return types.Bool(matcher.Match(httpReq.Request)) + if factory, ok := fac.(CELMatcherFactory); ok { + matcher, err := factory(matcherData) + if err != nil { + return types.WrapErr(err) + } + httpReq := celReq.Value().(celHTTPRequest) + if m, ok := matcher.(RequestMatcherWithError); ok { + match, err := m.MatchWithError(httpReq.Request) + if err != nil { + return types.WrapErr(err) + } + return types.Bool(match) + } + return types.Bool(matcher.Match(httpReq.Request)) + } + return types.NewErr("CELMatcherRuntimeFunction invalid matcher factory: %T", fac) } } @@ -733,9 +802,9 @@ const MatcherNameCtxKey = "matcher_name" // Interface guards var ( - _ caddy.Provisioner = (*MatchExpression)(nil) - _ RequestMatcher = (*MatchExpression)(nil) - _ caddyfile.Unmarshaler = (*MatchExpression)(nil) - _ json.Marshaler = (*MatchExpression)(nil) - _ json.Unmarshaler = (*MatchExpression)(nil) + _ caddy.Provisioner = (*MatchExpression)(nil) + _ RequestMatcherWithError = (*MatchExpression)(nil) + _ caddyfile.Unmarshaler = (*MatchExpression)(nil) + _ json.Marshaler = (*MatchExpression)(nil) + _ json.Unmarshaler = (*MatchExpression)(nil) ) diff --git a/modules/caddyhttp/celmatcher_test.go b/modules/caddyhttp/celmatcher_test.go index 26491b7ca..1bd8e527e 100644 --- a/modules/caddyhttp/celmatcher_test.go +++ b/modules/caddyhttp/celmatcher_test.go @@ -489,7 +489,11 @@ func TestMatchExpressionMatch(t *testing.T) { } } - if tc.expression.Match(req) != tc.wantResult { + matches, err := tc.expression.MatchWithError(req) + if err != nil { + t.Errorf("MatchExpression.Match() error = %v", err) + } + if matches != tc.wantResult { t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr) } }) @@ -531,8 +535,8 @@ func BenchmarkMatchExpressionMatch(b *testing.B) { } } b.ResetTimer() - for i := 0; i < b.N; i++ { - tc.expression.Match(req) + for b.Loop() { + tc.expression.MatchWithError(req) } }) } diff --git a/modules/caddyhttp/encode/caddyfile.go b/modules/caddyhttp/encode/caddyfile.go index e8ea4b807..8b8657080 100644 --- a/modules/caddyhttp/encode/caddyfile.go +++ b/modules/caddyhttp/encode/caddyfile.go @@ -57,21 +57,7 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { d.Next() // consume directive name prefer := []string{} - for _, arg := range d.RemainingArgs() { - mod, err := caddy.GetModule("http.encoders." + arg) - if err != nil { - return d.Errf("finding encoder module '%s': %v", mod, err) - } - encoding, ok := mod.New().(Encoding) - if !ok { - return d.Errf("module %s is not an HTTP encoding", mod) - } - if enc.EncodingsRaw == nil { - enc.EncodingsRaw = make(caddy.ModuleMap) - } - enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil) - prefer = append(prefer, arg) - } + remainingArgs := d.RemainingArgs() responseMatchers := make(map[string]caddyhttp.ResponseMatcher) for d.NextBlock(0) { @@ -111,6 +97,26 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } } + if len(prefer) == 0 && len(remainingArgs) == 0 { + remainingArgs = []string{"zstd", "gzip"} + } + + for _, arg := range remainingArgs { + mod, err := caddy.GetModule("http.encoders." + arg) + if err != nil { + return d.Errf("finding encoder module '%s': %v", mod, err) + } + encoding, ok := mod.New().(Encoding) + if !ok { + return d.Errf("module %s is not an HTTP encoding", mod) + } + if enc.EncodingsRaw == nil { + enc.EncodingsRaw = make(caddy.ModuleMap) + } + enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil) + prefer = append(prefer, arg) + } + // use the order in which the encoders were defined. enc.Prefer = prefer diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index f0d56a90d..67efdd0f2 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -92,6 +92,7 @@ func (enc *Encode) Provision(ctx caddy.Context) error { "application/font*", "application/geo+json*", "application/graphql+json*", + "application/graphql-response+json*", "application/javascript*", "application/json*", "application/ld+json*", @@ -156,7 +157,7 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh if _, ok := enc.writerPools[encName]; !ok { continue // encoding not offered } - w = enc.openResponseWriter(encName, w) + w = enc.openResponseWriter(encName, w, r.Method == http.MethodConnect) defer w.(*responseWriter).Close() // to comply with RFC 9110 section 8.8.3(.3), we modify the Etag when encoding @@ -201,14 +202,14 @@ func (enc *Encode) addEncoding(e Encoding) error { // openResponseWriter creates a new response writer that may (or may not) // encode the response with encodingName. The returned response writer MUST // be closed after the handler completes. -func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter) *responseWriter { +func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter, isConnect bool) *responseWriter { var rw responseWriter - return enc.initResponseWriter(&rw, encodingName, w) + return enc.initResponseWriter(&rw, encodingName, w, isConnect) } // initResponseWriter initializes the responseWriter instance // allocated in openResponseWriter, enabling mid-stack inlining. -func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter { +func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter, isConnect bool) *responseWriter { if rww, ok := wrappedRW.(*caddyhttp.ResponseWriterWrapper); ok { rw.ResponseWriter = rww } else { @@ -216,6 +217,7 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w } rw.encodingName = encodingName rw.config = enc + rw.isConnect = isConnect return rw } @@ -230,6 +232,7 @@ type responseWriter struct { config *Encode statusCode int wroteHeader bool + isConnect bool } // WriteHeader stores the status to write when the time comes @@ -245,6 +248,14 @@ func (rw *responseWriter) WriteHeader(status int) { rw.Header().Add("Vary", "Accept-Encoding") } + // write status immediately if status is 2xx and the request is CONNECT + // since it means the response is successful. + // see: https://github.com/caddyserver/caddy/issues/6733#issuecomment-2525058845 + if rw.isConnect && 200 <= status && status <= 299 { + rw.ResponseWriter.WriteHeader(status) + rw.wroteHeader = true + } + // write status immediately when status code is informational // see: https://caddy.community/t/disappear-103-early-hints-response-with-encode-enable-caddy-v2-7-6/23081/5 if 100 <= status && status <= 199 { @@ -260,6 +271,12 @@ func (enc *Encode) Match(rw *responseWriter) bool { // FlushError is an alternative Flush returning an error. It delays the actual Flush of the underlying // ResponseWriterWrapper until headers were written. func (rw *responseWriter) FlushError() error { + // WriteHeader wasn't called and is a CONNECT request, treat it as a success. + // otherwise, wait until header is written. + if rw.isConnect && !rw.wroteHeader && rw.statusCode == 0 { + rw.WriteHeader(http.StatusOK) + } + if !rw.wroteHeader { // flushing the underlying ResponseWriter will write header and status code, // but we need to delay that until we can determine if we must encode and @@ -288,6 +305,12 @@ func (rw *responseWriter) Write(p []byte) (int, error) { return 0, nil } + // WriteHeader wasn't called and is a CONNECT request, treat it as a success. + // otherwise, determine if the response should be compressed. + if rw.isConnect && !rw.wroteHeader && rw.statusCode == 0 { + rw.WriteHeader(http.StatusOK) + } + // sniff content-type and determine content-length if !rw.wroteHeader && rw.config.MinLength > 0 { var gtMinLength bool @@ -325,6 +348,49 @@ func (rw *responseWriter) Write(p []byte) (int, error) { } } +// used to mask ReadFrom method +type writerOnly struct { + io.Writer +} + +// copied from stdlib +const sniffLen = 512 + +// ReadFrom will try to use sendfile to copy from the reader to the response writer. +// It's only used if the response writer implements io.ReaderFrom and the data can't be compressed. +// It's based on stdlin http1.1 response writer implementation. +// https://github.com/golang/go/blob/f4e3ec3dbe3b8e04a058d266adf8e048bab563f2/src/net/http/server.go#L586 +func (rw *responseWriter) ReadFrom(r io.Reader) (int64, error) { + rf, ok := rw.ResponseWriter.(io.ReaderFrom) + // sendfile can't be used anyway + if !ok { + // mask ReadFrom to avoid infinite recursion + return io.Copy(writerOnly{rw}, r) + } + + var ns int64 + // try to sniff the content type and determine if the response should be compressed + if !rw.wroteHeader && rw.config.MinLength > 0 { + var ( + err error + buf [sniffLen]byte + ) + // mask ReadFrom to let Write determine if the response should be compressed + ns, err = io.CopyBuffer(writerOnly{rw}, io.LimitReader(r, sniffLen), buf[:]) + if err != nil || ns < sniffLen { + return ns, err + } + } + + // the response will be compressed, no sendfile support + if rw.w != nil { + nr, err := io.Copy(rw.w, r) + return nr + ns, err + } + nr, err := rf.ReadFrom(r) + return nr + ns, err +} + // Close writes any remaining buffered response and // deallocates any active resources. func (rw *responseWriter) Close() error { @@ -387,8 +453,7 @@ func (rw *responseWriter) init() { func hasVaryValue(hdr http.Header, target string) bool { for _, vary := range hdr.Values("Vary") { - vals := strings.Split(vary, ",") - for _, val := range vals { + for val := range strings.SplitSeq(vary, ",") { if strings.EqualFold(strings.TrimSpace(val), target) { return true } @@ -413,7 +478,7 @@ func AcceptedEncodings(r *http.Request, preferredOrder []string) []string { prefs := []encodingPreference{} - for _, accepted := range strings.Split(acceptEncHeader, ",") { + for accepted := range strings.SplitSeq(acceptEncHeader, ",") { parts := strings.Split(accepted, ";") encName := strings.ToLower(strings.TrimSpace(parts[0])) diff --git a/modules/caddyhttp/encode/encode_test.go b/modules/caddyhttp/encode/encode_test.go index d76945498..818f76745 100644 --- a/modules/caddyhttp/encode/encode_test.go +++ b/modules/caddyhttp/encode/encode_test.go @@ -2,14 +2,15 @@ package encode import ( "net/http" + "slices" "sync" "testing" ) func BenchmarkOpenResponseWriter(b *testing.B) { enc := new(Encode) - for n := 0; n < b.N; n++ { - enc.openResponseWriter("test", nil) + for b.Loop() { + enc.openResponseWriter("test", nil, false) } } @@ -112,7 +113,7 @@ func TestPreferOrder(t *testing.T) { } enc.Prefer = test.prefer result := AcceptedEncodings(r, enc.Prefer) - if !sliceEqual(result, test.expected) { + if !slices.Equal(result, test.expected) { t.Errorf("AcceptedEncodings() actual: %s expected: %s", result, test.expected) @@ -121,18 +122,6 @@ func TestPreferOrder(t *testing.T) { } } -func sliceEqual(a, b []string) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i] != b[i] { - return false - } - } - return true -} - func TestValidate(t *testing.T) { type testCase struct { name string diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go index a19b4e17a..52aa7a9f8 100644 --- a/modules/caddyhttp/fileserver/browse.go +++ b/modules/caddyhttp/fileserver/browse.go @@ -66,8 +66,15 @@ type Browse struct { // - `sort size` will sort by size in ascending order // The first option must be `sort_by` and the second option must be `order` (if exists). SortOptions []string `json:"sort,omitempty"` + + // FileLimit limits the number of up to n DirEntry values in directory order. + FileLimit int `json:"file_limit,omitempty"` } +const ( + defaultDirEntryLimit = 10000 +) + func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { if c := fsrv.logger.Check(zapcore.DebugLevel, "browse enabled; listing directory contents"); c != nil { c.Write(zap.String("path", dirPath), zap.String("root", root)) @@ -123,9 +130,9 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht // speed up browser/client experience and caching by supporting If-Modified-Since if ifModSinceStr := r.Header.Get("If-Modified-Since"); ifModSinceStr != "" { - ifModSince, err := time.ParseInLocation(http.TimeFormat, ifModSinceStr, time.Local) - lastModTrunc := listing.lastModified.Truncate(time.Second) - if err == nil && (lastModTrunc.Equal(ifModSince) || lastModTrunc.Before(ifModSince)) { + // basically a copy of stdlib file server's handling of If-Modified-Since + ifModSince, err := http.ParseTime(ifModSinceStr) + if err == nil && listing.lastModified.Truncate(time.Second).Compare(ifModSince) <= 0 { w.WriteHeader(http.StatusNotModified) return nil } @@ -206,7 +213,16 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht } func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs.FS, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) { - files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable + // modTime for the directory itself + stat, err := dir.Stat() + if err != nil { + return nil, err + } + dirLimit := defaultDirEntryLimit + if fsrv.Browse.FileLimit != 0 { + dirLimit = fsrv.Browse.FileLimit + } + files, err := dir.ReadDir(dirLimit) if err != nil && err != io.EOF { return nil, err } @@ -214,7 +230,7 @@ func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs // user can presumably browse "up" to parent folder if path is longer than "/" canGoUp := len(urlPath) > 1 - return fsrv.directoryListing(ctx, fileSystem, files, canGoUp, root, urlPath, repl), nil + return fsrv.directoryListing(ctx, fileSystem, stat.ModTime(), files, canGoUp, root, urlPath, repl), nil } // browseApplyQueryParams applies query parameters to the listing. diff --git a/modules/caddyhttp/fileserver/browse.html b/modules/caddyhttp/fileserver/browse.html index d2d698197..5d9dc7dbe 100644 --- a/modules/caddyhttp/fileserver/browse.html +++ b/modules/caddyhttp/fileserver/browse.html @@ -26,7 +26,7 @@ - {{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg"}} + {{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg" ".avif"}} {{- if eq .Tpl.Layout "grid"}} {{- else}} @@ -802,7 +802,7 @@ footer { {{.NumFiles}} file{{if ne 1 .NumFiles}}s{{end}} - {{.HumanTotalFileSize}} total + {{.HumanTotalFileSize}} total {{- if ne 0 .Limit}} @@ -828,6 +828,96 @@ footer { Grid + {{- if and (eq .Layout "grid") (eq .Sort "name") (ne .Order "asc")}} + + + Z + A + + + + + {{- else if and (eq .Layout "grid") (eq .Sort "name") (ne .Order "desc")}} + + + A + Z + + + + + {{- else if and (eq .Layout "grid")}} + + + A + Z + + + + + {{- end}} + {{- if and (eq .Layout "grid") (eq .Sort "size") (ne .Order "asc")}} + + + + + + + + + + {{- else if and (eq .Layout "grid") (eq .Sort "size") (ne .Order "desc")}} + + + + + + + + + + {{- else if and (eq .Layout "grid")}} + + + + + + + + + + {{- end}} + {{- if and (eq .Layout "grid") (eq .Sort "time") (ne .Order "asc")}} + + + + + + + + + + {{- else if and (eq .Layout "grid") (eq .Sort "time") (ne .Order "desc")}} + + + + + + + + + + {{- else if and (eq .Layout "grid")}} + + + + + + + + + + {{- end}}

{{- if eq .Layout "grid"}} @@ -868,7 +958,7 @@ footer { {{- end}} - + {{- if and (eq .Sort "name") (ne .Order "desc")}} Name @@ -1086,6 +1176,7 @@ footer { diff --git a/modules/caddyhttp/fileserver/browsetplcontext.go b/modules/caddyhttp/fileserver/browsetplcontext.go index 8e5d138f1..b9489c6a6 100644 --- a/modules/caddyhttp/fileserver/browsetplcontext.go +++ b/modules/caddyhttp/fileserver/browsetplcontext.go @@ -35,15 +35,16 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) -func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext { +func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS, parentModTime time.Time, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext { filesToHide := fsrv.transformHidePaths(repl) name, _ := url.PathUnescape(urlPath) tplCtx := &browseTemplateContext{ - Name: path.Base(name), - Path: urlPath, - CanGoUp: canGoUp, + Name: path.Base(name), + Path: urlPath, + CanGoUp: canGoUp, + lastModified: parentModTime, } for _, entry := range entries { @@ -131,6 +132,10 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS, }) } + // this time is used for the Last-Modified header and comparing If-Modified-Since from client + // both are expected to be in UTC, so we convert to UTC here + // see: https://github.com/caddyserver/caddy/issues/6828 + tplCtx.lastModified = tplCtx.lastModified.UTC() return tplCtx } diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go index 71b7638e4..80a37322b 100644 --- a/modules/caddyhttp/fileserver/caddyfile.go +++ b/modules/caddyhttp/fileserver/caddyfile.go @@ -16,6 +16,7 @@ package fileserver import ( "path/filepath" + "strconv" "strings" "github.com/caddyserver/caddy/v2" @@ -78,7 +79,7 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } - for d.NextBlock(0) { + for nesting := d.Nesting(); d.NextBlock(nesting); { switch d.Val() { case "fs": if !d.NextArg() { @@ -129,15 +130,29 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.Errf("unknown sort option '%s'", dVal) } } + case "file_limit": + fileLimit := d.RemainingArgs() + if len(fileLimit) != 1 { + return d.Err("file_limit should have an integer value") + } + val, _ := strconv.Atoi(fileLimit[0]) + if fsrv.Browse.FileLimit != 0 { + return d.Err("file_limit is already enabled") + } + fsrv.Browse.FileLimit = val default: return d.Errf("unknown subdirective '%s'", d.Val()) } } case "precompressed": - var order []string - for d.NextArg() { - modID := "http.precompressed." + d.Val() + fsrv.PrecompressedOrder = d.RemainingArgs() + if len(fsrv.PrecompressedOrder) == 0 { + fsrv.PrecompressedOrder = []string{"br", "zstd", "gzip"} + } + + for _, format := range fsrv.PrecompressedOrder { + modID := "http.precompressed." + format mod, err := caddy.GetModule(modID) if err != nil { return d.Errf("getting module named '%s': %v", modID, err) @@ -150,10 +165,8 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if fsrv.PrecompressedRaw == nil { fsrv.PrecompressedRaw = make(caddy.ModuleMap) } - fsrv.PrecompressedRaw[d.Val()] = caddyconfig.JSON(precompress, nil) - order = append(order, d.Val()) + fsrv.PrecompressedRaw[format] = caddyconfig.JSON(precompress, nil) } - fsrv.PrecompressedOrder = order case "status": if !d.NextArg() { @@ -263,7 +276,7 @@ func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) tryPolicy = h.Val() switch tryPolicy { - case tryPolicyFirstExist, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod: + case tryPolicyFirstExist, tryPolicyFirstExistFallback, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod: default: return nil, h.Errf("unrecognized try policy: %s", tryPolicy) } diff --git a/modules/caddyhttp/fileserver/command.go b/modules/caddyhttp/fileserver/command.go index a76998405..a04d7cade 100644 --- a/modules/caddyhttp/fileserver/command.go +++ b/modules/caddyhttp/fileserver/command.go @@ -66,6 +66,7 @@ respond with a file listing.`, cmd.Flags().BoolP("templates", "t", false, "Enable template rendering") cmd.Flags().BoolP("access-log", "a", false, "Enable the access log") cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs") + cmd.Flags().IntP("file-limit", "f", defaultDirEntryLimit, "Max directories to read") cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard and Gzip compression") cmd.Flags().StringSliceP("precompressed", "p", []string{}, "Specify precompression file extensions. Compression preference implied from flag order.") cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdFileServer) @@ -91,6 +92,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) { browse := fs.Bool("browse") templates := fs.Bool("templates") accessLog := fs.Bool("access-log") + fileLimit := fs.Int("file-limit") debug := fs.Bool("debug") revealSymlinks := fs.Bool("reveal-symlinks") compress := !fs.Bool("no-compress") @@ -151,7 +153,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) { } if browse { - handler.Browse = &Browse{RevealSymlinks: revealSymlinks} + handler.Browse = &Browse{RevealSymlinks: revealSymlinks, FileLimit: fileLimit} } handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "file_server", nil)) diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index 71de1db29..fbcd36e0a 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -90,6 +90,7 @@ type MatchFile struct { // How to choose a file in TryFiles. Can be: // // - first_exist + // - first_exist_fallback // - smallest_size // - largest_size // - most_recently_modified @@ -173,7 +174,7 @@ func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) { requestType := cel.ObjectType("http.Request") - matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcher, error) { + matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcherWithError, error) { values, err := caddyhttp.CELValueToMapStrList(data) if err != nil { return nil, err @@ -191,7 +192,7 @@ func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) { var try_policy string if len(values["try_policy"]) > 0 { - root = values["try_policy"][0] + try_policy = values["try_policy"][0] } m := MatchFile{ @@ -251,7 +252,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander { } for _, arg := range args { - if !(isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg)) { + if !isCELStringLiteral(arg) && !isCELCaddyPlaceholderCall(arg) { return nil, &common.Error{ Location: eh.OffsetLocation(arg.ID()), Message: "matcher only supports repeated string literal arguments", @@ -273,7 +274,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander { func (m *MatchFile) Provision(ctx caddy.Context) error { m.logger = ctx.Logger() - m.fsmap = ctx.Filesystems() + m.fsmap = ctx.FileSystems() if m.Root == "" { m.Root = "{http.vars.root}" @@ -296,6 +297,7 @@ func (m MatchFile) Validate() error { switch m.TryPolicy { case "", tryPolicyFirstExist, + tryPolicyFirstExistFallback, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod: @@ -313,12 +315,22 @@ func (m MatchFile) Validate() error { // - http.matchers.file.type: file or directory // - http.matchers.file.remainder: Portion remaining after splitting file path (if configured) func (m MatchFile) Match(r *http.Request) bool { + match, err := m.selectFile(r) + if err != nil { + // nolint:staticcheck + caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err) + } + return match +} + +// MatchWithError returns true if r matches m. +func (m MatchFile) MatchWithError(r *http.Request) (bool, error) { return m.selectFile(r) } // selectFile chooses a file according to m.TryPolicy by appending // the paths in m.TryFiles to m.Root, with placeholder replacements. -func (m MatchFile) selectFile(r *http.Request) (matched bool) { +func (m MatchFile) selectFile(r *http.Request) (bool, error) { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) root := filepath.Clean(repl.ReplaceAll(m.Root, ".")) @@ -330,7 +342,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { if c := m.logger.Check(zapcore.ErrorLevel, "use of unregistered filesystem"); c != nil { c.Write(zap.String("fs", fsName)) } - return false + return false, nil } type matchCandidate struct { fullpath, relative, splitRemainder string @@ -405,13 +417,13 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { } // setPlaceholders creates the placeholders for the matched file - setPlaceholders := func(candidate matchCandidate, info fs.FileInfo) { + setPlaceholders := func(candidate matchCandidate, isDir bool) { repl.Set("http.matchers.file.relative", filepath.ToSlash(candidate.relative)) repl.Set("http.matchers.file.absolute", filepath.ToSlash(candidate.fullpath)) repl.Set("http.matchers.file.remainder", filepath.ToSlash(candidate.splitRemainder)) fileType := "file" - if info.IsDir() { + if isDir { fileType = "directory" } repl.Set("http.matchers.file.type", fileType) @@ -419,17 +431,32 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { // match file according to the configured policy switch m.TryPolicy { - case "", tryPolicyFirstExist: - for _, pattern := range m.TryFiles { + case "", tryPolicyFirstExist, tryPolicyFirstExistFallback: + maxI := -1 + if m.TryPolicy == tryPolicyFirstExistFallback { + maxI = len(m.TryFiles) - 1 + } + + for i, pattern := range m.TryFiles { + // If the pattern is a status code, emit an error, + // which short-circuits the middleware pipeline and + // writes an HTTP error response. if err := parseErrorCode(pattern); err != nil { - caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err) - return + return false, err } + candidates := makeCandidates(pattern) for _, c := range candidates { + // Skip the IO if using fallback policy and it's the latest item + if i == maxI { + setPlaceholders(c, false) + + return true, nil + } + if info, exists := m.strictFileExists(fileSystem, c.fullpath); exists { - setPlaceholders(c, info) - return true + setPlaceholders(c, info.IsDir()) + return true, nil } } } @@ -450,10 +477,10 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { } } if largestInfo == nil { - return false + return false, nil } - setPlaceholders(largest, largestInfo) - return true + setPlaceholders(largest, largestInfo.IsDir()) + return true, nil case tryPolicySmallestSize: var smallestSize int64 @@ -471,10 +498,10 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { } } if smallestInfo == nil { - return false + return false, nil } - setPlaceholders(smallest, smallestInfo) - return true + setPlaceholders(smallest, smallestInfo.IsDir()) + return true, nil case tryPolicyMostRecentlyMod: var recent matchCandidate @@ -491,13 +518,13 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { } } if recentInfo == nil { - return false + return false, nil } - setPlaceholders(recent, recentInfo) - return true + setPlaceholders(recent, recentInfo.IsDir()) + return true, nil } - return + return false, nil } // parseErrorCode checks if the input is a status @@ -589,15 +616,16 @@ func isCELTryFilesLiteral(e ast.Expr) bool { return false } mapKeyStr := mapKey.AsLiteral().ConvertToType(types.StringType).Value() - if mapKeyStr == "try_files" || mapKeyStr == "split_path" { + switch mapKeyStr { + case "try_files", "split_path": if !isCELStringListLiteral(mapVal) { return false } - } else if mapKeyStr == "try_policy" || mapKeyStr == "root" { + case "try_policy", "root": if !(isCELStringExpr(mapVal)) { return false } - } else { + default: return false } } @@ -695,15 +723,16 @@ var globSafeRepl = strings.NewReplacer( ) const ( - tryPolicyFirstExist = "first_exist" - tryPolicyLargestSize = "largest_size" - tryPolicySmallestSize = "smallest_size" - tryPolicyMostRecentlyMod = "most_recently_modified" + tryPolicyFirstExist = "first_exist" + tryPolicyFirstExistFallback = "first_exist_fallback" + tryPolicyLargestSize = "largest_size" + tryPolicySmallestSize = "smallest_size" + tryPolicyMostRecentlyMod = "most_recently_modified" ) // Interface guards var ( - _ caddy.Validator = (*MatchFile)(nil) - _ caddyhttp.RequestMatcher = (*MatchFile)(nil) - _ caddyhttp.CELLibraryProducer = (*MatchFile)(nil) + _ caddy.Validator = (*MatchFile)(nil) + _ caddyhttp.RequestMatcherWithError = (*MatchFile)(nil) + _ caddyhttp.CELLibraryProducer = (*MatchFile)(nil) ) diff --git a/modules/caddyhttp/fileserver/matcher_test.go b/modules/caddyhttp/fileserver/matcher_test.go index 527c16bd1..f0ec4b392 100644 --- a/modules/caddyhttp/fileserver/matcher_test.go +++ b/modules/caddyhttp/fileserver/matcher_test.go @@ -117,7 +117,7 @@ func TestFileMatcher(t *testing.T) { }, } { m := &MatchFile{ - fsmap: &filesystems.FilesystemMap{}, + fsmap: &filesystems.FileSystemMap{}, Root: "./testdata", TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"}, } @@ -130,7 +130,10 @@ func TestFileMatcher(t *testing.T) { req := &http.Request{URL: u} repl := caddyhttp.NewTestReplacer(req) - result := m.Match(req) + result, err := m.MatchWithError(req) + if err != nil { + t.Errorf("Test %d: unexpected error: %v", i, err) + } if result != tc.matched { t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result) } @@ -226,7 +229,7 @@ func TestPHPFileMatcher(t *testing.T) { }, } { m := &MatchFile{ - fsmap: &filesystems.FilesystemMap{}, + fsmap: &filesystems.FileSystemMap{}, Root: "./testdata", TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"}, SplitPath: []string{".php"}, @@ -240,7 +243,10 @@ func TestPHPFileMatcher(t *testing.T) { req := &http.Request{URL: u} repl := caddyhttp.NewTestReplacer(req) - result := m.Match(req) + result, err := m.MatchWithError(req) + if err != nil { + t.Errorf("Test %d: unexpected error: %v", i, err) + } if result != tc.matched { t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result) } @@ -267,7 +273,7 @@ func TestPHPFileMatcher(t *testing.T) { func TestFirstSplit(t *testing.T) { m := MatchFile{ SplitPath: []string{".php"}, - fsmap: &filesystems.FilesystemMap{}, + fsmap: &filesystems.FileSystemMap{}, } actual, remainder := m.firstSplit("index.PHP/somewhere") expected := "index.PHP" @@ -289,6 +295,7 @@ var expressionTests = []struct { wantErr bool wantResult bool clientCertificate []byte + expectedPath string }{ { name: "file error no args (MatchFile)", @@ -354,6 +361,15 @@ var expressionTests = []struct { urlTarget: "https://example.com/nopenope.txt", wantResult: false, }, + { + name: "file match long pattern foo.txt with try_policy (MatchFile)", + expression: &caddyhttp.MatchExpression{ + Expr: `file({"root": "./testdata", "try_policy": "largest_size", "try_files": ["foo.txt", "large.txt"]})`, + }, + urlTarget: "https://example.com/", + wantResult: true, + expectedPath: "/large.txt", + }, } func TestMatchExpressionMatch(t *testing.T) { @@ -379,9 +395,24 @@ func TestMatchExpressionMatch(t *testing.T) { ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) - if tc.expression.Match(req) != tc.wantResult { + matches, err := tc.expression.MatchWithError(req) + if err != nil { + t.Errorf("MatchExpression.Match() error = %v", err) + return + } + if matches != tc.wantResult { t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr) } + + if tc.expectedPath != "" { + path, ok := repl.Get("http.matchers.file.relative") + if !ok { + t.Errorf("MatchExpression.Match() expected to return path '%s', but got none", tc.expectedPath) + } + if path != tc.expectedPath { + t.Errorf("MatchExpression.Match() expected to return path '%s', but got '%s'", tc.expectedPath, path) + } + } }) } } diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 4ae69b647..2777ecdeb 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -167,6 +167,8 @@ type FileServer struct { // If set, file Etags will be read from sidecar files // with any of these suffixes, instead of generating // our own Etag. + // Keep in mind that the Etag values in the files have to be quoted as per RFC7232. + // See https://datatracker.ietf.org/doc/html/rfc7232#section-2.3 for a few examples. EtagFileExtensions []string `json:"etag_file_extensions,omitempty"` fsmap caddy.FileSystems @@ -186,7 +188,7 @@ func (FileServer) CaddyModule() caddy.ModuleInfo { func (fsrv *FileServer) Provision(ctx caddy.Context) error { fsrv.logger = ctx.Logger() - fsrv.fsmap = ctx.Filesystems() + fsrv.fsmap = ctx.FileSystems() if fsrv.FileSystem == "" { fsrv.FileSystem = "{http.vars.fs}" @@ -204,7 +206,7 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error { // absolute paths before the server starts for very slight performance improvement for i, h := range fsrv.Hide { if !strings.Contains(h, "{") && strings.Contains(h, separator) { - if abs, err := filepath.Abs(h); err == nil { + if abs, err := caddy.FastAbs(h); err == nil { fsrv.Hide[i] = abs } } @@ -300,8 +302,10 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c info, err := fs.Stat(fileSystem, filename) if err != nil { err = fsrv.mapDirOpenError(fileSystem, err, filename) - if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid) { + if errors.Is(err, fs.ErrNotExist) { return fsrv.notFound(w, r, next) + } else if errors.Is(err, fs.ErrInvalid) { + return caddyhttp.Error(http.StatusBadRequest, err) } else if errors.Is(err, fs.ErrPermission) { return caddyhttp.Error(http.StatusForbidden, err) } @@ -453,7 +457,14 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c } defer file.Close() respHeader.Set("Content-Encoding", ae) - respHeader.Del("Accept-Ranges") + + // stdlib won't set Content-Length if Content-Encoding is set. + // set Range header if it's not present will force Content-Length to be set + if r.Header.Get("Range") == "" { + r.Header.Set("Range", "bytes=0-") + // remove this header, because it is not part of the request + defer r.Header.Del("Range") + } // try to get the etag from pre computed files if an etag suffix list was provided if etag == "" && fsrv.EtagFileExtensions != nil { @@ -611,6 +622,11 @@ func (fsrv *FileServer) mapDirOpenError(fileSystem fs.FS, originalErr error, nam return originalErr } + var pathErr *fs.PathError + if errors.As(originalErr, &pathErr) { + return fs.ErrInvalid + } + parts := strings.Split(name, separator) for i := range parts { if parts[i] == "" { @@ -636,7 +652,7 @@ func (fsrv *FileServer) transformHidePaths(repl *caddy.Replacer) []string { for i := range fsrv.Hide { hide[i] = repl.ReplaceAll(fsrv.Hide[i], "") if strings.Contains(hide[i], separator) { - abs, err := filepath.Abs(hide[i]) + abs, err := caddy.FastAbs(hide[i]) if err == nil { hide[i] = abs } @@ -655,7 +671,7 @@ func fileHidden(filename string, hide []string) bool { } // all path comparisons use the complete absolute path if possible - filenameAbs, err := filepath.Abs(filename) + filenameAbs, err := caddy.FastAbs(filename) if err == nil { filename = filenameAbs } @@ -677,11 +693,11 @@ func fileHidden(filename string, hide []string) bool { return true } } - } else if strings.HasPrefix(filename, h) { + } else if after, ok := strings.CutPrefix(filename, h); ok { // if there is a separator in h, and filename is exactly // prefixed with h, then we can do a prefix match so that // "/foo" matches "/foo/bar" but not "/foobar". - withoutPrefix := strings.TrimPrefix(filename, h) + withoutPrefix := after if strings.HasPrefix(withoutPrefix, separator) { return true } diff --git a/modules/caddyhttp/fileserver/testdata/large.txt b/modules/caddyhttp/fileserver/testdata/large.txt new file mode 100644 index 000000000..c36623744 --- /dev/null +++ b/modules/caddyhttp/fileserver/testdata/large.txt @@ -0,0 +1,3 @@ +This is a file with more content than the other files in this directory +such that tests using the largest_size policy pick this file, or the +smallest_size policy avoids this file. \ No newline at end of file diff --git a/modules/caddyhttp/headers/caddyfile.go b/modules/caddyhttp/headers/caddyfile.go index e55e9fab2..f060471b1 100644 --- a/modules/caddyhttp/headers/caddyfile.go +++ b/modules/caddyhttp/headers/caddyfile.go @@ -99,6 +99,16 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) handler.Response.Deferred = true continue } + if field == "match" { + responseMatchers := make(map[string]caddyhttp.ResponseMatcher) + err := caddyhttp.ParseNamedResponseMatcher(h.NewFromNextSegment(), responseMatchers) + if err != nil { + return nil, err + } + matcher := responseMatchers["match"] + handler.Response.Require = &matcher + continue + } if hasArgs { return nil, h.Err("cannot specify headers in both arguments and block") // because it would be weird } diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go index a3279d913..33d9e39ee 100644 --- a/modules/caddyhttp/headers/headers.go +++ b/modules/caddyhttp/headers/headers.go @@ -78,7 +78,7 @@ func (h Handler) Validate() error { return err } } - if h.Response != nil { + if h.Response != nil && h.Response.HeaderOps != nil { err := h.Response.validate() if err != nil { return err @@ -133,11 +133,22 @@ type HeaderOps struct { // Provision sets up the header operations. func (ops *HeaderOps) Provision(_ caddy.Context) error { + if ops == nil { + return nil // it's possible no ops are configured; fix #6893 + } for fieldName, replacements := range ops.Replace { for i, r := range replacements { if r.SearchRegexp == "" { continue } + + // Check if it contains placeholders + if containsPlaceholders(r.SearchRegexp) { + // Contains placeholders, skips precompilation, and recompiles at runtime + continue + } + + // Does not contain placeholders, safe to precompile re, err := regexp.Compile(r.SearchRegexp) if err != nil { return fmt.Errorf("replacement %d for header field '%s': %v", i, fieldName, err) @@ -148,6 +159,20 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error { return nil } +// containsPlaceholders checks if the string contains Caddy placeholder syntax {key} +func containsPlaceholders(s string) bool { + openIdx := strings.Index(s, "{") + if openIdx == -1 { + return false + } + closeIdx := strings.Index(s[openIdx+1:], "}") + if closeIdx == -1 { + return false + } + // Make sure there is content between the brackets + return closeIdx > 0 +} + func (ops HeaderOps) validate() error { for fieldName, replacements := range ops.Replace { for _, r := range replacements { @@ -200,9 +225,7 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) { for _, fieldName := range ops.Delete { fieldName = repl.ReplaceKnown(fieldName, "") if fieldName == "*" { - for existingField := range hdr { - delete(hdr, existingField) - } + clear(hdr) } } @@ -268,7 +291,15 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) { for fieldName, vals := range hdr { for i := range vals { if r.re != nil { + // Use precompiled regular expressions hdr[fieldName][i] = r.re.ReplaceAllString(hdr[fieldName][i], replace) + } else if r.SearchRegexp != "" { + // Runtime compilation of regular expressions + searchRegexp := repl.ReplaceKnown(r.SearchRegexp, "") + if re, err := regexp.Compile(searchRegexp); err == nil { + hdr[fieldName][i] = re.ReplaceAllString(hdr[fieldName][i], replace) + } + // If compilation fails, skip this replacement } else { hdr[fieldName][i] = strings.ReplaceAll(hdr[fieldName][i], search, replace) } @@ -290,6 +321,11 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) { for i := range vals { if r.re != nil { hdr[hdrFieldName][i] = r.re.ReplaceAllString(hdr[hdrFieldName][i], replace) + } else if r.SearchRegexp != "" { + searchRegexp := repl.ReplaceKnown(r.SearchRegexp, "") + if re, err := regexp.Compile(searchRegexp); err == nil { + hdr[hdrFieldName][i] = re.ReplaceAllString(hdr[hdrFieldName][i], replace) + } } else { hdr[hdrFieldName][i] = strings.ReplaceAll(hdr[hdrFieldName][i], search, replace) } diff --git a/modules/caddyhttp/headers/headers_test.go b/modules/caddyhttp/headers/headers_test.go index d74e6fc3a..a303c92dd 100644 --- a/modules/caddyhttp/headers/headers_test.go +++ b/modules/caddyhttp/headers/headers_test.go @@ -143,6 +143,28 @@ func TestHandler(t *testing.T) { "Cache-Control": []string{"no-cache"}, }, }, + { // same as above, but checks that response headers are left alone when "Require" conditions are unmet + handler: Handler{ + Response: &RespHeaderOps{ + Require: &caddyhttp.ResponseMatcher{ + Headers: http.Header{ + "Cache-Control": nil, + }, + }, + HeaderOps: &HeaderOps{ + Add: http.Header{ + "Cache-Control": []string{"no-cache"}, + }, + }, + }, + }, + respHeader: http.Header{ + "Cache-Control": []string{"something"}, + }, + expectedRespHeader: http.Header{ + "Cache-Control": []string{"something"}, + }, + }, { handler: Handler{ Response: &RespHeaderOps{ @@ -250,3 +272,107 @@ type nextHandler func(http.ResponseWriter, *http.Request) error func (f nextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) error { return f(w, r) } + +func TestContainsPlaceholders(t *testing.T) { + for i, tc := range []struct { + input string + expected bool + }{ + {"static", false}, + {"{placeholder}", true}, + {"prefix-{placeholder}-suffix", true}, + {"{}", false}, + {"no-braces", false}, + {"{unclosed", false}, + {"unopened}", false}, + } { + actual := containsPlaceholders(tc.input) + if actual != tc.expected { + t.Errorf("Test %d: containsPlaceholders(%q) = %v, expected %v", i, tc.input, actual, tc.expected) + } + } +} + +func TestHeaderProvisionSkipsPlaceholders(t *testing.T) { + ops := &HeaderOps{ + Replace: map[string][]Replacement{ + "Static": { + Replacement{SearchRegexp: ":443", Replace: "STATIC"}, + }, + "Dynamic": { + Replacement{SearchRegexp: ":{http.request.local.port}", Replace: "DYNAMIC"}, + }, + }, + } + + err := ops.Provision(caddy.Context{}) + if err != nil { + t.Fatalf("Provision failed: %v", err) + } + + // Static regex should be precompiled + if ops.Replace["Static"][0].re == nil { + t.Error("Expected static regex to be precompiled") + } + + // Dynamic regex with placeholder should not be precompiled + if ops.Replace["Dynamic"][0].re != nil { + t.Error("Expected dynamic regex with placeholder to not be precompiled") + } +} + +func TestPlaceholderInSearchRegexp(t *testing.T) { + handler := Handler{ + Response: &RespHeaderOps{ + HeaderOps: &HeaderOps{ + Replace: map[string][]Replacement{ + "Test-Header": { + Replacement{ + SearchRegexp: ":{http.request.local.port}", + Replace: "PLACEHOLDER-WORKS", + }, + }, + }, + }, + }, + } + + // Provision the handler + err := handler.Provision(caddy.Context{}) + if err != nil { + t.Fatalf("Provision failed: %v", err) + } + + replacement := handler.Response.HeaderOps.Replace["Test-Header"][0] + t.Logf("After provision - SearchRegexp: %q, re: %v", replacement.SearchRegexp, replacement.re) + + rr := httptest.NewRecorder() + + req := httptest.NewRequest("GET", "http://localhost:443/", nil) + repl := caddy.NewReplacer() + repl.Set("http.request.local.port", "443") + + ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) + req = req.WithContext(ctx) + + rr.Header().Set("Test-Header", "prefix:443suffix") + t.Logf("Initial header: %v", rr.Header()) + + next := nextHandler(func(w http.ResponseWriter, r *http.Request) error { + w.WriteHeader(200) + return nil + }) + + err = handler.ServeHTTP(rr, req, next) + if err != nil { + t.Fatalf("ServeHTTP failed: %v", err) + } + + t.Logf("Final header: %v", rr.Header()) + + result := rr.Header().Get("Test-Header") + expected := "prefixPLACEHOLDER-WORKSsuffix" + if result != expected { + t.Errorf("Expected header value %q, got %q", expected, result) + } +} diff --git a/modules/caddyhttp/http2listener.go b/modules/caddyhttp/http2listener.go index 51b356a77..afc5e51ab 100644 --- a/modules/caddyhttp/http2listener.go +++ b/modules/caddyhttp/http2listener.go @@ -1,102 +1,110 @@ package caddyhttp import ( - "context" "crypto/tls" - weakrand "math/rand" + "io" "net" - "net/http" - "sync/atomic" - "time" + "go.uber.org/zap" "golang.org/x/net/http2" ) -// http2Listener wraps the listener to solve the following problems: -// 1. server h2 natively without using h2c hack when listener handles tls connection but -// don't return *tls.Conn -// 2. graceful shutdown. the shutdown logic is copied from stdlib http.Server, it's an extra maintenance burden but -// whatever, the shutdown logic maybe extracted to be used with h2c graceful shutdown. http2.Server supports graceful shutdown -// sending GO_AWAY frame to connected clients, but doesn't track connection status. It requires explicit call of http2.ConfigureServer -type http2Listener struct { - cnt uint64 - net.Listener - server *http.Server - h2server *http2.Server -} - -type connectionStateConn interface { - net.Conn +type connectionStater interface { ConnectionState() tls.ConnectionState } +// http2Listener wraps the listener to solve the following problems: +// 1. prevent genuine h2c connections from succeeding if h2c is not enabled +// and the connection doesn't implment connectionStater or the resulting NegotiatedProtocol +// isn't http2. +// This does allow a connection to pass as tls enabled even if it's not, listener wrappers +// can do this. +// 2. After wrapping the connection doesn't implement connectionStater, emit a warning so that listener +// wrapper authors will hopefully implement it. +// 3. check if the connection matches a specific http version. h2/h2c has a distinct preface. +type http2Listener struct { + useTLS bool + useH1 bool + useH2 bool + net.Listener + logger *zap.Logger +} + func (h *http2Listener) Accept() (net.Conn, error) { - for { - conn, err := h.Listener.Accept() - if err != nil { - return nil, err - } + conn, err := h.Listener.Accept() + if err != nil { + return nil, err + } - if csc, ok := conn.(connectionStateConn); ok { - // *tls.Conn will return empty string because it's only populated after handshake is complete - if csc.ConnectionState().NegotiatedProtocol == http2.NextProtoTLS { - go h.serveHttp2(csc) - continue - } - } + _, isConnectionStater := conn.(connectionStater) + // emit a warning + if h.useTLS && !isConnectionStater { + h.logger.Warn("tls is enabled, but listener wrapper returns a connection that doesn't implement connectionStater") + } else if !h.useTLS && isConnectionStater { + h.logger.Warn("tls is disabled, but listener wrapper returns a connection that implements connectionStater") + } + // if both h1 and h2 are enabled, we don't need to check the preface + if h.useH1 && h.useH2 { return conn, nil } -} -func (h *http2Listener) serveHttp2(csc connectionStateConn) { - atomic.AddUint64(&h.cnt, 1) - h.runHook(csc, http.StateNew) - defer func() { - csc.Close() - atomic.AddUint64(&h.cnt, ^uint64(0)) - h.runHook(csc, http.StateClosed) - }() - h.h2server.ServeConn(csc, &http2.ServeConnOpts{ - Context: h.server.ConnContext(context.Background(), csc), - BaseConfig: h.server, - Handler: h.server.Handler, - }) -} - -const shutdownPollIntervalMax = 500 * time.Millisecond - -func (h *http2Listener) Shutdown(ctx context.Context) error { - pollIntervalBase := time.Millisecond - nextPollInterval := func() time.Duration { - // Add 10% jitter. - //nolint:gosec - interval := pollIntervalBase + time.Duration(weakrand.Intn(int(pollIntervalBase/10))) - // Double and clamp for next time. - pollIntervalBase *= 2 - if pollIntervalBase > shutdownPollIntervalMax { - pollIntervalBase = shutdownPollIntervalMax - } - return interval + // impossible both are false, either useH1 or useH2 must be true, + // or else the listener wouldn't be created + h2Conn := &http2Conn{ + h2Expected: h.useH2, + Conn: conn, } + if isConnectionStater { + return http2StateConn{h2Conn}, nil + } + return h2Conn, nil +} - timer := time.NewTimer(nextPollInterval()) - defer timer.Stop() - for { - if atomic.LoadUint64(&h.cnt) == 0 { - return nil +type http2StateConn struct { + *http2Conn +} + +func (conn http2StateConn) ConnectionState() tls.ConnectionState { + return conn.Conn.(connectionStater).ConnectionState() +} + +type http2Conn struct { + // current index where the preface should match, + // no matching is done if idx is >= len(http2.ClientPreface) + idx int + // whether the connection is expected to be h2/h2c + h2Expected bool + // log if one such connection is detected + logger *zap.Logger + net.Conn +} + +func (c *http2Conn) Read(p []byte) (int, error) { + if c.idx >= len(http2.ClientPreface) { + return c.Conn.Read(p) + } + n, err := c.Conn.Read(p) + for i := range n { + // first mismatch + if p[i] != http2.ClientPreface[c.idx] { + // close the connection if h2 is expected + if c.h2Expected { + c.logger.Debug("h1 connection detected, but h1 is not enabled") + _ = c.Conn.Close() + return 0, io.EOF + } + // no need to continue matching anymore + c.idx = len(http2.ClientPreface) + return n, err } - select { - case <-ctx.Done(): - return ctx.Err() - case <-timer.C: - timer.Reset(nextPollInterval()) + c.idx++ + // matching complete + if c.idx == len(http2.ClientPreface) && !c.h2Expected { + c.logger.Debug("h2/h2c connection detected, but h2/h2c is not enabled") + _ = c.Conn.Close() + return 0, io.EOF } } -} - -func (h *http2Listener) runHook(conn net.Conn, state http.ConnState) { - if h.server.ConnState != nil { - h.server.ConnState(conn, state) - } + return n, err } diff --git a/modules/caddyhttp/intercept/intercept.go b/modules/caddyhttp/intercept/intercept.go index 29889dcc0..cb23adf0a 100644 --- a/modules/caddyhttp/intercept/intercept.go +++ b/modules/caddyhttp/intercept/intercept.go @@ -118,6 +118,11 @@ func (irh interceptedResponseHandler) WriteHeader(statusCode int) { irh.ResponseRecorder.WriteHeader(statusCode) } +// EXPERIMENTAL: Subject to change or removal. +func (irh interceptedResponseHandler) Unwrap() http.ResponseWriter { + return irh.ResponseRecorder +} + // EXPERIMENTAL: Subject to change or removal. func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { buf := bufPool.Get().(*bytes.Buffer) diff --git a/modules/caddyhttp/ip_matchers.go b/modules/caddyhttp/ip_matchers.go index 99eb39dff..9335112e8 100644 --- a/modules/caddyhttp/ip_matchers.go +++ b/modules/caddyhttp/ip_matchers.go @@ -20,7 +20,6 @@ import ( "net" "net/http" "net/netip" - "reflect" "strings" "github.com/google/cel-go/cel" @@ -108,8 +107,8 @@ func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) { // internal data type of the MatchPath value. []*cel.Type{cel.ListType(cel.StringType)}, // function to convert a constant list of strings to a MatchPath instance. - func(data ref.Val) (RequestMatcher, error) { - refStringList := reflect.TypeOf([]string{}) + func(data ref.Val) (RequestMatcherWithError, error) { + refStringList := stringSliceType strList, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -145,9 +144,23 @@ func (m *MatchRemoteIP) Provision(ctx caddy.Context) error { // Match returns true if r matches m. func (m MatchRemoteIP) Match(r *http.Request) bool { - if r.TLS != nil && !r.TLS.HandshakeComplete { - return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed + match, err := m.MatchWithError(r) + if err != nil { + SetVar(r.Context(), MatcherErrorVarKey, err) } + return match +} + +// MatchWithError returns true if r matches m. +func (m MatchRemoteIP) MatchWithError(r *http.Request) (bool, error) { + // if handshake is not finished, we infer 0-RTT that has + // not verified remote IP; could be spoofed, so we throw + // HTTP 425 status to tell the client to try again after + // the handshake is complete + if r.TLS != nil && !r.TLS.HandshakeComplete { + return false, Error(http.StatusTooEarly, fmt.Errorf("TLS handshake not complete, remote IP cannot be verified")) + } + address := r.RemoteAddr clientIP, zoneID, err := parseIPZoneFromString(address) if err != nil { @@ -155,7 +168,7 @@ func (m MatchRemoteIP) Match(r *http.Request) bool { c.Write(zap.Error(err)) } - return false + return false, nil } matches, zoneFilter := matchIPByCidrZones(clientIP, zoneID, m.cidrs, m.zones) if !matches && !zoneFilter { @@ -163,7 +176,7 @@ func (m MatchRemoteIP) Match(r *http.Request) bool { c.Write(zap.String("zone", zoneID)) } } - return matches + return matches, nil } // CaddyModule returns the Caddy module information. @@ -207,8 +220,8 @@ func (MatchClientIP) CELLibrary(ctx caddy.Context) (cel.Library, error) { // internal data type of the MatchPath value. []*cel.Type{cel.ListType(cel.StringType)}, // function to convert a constant list of strings to a MatchPath instance. - func(data ref.Val) (RequestMatcher, error) { - refStringList := reflect.TypeOf([]string{}) + func(data ref.Val) (RequestMatcherWithError, error) { + refStringList := stringSliceType strList, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -238,20 +251,34 @@ func (m *MatchClientIP) Provision(ctx caddy.Context) error { // Match returns true if r matches m. func (m MatchClientIP) Match(r *http.Request) bool { - if r.TLS != nil && !r.TLS.HandshakeComplete { - return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed + match, err := m.MatchWithError(r) + if err != nil { + SetVar(r.Context(), MatcherErrorVarKey, err) } + return match +} + +// MatchWithError returns true if r matches m. +func (m MatchClientIP) MatchWithError(r *http.Request) (bool, error) { + // if handshake is not finished, we infer 0-RTT that has + // not verified remote IP; could be spoofed, so we throw + // HTTP 425 status to tell the client to try again after + // the handshake is complete + if r.TLS != nil && !r.TLS.HandshakeComplete { + return false, Error(http.StatusTooEarly, fmt.Errorf("TLS handshake not complete, remote IP cannot be verified")) + } + address := GetVar(r.Context(), ClientIPVarKey).(string) clientIP, zoneID, err := parseIPZoneFromString(address) if err != nil { m.logger.Error("getting client IP", zap.Error(err)) - return false + return false, nil } matches, zoneFilter := matchIPByCidrZones(clientIP, zoneID, m.cidrs, m.zones) if !matches && !zoneFilter { m.logger.Debug("zone ID from client IP did not match", zap.String("zone", zoneID)) } - return matches + return matches, nil } func provisionCidrsZonesFromRanges(ranges []string) ([]*netip.Prefix, []string, error) { @@ -326,13 +353,13 @@ func matchIPByCidrZones(clientIP netip.Addr, zoneID string, cidrs []*netip.Prefi // Interface guards var ( - _ RequestMatcher = (*MatchRemoteIP)(nil) - _ caddy.Provisioner = (*MatchRemoteIP)(nil) - _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil) - _ CELLibraryProducer = (*MatchRemoteIP)(nil) + _ RequestMatcherWithError = (*MatchRemoteIP)(nil) + _ caddy.Provisioner = (*MatchRemoteIP)(nil) + _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil) + _ CELLibraryProducer = (*MatchRemoteIP)(nil) - _ RequestMatcher = (*MatchClientIP)(nil) - _ caddy.Provisioner = (*MatchClientIP)(nil) - _ caddyfile.Unmarshaler = (*MatchClientIP)(nil) - _ CELLibraryProducer = (*MatchClientIP)(nil) + _ RequestMatcherWithError = (*MatchClientIP)(nil) + _ caddy.Provisioner = (*MatchClientIP)(nil) + _ caddyfile.Unmarshaler = (*MatchClientIP)(nil) + _ CELLibraryProducer = (*MatchClientIP)(nil) ) diff --git a/modules/caddyhttp/logging.go b/modules/caddyhttp/logging.go index 0a389fe16..87298ac3c 100644 --- a/modules/caddyhttp/logging.go +++ b/modules/caddyhttp/logging.go @@ -211,6 +211,11 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi } return } + fields = func() []zapcore.Field { + return []zapcore.Field{ + zap.Error(err), + } + } status = http.StatusInternalServerError msg = err.Error() return diff --git a/modules/caddyhttp/marshalers.go b/modules/caddyhttp/marshalers.go index c985bb926..9bce377f4 100644 --- a/modules/caddyhttp/marshalers.go +++ b/modules/caddyhttp/marshalers.go @@ -51,6 +51,9 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error { Header: r.Header, ShouldLogCredentials: r.ShouldLogCredentials, }) + if r.TransferEncoding != nil { + enc.AddArray("transfer_encoding", LoggableStringArray(r.TransferEncoding)) + } if r.TLS != nil { enc.AddObject("tls", LoggableTLSConnState(*r.TLS)) } diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 93a36237b..22976cfbd 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -23,7 +23,6 @@ import ( "net/textproto" "net/url" "path" - "reflect" "regexp" "runtime" "slices" @@ -296,6 +295,12 @@ func (m MatchHost) Provision(_ caddy.Context) error { // Match returns true if r matches m. func (m MatchHost) Match(r *http.Request) bool { + match, _ := m.MatchWithError(r) + return match +} + +// MatchWithError returns true if r matches m. +func (m MatchHost) MatchWithError(r *http.Request) (bool, error) { reqHost, _, err := net.SplitHostPort(r.Host) if err != nil { // OK; probably didn't have a port @@ -315,7 +320,7 @@ func (m MatchHost) Match(r *http.Request) bool { return m[i] >= reqHost }) if pos < len(m) && m[pos] == reqHost { - return true + return true, nil } } @@ -346,13 +351,13 @@ outer: continue outer } } - return true + return true, nil } else if strings.EqualFold(reqHost, host) { - return true + return true, nil } } - return false + return false, nil } // CELLibrary produces options that expose this matcher for use in CEL @@ -366,8 +371,8 @@ func (MatchHost) CELLibrary(ctx caddy.Context) (cel.Library, error) { "host", "host_match_request_list", []*cel.Type{cel.ListType(cel.StringType)}, - func(data ref.Val) (RequestMatcher, error) { - refStringList := reflect.TypeOf([]string{}) + func(data ref.Val) (RequestMatcherWithError, error) { + refStringList := stringSliceType strList, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -411,6 +416,12 @@ func (m MatchPath) Provision(_ caddy.Context) error { // Match returns true if r matches m. func (m MatchPath) Match(r *http.Request) bool { + match, _ := m.MatchWithError(r) + return match +} + +// MatchWithError returns true if r matches m. +func (m MatchPath) MatchWithError(r *http.Request) (bool, error) { // Even though RFC 9110 says that path matching is case-sensitive // (https://www.rfc-editor.org/rfc/rfc9110.html#section-4.2.3), // we do case-insensitive matching to mitigate security issues @@ -436,7 +447,7 @@ func (m MatchPath) Match(r *http.Request) bool { // special case: whole path is wildcard; this is unnecessary // as it matches all requests, which is the same as no matcher if matchPattern == "*" { - return true + return true, nil } // Clean the path, merge doubled slashes, etc. @@ -464,7 +475,7 @@ func (m MatchPath) Match(r *http.Request) bool { if strings.Contains(matchPattern, "%") { reqPathForPattern := CleanPath(r.URL.EscapedPath(), mergeSlashes) if m.matchPatternWithEscapeSequence(reqPathForPattern, matchPattern) { - return true + return true, nil } // doing prefix/suffix/substring matches doesn't make sense @@ -483,7 +494,7 @@ func (m MatchPath) Match(r *http.Request) bool { strings.HasPrefix(matchPattern, "*") && strings.HasSuffix(matchPattern, "*") { if strings.Contains(reqPathForPattern, matchPattern[1:len(matchPattern)-1]) { - return true + return true, nil } continue } @@ -495,7 +506,7 @@ func (m MatchPath) Match(r *http.Request) bool { // treat it as a fast suffix match if strings.HasPrefix(matchPattern, "*") { if strings.HasSuffix(reqPathForPattern, matchPattern[1:]) { - return true + return true, nil } continue } @@ -504,7 +515,7 @@ func (m MatchPath) Match(r *http.Request) bool { // treat it as a fast prefix match if strings.HasSuffix(matchPattern, "*") { if strings.HasPrefix(reqPathForPattern, matchPattern[:len(matchPattern)-1]) { - return true + return true, nil } continue } @@ -515,10 +526,10 @@ func (m MatchPath) Match(r *http.Request) bool { // because we can't handle it anyway matches, _ := path.Match(matchPattern, reqPathForPattern) if matches { - return true + return true, nil } } - return false + return false, nil } func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool { @@ -540,7 +551,6 @@ func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) b if iPattern >= len(matchPath) || iPath >= len(escapedPath) { break } - // get the next character from the request path pathCh := string(escapedPath[iPath]) @@ -642,8 +652,8 @@ func (MatchPath) CELLibrary(ctx caddy.Context) (cel.Library, error) { // internal data type of the MatchPath value. []*cel.Type{cel.ListType(cel.StringType)}, // function to convert a constant list of strings to a MatchPath instance. - func(data ref.Val) (RequestMatcher, error) { - refStringList := reflect.TypeOf([]string{}) + func(data ref.Val) (RequestMatcherWithError, error) { + refStringList := stringSliceType strList, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -677,6 +687,12 @@ func (MatchPathRE) CaddyModule() caddy.ModuleInfo { // Match returns true if r matches m. func (m MatchPathRE) Match(r *http.Request) bool { + match, _ := m.MatchWithError(r) + return match +} + +// MatchWithError returns true if r matches m. +func (m MatchPathRE) MatchWithError(r *http.Request) (bool, error) { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) // Clean the path, merges doubled slashes, etc. @@ -684,7 +700,7 @@ func (m MatchPathRE) Match(r *http.Request) bool { // the path matcher. See #4407 cleanedPath := cleanPath(r.URL.Path) - return m.MatchRegexp.Match(cleanedPath, repl) + return m.MatchRegexp.Match(cleanedPath, repl), nil } // CELLibrary produces options that expose this matcher for use in CEL @@ -698,7 +714,7 @@ func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { "path_regexp", "path_regexp_request_string", []*cel.Type{cel.StringType}, - func(data ref.Val) (RequestMatcher, error) { + func(data ref.Val) (RequestMatcherWithError, error) { pattern := data.(types.String) matcher := MatchPathRE{MatchRegexp{ Name: ctx.Value(MatcherNameCtxKey).(string), @@ -715,8 +731,8 @@ func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { "path_regexp", "path_regexp_request_string_string", []*cel.Type{cel.StringType, cel.StringType}, - func(data ref.Val) (RequestMatcher, error) { - refStringList := reflect.TypeOf([]string{}) + func(data ref.Val) (RequestMatcherWithError, error) { + refStringList := stringSliceType params, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -764,7 +780,13 @@ func (m *MatchMethod) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // Match returns true if r matches m. func (m MatchMethod) Match(r *http.Request) bool { - return slices.Contains(m, r.Method) + match, _ := m.MatchWithError(r) + return match +} + +// MatchWithError returns true if r matches m. +func (m MatchMethod) MatchWithError(r *http.Request) (bool, error) { + return slices.Contains(m, r.Method), nil } // CELLibrary produces options that expose this matcher for use in CEL @@ -778,8 +800,8 @@ func (MatchMethod) CELLibrary(_ caddy.Context) (cel.Library, error) { "method", "method_request_list", []*cel.Type{cel.ListType(cel.StringType)}, - func(data ref.Val) (RequestMatcher, error) { - refStringList := reflect.TypeOf([]string{}) + func(data ref.Val) (RequestMatcherWithError, error) { + refStringList := stringSliceType strList, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -823,10 +845,17 @@ func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // Match returns true if r matches m. An empty m matches an empty query string. func (m MatchQuery) Match(r *http.Request) bool { + match, _ := m.MatchWithError(r) + return match +} + +// MatchWithError returns true if r matches m. +// An empty m matches an empty query string. +func (m MatchQuery) MatchWithError(r *http.Request) (bool, error) { // If no query keys are configured, this only // matches an empty query string. if len(m) == 0 { - return len(r.URL.Query()) == 0 + return len(r.URL.Query()) == 0, nil } repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) @@ -843,7 +872,7 @@ func (m MatchQuery) Match(r *http.Request) bool { // "Relying on parser alignment for security is doomed." Overall conclusion is that // splitting on & and rejecting ; in key=value pairs is safer than accepting raw ;. // We regard the Go team's decision as sound and thus reject malformed query strings. - return false + return false, nil } // Count the amount of matched keys, to ensure we AND @@ -854,7 +883,7 @@ func (m MatchQuery) Match(r *http.Request) bool { param = repl.ReplaceAll(param, "") paramVal, found := parsed[param] if !found { - return false + return false, nil } for _, v := range vals { v = repl.ReplaceAll(v, "") @@ -864,7 +893,7 @@ func (m MatchQuery) Match(r *http.Request) bool { } } } - return matchedKeys == len(m) + return matchedKeys == len(m), nil } // CELLibrary produces options that expose this matcher for use in CEL @@ -878,7 +907,7 @@ func (MatchQuery) CELLibrary(_ caddy.Context) (cel.Library, error) { "query", "query_matcher_request_map", []*cel.Type{CELTypeJSON}, - func(data ref.Val) (RequestMatcher, error) { + func(data ref.Val) (RequestMatcherWithError, error) { mapStrListStr, err := CELValueToMapStrList(data) if err != nil { return nil, err @@ -940,8 +969,14 @@ func (m *MatchHeader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // Match returns true if r matches m. func (m MatchHeader) Match(r *http.Request) bool { + match, _ := m.MatchWithError(r) + return match +} + +// MatchWithError returns true if r matches m. +func (m MatchHeader) MatchWithError(r *http.Request) (bool, error) { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) - return matchHeaders(r.Header, http.Header(m), r.Host, repl) + return matchHeaders(r.Header, http.Header(m), r.Host, r.TransferEncoding, repl), nil } // CELLibrary produces options that expose this matcher for use in CEL @@ -956,7 +991,7 @@ func (MatchHeader) CELLibrary(_ caddy.Context) (cel.Library, error) { "header", "header_matcher_request_map", []*cel.Type{CELTypeJSON}, - func(data ref.Val) (RequestMatcher, error) { + func(data ref.Val) (RequestMatcherWithError, error) { mapStrListStr, err := CELValueToMapStrList(data) if err != nil { return nil, err @@ -967,22 +1002,26 @@ func (MatchHeader) CELLibrary(_ caddy.Context) (cel.Library, error) { } // getHeaderFieldVals returns the field values for the given fieldName from input. -// The host parameter should be obtained from the http.Request.Host field since -// net/http removes it from the header map. -func getHeaderFieldVals(input http.Header, fieldName, host string) []string { +// The host parameter should be obtained from the http.Request.Host field, and the +// transferEncoding from http.Request.TransferEncoding, since net/http removes them +// from the header map. +func getHeaderFieldVals(input http.Header, fieldName, host string, transferEncoding []string) []string { fieldName = textproto.CanonicalMIMEHeaderKey(fieldName) if fieldName == "Host" && host != "" { return []string{host} } + if fieldName == "Transfer-Encoding" && input[fieldName] == nil { + return transferEncoding + } return input[fieldName] } // matchHeaders returns true if input matches the criteria in against without regex. // The host parameter should be obtained from the http.Request.Host field since // net/http removes it from the header map. -func matchHeaders(input, against http.Header, host string, repl *caddy.Replacer) bool { +func matchHeaders(input, against http.Header, host string, transferEncoding []string, repl *caddy.Replacer) bool { for field, allowedFieldVals := range against { - actualFieldVals := getHeaderFieldVals(input, field, host) + actualFieldVals := getHeaderFieldVals(input, field, host, transferEncoding) if allowedFieldVals != nil && len(allowedFieldVals) == 0 && actualFieldVals != nil { // a non-nil but empty list of allowed values means // match if the header field exists at all @@ -1075,8 +1114,14 @@ func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // Match returns true if r matches m. func (m MatchHeaderRE) Match(r *http.Request) bool { + match, _ := m.MatchWithError(r) + return match +} + +// MatchWithError returns true if r matches m. +func (m MatchHeaderRE) MatchWithError(r *http.Request) (bool, error) { for field, rm := range m { - actualFieldVals := getHeaderFieldVals(r.Header, field, r.Host) + actualFieldVals := getHeaderFieldVals(r.Header, field, r.Host, r.TransferEncoding) match := false fieldVal: for _, actualFieldVal := range actualFieldVals { @@ -1087,10 +1132,10 @@ func (m MatchHeaderRE) Match(r *http.Request) bool { } } if !match { - return false + return false, nil } } - return true + return true, nil } // Provision compiles m's regular expressions. @@ -1126,8 +1171,8 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { "header_regexp", "header_regexp_request_string_string", []*cel.Type{cel.StringType, cel.StringType}, - func(data ref.Val) (RequestMatcher, error) { - refStringList := reflect.TypeOf([]string{}) + func(data ref.Val) (RequestMatcherWithError, error) { + refStringList := stringSliceType params, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -1149,8 +1194,8 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { "header_regexp", "header_regexp_request_string_string_string", []*cel.Type{cel.StringType, cel.StringType, cel.StringType}, - func(data ref.Val) (RequestMatcher, error) { - refStringList := reflect.TypeOf([]string{}) + func(data ref.Val) (RequestMatcherWithError, error) { + refStringList := stringSliceType params, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -1187,31 +1232,37 @@ func (MatchProtocol) CaddyModule() caddy.ModuleInfo { // Match returns true if r matches m. func (m MatchProtocol) Match(r *http.Request) bool { + match, _ := m.MatchWithError(r) + return match +} + +// MatchWithError returns true if r matches m. +func (m MatchProtocol) MatchWithError(r *http.Request) (bool, error) { switch string(m) { case "grpc": - return strings.HasPrefix(r.Header.Get("content-type"), "application/grpc") + return strings.HasPrefix(r.Header.Get("content-type"), "application/grpc"), nil case "https": - return r.TLS != nil + return r.TLS != nil, nil case "http": - return r.TLS == nil + return r.TLS == nil, nil case "http/1.0": - return r.ProtoMajor == 1 && r.ProtoMinor == 0 + return r.ProtoMajor == 1 && r.ProtoMinor == 0, nil case "http/1.0+": - return r.ProtoAtLeast(1, 0) + return r.ProtoAtLeast(1, 0), nil case "http/1.1": - return r.ProtoMajor == 1 && r.ProtoMinor == 1 + return r.ProtoMajor == 1 && r.ProtoMinor == 1, nil case "http/1.1+": - return r.ProtoAtLeast(1, 1) + return r.ProtoAtLeast(1, 1), nil case "http/2": - return r.ProtoMajor == 2 + return r.ProtoMajor == 2, nil case "http/2+": - return r.ProtoAtLeast(2, 0) + return r.ProtoAtLeast(2, 0), nil case "http/3": - return r.ProtoMajor == 3 + return r.ProtoMajor == 3, nil case "http/3+": - return r.ProtoAtLeast(3, 0) + return r.ProtoAtLeast(3, 0), nil } - return false + return false, nil } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. @@ -1238,7 +1289,7 @@ func (MatchProtocol) CELLibrary(_ caddy.Context) (cel.Library, error) { "protocol", "protocol_request_string", []*cel.Type{cel.StringType}, - func(data ref.Val) (RequestMatcher, error) { + func(data ref.Val) (RequestMatcherWithError, error) { protocolStr, ok := data.(types.String) if !ok { return nil, errors.New("protocol argument was not a string") @@ -1258,16 +1309,22 @@ func (MatchTLS) CaddyModule() caddy.ModuleInfo { // Match returns true if r matches m. func (m MatchTLS) Match(r *http.Request) bool { + match, _ := m.MatchWithError(r) + return match +} + +// MatchWithError returns true if r matches m. +func (m MatchTLS) MatchWithError(r *http.Request) (bool, error) { if r.TLS == nil { - return false + return false, nil } if m.HandshakeComplete != nil { if (!*m.HandshakeComplete && r.TLS.HandshakeComplete) || (*m.HandshakeComplete && !r.TLS.HandshakeComplete) { - return false + return false, nil } } - return true + return true, nil } // UnmarshalCaddyfile parses Caddyfile tokens for this matcher. Syntax: @@ -1283,6 +1340,8 @@ func (m *MatchTLS) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { case "early_data": var false bool m.HandshakeComplete = &false + default: + return d.Errf("unrecognized option '%s'", d.Val()) } } if d.NextArg() { @@ -1337,7 +1396,15 @@ func (m *MatchNot) Provision(ctx caddy.Context) error { for _, modMap := range matcherSets.([]map[string]any) { var ms MatcherSet for _, modIface := range modMap { - ms = append(ms, modIface.(RequestMatcher)) + if mod, ok := modIface.(RequestMatcherWithError); ok { + ms = append(ms, mod) + continue + } + if mod, ok := modIface.(RequestMatcher); ok { + ms = append(ms, mod) + continue + } + return fmt.Errorf("module is not a request matcher: %T", modIface) } m.MatcherSets = append(m.MatcherSets, ms) } @@ -1348,12 +1415,24 @@ func (m *MatchNot) Provision(ctx caddy.Context) error { // the embedded matchers, false is returned if any of its matcher // sets return true. func (m MatchNot) Match(r *http.Request) bool { + match, _ := m.MatchWithError(r) + return match +} + +// MatchWithError returns true if r matches m. Since this matcher +// negates the embedded matchers, false is returned if any of its +// matcher sets return true. +func (m MatchNot) MatchWithError(r *http.Request) (bool, error) { for _, ms := range m.MatcherSets { - if ms.Match(r) { - return false + matches, err := ms.MatchWithError(r) + if err != nil { + return false, err + } + if matches { + return false, nil } } - return true + return true, nil } // MatchRegexp is an embedable type for matching @@ -1466,10 +1545,10 @@ func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } -// ParseCaddyfileNestedMatcher parses the Caddyfile tokens for a nested +// ParseCaddyfileNestedMatcherSet parses the Caddyfile tokens for a nested // matcher set, and returns its raw module map value. func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, error) { - matcherMap := make(map[string]RequestMatcher) + matcherMap := make(map[string]any) // in case there are multiple instances of the same matcher, concatenate // their tokens (we expect that UnmarshalCaddyfile should be able to @@ -1494,11 +1573,15 @@ func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, er if err != nil { return nil, err } - rm, ok := unm.(RequestMatcher) - if !ok { - return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) + if rm, ok := unm.(RequestMatcherWithError); ok { + matcherMap[matcherName] = rm + continue } - matcherMap[matcherName] = rm + if rm, ok := unm.(RequestMatcher); ok { + matcherMap[matcherName] = rm + continue + } + return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) } // we should now have a functional matcher, but we also @@ -1524,24 +1607,28 @@ const regexpPlaceholderPrefix = "http.regexp" // holds an optional error emitted from a request matcher, // to short-circuit the handler chain, since matchers cannot // return errors via the RequestMatcher interface. +// +// Deprecated: Matchers should implement RequestMatcherWithError +// which can return an error directly, instead of smuggling it +// through the vars map. const MatcherErrorVarKey = "matchers.error" // Interface guards var ( - _ RequestMatcher = (*MatchHost)(nil) - _ caddy.Provisioner = (*MatchHost)(nil) - _ RequestMatcher = (*MatchPath)(nil) - _ RequestMatcher = (*MatchPathRE)(nil) - _ caddy.Provisioner = (*MatchPathRE)(nil) - _ RequestMatcher = (*MatchMethod)(nil) - _ RequestMatcher = (*MatchQuery)(nil) - _ RequestMatcher = (*MatchHeader)(nil) - _ RequestMatcher = (*MatchHeaderRE)(nil) - _ caddy.Provisioner = (*MatchHeaderRE)(nil) - _ RequestMatcher = (*MatchProtocol)(nil) - _ RequestMatcher = (*MatchNot)(nil) - _ caddy.Provisioner = (*MatchNot)(nil) - _ caddy.Provisioner = (*MatchRegexp)(nil) + _ RequestMatcherWithError = (*MatchHost)(nil) + _ caddy.Provisioner = (*MatchHost)(nil) + _ RequestMatcherWithError = (*MatchPath)(nil) + _ RequestMatcherWithError = (*MatchPathRE)(nil) + _ caddy.Provisioner = (*MatchPathRE)(nil) + _ RequestMatcherWithError = (*MatchMethod)(nil) + _ RequestMatcherWithError = (*MatchQuery)(nil) + _ RequestMatcherWithError = (*MatchHeader)(nil) + _ RequestMatcherWithError = (*MatchHeaderRE)(nil) + _ caddy.Provisioner = (*MatchHeaderRE)(nil) + _ RequestMatcherWithError = (*MatchProtocol)(nil) + _ RequestMatcherWithError = (*MatchNot)(nil) + _ caddy.Provisioner = (*MatchNot)(nil) + _ caddy.Provisioner = (*MatchRegexp)(nil) _ caddyfile.Unmarshaler = (*MatchHost)(nil) _ caddyfile.Unmarshaler = (*MatchPath)(nil) diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index 05eaade5b..068207e85 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -158,7 +158,10 @@ func TestHostMatcher(t *testing.T) { t.Errorf("Test %d %v: provisioning failed: %v", i, tc.match, err) } - actual := tc.match.Match(req) + actual, err := tc.match.MatchWithError(req) + if err != nil { + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err) + } if actual != tc.expect { t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input) continue @@ -430,7 +433,10 @@ func TestPathMatcher(t *testing.T) { ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) - actual := tc.match.Match(req) + actual, err := tc.match.MatchWithError(req) + if err != nil { + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err) + } if actual != tc.expect { t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input) continue @@ -451,7 +457,10 @@ func TestPathMatcherWindows(t *testing.T) { req = req.WithContext(ctx) match := MatchPath{"*.php"} - matched := match.Match(req) + matched, err := match.MatchWithError(req) + if err != nil { + t.Errorf("Expected no error, but got: %v", err) + } if !matched { t.Errorf("Expected to match; should ignore trailing dots and spaces") } @@ -555,7 +564,10 @@ func TestPathREMatcher(t *testing.T) { req = req.WithContext(ctx) addHTTPVarsToReplacer(repl, req, httptest.NewRecorder()) - actual := tc.match.Match(req) + actual, err := tc.match.MatchWithError(req) + if err != nil { + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err) + } if actual != tc.expect { t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'", i, tc.match.Pattern, tc.expect, actual, tc.input) @@ -691,7 +703,10 @@ func TestHeaderMatcher(t *testing.T) { ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) - actual := tc.match.Match(req) + actual, err := tc.match.MatchWithError(req) + if err != nil { + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err) + } if actual != tc.expect { t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input) continue @@ -818,7 +833,10 @@ func TestQueryMatcher(t *testing.T) { repl.Set("http.vars.debug", "1") repl.Set("http.vars.key", "somekey") req = req.WithContext(ctx) - actual := tc.match.Match(req) + actual, err := tc.match.MatchWithError(req) + if err != nil { + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err) + } if actual != tc.expect { t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input) continue @@ -887,7 +905,10 @@ func TestHeaderREMatcher(t *testing.T) { req = req.WithContext(ctx) addHTTPVarsToReplacer(repl, req, httptest.NewRecorder()) - actual := tc.match.Match(req) + actual, err := tc.match.MatchWithError(req) + if err != nil { + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err) + } if actual != tc.expect { t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'", i, tc.match, tc.expect, actual, tc.input) @@ -926,8 +947,8 @@ func BenchmarkHeaderREMatcher(b *testing.B) { ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) addHTTPVarsToReplacer(repl, req, httptest.NewRecorder()) - for run := 0; run < b.N; run++ { - match.Match(req) + for b.Loop() { + match.MatchWithError(req) } } @@ -971,7 +992,6 @@ func TestVarREMatcher(t *testing.T) { expect: true, }, } { - i := i // capture range value tc := tc // capture range value t.Run(tc.desc, func(t *testing.T) { t.Parallel() @@ -998,7 +1018,10 @@ func TestVarREMatcher(t *testing.T) { tc.input.ServeHTTP(httptest.NewRecorder(), req, emptyHandler) - actual := tc.match.Match(req) + actual, err := tc.match.MatchWithError(req) + if err != nil { + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err) + } if actual != tc.expect { t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'", i, tc.match, tc.expect, actual, tc.input) @@ -1123,7 +1146,10 @@ func TestNotMatcher(t *testing.T) { ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) req = req.WithContext(ctx) - actual := tc.match.Match(req) + actual, err := tc.match.MatchWithError(req) + if err != nil { + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err) + } if actual != tc.expect { t.Errorf("Test %d %+v: Expected %t, got %t for: host=%s path=%s'", i, tc.match, tc.expect, actual, tc.host, tc.path) continue @@ -1153,9 +1179,8 @@ func BenchmarkLargeHostMatcher(b *testing.B) { b.Fatal(err) } - b.ResetTimer() - for i := 0; i < b.N; i++ { - matcher.Match(req) + for b.Loop() { + matcher.MatchWithError(req) } } @@ -1167,9 +1192,8 @@ func BenchmarkHostMatcherWithoutPlaceholder(b *testing.B) { match := MatchHost{"localhost"} - b.ResetTimer() - for i := 0; i < b.N; i++ { - match.Match(req) + for b.Loop() { + match.MatchWithError(req) } } @@ -1185,8 +1209,7 @@ func BenchmarkHostMatcherWithPlaceholder(b *testing.B) { req = req.WithContext(ctx) match := MatchHost{"{env.GO_BENCHMARK_DOMAIN}"} - b.ResetTimer() - for i := 0; i < b.N; i++ { - match.Match(req) + for b.Loop() { + match.MatchWithError(req) } } diff --git a/modules/caddyhttp/metrics.go b/modules/caddyhttp/metrics.go index 111389218..9bb97e0b4 100644 --- a/modules/caddyhttp/metrics.go +++ b/modules/caddyhttp/metrics.go @@ -4,21 +4,30 @@ import ( "context" "errors" "net/http" + "strings" "sync" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/internal/metrics" ) // Metrics configures metrics observations. // EXPERIMENTAL and subject to change or removal. -type Metrics struct{} +type Metrics struct { + // Enable per-host metrics. Enabling this option may + // incur high-memory consumption, depending on the number of hosts + // managed by Caddy. + PerHost bool `json:"per_host,omitempty"` -var httpMetrics = struct { - init sync.Once + init sync.Once + httpMetrics *httpMetrics `json:"-"` +} + +type httpMetrics struct { requestInFlight *prometheus.GaugeVec requestCount *prometheus.CounterVec requestErrors *prometheus.CounterVec @@ -26,27 +35,28 @@ var httpMetrics = struct { requestSize *prometheus.HistogramVec responseSize *prometheus.HistogramVec responseDuration *prometheus.HistogramVec -}{ - init: sync.Once{}, } -func initHTTPMetrics() { +func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) { const ns, sub = "caddy", "http" - + registry := ctx.GetMetricsRegistry() basicLabels := []string{"server", "handler"} - httpMetrics.requestInFlight = promauto.NewGaugeVec(prometheus.GaugeOpts{ + if metrics.PerHost { + basicLabels = append(basicLabels, "host") + } + metrics.httpMetrics.requestInFlight = promauto.With(registry).NewGaugeVec(prometheus.GaugeOpts{ Namespace: ns, Subsystem: sub, Name: "requests_in_flight", Help: "Number of requests currently handled by this server.", }, basicLabels) - httpMetrics.requestErrors = promauto.NewCounterVec(prometheus.CounterOpts{ + metrics.httpMetrics.requestErrors = promauto.With(registry).NewCounterVec(prometheus.CounterOpts{ Namespace: ns, Subsystem: sub, Name: "request_errors_total", Help: "Number of requests resulting in middleware errors.", }, basicLabels) - httpMetrics.requestCount = promauto.NewCounterVec(prometheus.CounterOpts{ + metrics.httpMetrics.requestCount = promauto.With(registry).NewCounterVec(prometheus.CounterOpts{ Namespace: ns, Subsystem: sub, Name: "requests_total", @@ -58,28 +68,31 @@ func initHTTPMetrics() { sizeBuckets := prometheus.ExponentialBuckets(256, 4, 8) httpLabels := []string{"server", "handler", "code", "method"} - httpMetrics.requestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + if metrics.PerHost { + httpLabels = append(httpLabels, "host") + } + metrics.httpMetrics.requestDuration = promauto.With(registry).NewHistogramVec(prometheus.HistogramOpts{ Namespace: ns, Subsystem: sub, Name: "request_duration_seconds", Help: "Histogram of round-trip request durations.", Buckets: durationBuckets, }, httpLabels) - httpMetrics.requestSize = promauto.NewHistogramVec(prometheus.HistogramOpts{ + metrics.httpMetrics.requestSize = promauto.With(registry).NewHistogramVec(prometheus.HistogramOpts{ Namespace: ns, Subsystem: sub, Name: "request_size_bytes", Help: "Total size of the request. Includes body", Buckets: sizeBuckets, }, httpLabels) - httpMetrics.responseSize = promauto.NewHistogramVec(prometheus.HistogramOpts{ + metrics.httpMetrics.responseSize = promauto.With(registry).NewHistogramVec(prometheus.HistogramOpts{ Namespace: ns, Subsystem: sub, Name: "response_size_bytes", Help: "Size of the returned response.", Buckets: sizeBuckets, }, httpLabels) - httpMetrics.responseDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + metrics.httpMetrics.responseDuration = promauto.With(registry).NewHistogramVec(prometheus.HistogramOpts{ Namespace: ns, Subsystem: sub, Name: "response_duration_seconds", @@ -101,14 +114,15 @@ func serverNameFromContext(ctx context.Context) string { type metricsInstrumentedHandler struct { handler string mh MiddlewareHandler + metrics *Metrics } -func newMetricsInstrumentedHandler(handler string, mh MiddlewareHandler) *metricsInstrumentedHandler { - httpMetrics.init.Do(func() { - initHTTPMetrics() +func newMetricsInstrumentedHandler(ctx caddy.Context, handler string, mh MiddlewareHandler, metrics *Metrics) *metricsInstrumentedHandler { + metrics.init.Do(func() { + initHTTPMetrics(ctx, metrics) }) - return &metricsInstrumentedHandler{handler, mh} + return &metricsInstrumentedHandler{handler, mh, metrics} } func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { @@ -119,7 +133,12 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re // of a panic statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""} - inFlight := httpMetrics.requestInFlight.With(labels) + if h.metrics.PerHost { + labels["host"] = strings.ToLower(r.Host) + statusLabels["host"] = strings.ToLower(r.Host) + } + + inFlight := h.metrics.httpMetrics.requestInFlight.With(labels) inFlight.Inc() defer inFlight.Dec() @@ -131,13 +150,13 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re writeHeaderRecorder := ShouldBufferFunc(func(status int, header http.Header) bool { statusLabels["code"] = metrics.SanitizeCode(status) ttfb := time.Since(start).Seconds() - httpMetrics.responseDuration.With(statusLabels).Observe(ttfb) + h.metrics.httpMetrics.responseDuration.With(statusLabels).Observe(ttfb) return false }) wrec := NewResponseRecorder(w, nil, writeHeaderRecorder) err := h.mh.ServeHTTP(wrec, r, next) dur := time.Since(start).Seconds() - httpMetrics.requestCount.With(labels).Inc() + h.metrics.httpMetrics.requestCount.With(labels).Inc() observeRequest := func(status int) { // If the code hasn't been set yet, and we didn't encounter an error, we're @@ -148,9 +167,9 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re statusLabels["code"] = metrics.SanitizeCode(status) } - httpMetrics.requestDuration.With(statusLabels).Observe(dur) - httpMetrics.requestSize.With(statusLabels).Observe(float64(computeApproximateRequestSize(r))) - httpMetrics.responseSize.With(statusLabels).Observe(float64(wrec.Size())) + h.metrics.httpMetrics.requestDuration.With(statusLabels).Observe(dur) + h.metrics.httpMetrics.requestSize.With(statusLabels).Observe(float64(computeApproximateRequestSize(r))) + h.metrics.httpMetrics.responseSize.With(statusLabels).Observe(float64(wrec.Size())) } if err != nil { @@ -159,7 +178,7 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re observeRequest(handlerErr.StatusCode) } - httpMetrics.requestErrors.With(labels).Inc() + h.metrics.httpMetrics.requestErrors.With(labels).Inc() return err } diff --git a/modules/caddyhttp/metrics_test.go b/modules/caddyhttp/metrics_test.go index 8f88549d5..4e1aa8b30 100644 --- a/modules/caddyhttp/metrics_test.go +++ b/modules/caddyhttp/metrics_test.go @@ -6,10 +6,12 @@ import ( "net/http" "net/http/httptest" "strings" + "sync" "testing" - "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" + + "github.com/caddyserver/caddy/v2" ) func TestServerNameFromContext(t *testing.T) { @@ -27,10 +29,15 @@ func TestServerNameFromContext(t *testing.T) { } func TestMetricsInstrumentedHandler(t *testing.T) { + ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) + metrics := &Metrics{ + init: sync.Once{}, + httpMetrics: &httpMetrics{}, + } handlerErr := errors.New("oh noes") response := []byte("hello world!") h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { - if actual := testutil.ToFloat64(httpMetrics.requestInFlight); actual != 1.0 { + if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 1.0 { t.Errorf("Not same: expected %#v, but got %#v", 1.0, actual) } if handlerErr == nil { @@ -43,7 +50,7 @@ func TestMetricsInstrumentedHandler(t *testing.T) { return h.ServeHTTP(w, r) }) - ih := newMetricsInstrumentedHandler("bar", mh) + ih := newMetricsInstrumentedHandler(ctx, "bar", mh, metrics) r := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder() @@ -51,7 +58,7 @@ func TestMetricsInstrumentedHandler(t *testing.T) { if actual := ih.ServeHTTP(w, r, h); actual != handlerErr { t.Errorf("Not same: expected %#v, but got %#v", handlerErr, actual) } - if actual := testutil.ToFloat64(httpMetrics.requestInFlight); actual != 0.0 { + if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 0.0 { t.Errorf("Not same: expected %#v, but got %#v", 0.0, actual) } @@ -64,7 +71,7 @@ func TestMetricsInstrumentedHandler(t *testing.T) { mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { return nil }) - ih = newMetricsInstrumentedHandler("empty", mh) + ih = newMetricsInstrumentedHandler(ctx, "empty", mh, metrics) r = httptest.NewRequest("GET", "/", nil) w = httptest.NewRecorder() @@ -83,7 +90,7 @@ func TestMetricsInstrumentedHandler(t *testing.T) { return Error(http.StatusTooManyRequests, nil) }) - ih = newMetricsInstrumentedHandler("foo", mh) + ih = newMetricsInstrumentedHandler(ctx, "foo", mh, metrics) r = httptest.NewRequest("GET", "/", nil) w = httptest.NewRecorder() @@ -183,7 +190,183 @@ func TestMetricsInstrumentedHandler(t *testing.T) { caddy_http_request_errors_total{handler="bar",server="UNKNOWN"} 1 caddy_http_request_errors_total{handler="foo",server="UNKNOWN"} 1 ` - if err := testutil.GatherAndCompare(prometheus.DefaultGatherer, strings.NewReader(expected), + if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected), + "caddy_http_request_size_bytes", + "caddy_http_response_size_bytes", + // caddy_http_request_duration_seconds_sum will vary based on how long the test took to run, + // so we check just the _bucket and _count metrics + "caddy_http_request_duration_seconds_bucket", + "caddy_http_request_duration_seconds_count", + "caddy_http_request_errors_total", + ); err != nil { + t.Errorf("received unexpected error: %s", err) + } +} + +func TestMetricsInstrumentedHandlerPerHost(t *testing.T) { + ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) + metrics := &Metrics{ + PerHost: true, + init: sync.Once{}, + httpMetrics: &httpMetrics{}, + } + handlerErr := errors.New("oh noes") + response := []byte("hello world!") + h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 1.0 { + t.Errorf("Not same: expected %#v, but got %#v", 1.0, actual) + } + if handlerErr == nil { + w.Write(response) + } + return handlerErr + }) + + mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { + return h.ServeHTTP(w, r) + }) + + ih := newMetricsInstrumentedHandler(ctx, "bar", mh, metrics) + + r := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + + if actual := ih.ServeHTTP(w, r, h); actual != handlerErr { + t.Errorf("Not same: expected %#v, but got %#v", handlerErr, actual) + } + if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 0.0 { + t.Errorf("Not same: expected %#v, but got %#v", 0.0, actual) + } + + handlerErr = nil + if err := ih.ServeHTTP(w, r, h); err != nil { + t.Errorf("Received unexpected error: %v", err) + } + + // an empty handler - no errors, no header written + mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { + return nil + }) + ih = newMetricsInstrumentedHandler(ctx, "empty", mh, metrics) + r = httptest.NewRequest("GET", "/", nil) + w = httptest.NewRecorder() + + if err := ih.ServeHTTP(w, r, h); err != nil { + t.Errorf("Received unexpected error: %v", err) + } + if actual := w.Result().StatusCode; actual != 200 { + t.Errorf("Not same: expected status code %#v, but got %#v", 200, actual) + } + if actual := w.Result().Header; len(actual) != 0 { + t.Errorf("Not empty: expected headers to be empty, but got %#v", actual) + } + + // handler returning an error with an HTTP status + mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { + return Error(http.StatusTooManyRequests, nil) + }) + + ih = newMetricsInstrumentedHandler(ctx, "foo", mh, metrics) + + r = httptest.NewRequest("GET", "/", nil) + w = httptest.NewRecorder() + + if err := ih.ServeHTTP(w, r, nil); err == nil { + t.Errorf("expected error to be propagated") + } + + expected := ` + # HELP caddy_http_request_duration_seconds Histogram of round-trip request durations. + # TYPE caddy_http_request_duration_seconds histogram + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="0.005"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="0.01"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="0.025"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="0.05"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="0.1"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="0.25"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="0.5"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="1"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="2.5"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="5"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="10"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="+Inf"} 1 + caddy_http_request_duration_seconds_count{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN"} 1 + # HELP caddy_http_request_size_bytes Total size of the request. Includes body + # TYPE caddy_http_request_size_bytes histogram + caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="256"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="1024"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="4096"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="16384"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="65536"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="262144"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="1.048576e+06"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="4.194304e+06"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="+Inf"} 1 + caddy_http_request_size_bytes_sum{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN"} 23 + caddy_http_request_size_bytes_count{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="256"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="1024"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="4096"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="16384"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="65536"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="262144"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="1.048576e+06"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="4.194304e+06"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="+Inf"} 1 + caddy_http_request_size_bytes_sum{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN"} 23 + caddy_http_request_size_bytes_count{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="256"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="1024"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="4096"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="16384"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="65536"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="262144"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="1.048576e+06"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="4.194304e+06"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="+Inf"} 1 + caddy_http_request_size_bytes_sum{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN"} 23 + caddy_http_request_size_bytes_count{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN"} 1 + # HELP caddy_http_response_size_bytes Size of the returned response. + # TYPE caddy_http_response_size_bytes histogram + caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="256"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="1024"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="4096"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="16384"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="65536"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="262144"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="1.048576e+06"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="4.194304e+06"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="+Inf"} 1 + caddy_http_response_size_bytes_sum{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN"} 12 + caddy_http_response_size_bytes_count{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="256"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="1024"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="4096"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="16384"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="65536"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="262144"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="1.048576e+06"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="4.194304e+06"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="+Inf"} 1 + caddy_http_response_size_bytes_sum{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN"} 0 + caddy_http_response_size_bytes_count{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="256"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="1024"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="4096"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="16384"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="65536"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="262144"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="1.048576e+06"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="4.194304e+06"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="+Inf"} 1 + caddy_http_response_size_bytes_sum{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN"} 0 + caddy_http_response_size_bytes_count{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN"} 1 + # HELP caddy_http_request_errors_total Number of requests resulting in middleware errors. + # TYPE caddy_http_request_errors_total counter + caddy_http_request_errors_total{handler="bar",host="example.com",server="UNKNOWN"} 1 + caddy_http_request_errors_total{handler="foo",host="example.com",server="UNKNOWN"} 1 + ` + if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected), "caddy_http_request_size_bytes", "caddy_http_response_size_bytes", // caddy_http_request_duration_seconds_sum will vary based on how long the test took to run, diff --git a/modules/caddyhttp/proxyprotocol/listenerwrapper.go b/modules/caddyhttp/proxyprotocol/listenerwrapper.go index f5f2099ce..f1d170c38 100644 --- a/modules/caddyhttp/proxyprotocol/listenerwrapper.go +++ b/modules/caddyhttp/proxyprotocol/listenerwrapper.go @@ -25,7 +25,12 @@ import ( ) // ListenerWrapper provides PROXY protocol support to Caddy by implementing -// the caddy.ListenerWrapper interface. It must be loaded before the `tls` listener. +// the caddy.ListenerWrapper interface. If a connection is received via Unix +// socket, it's trusted. Otherwise, it's checked against the Allow/Deny lists, +// then it's handled by the FallbackPolicy. +// +// It must be loaded before the `tls` listener because the PROXY protocol +// encapsulates the TLS data. // // Credit goes to https://github.com/mastercactapus/caddy2-proxyprotocol for having // initially implemented this as a plugin. @@ -45,8 +50,35 @@ type ListenerWrapper struct { Deny []string `json:"deny,omitempty"` deny []netip.Prefix - // Accepted values are: ignore, use, reject, require, skip - // default: ignore + // FallbackPolicy specifies the policy to use if the downstream + // IP address is not in the Allow list nor is in the Deny list. + // + // NOTE: The generated docs which describe the value of this + // field is wrong because of how this type unmarshals JSON in a + // custom way. The field expects a string, not a number. + // + // Accepted values are: IGNORE, USE, REJECT, REQUIRE, SKIP + // + // - IGNORE: address from PROXY header, but accept connection + // + // - USE: address from PROXY header + // + // - REJECT: connection when PROXY header is sent + // Note: even though the first read on the connection returns an error if + // a PROXY header is present, subsequent reads do not. It is the task of + // the code using the connection to handle that case properly. + // + // - REQUIRE: connection to send PROXY header, reject if not present + // Note: even though the first read on the connection returns an error if + // a PROXY header is not present, subsequent reads do not. It is the task + // of the code using the connection to handle that case properly. + // + // - SKIP: accepts a connection without requiring the PROXY header. + // Note: an example usage can be found in the SkipProxyHeaderForCIDR + // function. + // + // Default: IGNORE + // // Policy definitions are here: https://pkg.go.dev/github.com/pires/go-proxyproto@v0.7.0#Policy FallbackPolicy Policy `json:"fallback_policy,omitempty"` diff --git a/modules/caddyhttp/push/link.go b/modules/caddyhttp/push/link.go index 855dffd05..2a4af5803 100644 --- a/modules/caddyhttp/push/link.go +++ b/modules/caddyhttp/push/link.go @@ -41,7 +41,7 @@ func parseLinkHeader(header string) []linkResource { return resources } - for _, link := range strings.Split(header, comma) { + for link := range strings.SplitSeq(header, comma) { l := linkResource{params: make(map[string]string)} li, ri := strings.Index(link, "<"), strings.Index(link, ">") @@ -51,7 +51,7 @@ func parseLinkHeader(header string) []linkResource { l.uri = strings.TrimSpace(link[li+1 : ri]) - for _, param := range strings.Split(strings.TrimSpace(link[ri+1:]), semicolon) { + for param := range strings.SplitSeq(strings.TrimSpace(link[ri+1:]), semicolon) { before, after, isCut := strings.Cut(strings.TrimSpace(param), equal) key := strings.TrimSpace(before) if key == "" { diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index 2c0f32357..9c3ab85f2 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -172,8 +172,12 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo // current URI, including any internal rewrites case "http.request.uri": return req.URL.RequestURI(), true + case "http.request.uri_escaped": + return url.QueryEscape(req.URL.RequestURI()), true case "http.request.uri.path": return req.URL.Path, true + case "http.request.uri.path_escaped": + return url.QueryEscape(req.URL.Path), true case "http.request.uri.path.file": _, file := path.Split(req.URL.Path) return file, true @@ -186,6 +190,13 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo return path.Ext(req.URL.Path), true case "http.request.uri.query": return req.URL.RawQuery, true + case "http.request.uri.query_escaped": + return url.QueryEscape(req.URL.RawQuery), true + case "http.request.uri.prefixed_query": + if req.URL.RawQuery == "" { + return "", true + } + return "?" + req.URL.RawQuery, true case "http.request.duration": start := GetVar(req.Context(), "start_time").(time.Time) return time.Since(start), true @@ -239,6 +250,12 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo case "http.request.orig_uri.query": or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request) return or.URL.RawQuery, true + case "http.request.orig_uri.prefixed_query": + or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request) + if or.URL.RawQuery == "" { + return "", true + } + return "?" + or.URL.RawQuery, true } // remote IP range/prefix (e.g. keep top 24 bits of 1.2.3.4 => "1.2.3.0/24") @@ -272,7 +289,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo return prefix.String(), true } - // hostname labels + // hostname labels (case insensitive, so normalize to lowercase) if strings.HasPrefix(key, reqHostLabelsReplPrefix) { idxStr := key[len(reqHostLabelsReplPrefix):] idx, err := strconv.Atoi(idxStr) @@ -287,7 +304,7 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo if idx >= len(hostLabels) { return "", true } - return hostLabels[len(hostLabels)-idx-1], true + return strings.ToLower(hostLabels[len(hostLabels)-idx-1]), true } // path parts @@ -352,13 +369,13 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo } } - switch { - case key == "http.shutting_down": + switch key { + case "http.shutting_down": server := req.Context().Value(ServerCtxKey).(*Server) server.shutdownAtMu.RLock() defer server.shutdownAtMu.RUnlock() return !server.shutdownAt.IsZero(), true - case key == "http.time_until_shutdown": + case "http.time_until_shutdown": server := req.Context().Value(ServerCtxKey).(*Server) server.shutdownAtMu.RLock() defer server.shutdownAtMu.RUnlock() diff --git a/modules/caddyhttp/replacer_test.go b/modules/caddyhttp/replacer_test.go index 50a2e8c62..c75fe82ed 100644 --- a/modules/caddyhttp/replacer_test.go +++ b/modules/caddyhttp/replacer_test.go @@ -28,7 +28,7 @@ import ( ) func TestHTTPVarReplacement(t *testing.T) { - req, _ := http.NewRequest(http.MethodGet, "/foo/bar.tar.gz", nil) + req, _ := http.NewRequest(http.MethodGet, "/foo/bar.tar.gz?a=1&b=2", nil) repl := caddy.NewReplacer() localAddr, _ := net.ResolveTCPAddr("tcp", "192.168.159.1:80") ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) @@ -142,6 +142,22 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV get: "http.request.host.labels.2", expect: "", }, + { + get: "http.request.uri", + expect: "/foo/bar.tar.gz?a=1&b=2", + }, + { + get: "http.request.uri_escaped", + expect: "%2Ffoo%2Fbar.tar.gz%3Fa%3D1%26b%3D2", + }, + { + get: "http.request.uri.path", + expect: "/foo/bar.tar.gz", + }, + { + get: "http.request.uri.path_escaped", + expect: "%2Ffoo%2Fbar.tar.gz", + }, { get: "http.request.uri.path.file", expect: "bar.tar.gz", @@ -155,6 +171,26 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV get: "http.request.uri.path.file.ext", expect: ".gz", }, + { + get: "http.request.uri.query", + expect: "a=1&b=2", + }, + { + get: "http.request.uri.query_escaped", + expect: "a%3D1%26b%3D2", + }, + { + get: "http.request.uri.query.a", + expect: "1", + }, + { + get: "http.request.uri.query.b", + expect: "2", + }, + { + get: "http.request.uri.prefixed_query", + expect: "?a=1&b=2", + }, { get: "http.request.tls.cipher_suite", expect: "TLS_AES_256_GCM_SHA384", diff --git a/modules/caddyhttp/requestbody/caddyfile.go b/modules/caddyhttp/requestbody/caddyfile.go index 8378ad7f4..e2382d546 100644 --- a/modules/caddyhttp/requestbody/caddyfile.go +++ b/modules/caddyhttp/requestbody/caddyfile.go @@ -68,6 +68,12 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) } rb.WriteTimeout = timeout + case "set": + var setStr string + if !h.AllArgs(&setStr) { + return nil, h.ArgErr() + } + rb.Set = setStr default: return nil, h.Errf("unrecognized request_body subdirective '%s'", h.Val()) } diff --git a/modules/caddyhttp/requestbody/requestbody.go b/modules/caddyhttp/requestbody/requestbody.go index 1c804aa13..1fa654811 100644 --- a/modules/caddyhttp/requestbody/requestbody.go +++ b/modules/caddyhttp/requestbody/requestbody.go @@ -15,8 +15,10 @@ package requestbody import ( + "errors" "io" "net/http" + "strings" "time" "go.uber.org/zap" @@ -42,6 +44,10 @@ type RequestBody struct { // EXPERIMENTAL. Subject to change/removal. WriteTimeout time.Duration `json:"write_timeout,omitempty"` + // This field permit to replace body on the fly + // EXPERIMENTAL. Subject to change/removal. + Set string `json:"set,omitempty"` + logger *zap.Logger } @@ -59,6 +65,18 @@ func (rb *RequestBody) Provision(ctx caddy.Context) error { } func (rb RequestBody) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { + if rb.Set != "" { + if r.Body != nil { + err := r.Body.Close() + if err != nil { + return err + } + } + repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + replacedBody := repl.ReplaceAll(rb.Set, "") + r.Body = io.NopCloser(strings.NewReader(replacedBody)) + r.ContentLength = int64(len(replacedBody)) + } if r.Body == nil { return next.ServeHTTP(w, r) } @@ -94,7 +112,8 @@ type errorWrapper struct { func (ew errorWrapper) Read(p []byte) (n int, err error) { n, err = ew.ReadCloser.Read(p) - if err != nil && err.Error() == "http: request body too large" { + var mbe *http.MaxBytesError + if errors.As(err, &mbe) { err = caddyhttp.Error(http.StatusRequestEntityTooLarge, err) } return diff --git a/modules/caddyhttp/responsematchers.go b/modules/caddyhttp/responsematchers.go index cf96b00cd..a6b34c76d 100644 --- a/modules/caddyhttp/responsematchers.go +++ b/modules/caddyhttp/responsematchers.go @@ -41,7 +41,7 @@ func (rm ResponseMatcher) Match(statusCode int, hdr http.Header) bool { if !rm.matchStatusCode(statusCode) { return false } - return matchHeaders(hdr, rm.Headers, "", nil) + return matchHeaders(hdr, rm.Headers, "", []string{}, nil) } func (rm ResponseMatcher) matchStatusCode(statusCode int) bool { diff --git a/modules/caddyhttp/responsewriter.go b/modules/caddyhttp/responsewriter.go index 3c0f89d0b..904c30c03 100644 --- a/modules/caddyhttp/responsewriter.go +++ b/modules/caddyhttp/responsewriter.go @@ -154,16 +154,16 @@ func (rr *responseRecorder) WriteHeader(statusCode int) { // connections by manually setting headers and writing status 101 rr.statusCode = statusCode + // decide whether we should buffer the response + if rr.shouldBuffer == nil { + rr.stream = true + } else { + rr.stream = !rr.shouldBuffer(rr.statusCode, rr.ResponseWriterWrapper.Header()) + } + // 1xx responses aren't final; just informational if statusCode < 100 || statusCode > 199 { rr.wroteHeader = true - - // decide whether we should buffer the response - if rr.shouldBuffer == nil { - rr.stream = true - } else { - rr.stream = !rr.shouldBuffer(rr.statusCode, rr.ResponseWriterWrapper.Header()) - } } // if informational or not buffered, immediately write header diff --git a/modules/caddyhttp/reverseproxy/buffering_test.go b/modules/caddyhttp/reverseproxy/buffering_test.go new file mode 100644 index 000000000..68514814c --- /dev/null +++ b/modules/caddyhttp/reverseproxy/buffering_test.go @@ -0,0 +1,84 @@ +package reverseproxy + +import ( + "io" + "testing" +) + +type zeroReader struct{} + +func (zeroReader) Read(p []byte) (int, error) { + for i := range p { + p[i] = 0 + } + return len(p), nil +} + +func TestBuffering(t *testing.T) { + var ( + h Handler + zr zeroReader + ) + type args struct { + body io.ReadCloser + limit int64 + } + tests := []struct { + name string + args args + resultCheck func(io.ReadCloser, int64, args) bool + }{ + { + name: "0 limit, body is returned as is", + args: args{ + body: io.NopCloser(&zr), + limit: 0, + }, + resultCheck: func(res io.ReadCloser, read int64, args args) bool { + return res == args.body && read == args.limit && read == 0 + }, + }, + { + name: "negative limit, body is read completely", + args: args{ + body: io.NopCloser(io.LimitReader(&zr, 100)), + limit: -1, + }, + resultCheck: func(res io.ReadCloser, read int64, args args) bool { + brc, ok := res.(bodyReadCloser) + return ok && brc.body == nil && brc.buf.Len() == 100 && read == 100 + }, + }, + { + name: "positive limit, body is read partially", + args: args{ + body: io.NopCloser(io.LimitReader(&zr, 100)), + limit: 50, + }, + resultCheck: func(res io.ReadCloser, read int64, args args) bool { + brc, ok := res.(bodyReadCloser) + return ok && brc.body != nil && brc.buf.Len() == 50 && read == 50 + }, + }, + { + name: "positive limit, body is read completely", + args: args{ + body: io.NopCloser(io.LimitReader(&zr, 100)), + limit: 101, + }, + resultCheck: func(res io.ReadCloser, read int64, args args) bool { + brc, ok := res.(bodyReadCloser) + return ok && brc.body == nil && brc.buf.Len() == 100 && read == 100 + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, read := h.bufferedBody(tt.args.body, tt.args.limit) + if !tt.resultCheck(res, read, tt.args) { + t.Error("Handler.bufferedBody() test failed") + return + } + }) + } +} diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index ab1dcdd02..8439d1d51 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -33,6 +33,7 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" "github.com/caddyserver/caddy/v2/modules/caddytls" + "github.com/caddyserver/caddy/v2/modules/internal/network" ) func init() { @@ -664,9 +665,10 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if d.NextArg() { return d.ArgErr() } - if subdir == "request_buffers" { + switch subdir { + case "request_buffers": h.RequestBuffers = size - } else if subdir == "response_buffers" { + case "response_buffers": h.ResponseBuffers = size } @@ -979,7 +981,9 @@ func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error // read_buffer // write_buffer // max_response_header -// forward_proxy_url +// network_proxy { +// ... +// } // dial_timeout // dial_fallback_delay // response_header_timeout @@ -990,6 +994,9 @@ func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error // tls_insecure_skip_verify // tls_timeout // tls_trusted_ca_certs +// tls_trust_pool { +// ... +// } // tls_server_name // tls_renegotiation // tls_except_ports @@ -1068,10 +1075,24 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } case "forward_proxy_url": + caddy.Log().Warn("The 'forward_proxy_url' field is deprecated. Use 'network_proxy ' instead.") if !d.NextArg() { return d.ArgErr() } - h.ForwardProxyURL = d.Val() + u := network.ProxyFromURL{URL: d.Val()} + h.NetworkProxyRaw = caddyconfig.JSONModuleObject(u, "from", "url", nil) + + case "network_proxy": + if !d.NextArg() { + return d.ArgErr() + } + modStem := d.Val() + modID := "caddy.network_proxy." + modStem + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return err + } + h.NetworkProxyRaw = caddyconfig.JSONModuleObject(unm, "from", modStem, nil) case "dial_timeout": if !d.NextArg() { diff --git a/modules/caddyhttp/reverseproxy/command.go b/modules/caddyhttp/reverseproxy/command.go index f9304efa2..54955bcf7 100644 --- a/modules/caddyhttp/reverseproxy/command.go +++ b/modules/caddyhttp/reverseproxy/command.go @@ -122,9 +122,10 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) { } } if fromAddr.Port == "" { - if fromAddr.Scheme == "http" { + switch fromAddr.Scheme { + case "http": fromAddr.Port = httpPort - } else if fromAddr.Scheme == "https" { + case "https": fromAddr.Port = httpsPort } } diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go index 68eee32be..2325af9a7 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go @@ -17,7 +17,9 @@ package fastcgi import ( "encoding/json" "net/http" + "slices" "strconv" + "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -130,15 +132,18 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // is equivalent to a route consisting of: // // # Add trailing slash for directory requests +// # This redirection is automatically disabled if "{http.request.uri.path}/index.php" +// # doesn't appear in the try_files list // @canonicalPath { // file {path}/index.php // not path */ // } // redir @canonicalPath {path}/ 308 // -// # If the requested file does not exist, try index files +// # If the requested file does not exist, try index files and assume index.php always exists // @indexFiles file { // try_files {path} {path}/index.php index.php +// try_policy first_exist_fallback // split_path .php // } // rewrite @indexFiles {http.matchers.file.relative} @@ -179,7 +184,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error indexFile := "index.php" // set up for explicitly overriding try_files - tryFiles := []string{} + var tryFiles []string // if the user specified a matcher token, use that // matcher in a route that wraps both of our routes; @@ -310,37 +315,54 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error // if the index is turned off, we skip the redirect and try_files if indexFile != "off" { - // route to redirect to canonical path if index PHP file - redirMatcherSet := caddy.ModuleMap{ - "file": h.JSON(fileserver.MatchFile{ - TryFiles: []string{"{http.request.uri.path}/" + indexFile}, - }), - "not": h.JSON(caddyhttp.MatchNot{ - MatcherSetsRaw: []caddy.ModuleMap{ - { - "path": h.JSON(caddyhttp.MatchPath{"*/"}), - }, - }, - }), - } - redirHandler := caddyhttp.StaticResponse{ - StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)), - Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}}, - } - redirRoute := caddyhttp.Route{ - MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet}, - HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)}, - } + var dirRedir bool + dirIndex := "{http.request.uri.path}/" + indexFile + tryPolicy := "first_exist_fallback" // if tryFiles wasn't overridden, use a reasonable default if len(tryFiles) == 0 { - tryFiles = []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile} + tryFiles = []string{"{http.request.uri.path}", dirIndex, indexFile} + dirRedir = true + } else { + if !strings.HasSuffix(tryFiles[len(tryFiles)-1], ".php") { + // use first_exist strategy if the last file is not a PHP file + tryPolicy = "" + } + + dirRedir = slices.Contains(tryFiles, dirIndex) + } + + if dirRedir { + // route to redirect to canonical path if index PHP file + redirMatcherSet := caddy.ModuleMap{ + "file": h.JSON(fileserver.MatchFile{ + TryFiles: []string{dirIndex}, + }), + "not": h.JSON(caddyhttp.MatchNot{ + MatcherSetsRaw: []caddy.ModuleMap{ + { + "path": h.JSON(caddyhttp.MatchPath{"*/"}), + }, + }, + }), + } + redirHandler := caddyhttp.StaticResponse{ + StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)), + Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/{http.request.orig_uri.prefixed_query}"}}, + } + redirRoute := caddyhttp.Route{ + MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet}, + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)}, + } + + routes = append(routes, redirRoute) } // route to rewrite to PHP index file rewriteMatcherSet := caddy.ModuleMap{ "file": h.JSON(fileserver.MatchFile{ TryFiles: tryFiles, + TryPolicy: tryPolicy, SplitPath: extensions, }), } @@ -352,7 +374,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)}, } - routes = append(routes, redirRoute, rewriteRoute) + routes = append(routes, rewriteRoute) } // route to actually reverse proxy requests to PHP files; diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client.go b/modules/caddyhttp/reverseproxy/fastcgi/client.go index 7284fe672..684394f53 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/client.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/client.go @@ -41,6 +41,8 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" + + "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) // FCGIListenSockFileno describes listen socket file number. @@ -136,6 +138,15 @@ type client struct { // Do made the request and returns a io.Reader that translates the data read // from fcgi responder out of fcgi packet before returning it. func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error) { + // check for CONTENT_LENGTH, since the lack of it or wrong value will cause the backend to hang + if clStr, ok := p["CONTENT_LENGTH"]; !ok { + return nil, caddyhttp.Error(http.StatusLengthRequired, nil) + } else if _, err := strconv.ParseUint(clStr, 10, 64); err != nil { + // stdlib won't return a negative Content-Length, but we check just in case, + // the most likely cause is from a missing content length, which is -1 + return nil, caddyhttp.Error(http.StatusLengthRequired, err) + } + writer := &streamWriter{c: c} writer.buf = bufPool.Get().(*bytes.Buffer) writer.buf.Reset() diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go index 3985465ba..d451dd380 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go @@ -228,7 +228,7 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) { ip = strings.Replace(ip, "]", "", 1) // make sure file root is absolute - root, err := filepath.Abs(repl.ReplaceAll(t.Root, ".")) + root, err := caddy.FastAbs(repl.ReplaceAll(t.Root, ".")) if err != nil { return nil, err } diff --git a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go index 8350096ae..347f6dfbf 100644 --- a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go @@ -17,6 +17,7 @@ package forwardauth import ( "encoding/json" "net/http" + "sort" "strings" "github.com/caddyserver/caddy/v2" @@ -170,42 +171,66 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) return nil, dispenser.Errf("the 'uri' subdirective is required") } - // set up handler for good responses; when a response - // has 2xx status, then we will copy some headers from - // the response onto the original request, and allow - // handling to continue down the middleware chain, - // by _not_ executing a terminal handler. + // Set up handler for good responses; when a response has 2xx status, + // then we will copy some headers from the response onto the original + // request, and allow handling to continue down the middleware chain, + // by _not_ executing a terminal handler. We must have at least one + // route in the response handler, even if it's no-op, so that the + // response handling logic in reverse_proxy doesn't skip this entry. goodResponseHandler := caddyhttp.ResponseHandler{ Match: &caddyhttp.ResponseMatcher{ StatusCode: []int{2}, }, - Routes: []caddyhttp.Route{}, - } - - handler := &headers.Handler{ - Request: &headers.HeaderOps{ - Set: http.Header{}, + Routes: []caddyhttp.Route{ + { + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject( + &caddyhttp.VarsMiddleware{}, + "handler", + "vars", + nil, + )}, + }, }, } - // the list of headers to copy may be empty, but that's okay; we - // need at least one handler in the routes for the response handling - // logic in reverse_proxy to not skip this entry as empty. - for from, to := range headersToCopy { - handler.Request.Set.Set(to, "{http.reverse_proxy.header."+http.CanonicalHeaderKey(from)+"}") + // Sort the headers so that the order in the JSON output is deterministic. + sortedHeadersToCopy := make([]string, 0, len(headersToCopy)) + for k := range headersToCopy { + sortedHeadersToCopy = append(sortedHeadersToCopy, k) } + sort.Strings(sortedHeadersToCopy) - goodResponseHandler.Routes = append( - goodResponseHandler.Routes, - caddyhttp.Route{ + // Set up handlers to copy headers from the auth response onto the + // original request. We use vars matchers to test that the placeholder + // values aren't empty, because the header handler would not replace + // placeholders which have no value. + copyHeaderRoutes := []caddyhttp.Route{} + for _, from := range sortedHeadersToCopy { + to := http.CanonicalHeaderKey(headersToCopy[from]) + placeholderName := "http.reverse_proxy.header." + http.CanonicalHeaderKey(from) + handler := &headers.Handler{ + Request: &headers.HeaderOps{ + Set: http.Header{ + to: []string{"{" + placeholderName + "}"}, + }, + }, + } + copyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{ + MatcherSetsRaw: []caddy.ModuleMap{{ + "not": h.JSON(caddyhttp.MatchNot{MatcherSetsRaw: []caddy.ModuleMap{{ + "vars": h.JSON(caddyhttp.VarsMatcher{"{" + placeholderName + "}": []string{""}}), + }}}), + }}, HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject( handler, "handler", "headers", nil, )}, - }, - ) + }) + } + + goodResponseHandler.Routes = append(goodResponseHandler.Routes, copyHeaderRoutes...) // note that when a response has any other status than 2xx, then we // use the reverse proxy's default behaviour of copying the response diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index 1735e45a4..ac42570b2 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -72,7 +72,7 @@ type HealthChecks struct { // health checks (that is, health checks which occur in a // background goroutine independently). type ActiveHealthChecks struct { - // DEPRECATED: Use 'uri' instead. This field will be removed. TODO: remove this field + // Deprecated: Use 'uri' instead. This field will be removed. TODO: remove this field Path string `json:"path,omitempty"` // The URI (path and query) to use for health checks @@ -309,7 +309,9 @@ func (h *Handler) doActiveHealthCheckForAllHosts() { } }() - networkAddr, err := caddy.NewReplacer().ReplaceOrErr(upstream.Dial, true, true) + repl := caddy.NewReplacer() + + networkAddr, err := repl.ReplaceOrErr(upstream.Dial, true, true) if err != nil { if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "invalid use of placeholders in dial address for active health checks"); c != nil { c.Write( @@ -344,14 +346,24 @@ func (h *Handler) doActiveHealthCheckForAllHosts() { return } hostAddr := addr.JoinHostPort(0) - dialAddr := hostAddr if addr.IsUnixNetwork() || addr.IsFdNetwork() { // this will be used as the Host portion of a http.Request URL, and // paths to socket files would produce an error when creating URL, // so use a fake Host value instead; unix sockets are usually local hostAddr = "localhost" } - err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: dialAddr}, hostAddr, networkAddr, upstream) + + // Fill in the dial info for the upstream + // If the upstream is set, use that instead + dialInfoUpstream := upstream + if h.HealthChecks.Active.Upstream != "" { + dialInfoUpstream = &Upstream{ + Dial: h.HealthChecks.Active.Upstream, + } + } + dialInfo, _ := dialInfoUpstream.fillDialInfo(repl) + + err = h.doActiveHealthCheck(dialInfo, hostAddr, networkAddr, upstream) if err != nil { if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "active health check failed"); c != nil { c.Write( @@ -472,7 +484,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ markHealthy := func() { // increment passes and then check if it has reached the threshold to be healthy - err := upstream.Host.countHealthPass(1) + err := upstream.countHealthPass(1) if err != nil { if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "could not count active health pass"); c != nil { c.Write( diff --git a/modules/caddyhttp/reverseproxy/hosts.go b/modules/caddyhttp/reverseproxy/hosts.go index 0a676e431..300003f2b 100644 --- a/modules/caddyhttp/reverseproxy/hosts.go +++ b/modules/caddyhttp/reverseproxy/hosts.go @@ -17,7 +17,6 @@ package reverseproxy import ( "context" "fmt" - "net/http" "net/netip" "strconv" "sync/atomic" @@ -100,8 +99,7 @@ func (u *Upstream) Full() bool { // fillDialInfo returns a filled DialInfo for upstream u, using the request // context. Note that the returned value is not a pointer. -func (u *Upstream) fillDialInfo(r *http.Request) (DialInfo, error) { - repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) +func (u *Upstream) fillDialInfo(repl *caddy.Replacer) (DialInfo, error) { var addr caddy.NetworkAddress // use provided dial address diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index bc8233788..1a6be4d05 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -24,7 +24,6 @@ import ( weakrand "math/rand" "net" "net/http" - "net/url" "os" "reflect" "slices" @@ -38,8 +37,10 @@ import ( "golang.org/x/net/http2" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddytls" + "github.com/caddyserver/caddy/v2/modules/internal/network" ) func init() { @@ -90,6 +91,7 @@ type HTTPTransport struct { // forward_proxy_url -> upstream // // Default: http.ProxyFromEnvironment + // DEPRECATED: Use NetworkProxyRaw|`network_proxy` instead. Subject to removal. ForwardProxyURL string `json:"forward_proxy_url,omitempty"` // How long to wait before timing out trying to connect to @@ -141,8 +143,24 @@ type HTTPTransport struct { // The pre-configured underlying HTTP transport. Transport *http.Transport `json:"-"` + // The module that provides the network (forward) proxy + // URL that the HTTP transport will use to proxy + // requests to the upstream. See [http.Transport.Proxy](https://pkg.go.dev/net/http#Transport.Proxy) + // for information regarding supported protocols. + // + // Providing a value to this parameter results in requests + // flowing through the reverse_proxy in the following way: + // + // User Agent -> + // reverse_proxy -> + // [proxy provided by the module] -> upstream + // + // If nil, defaults to reading the `HTTP_PROXY`, + // `HTTPS_PROXY`, and `NO_PROXY` environment variables. + NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy inline_key=from"` + h2cTransport *http2.Transport - h3Transport *http3.RoundTripper // TODO: EXPERIMENTAL (May 2024) + h3Transport *http3.Transport // TODO: EXPERIMENTAL (May 2024) } // CaddyModule returns the Caddy module information. @@ -153,12 +171,25 @@ func (HTTPTransport) CaddyModule() caddy.ModuleInfo { } } +var ( + allowedVersions = []string{"1.1", "2", "h2c", "3"} + allowedVersionsString = strings.Join(allowedVersions, ", ") +) + // Provision sets up h.Transport with a *http.Transport // that is ready to use. func (h *HTTPTransport) Provision(ctx caddy.Context) error { if len(h.Versions) == 0 { h.Versions = []string{"1.1", "2"} } + // some users may provide http versions not recognized by caddy, instead of trying to + // guess the version, we just error out and let the user fix their config + // see: https://github.com/caddyserver/caddy/issues/7111 + for _, v := range h.Versions { + if !slices.Contains(allowedVersions, v) { + return fmt.Errorf("unsupported HTTP version: %s, supported version: %s", v, allowedVersionsString) + } + } rt, err := h.NewTransport(ctx) if err != nil { @@ -328,16 +359,22 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e } // negotiate any HTTP/SOCKS proxy for the HTTP transport - var proxy func(*http.Request) (*url.URL, error) + proxy := http.ProxyFromEnvironment if h.ForwardProxyURL != "" { - pUrl, err := url.Parse(h.ForwardProxyURL) + caddyCtx.Logger().Warn("forward_proxy_url is deprecated; use network_proxy instead") + u := network.ProxyFromURL{URL: h.ForwardProxyURL} + h.NetworkProxyRaw = caddyconfig.JSONModuleObject(u, "from", "url", nil) + } + if len(h.NetworkProxyRaw) != 0 { + proxyMod, err := caddyCtx.LoadModule(h, "NetworkProxyRaw") if err != nil { - return nil, fmt.Errorf("failed to parse transport proxy url: %v", err) + return nil, fmt.Errorf("failed to load network_proxy module: %v", err) + } + if m, ok := proxyMod.(caddy.ProxyFuncProducer); ok { + proxy = m.ProxyFunc() + } else { + return nil, fmt.Errorf("network_proxy module is not `(func(*http.Request) (*url.URL, error))``") } - caddyCtx.Logger().Info("setting transport proxy url", zap.String("url", h.ForwardProxyURL)) - proxy = http.ProxyURL(pUrl) - } else { - proxy = http.ProxyFromEnvironment } rt := &http.Transport{ @@ -358,10 +395,52 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e if err != nil { return nil, fmt.Errorf("making TLS client config: %v", err) } + + // servername has a placeholder, so we need to replace it + if strings.Contains(h.TLS.ServerName, "{") { + rt.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + // reuses the dialer from above to establish a plaintext connection + conn, err := dialContext(ctx, network, addr) + if err != nil { + return nil, err + } + + // but add our own handshake logic + repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + tlsConfig := rt.TLSClientConfig.Clone() + tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "") + tlsConn := tls.Client(conn, tlsConfig) + + // complete the handshake before returning the connection + if rt.TLSHandshakeTimeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, rt.TLSHandshakeTimeout) + defer cancel() + } + err = tlsConn.HandshakeContext(ctx) + if err != nil { + _ = tlsConn.Close() + return nil, err + } + return tlsConn, nil + } + } } if h.KeepAlive != nil { + // according to https://pkg.go.dev/net#Dialer.KeepAliveConfig, + // KeepAlive is ignored if KeepAliveConfig.Enable is true. + // If configured to 0, a system-dependent default is used. + // To disable tcp keepalive, choose a negative value, + // so KeepAliveConfig.Enable is false and KeepAlive is negative. + + // This is different from http keepalive where a tcp connection + // can transfer multiple http requests/responses. dialer.KeepAlive = time.Duration(h.KeepAlive.ProbeInterval) + dialer.KeepAliveConfig = net.KeepAliveConfig{ + Enable: h.KeepAlive.ProbeInterval > 0, + Interval: time.Duration(h.KeepAlive.ProbeInterval), + } if h.KeepAlive.Enabled != nil { rt.DisableKeepAlives = !*h.KeepAlive.Enabled } @@ -393,7 +472,7 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e // do (that'd add latency and complexity, besides, we expect that // site owners control the backends), so it must be exclusive if len(h.Versions) == 1 && h.Versions[0] == "3" { - h.h3Transport = new(http3.RoundTripper) + h.h3Transport = new(http3.Transport) if h.TLS != nil { var err error h.h3Transport.TLSClientConfig, err = h.TLS.MakeTLSClientConfig(caddyCtx) @@ -429,45 +508,9 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e return rt, nil } -// replaceTLSServername checks TLS servername to see if it needs replacing -// if it does need replacing, it creates a new cloned HTTPTransport object to avoid any races -// and does the replacing of the TLS servername on that and returns the new object -// if no replacement is necessary it returns the original -func (h *HTTPTransport) replaceTLSServername(repl *caddy.Replacer) *HTTPTransport { - // check whether we have TLS and need to replace the servername in the TLSClientConfig - if h.TLSEnabled() && strings.Contains(h.TLS.ServerName, "{") { - // make a new h, "copy" the parts we don't need to touch, add a new *tls.Config and replace servername - newtransport := &HTTPTransport{ - Resolver: h.Resolver, - TLS: h.TLS, - KeepAlive: h.KeepAlive, - Compression: h.Compression, - MaxConnsPerHost: h.MaxConnsPerHost, - DialTimeout: h.DialTimeout, - FallbackDelay: h.FallbackDelay, - ResponseHeaderTimeout: h.ResponseHeaderTimeout, - ExpectContinueTimeout: h.ExpectContinueTimeout, - MaxResponseHeaderSize: h.MaxResponseHeaderSize, - WriteBufferSize: h.WriteBufferSize, - ReadBufferSize: h.ReadBufferSize, - Versions: h.Versions, - Transport: h.Transport.Clone(), - h2cTransport: h.h2cTransport, - } - newtransport.Transport.TLSClientConfig.ServerName = repl.ReplaceAll(newtransport.Transport.TLSClientConfig.ServerName, "") - return newtransport - } - - return h -} - // RoundTrip implements http.RoundTripper. func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { - // Try to replace TLS servername if needed - repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) - transport := h.replaceTLSServername(repl) - - transport.SetScheme(req) + h.SetScheme(req) // use HTTP/3 if enabled (TODO: This is EXPERIMENTAL) if h.h3Transport != nil { @@ -483,7 +526,7 @@ func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { return h.h2cTransport.RoundTrip(req) } - return transport.Transport.RoundTrip(req) + return h.Transport.RoundTrip(req) } // SetScheme ensures that the outbound request req @@ -510,13 +553,7 @@ func (h *HTTPTransport) shouldUseTLS(req *http.Request) bool { } port := req.URL.Port() - for i := range h.TLS.ExceptPorts { - if h.TLS.ExceptPorts[i] == port { - return false - } - } - - return true + return !slices.Contains(h.TLS.ExceptPorts, port) } // TLSEnabled returns true if TLS is enabled. @@ -545,11 +582,11 @@ type TLSConfig struct { // Certificate authority module which provides the certificate pool of trusted certificates CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"` - // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.inline` module instead. + // Deprecated: Use the `ca` field with the `tls.ca_pool.source.inline` module instead. // Optional list of base64-encoded DER-encoded CA certificates to trust. RootCAPool []string `json:"root_ca_pool,omitempty"` - // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.file` module instead. + // Deprecated: Use the `ca` field with the `tls.ca_pool.source.file` module instead. // List of PEM-encoded CA certificate files to add to the same trust // store as RootCAPool (or root_ca_pool in the JSON). RootCAPEMFiles []string `json:"root_ca_pem_files,omitempty"` @@ -628,7 +665,7 @@ func (t *TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) return nil, fmt.Errorf("getting tls app: %v", err) } tlsApp := tlsAppIface.(*caddytls.TLS) - err = tlsApp.Manage([]string{t.ClientCertificateAutomate}) + err = tlsApp.Manage(map[string]struct{}{t.ClientCertificateAutomate: {}}) if err != nil { return nil, fmt.Errorf("managing client certificate: %v", err) } diff --git a/modules/caddyhttp/reverseproxy/metrics.go b/modules/caddyhttp/reverseproxy/metrics.go index f744756d3..248842730 100644 --- a/modules/caddyhttp/reverseproxy/metrics.go +++ b/modules/caddyhttp/reverseproxy/metrics.go @@ -1,32 +1,47 @@ package reverseproxy import ( + "errors" "runtime/debug" "sync" "time" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" "go.uber.org/zap" "go.uber.org/zap/zapcore" + + "github.com/caddyserver/caddy/v2" ) var reverseProxyMetrics = struct { - init sync.Once + once sync.Once upstreamsHealthy *prometheus.GaugeVec logger *zap.Logger }{} -func initReverseProxyMetrics(handler *Handler) { +func initReverseProxyMetrics(handler *Handler, registry *prometheus.Registry) { const ns, sub = "caddy", "reverse_proxy" upstreamsLabels := []string{"upstream"} - reverseProxyMetrics.upstreamsHealthy = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: ns, - Subsystem: sub, - Name: "upstreams_healthy", - Help: "Health status of reverse proxy upstreams.", - }, upstreamsLabels) + reverseProxyMetrics.once.Do(func() { + reverseProxyMetrics.upstreamsHealthy = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: ns, + Subsystem: sub, + Name: "upstreams_healthy", + Help: "Health status of reverse proxy upstreams.", + }, upstreamsLabels) + }) + + // duplicate registration could happen if multiple sites with reverse proxy are configured; so ignore the error because + // there's no good way to capture having multiple sites with reverse proxy. If this happens, the metrics will be + // registered twice, but the second registration will be ignored. + if err := registry.Register(reverseProxyMetrics.upstreamsHealthy); err != nil && + !errors.Is(err, prometheus.AlreadyRegisteredError{ + ExistingCollector: reverseProxyMetrics.upstreamsHealthy, + NewCollector: reverseProxyMetrics.upstreamsHealthy, + }) { + panic(err) + } reverseProxyMetrics.logger = handler.logger.Named("reverse_proxy.metrics") } @@ -35,17 +50,14 @@ type metricsUpstreamsHealthyUpdater struct { handler *Handler } -func newMetricsUpstreamsHealthyUpdater(handler *Handler) *metricsUpstreamsHealthyUpdater { - reverseProxyMetrics.init.Do(func() { - initReverseProxyMetrics(handler) - }) - +func newMetricsUpstreamsHealthyUpdater(handler *Handler, ctx caddy.Context) *metricsUpstreamsHealthyUpdater { + initReverseProxyMetrics(handler, ctx.GetMetricsRegistry()) reverseProxyMetrics.upstreamsHealthy.Reset() return &metricsUpstreamsHealthyUpdater{handler} } -func (m *metricsUpstreamsHealthyUpdater) Init() { +func (m *metricsUpstreamsHealthyUpdater) init() { go func() { defer func() { if err := recover(); err != nil { diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index e9b8c4646..a2a8d6b86 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -17,6 +17,8 @@ package reverseproxy import ( "bytes" "context" + "crypto/rand" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -109,11 +111,6 @@ type Handler struct { // response is recognized as a streaming response, or if its // content length is -1; for such responses, writes are flushed // to the client immediately. - // - // Normally, a request will be canceled if the client disconnects - // before the response is received from the backend. If explicitly - // set to -1, client disconnection will be ignored and the request - // will be completed to help facilitate low-latency streaming. FlushInterval caddy.Duration `json:"flush_interval,omitempty"` // A list of IP ranges (supports CIDR notation) from which @@ -247,6 +244,19 @@ func (h *Handler) Provision(ctx caddy.Context) error { return fmt.Errorf("loading transport: %v", err) } h.Transport = mod.(http.RoundTripper) + // enable request buffering for fastcgi if not configured + // This is because most fastcgi servers are php-fpm that require the content length to be set to read the body, golang + // std has fastcgi implementation that doesn't need this value to process the body, but we can safely assume that's + // not used. + // http3 requests have a negative content length for GET and HEAD requests, if that header is not sent. + // see: https://github.com/caddyserver/caddy/issues/6678#issuecomment-2472224182 + // Though it appears even if CONTENT_LENGTH is invalid, php-fpm can handle just fine if the body is empty (no Stdin records sent). + // php-fpm will hang if there is any data in the body though, https://github.com/caddyserver/caddy/issues/5420#issuecomment-2415943516 + + // TODO: better default buffering for fastcgi requests without content length, in theory a value of 1 should be enough, make it bigger anyway + if module, ok := h.Transport.(caddy.Module); ok && module.CaddyModule().ID.Name() == "fastcgi" && h.RequestBuffers == 0 { + h.RequestBuffers = 4096 + } } if h.LoadBalancing != nil && h.LoadBalancing.SelectionPolicyRaw != nil { mod, err := ctx.LoadModule(h.LoadBalancing, "SelectionPolicyRaw") @@ -373,8 +383,8 @@ func (h *Handler) Provision(ctx caddy.Context) error { } } - upstreamHealthyUpdater := newMetricsUpstreamsHealthyUpdater(h) - upstreamHealthyUpdater.Init() + upstreamHealthyUpdater := newMetricsUpstreamsHealthyUpdater(h, ctx) + upstreamHealthyUpdater.init() return nil } @@ -400,6 +410,23 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("preparing request for upstream round-trip: %v", err)) } + // websocket over http2, assuming backend doesn't support this, the request will be modified to http1.1 upgrade + // TODO: once we can reliably detect backend support this, it can be removed for those backends + if r.ProtoMajor == 2 && r.Method == http.MethodConnect && r.Header.Get(":protocol") == "websocket" { + clonedReq.Header.Del(":protocol") + // keep the body for later use. http1.1 upgrade uses http.NoBody + caddyhttp.SetVar(clonedReq.Context(), "h2_websocket_body", clonedReq.Body) + clonedReq.Body = http.NoBody + clonedReq.Method = http.MethodGet + clonedReq.Header.Set("Upgrade", "websocket") + clonedReq.Header.Set("Connection", "Upgrade") + key := make([]byte, 16) + _, randErr := rand.Read(key) + if randErr != nil { + return randErr + } + clonedReq.Header["Sec-WebSocket-Key"] = []string{base64.StdEncoding.EncodeToString(key)} + } // we will need the original headers and Host value if // header operations are configured; this is so that each @@ -497,7 +524,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h if proxyErr == nil { proxyErr = caddyhttp.Error(http.StatusServiceUnavailable, errNoUpstream) } - if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) { + if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) { return true, proxyErr } return false, proxyErr @@ -506,7 +533,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h // the dial address may vary per-request if placeholders are // used, so perform those replacements here; the resulting // DialInfo struct should have valid network address syntax - dialInfo, err := upstream.fillDialInfo(r) + dialInfo, err := upstream.fillDialInfo(repl) if err != nil { return true, fmt.Errorf("making dial info: %v", err) } @@ -555,7 +582,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h // ding the health status of the upstream (an error can still // occur after the roundtrip if, for example, a response handler // after the roundtrip returns an error) - if succ, ok := proxyErr.(roundtripSucceeded); ok { + if succ, ok := proxyErr.(roundtripSucceededError); ok { return true, succ.error } @@ -563,13 +590,37 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h h.countFailure(upstream) // if we've tried long enough, break - if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) { + if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) { return true, proxyErr } return false, proxyErr } +// Mapping of the canonical form of the headers, to the RFC 6455 form, +// i.e. `WebSocket` with uppercase 'S'. +var websocketHeaderMapping = map[string]string{ + "Sec-Websocket-Accept": "Sec-WebSocket-Accept", + "Sec-Websocket-Extensions": "Sec-WebSocket-Extensions", + "Sec-Websocket-Key": "Sec-WebSocket-Key", + "Sec-Websocket-Protocol": "Sec-WebSocket-Protocol", + "Sec-Websocket-Version": "Sec-WebSocket-Version", +} + +// normalizeWebsocketHeaders ensures we use the standard casing as per +// RFC 6455, i.e. `WebSocket` with uppercase 'S'. Most servers don't +// care about this difference (read headers case insensitively), but +// some do, so this maximizes compatibility with upstreams. +// See https://github.com/caddyserver/caddy/pull/6621 +func normalizeWebsocketHeaders(header http.Header) { + for k, rk := range websocketHeaderMapping { + if v, ok := header[k]; ok { + delete(header, k) + header[rk] = v + } + } +} + // prepareRequest clones req so that it can be safely modified without // changing the original request or introducing data races. It then // modifies it so that it is ready to be proxied, except for directing @@ -602,7 +653,8 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http. if h.RequestBuffers != 0 && req.Body != nil { var readBytes int64 req.Body, readBytes = h.bufferedBody(req.Body, h.RequestBuffers) - if h.RequestBuffers == -1 { + // set Content-Length when body is fully buffered + if b, ok := req.Body.(bodyReadCloser); ok && b.body == nil { req.ContentLength = readBytes req.Header.Set("Content-Length", strconv.FormatInt(req.ContentLength, 10)) } @@ -632,7 +684,7 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http. req.Header.Set("Early-Data", "1") } - reqUpType := upgradeType(req.Header) + reqUpgradeType := upgradeType(req.Header) removeConnectionHeaders(req.Header) // Remove hop-by-hop headers to the backend. Especially @@ -653,9 +705,10 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http. // After stripping all the hop-by-hop connection headers above, add back any // necessary for protocol upgrades, such as for websockets. - if reqUpType != "" { + if reqUpgradeType != "" { req.Header.Set("Connection", "Upgrade") - req.Header.Set("Upgrade", reqUpType) + req.Header.Set("Upgrade", reqUpgradeType) + normalizeWebsocketHeaders(req.Header) } // Set up the PROXY protocol info @@ -680,6 +733,9 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http. return nil, err } + // Via header(s) + req.Header.Add("Via", fmt.Sprintf("%d.%d Caddy", req.ProtoMajor, req.ProtoMinor)) + return req, nil } @@ -783,39 +839,45 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe shouldLogCredentials := server.Logs != nil && server.Logs.ShouldLogCredentials // Forward 1xx status codes, backported from https://github.com/golang/go/pull/53164 + var ( + roundTripMutex sync.Mutex + roundTripDone bool + ) trace := &httptrace.ClientTrace{ Got1xxResponse: func(code int, header textproto.MIMEHeader) error { + roundTripMutex.Lock() + defer roundTripMutex.Unlock() + if roundTripDone { + // If RoundTrip has returned, don't try to further modify + // the ResponseWriter's header map. + return nil + } h := rw.Header() copyHeader(h, http.Header(header)) rw.WriteHeader(code) // Clear headers coming from the backend // (it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses) - for k := range header { - delete(h, k) - } + clear(h) return nil }, } req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) - // if FlushInterval is explicitly configured to -1 (i.e. flush continuously to achieve - // low-latency streaming), don't let the transport cancel the request if the client - // disconnects: user probably wants us to finish sending the data to the upstream - // regardless, and we should expect client disconnection in low-latency streaming - // scenarios (see issue #4922) - if h.FlushInterval == -1 { - req = req.WithContext(context.WithoutCancel(req.Context())) - } - - // do the round-trip; emit debug log with values we know are - // safe, or if there is no error, emit fuller log entry - // we are wrapping the RoundTripper to make it observable + // do the round-trip start := time.Now() otelTransport := otelhttp.NewTransport(h.Transport) res, err := otelTransport.RoundTrip(req) duration := time.Since(start) + + // record that the round trip is done for the 1xx response handler + roundTripMutex.Lock() + roundTripDone = true + roundTripMutex.Unlock() + + // emit debug log with values we know are safe, + // or if there is no error, emit fuller log entry logger := h.logger.With( zap.String("upstream", di.Upstream.String()), zap.Duration("duration", duration), @@ -825,13 +887,15 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe }), ) + const logMessage = "upstream roundtrip" + if err != nil { - if c := logger.Check(zapcore.DebugLevel, "upstream roundtrip"); c != nil { + if c := logger.Check(zapcore.DebugLevel, logMessage); c != nil { c.Write(zap.Error(err)) } return err } - if c := logger.Check(zapcore.DebugLevel, "upstream roundtrip"); c != nil { + if c := logger.Check(zapcore.DebugLevel, logMessage); c != nil { c.Write( zap.Object("headers", caddyhttp.LoggableHTTPHeader{ Header: res.Header, @@ -929,10 +993,10 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe res.Body.Close() } - // wrap any route error in roundtripSucceeded so caller knows that + // wrap any route error in roundtripSucceededError so caller knows that // the roundtrip was successful and to not retry if routeErr != nil { - return roundtripSucceeded{routeErr} + return roundtripSucceededError{routeErr} } // we're done handling the response, and we don't want to @@ -967,6 +1031,14 @@ func (h *Handler) finalizeResponse( res.Header.Del(h) } + // delete our Server header and use Via instead (see #6275) + rw.Header().Del("Server") + var protoPrefix string + if !strings.HasPrefix(strings.ToUpper(res.Proto), "HTTP/") { + protoPrefix = res.Proto[:strings.Index(res.Proto, "/")+1] + } + rw.Header().Add("Via", fmt.Sprintf("%s%d.%d Caddy", protoPrefix, res.ProtoMajor, res.ProtoMinor)) + // apply any response header operations if h.Headers != nil && h.Headers.Response != nil { if h.Headers.Response.Require == nil || @@ -1051,7 +1123,7 @@ func (h *Handler) finalizeResponse( // If true is returned, it has already blocked long enough before // the next retry (i.e. no more sleeping is needed). If false is // returned, the handler should stop trying to proxy the request. -func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int, proxyErr error, req *http.Request) bool { +func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int, proxyErr error, req *http.Request, logger *zap.Logger) bool { // no retries are configured if lb.TryDuration == 0 && lb.Retries == 0 { return false @@ -1080,13 +1152,18 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int // we have to assume the upstream received the request, and // retries need to be carefully decided, because some requests // are not idempotent - if !isDialError && !(isHandlerError && errors.Is(herr, errNoUpstream)) { + if !isDialError && (!isHandlerError || !errors.Is(herr, errNoUpstream)) { if lb.RetryMatch == nil && req.Method != "GET" { // by default, don't retry requests if they aren't GET return false } - if !lb.RetryMatch.AnyMatch(req) { + match, err := lb.RetryMatch.AnyMatchWithError(req) + if err != nil { + logger.Error("error matching request for retry", zap.Error(err)) + return false + } + if !match { return false } } @@ -1159,6 +1236,10 @@ func (h Handler) provisionUpstream(upstream *Upstream) { // then returns a reader for the buffer along with how many bytes were buffered. Always close // the return value when done with it, just like if it was the original body! If limit is 0 // (which it shouldn't be), this function returns its input; i.e. is a no-op, for safety. +// Otherwise, it returns bodyReadCloser, the original body will be closed and body will be nil +// if it's explicitly configured to buffer all or EOF is reached when reading. +// TODO: the error during reading is discarded if the limit is negative, should the error be propagated +// to upstream/downstream? func (h Handler) bufferedBody(originalBody io.ReadCloser, limit int64) (io.ReadCloser, int64) { if limit == 0 { return originalBody, 0 @@ -1167,13 +1248,14 @@ func (h Handler) bufferedBody(originalBody io.ReadCloser, limit int64) (io.ReadC buf := bufPool.Get().(*bytes.Buffer) buf.Reset() if limit > 0 { - n, err := io.CopyN(buf, originalBody, limit) - if (err != nil && err != io.EOF) || n == limit { + var err error + written, err = io.CopyN(buf, originalBody, limit) + if (err != nil && err != io.EOF) || written == limit { return bodyReadCloser{ Reader: io.MultiReader(buf, originalBody), buf: buf, body: originalBody, - }, n + }, written } } else { written, _ = io.Copy(buf, originalBody) @@ -1276,7 +1358,7 @@ func upgradeType(h http.Header) string { // See RFC 7230, section 6.1 func removeConnectionHeaders(h http.Header) { for _, f := range h["Connection"] { - for _, sf := range strings.Split(f, ",") { + for sf := range strings.SplitSeq(f, ",") { if sf = textproto.TrimString(sf); sf != "" { h.Del(sf) } @@ -1404,9 +1486,9 @@ type TLSTransport interface { EnableTLS(base *TLSConfig) error } -// roundtripSucceeded is an error type that is returned if the +// roundtripSucceededError is an error type that is returned if the // roundtrip succeeded, but an error occurred after-the-fact. -type roundtripSucceeded struct{ error } +type roundtripSucceededError struct{ error } // bodyReadCloser is a reader that, upon closing, will return // its buffer to the pool and close the underlying body reader. diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go index 293ff75e2..585fc3400 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go @@ -111,8 +111,8 @@ func (r *WeightedRoundRobinSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) if err != nil { return d.Errf("invalid weight value '%s': %v", weight, err) } - if weightInt < 1 { - return d.Errf("invalid weight value '%s': weight should be non-zero and positive", weight) + if weightInt < 0 { + return d.Errf("invalid weight value '%s': weight should be non-negative", weight) } r.Weights = append(r.Weights, weightInt) } @@ -136,8 +136,15 @@ func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, return pool[0] } var index, totalWeight int + var weights []int + + for _, w := range r.Weights { + if w > 0 { + weights = append(weights, w) + } + } currentWeight := int(atomic.AddUint32(&r.index, 1)) % r.totalWeight - for i, weight := range r.Weights { + for i, weight := range weights { totalWeight += weight if currentWeight < totalWeight { index = i @@ -145,9 +152,9 @@ func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, } } - upstreams := make([]*Upstream, 0, len(r.Weights)) - for _, upstream := range pool { - if !upstream.Available() { + upstreams := make([]*Upstream, 0, len(weights)) + for i, upstream := range pool { + if !upstream.Available() || r.Weights[i] == 0 { continue } upstreams = append(upstreams, upstream) @@ -212,10 +219,7 @@ func (r RandomChoiceSelection) Validate() error { // Select returns an available host, if any. func (r RandomChoiceSelection) Select(pool UpstreamPool, _ *http.Request, _ http.ResponseWriter) *Upstream { - k := r.Choose - if k > len(pool) { - k = len(pool) - } + k := min(r.Choose, len(pool)) choices := make([]*Upstream, k) for i, upstream := range pool { if !upstream.Available() { @@ -801,7 +805,7 @@ func leastRequests(upstreams []*Upstream) *Upstream { return nil } var best []*Upstream - var bestReqs int = -1 + bestReqs := -1 for _, upstream := range upstreams { if upstream == nil { continue diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies_test.go b/modules/caddyhttp/reverseproxy/selectionpolicies_test.go index a4701ce86..580abbdde 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies_test.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies_test.go @@ -131,6 +131,58 @@ func TestWeightedRoundRobinPolicy(t *testing.T) { } } +func TestWeightedRoundRobinPolicyWithZeroWeight(t *testing.T) { + pool := testPool() + wrrPolicy := WeightedRoundRobinSelection{ + Weights: []int{0, 2, 1}, + totalWeight: 3, + } + req, _ := http.NewRequest("GET", "/", nil) + + h := wrrPolicy.Select(pool, req, nil) + if h != pool[1] { + t.Error("Expected first weighted round robin host to be second host in the pool.") + } + + h = wrrPolicy.Select(pool, req, nil) + if h != pool[2] { + t.Error("Expected second weighted round robin host to be third host in the pool.") + } + + h = wrrPolicy.Select(pool, req, nil) + if h != pool[1] { + t.Error("Expected third weighted round robin host to be second host in the pool.") + } + + // mark second host as down + pool[1].setHealthy(false) + h = wrrPolicy.Select(pool, req, nil) + if h != pool[2] { + t.Error("Expect select next available host.") + } + + h = wrrPolicy.Select(pool, req, nil) + if h != pool[2] { + t.Error("Expect select only available host.") + } + // mark second host as up + pool[1].setHealthy(true) + + h = wrrPolicy.Select(pool, req, nil) + if h != pool[1] { + t.Error("Expect select first host on availability.") + } + + // test next select in full cycle + expected := []*Upstream{pool[1], pool[2], pool[1], pool[1], pool[2], pool[1]} + for i, want := range expected { + got := wrrPolicy.Select(pool, req, nil) + if want != got { + t.Errorf("Selection %d: got host[%s], want host[%s]", i+1, got, want) + } + } +} + func TestLeastConnPolicy(t *testing.T) { pool := testPool() lcPolicy := LeastConnSelection{} diff --git a/modules/caddyhttp/reverseproxy/streaming.go b/modules/caddyhttp/reverseproxy/streaming.go index c871a3fa1..374c208eb 100644 --- a/modules/caddyhttp/reverseproxy/streaming.go +++ b/modules/caddyhttp/reverseproxy/streaming.go @@ -19,6 +19,7 @@ package reverseproxy import ( + "bufio" "context" "errors" "fmt" @@ -33,8 +34,29 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" "golang.org/x/net/http/httpguts" + + "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) +type h2ReadWriteCloser struct { + io.ReadCloser + http.ResponseWriter +} + +func (rwc h2ReadWriteCloser) Write(p []byte) (n int, err error) { + n, err = rwc.ResponseWriter.Write(p) + if err != nil { + return 0, err + } + + //nolint:bodyclose + err = http.NewResponseController(rwc.ResponseWriter).Flush() + if err != nil { + return 0, err + } + return n, nil +} + func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup, rw http.ResponseWriter, req *http.Request, res *http.Response) { reqUpType := upgradeType(req.Header) resUpType := upgradeType(res.Header) @@ -66,30 +88,65 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup, // write header first, response headers should not be counted in size // like the rest of handler chain. copyHeader(rw.Header(), res.Header) - rw.WriteHeader(res.StatusCode) + normalizeWebsocketHeaders(rw.Header()) - logger.Debug("upgrading connection") + var ( + conn io.ReadWriteCloser + brw *bufio.ReadWriter + ) + // websocket over http2, assuming backend doesn't support this, the request will be modified to http1.1 upgrade + // TODO: once we can reliably detect backend support this, it can be removed for those backends + if body, ok := caddyhttp.GetVar(req.Context(), "h2_websocket_body").(io.ReadCloser); ok { + req.Body = body + rw.Header().Del("Upgrade") + rw.Header().Del("Connection") + delete(rw.Header(), "Sec-WebSocket-Accept") + rw.WriteHeader(http.StatusOK) - //nolint:bodyclose - conn, brw, hijackErr := http.NewResponseController(rw).Hijack() - if errors.Is(hijackErr, http.ErrNotSupported) { - if c := logger.Check(zapcore.ErrorLevel, "can't switch protocols using non-Hijacker ResponseWriter"); c != nil { - c.Write(zap.String("type", fmt.Sprintf("%T", rw))) + if c := logger.Check(zap.DebugLevel, "upgrading connection"); c != nil { + c.Write(zap.Int("http_version", 2)) } - return - } - if hijackErr != nil { - if c := logger.Check(zapcore.ErrorLevel, "hijack failed on protocol switch"); c != nil { - c.Write(zap.Error(hijackErr)) + //nolint:bodyclose + flushErr := http.NewResponseController(rw).Flush() + if flushErr != nil { + if c := h.logger.Check(zap.ErrorLevel, "failed to flush http2 websocket response"); c != nil { + c.Write(zap.Error(flushErr)) + } + return + } + conn = h2ReadWriteCloser{req.Body, rw} + // bufio is not needed, use minimal buffer + brw = bufio.NewReadWriter(bufio.NewReaderSize(conn, 1), bufio.NewWriterSize(conn, 1)) + } else { + rw.WriteHeader(res.StatusCode) + + if c := logger.Check(zap.DebugLevel, "upgrading connection"); c != nil { + c.Write(zap.Int("http_version", req.ProtoMajor)) + } + + var hijackErr error + //nolint:bodyclose + conn, brw, hijackErr = http.NewResponseController(rw).Hijack() + if errors.Is(hijackErr, http.ErrNotSupported) { + if c := h.logger.Check(zap.ErrorLevel, "can't switch protocols using non-Hijacker ResponseWriter"); c != nil { + c.Write(zap.String("type", fmt.Sprintf("%T", rw))) + } + return + } + + if hijackErr != nil { + if c := h.logger.Check(zap.ErrorLevel, "hijack failed on protocol switch"); c != nil { + c.Write(zap.Error(hijackErr)) + } + return } - return } // adopted from https://github.com/golang/go/commit/8bcf2834afdf6a1f7937390903a41518715ef6f5 backConnCloseCh := make(chan struct{}) go func() { - // Ensure that the cancelation of a request closes the backend. + // Ensure that the cancellation of a request closes the backend. // See issue https://golang.org/issue/35559. select { case <-req.Context().Done(): @@ -102,7 +159,7 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup, start := time.Now() defer func() { conn.Close() - if c := logger.Check(zapcore.DebugLevel, "hijack failed on protocol switch"); c != nil { + if c := logger.Check(zapcore.DebugLevel, "connection closed"); c != nil { c.Write(zap.Duration("duration", time.Since(start))) } }() diff --git a/modules/caddyhttp/reverseproxy/upstreams_test.go b/modules/caddyhttp/reverseproxy/upstreams_test.go index 48e2d2a63..8caf8696a 100644 --- a/modules/caddyhttp/reverseproxy/upstreams_test.go +++ b/modules/caddyhttp/reverseproxy/upstreams_test.go @@ -52,5 +52,4 @@ func TestResolveIpVersion(t *testing.T) { t.Errorf("resolveIpVersion(): Expected %s got %s", test.expectedIpVersion, ipVersion) } } - } diff --git a/modules/caddyhttp/rewrite/caddyfile.go b/modules/caddyhttp/rewrite/caddyfile.go index 89f44c79b..5f9b97adf 100644 --- a/modules/caddyhttp/rewrite/caddyfile.go +++ b/modules/caddyhttp/rewrite/caddyfile.go @@ -110,9 +110,6 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err return nil, h.ArgErr() } rewr.StripPathPrefix = args[1] - if !strings.HasPrefix(rewr.StripPathPrefix, "/") { - rewr.StripPathPrefix = "/" + rewr.StripPathPrefix - } case "strip_suffix": if len(args) != 2 { diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go index e76682729..2b18744db 100644 --- a/modules/caddyhttp/rewrite/rewrite.go +++ b/modules/caddyhttp/rewrite/rewrite.go @@ -259,6 +259,9 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool { // strip path prefix or suffix if rewr.StripPathPrefix != "" { prefix := repl.ReplaceAll(rewr.StripPathPrefix, "") + if !strings.HasPrefix(prefix, "/") { + prefix = "/" + prefix + } mergeSlashes := !strings.Contains(prefix, "//") changePath(r, func(escapedPath string) string { escapedPath = caddyhttp.CleanPath(escapedPath, mergeSlashes) @@ -374,11 +377,7 @@ func buildQueryString(qs string, repl *caddy.Replacer) string { // performed in normalized/unescaped space. func trimPathPrefix(escapedPath, prefix string) string { var iPath, iPrefix int - for { - if iPath >= len(escapedPath) || iPrefix >= len(prefix) { - break - } - + for iPath < len(escapedPath) && iPrefix < len(prefix) { prefixCh := prefix[iPrefix] ch := string(escapedPath[iPath]) diff --git a/modules/caddyhttp/rewrite/rewrite_test.go b/modules/caddyhttp/rewrite/rewrite_test.go index aaa142bc2..81360baee 100644 --- a/modules/caddyhttp/rewrite/rewrite_test.go +++ b/modules/caddyhttp/rewrite/rewrite_test.go @@ -235,6 +235,11 @@ func TestRewrite(t *testing.T) { input: newRequest(t, "GET", "/prefix/foo/bar"), expect: newRequest(t, "GET", "/foo/bar"), }, + { + rule: Rewrite{StripPathPrefix: "prefix"}, + input: newRequest(t, "GET", "/prefix/foo/bar"), + expect: newRequest(t, "GET", "/foo/bar"), + }, { rule: Rewrite{StripPathPrefix: "/prefix"}, input: newRequest(t, "GET", "/prefix"), diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index 54a3f38e6..ccb5f2515 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -254,18 +254,13 @@ func wrapRoute(route Route) Middleware { nextCopy := next // route must match at least one of the matcher sets - if !route.MatcherSets.AnyMatch(req) { + matches, err := route.MatcherSets.AnyMatchWithError(req) + if err != nil { // allow matchers the opportunity to short circuit // the request and trigger the error handling chain - err, ok := GetVar(req.Context(), MatcherErrorVarKey).(error) - if ok { - // clear out the error from context, otherwise - // it will cascade to the error routes (#4916) - SetVar(req.Context(), MatcherErrorVarKey, nil) - // return the matcher's error - return err - } - + return err + } + if !matches { // call the next handler, and skip this one, // since the matcher didn't match return nextCopy.ServeHTTP(rw, req) @@ -314,11 +309,11 @@ func wrapRoute(route Route) Middleware { // we need to pull this particular MiddlewareHandler // pointer into its own stack frame to preserve it so it // won't be overwritten in future loop iterations. -func wrapMiddleware(_ caddy.Context, mh MiddlewareHandler, metrics *Metrics) Middleware { +func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler, metrics *Metrics) Middleware { handlerToUse := mh if metrics != nil { // wrap the middleware with metrics instrumentation - handlerToUse = newMetricsInstrumentedHandler(caddy.GetModuleName(mh), mh) + handlerToUse = newMetricsInstrumentedHandler(ctx, caddy.GetModuleName(mh), mh, metrics) } return func(next Handler) Handler { @@ -341,19 +336,58 @@ func wrapMiddleware(_ caddy.Context, mh MiddlewareHandler, metrics *Metrics) Mid // MatcherSet is a set of matchers which // must all match in order for the request // to be matched successfully. -type MatcherSet []RequestMatcher +type MatcherSet []any // Match returns true if the request matches all // matchers in mset or if there are no matchers. func (mset MatcherSet) Match(r *http.Request) bool { for _, m := range mset { - if !m.Match(r) { - return false + if me, ok := m.(RequestMatcherWithError); ok { + match, _ := me.MatchWithError(r) + if !match { + return false + } + continue } + if me, ok := m.(RequestMatcher); ok { + if !me.Match(r) { + return false + } + continue + } + return false } return true } +// MatchWithError returns true if r matches m. +func (mset MatcherSet) MatchWithError(r *http.Request) (bool, error) { + for _, m := range mset { + if me, ok := m.(RequestMatcherWithError); ok { + match, err := me.MatchWithError(r) + if err != nil || !match { + return match, err + } + continue + } + if me, ok := m.(RequestMatcher); ok { + if !me.Match(r) { + // for backwards compatibility + err, ok := GetVar(r.Context(), MatcherErrorVarKey).(error) + if ok { + // clear out the error from context since we've consumed it + SetVar(r.Context(), MatcherErrorVarKey, nil) + return false, err + } + return false, nil + } + continue + } + return false, fmt.Errorf("matcher is not a RequestMatcher or RequestMatcherWithError: %#v", m) + } + return true, nil +} + // RawMatcherSets is a group of matcher sets // in their raw, JSON form. type RawMatcherSets []caddy.ModuleMap @@ -366,25 +400,50 @@ type MatcherSets []MatcherSet // AnyMatch returns true if req matches any of the // matcher sets in ms or if there are no matchers, // in which case the request always matches. +// +// Deprecated: Use AnyMatchWithError instead. func (ms MatcherSets) AnyMatch(req *http.Request) bool { for _, m := range ms { - if m.Match(req) { - return true + match, err := m.MatchWithError(req) + if err != nil { + SetVar(req.Context(), MatcherErrorVarKey, err) + return false + } + if match { + return match } } return len(ms) == 0 } +// AnyMatchWithError returns true if req matches any of the +// matcher sets in ms or if there are no matchers, in which +// case the request always matches. If any matcher returns +// an error, we cut short and return the error. +func (ms MatcherSets) AnyMatchWithError(req *http.Request) (bool, error) { + for _, m := range ms { + match, err := m.MatchWithError(req) + if err != nil || match { + return match, err + } + } + return len(ms) == 0, nil +} + // FromInterface fills ms from an 'any' value obtained from LoadModule. func (ms *MatcherSets) FromInterface(matcherSets any) error { for _, matcherSetIfaces := range matcherSets.([]map[string]any) { var matcherSet MatcherSet for _, matcher := range matcherSetIfaces { - reqMatcher, ok := matcher.(RequestMatcher) - if !ok { - return fmt.Errorf("decoded module is not a RequestMatcher: %#v", matcher) + if m, ok := matcher.(RequestMatcherWithError); ok { + matcherSet = append(matcherSet, m) + continue } - matcherSet = append(matcherSet, reqMatcher) + if m, ok := matcher.(RequestMatcher); ok { + matcherSet = append(matcherSet, m) + continue + } + return fmt.Errorf("decoded module is not a RequestMatcher or RequestMatcherWithError: %#v", matcher) } *ms = append(*ms, matcherSet) } diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 5aa7e0f63..f6666c14f 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -61,6 +61,7 @@ type Server struct { ReadTimeout caddy.Duration `json:"read_timeout,omitempty"` // ReadHeaderTimeout is like ReadTimeout but for request headers. + // Default is 1 minute. ReadHeaderTimeout caddy.Duration `json:"read_header_timeout,omitempty"` // WriteTimeout is how long to allow a write to a client. Note @@ -226,6 +227,7 @@ type Server struct { // If set, metrics observations will be enabled. // This setting is EXPERIMENTAL and subject to change. + // DEPRECATED: Use the app-level `metrics` field. Metrics *Metrics `json:"metrics,omitempty"` name string @@ -233,7 +235,8 @@ type Server struct { primaryHandlerChain Handler errorHandlerChain Handler listenerWrappers []caddy.ListenerWrapper - listeners []net.Listener + listeners []net.Listener // stdlib http.Server will close these + quicListeners []http3.QUICListener // http3 now leave the quic.Listener management to us tlsApp *caddytls.TLS events *caddyevents.App @@ -243,10 +246,9 @@ type Server struct { traceLogger *zap.Logger ctx caddy.Context - server *http.Server - h3server *http3.Server - h2listeners []*http2Listener - addresses []caddy.NetworkAddress + server *http.Server + h3server *http3.Server + addresses []caddy.NetworkAddress trustedProxies IPRangeSource @@ -263,11 +265,11 @@ type Server struct { // ServeHTTP is the entry point for all HTTP requests. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // If there are listener wrappers that process tls connections but don't return a *tls.Conn, this field will be nil. - // TODO: Can be removed if https://github.com/golang/go/pull/56110 is ever merged. + // TODO: Scheduled to be removed later because https://github.com/golang/go/pull/56110 has been merged. if r.TLS == nil { // not all requests have a conn (like virtual requests) - see #5698 if conn, ok := r.Context().Value(ConnCtxKey).(net.Conn); ok { - if csc, ok := conn.(connectionStateConn); ok { + if csc, ok := conn.(connectionStater); ok { r.TLS = new(tls.ConnectionState) *r.TLS = csc.ConnectionState() } @@ -406,7 +408,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { if fields == nil { fields = errFields() } - c.Write(fields...) } } @@ -614,22 +615,7 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error // create HTTP/3 server if not done already if s.h3server == nil { s.h3server = &http3.Server{ - // Currently when closing a http3.Server, only listeners are closed. But caddy reuses these listeners - // if possible, requests are still read and handled by the old handler. Close these connections manually. - // see issue: https://github.com/caddyserver/caddy/issues/6195 - // Will interrupt ongoing requests. - // TODO: remove the handler wrap after http3.Server.CloseGracefully is implemented, see App.Stop - Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - select { - case <-s.ctx.Done(): - if quicConn, ok := request.Context().Value(quicConnCtxKey).(quic.Connection); ok { - //nolint:errcheck - quicConn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeRequestRejected), "") - } - default: - s.ServeHTTP(writer, request) - } - }), + Handler: s, TLSConfig: tlsCfg, MaxHeaderBytes: s.MaxHeaderBytes, QUICConfig: &quic.Config{ @@ -637,12 +623,11 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error Tracer: qlog.DefaultConnectionTracer, }, IdleTimeout: time.Duration(s.IdleTimeout), - ConnContext: func(ctx context.Context, c quic.Connection) context.Context { - return context.WithValue(ctx, quicConnCtxKey, c) - }, } } + s.quicListeners = append(s.quicListeners, h3ln) + //nolint:errcheck go s.h3server.ServeListener(h3ln) @@ -999,10 +984,10 @@ func trustedRealClientIP(r *http.Request, headers []string, clientIP string) str // Since there can be many header values, we need to // join them together before splitting to get the full list - allValues := strings.Split(strings.Join(values, ","), ",") + allValues := strings.SplitSeq(strings.Join(values, ","), ",") // Get first valid left-most IP address - for _, part := range allValues { + for part := range allValues { // Some proxies may retain the port number, so split if possible host, _, err := net.SplitHostPort(part) if err != nil { @@ -1097,12 +1082,10 @@ const ( OriginalRequestCtxKey caddy.CtxKey = "original_request" // For referencing underlying net.Conn + // This will eventually be deprecated and not used. To refer to the underlying connection, implement a middleware plugin + // that RegisterConnContext during provisioning. ConnCtxKey caddy.CtxKey = "conn" - // For referencing underlying quic.Connection - // TODO: export if needed later - quicConnCtxKey caddy.CtxKey = "quic_conn" - // For tracking whether the client is a trusted proxy TrustedProxyVarKey string = "trusted_proxy" diff --git a/modules/caddyhttp/server_test.go b/modules/caddyhttp/server_test.go index 2c8033d45..d47f55c63 100644 --- a/modules/caddyhttp/server_test.go +++ b/modules/caddyhttp/server_test.go @@ -69,12 +69,13 @@ func TestServer_LogRequest(t *testing.T) { }`, buf.String()) } -func TestServer_LogRequest_WithTraceID(t *testing.T) { +func TestServer_LogRequest_WithTrace(t *testing.T) { s := &Server{} extra := new(ExtraLogFields) ctx := context.WithValue(context.Background(), ExtraLogFieldsCtxKey, extra) extra.Add(zap.String("traceID", "1234567890abcdef")) + extra.Add(zap.String("spanID", "12345678")) req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx) rec := httptest.NewRecorder() @@ -93,7 +94,8 @@ func TestServer_LogRequest_WithTraceID(t *testing.T) { "msg":"handled request", "level":"info", "bytes_read":0, "duration":"50ms", "resp_headers": {}, "size":0, "status":0, "user_id":"", - "traceID":"1234567890abcdef" + "traceID":"1234567890abcdef", + "spanID":"12345678" }`, buf.String()) } @@ -114,9 +116,7 @@ func BenchmarkServer_LogRequest(b *testing.B) { buf := io.Discard accLog := testLogger(buf.Write) - b.ResetTimer() - - for i := 0; i < b.N; i++ { + for b.Loop() { s.logRequest(accLog, req, wrec, &duration, repl, bodyReader, false) } } @@ -137,19 +137,18 @@ func BenchmarkServer_LogRequest_NopLogger(b *testing.B) { accLog := zap.NewNop() - b.ResetTimer() - - for i := 0; i < b.N; i++ { + for b.Loop() { s.logRequest(accLog, req, wrec, &duration, repl, bodyReader, false) } } -func BenchmarkServer_LogRequest_WithTraceID(b *testing.B) { +func BenchmarkServer_LogRequest_WithTrace(b *testing.B) { s := &Server{} extra := new(ExtraLogFields) ctx := context.WithValue(context.Background(), ExtraLogFieldsCtxKey, extra) extra.Add(zap.String("traceID", "1234567890abcdef")) + extra.Add(zap.String("spanID", "12345678")) req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx) rec := httptest.NewRecorder() @@ -162,12 +161,11 @@ func BenchmarkServer_LogRequest_WithTraceID(b *testing.B) { buf := io.Discard accLog := testLogger(buf.Write) - b.ResetTimer() - - for i := 0; i < b.N; i++ { + for b.Loop() { s.logRequest(accLog, req, wrec, &duration, repl, bodyReader, false) } } + func TestServer_TrustedRealClientIP_NoTrustedHeaders(t *testing.T) { req := httptest.NewRequest("GET", "/", nil) req.RemoteAddr = "192.0.2.1:12345" diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go index 1b93ede4b..12108ac03 100644 --- a/modules/caddyhttp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -22,6 +22,7 @@ import ( "net/http" "net/textproto" "os" + "slices" "strconv" "strings" "text/template" @@ -323,13 +324,7 @@ func cmdRespond(fl caddycmd.Flags) (int, error) { // figure out if status code was explicitly specified; this lets // us set a non-zero value as the default but is a little hacky - var statusCodeFlagSpecified bool - for _, fl := range os.Args { - if fl == "--status" { - statusCodeFlagSpecified = true - break - } - } + statusCodeFlagSpecified := slices.Contains(os.Args, "--status") // try to determine what kind of parameter the unnamed argument is if arg != "" { diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go index 1b1020f1b..ee553e7a5 100644 --- a/modules/caddyhttp/templates/tplcontext.go +++ b/modules/caddyhttp/templates/tplcontext.go @@ -380,7 +380,7 @@ func (TemplateContext) funcMarkdown(input any) (string, error) { return buf.String(), nil } -// splitFrontMatter parses front matter out from the beginning of input, +// funcSplitFrontMatter parses front matter out from the beginning of input, // and returns the separated key-value pairs and the body/content. input // must be a "stringy" value. func (TemplateContext) funcSplitFrontMatter(input any) (parsedMarkdownDoc, error) { diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go index 77e06e3cb..d01f4a431 100644 --- a/modules/caddyhttp/vars.go +++ b/modules/caddyhttp/vars.go @@ -28,6 +28,8 @@ import ( "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) +var stringSliceType = reflect.TypeFor[[]string]() + func init() { caddy.RegisterModule(VarsMiddleware{}) caddy.RegisterModule(VarsMatcher{}) @@ -166,8 +168,14 @@ func (m *VarsMatcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // Match matches a request based on variables in the context, // or placeholders if the key is not a variable. func (m VarsMatcher) Match(r *http.Request) bool { + match, _ := m.MatchWithError(r) + return match +} + +// MatchWithError returns true if r matches m. +func (m VarsMatcher) MatchWithError(r *http.Request) (bool, error) { if len(m) == 0 { - return true + return true, nil } vars := r.Context().Value(VarsCtxKey).(map[string]any) @@ -200,11 +208,11 @@ func (m VarsMatcher) Match(r *http.Request) bool { varStr = fmt.Sprintf("%v", vv) } if varStr == matcherValExpanded { - return true + return true, nil } } } - return false + return false, nil } // CELLibrary produces options that expose this matcher for use in CEL @@ -219,7 +227,7 @@ func (VarsMatcher) CELLibrary(_ caddy.Context) (cel.Library, error) { "vars", "vars_matcher_request_map", []*cel.Type{CELTypeJSON}, - func(data ref.Val) (RequestMatcher, error) { + func(data ref.Val) (RequestMatcherWithError, error) { mapStrListStr, err := CELValueToMapStrList(data) if err != nil { return nil, err @@ -294,6 +302,12 @@ func (m MatchVarsRE) Provision(ctx caddy.Context) error { // Match returns true if r matches m. func (m MatchVarsRE) Match(r *http.Request) bool { + match, _ := m.MatchWithError(r) + return match +} + +// MatchWithError returns true if r matches m. +func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) { vars := r.Context().Value(VarsCtxKey).(map[string]any) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) for key, val := range m { @@ -322,10 +336,10 @@ func (m MatchVarsRE) Match(r *http.Request) bool { valExpanded := repl.ReplaceAll(varStr, "") if match := val.Match(valExpanded, repl); match { - return match + return match, nil } } - return false + return false, nil } // CELLibrary produces options that expose this matcher for use in CEL @@ -340,8 +354,8 @@ func (MatchVarsRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { "vars_regexp", "vars_regexp_request_string_string", []*cel.Type{cel.StringType, cel.StringType}, - func(data ref.Val) (RequestMatcher, error) { - refStringList := reflect.TypeOf([]string{}) + func(data ref.Val) (RequestMatcherWithError, error) { + refStringList := stringSliceType params, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -363,8 +377,8 @@ func (MatchVarsRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { "vars_regexp", "vars_regexp_request_string_string_string", []*cel.Type{cel.StringType, cel.StringType, cel.StringType}, - func(data ref.Val) (RequestMatcher, error) { - refStringList := reflect.TypeOf([]string{}) + func(data ref.Val) (RequestMatcherWithError, error) { + refStringList := stringSliceType params, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -435,8 +449,10 @@ func SetVar(ctx context.Context, key string, value any) { // Interface guards var ( - _ MiddlewareHandler = (*VarsMiddleware)(nil) - _ caddyfile.Unmarshaler = (*VarsMiddleware)(nil) - _ RequestMatcher = (*VarsMatcher)(nil) - _ caddyfile.Unmarshaler = (*VarsMatcher)(nil) + _ MiddlewareHandler = (*VarsMiddleware)(nil) + _ caddyfile.Unmarshaler = (*VarsMiddleware)(nil) + _ RequestMatcherWithError = (*VarsMatcher)(nil) + _ caddyfile.Unmarshaler = (*VarsMatcher)(nil) + _ RequestMatcherWithError = (*MatchVarsRE)(nil) + _ caddyfile.Unmarshaler = (*MatchVarsRE)(nil) ) diff --git a/modules/caddypki/acmeserver/caddyfile.go b/modules/caddypki/acmeserver/caddyfile.go index c4d111128..a7dc8e337 100644 --- a/modules/caddypki/acmeserver/caddyfile.go +++ b/modules/caddypki/acmeserver/caddyfile.go @@ -15,8 +15,6 @@ package acmeserver import ( - "time" - "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddypki" @@ -74,14 +72,10 @@ func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error if !h.NextArg() { return nil, h.ArgErr() } - dur, err := caddy.ParseDuration(h.Val()) if err != nil { return nil, err } - if d := time.Duration(ca.IntermediateLifetime); d > 0 && dur > d { - return nil, h.Errf("certificate lifetime (%s) exceeds intermediate certificate lifetime (%s)", dur, d) - } acmeServer.Lifetime = caddy.Duration(dur) case "resolvers": acmeServer.Resolvers = h.RemainingArgs() @@ -97,19 +91,17 @@ func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error acmeServer.Policy.AllowWildcardNames = true case "allow": r := &RuleSet{} - for h.Next() { - for h.NextBlock(h.Nesting() - 1) { - if h.CountRemainingArgs() == 0 { - return nil, h.ArgErr() // TODO: - } - switch h.Val() { - case "domains": - r.Domains = append(r.Domains, h.RemainingArgs()...) - case "ip_ranges": - r.IPRanges = append(r.IPRanges, h.RemainingArgs()...) - default: - return nil, h.Errf("unrecognized 'allow' subdirective: %s", h.Val()) - } + for nesting := h.Nesting(); h.NextBlock(nesting); { + if h.CountRemainingArgs() == 0 { + return nil, h.ArgErr() // TODO: + } + switch h.Val() { + case "domains": + r.Domains = append(r.Domains, h.RemainingArgs()...) + case "ip_ranges": + r.IPRanges = append(r.IPRanges, h.RemainingArgs()...) + default: + return nil, h.Errf("unrecognized 'allow' subdirective: %s", h.Val()) } } if acmeServer.Policy == nil { @@ -118,19 +110,17 @@ func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error acmeServer.Policy.Allow = r case "deny": r := &RuleSet{} - for h.Next() { - for h.NextBlock(h.Nesting() - 1) { - if h.CountRemainingArgs() == 0 { - return nil, h.ArgErr() // TODO: - } - switch h.Val() { - case "domains": - r.Domains = append(r.Domains, h.RemainingArgs()...) - case "ip_ranges": - r.IPRanges = append(r.IPRanges, h.RemainingArgs()...) - default: - return nil, h.Errf("unrecognized 'deny' subdirective: %s", h.Val()) - } + for nesting := h.Nesting(); h.NextBlock(nesting); { + if h.CountRemainingArgs() == 0 { + return nil, h.ArgErr() // TODO: + } + switch h.Val() { + case "domains": + r.Domains = append(r.Domains, h.RemainingArgs()...) + case "ip_ranges": + r.IPRanges = append(r.IPRanges, h.RemainingArgs()...) + default: + return nil, h.Errf("unrecognized 'deny' subdirective: %s", h.Val()) } } if acmeServer.Policy == nil { diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index 9dfeff724..4830570bf 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -28,7 +28,7 @@ import ( "github.com/caddyserver/certmagic" "github.com/caddyserver/zerossl" - "github.com/mholt/acmez/v2/acme" + "github.com/mholt/acmez/v3/acme" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -60,6 +60,14 @@ type ACMEIssuer struct { // other than ACME transactions. Email string `json:"email,omitempty"` + // Optionally select an ACME profile to use for certificate + // orders. Must be a profile name offered by the ACME server, + // which are listed at its directory endpoint. + // + // EXPERIMENTAL: Subject to change. + // See https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/ + Profile string `json:"profile,omitempty"` + // If you have an existing account with the ACME server, put // the private key here in PEM format. The ACME client will // look up your account information with this key first before @@ -98,6 +106,9 @@ type ACMEIssuer struct { // be used. EXPERIMENTAL: Subject to change. CertificateLifetime caddy.Duration `json:"certificate_lifetime,omitempty"` + // Forward proxy module + NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy inline_key=from"` + rootPool *x509.CertPool logger *zap.Logger @@ -138,15 +149,30 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { iss.AccountKey = accountKey } - // DNS providers - if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.ProviderRaw != nil { - val, err := ctx.LoadModule(iss.Challenges.DNS, "ProviderRaw") - if err != nil { - return fmt.Errorf("loading DNS provider module: %v", err) + // DNS challenge provider, if not already established + if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.solver == nil { + var prov certmagic.DNSProvider + if iss.Challenges.DNS.ProviderRaw != nil { + // a challenge provider has been locally configured - use it + val, err := ctx.LoadModule(iss.Challenges.DNS, "ProviderRaw") + if err != nil { + return fmt.Errorf("loading DNS provider module: %v", err) + } + prov = val.(certmagic.DNSProvider) + } else if tlsAppIface, err := ctx.AppIfConfigured("tls"); err == nil { + // no locally configured DNS challenge provider, but if there is + // a global DNS module configured with the TLS app, use that + tlsApp := tlsAppIface.(*TLS) + if tlsApp.dns != nil { + prov = tlsApp.dns.(certmagic.DNSProvider) + } + } + if prov == nil { + return fmt.Errorf("DNS challenge enabled, but no DNS provider configured") } iss.Challenges.DNS.solver = &certmagic.DNS01Solver{ DNSManager: certmagic.DNSManager{ - DNSProvider: val.(certmagic.DNSProvider), + DNSProvider: prov, TTL: time.Duration(iss.Challenges.DNS.TTL), PropagationDelay: time.Duration(iss.Challenges.DNS.PropagationDelay), PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout), @@ -171,7 +197,7 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { } var err error - iss.template, err = iss.makeIssuerTemplate() + iss.template, err = iss.makeIssuerTemplate(ctx) if err != nil { return err } @@ -179,11 +205,12 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { return nil } -func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) { +func (iss *ACMEIssuer) makeIssuerTemplate(ctx caddy.Context) (certmagic.ACMEIssuer, error) { template := certmagic.ACMEIssuer{ CA: iss.CA, TestCA: iss.TestCA, Email: iss.Email, + Profile: iss.Profile, AccountKeyPEM: iss.AccountKey, CertObtainTimeout: time.Duration(iss.ACMETimeout), TrustedRoots: iss.rootPool, @@ -192,6 +219,18 @@ func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) { Logger: iss.logger, } + if len(iss.NetworkProxyRaw) != 0 { + proxyMod, err := ctx.LoadModule(iss, "NetworkProxyRaw") + if err != nil { + return template, fmt.Errorf("failed to load network_proxy module: %v", err) + } + if m, ok := proxyMod.(caddy.ProxyFuncProducer); ok { + template.HTTPProxy = m.ProxyFunc() + } else { + return template, fmt.Errorf("network_proxy module is not `(func(*http.Request) (*url.URL, error))``") + } + } + if iss.Challenges != nil { if iss.Challenges.HTTP != nil { template.DisableHTTPChallenge = iss.Challenges.HTTP.Disabled @@ -338,6 +377,7 @@ func (iss *ACMEIssuer) generateZeroSSLEABCredentials(ctx context.Context, acct a // dir // test_dir // email +// profile // timeout // disable_http_challenge // disable_tlsalpn_challenge @@ -400,6 +440,11 @@ func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } + case "profile": + if !d.AllArgs(&iss.Profile) { + return d.ArgErr() + } + case "timeout": var timeoutStr string if !d.AllArgs(&timeoutStr) { @@ -477,21 +522,20 @@ func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { iss.TrustedRootsPEMFiles = d.RemainingArgs() case "dns": - if !d.NextArg() { - return d.ArgErr() - } - provName := d.Val() if iss.Challenges == nil { iss.Challenges = new(ChallengesConfig) } if iss.Challenges.DNS == nil { iss.Challenges.DNS = new(DNSChallengeConfig) } - unm, err := caddyfile.UnmarshalModule(d, "dns.providers."+provName) - if err != nil { - return err + if d.NextArg() { + provName := d.Val() + unm, err := caddyfile.UnmarshalModule(d, "dns.providers."+provName) + if err != nil { + return err + } + iss.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil) } - iss.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil) case "propagation_delay": if !d.NextArg() { diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index 1f1042ba0..2cb931504 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -25,9 +25,10 @@ import ( "strings" "github.com/caddyserver/certmagic" - "github.com/mholt/acmez/v2" + "github.com/mholt/acmez/v3" "go.uber.org/zap" "go.uber.org/zap/zapcore" + "golang.org/x/net/idna" "github.com/caddyserver/caddy/v2" ) @@ -183,7 +184,12 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { repl := caddy.NewReplacer() subjects := make([]string, len(ap.SubjectsRaw)) for i, sub := range ap.SubjectsRaw { - subjects[i] = repl.ReplaceAll(sub, "") + sub = repl.ReplaceAll(sub, "") + subASCII, err := idna.ToASCII(sub) + if err != nil { + return fmt.Errorf("could not convert automation policy subject '%s' to punycode: %v", sub, err) + } + subjects[i] = subASCII } ap.subjects = subjects @@ -322,12 +328,6 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { return err } - // check the rate limiter last because - // doing so makes a reservation - if !onDemandRateLimiter.Allow() { - return fmt.Errorf("on-demand rate limit exceeded") - } - return nil }, Managers: ap.Managers, @@ -390,10 +390,8 @@ func (ap *AutomationPolicy) onlyInternalIssuer() bool { // isWildcardOrDefault determines if the subjects include any wildcard domains, // or is the "default" policy (i.e. no subjects) which is unbounded. func (ap *AutomationPolicy) isWildcardOrDefault() bool { - isWildcardOrDefault := false - if len(ap.subjects) == 0 { - isWildcardOrDefault = true - } + isWildcardOrDefault := len(ap.subjects) == 0 + for _, sub := range ap.subjects { if strings.HasPrefix(sub, "*") { isWildcardOrDefault = true diff --git a/modules/caddytls/certmanagers.go b/modules/caddytls/certmanagers.go index 56950bc84..0a9d459df 100644 --- a/modules/caddytls/certmanagers.go +++ b/modules/caddytls/certmanagers.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "fmt" "io" + "net" "net/http" "net/url" "strings" @@ -143,6 +144,10 @@ func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientH qs.Set("server_name", hello.ServerName) qs.Set("signature_schemes", strings.Join(sigs, ",")) qs.Set("cipher_suites", strings.Join(suites, ",")) + localIP, _, err := net.SplitHostPort(hello.Conn.LocalAddr().String()) + if err == nil && localIP != "" { + qs.Set("local_ip", localIP) + } parsed.RawQuery = qs.Encode() req, err := http.NewRequestWithContext(hcg.ctx, http.MethodGet, parsed.String(), nil) diff --git a/modules/caddytls/certselection.go b/modules/caddytls/certselection.go index a561e3a1d..ac210d3b6 100644 --- a/modules/caddytls/certselection.go +++ b/modules/caddytls/certselection.go @@ -87,13 +87,7 @@ nextChoice: } if len(p.AnyTag) > 0 { - var found bool - for _, tag := range p.AnyTag { - if cert.HasTag(tag) { - found = true - break - } - } + found := slices.ContainsFunc(p.AnyTag, cert.HasTag) if !found { continue } diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index f415fffa0..724271a8e 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -24,10 +24,11 @@ import ( "fmt" "io" "os" - "path/filepath" + "reflect" + "slices" "strings" - "github.com/mholt/acmez/v2" + "github.com/mholt/acmez/v3" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -94,7 +95,7 @@ func (cp ConnectionPolicies) Provision(ctx caddy.Context) error { // TLSConfig returns a standard-lib-compatible TLS configuration which // selects the first matching policy based on the ClientHello. -func (cp ConnectionPolicies) TLSConfig(_ caddy.Context) *tls.Config { +func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { // using ServerName to match policies is extremely common, especially in configs // with lots and lots of different policies; we can fast-track those by indexing // them by SNI, so we don't have to iterate potentially thousands of policies @@ -105,6 +106,7 @@ func (cp ConnectionPolicies) TLSConfig(_ caddy.Context) *tls.Config { for _, m := range p.matchers { if sni, ok := m.(MatchServerName); ok { for _, sniName := range sni { + // index for fast lookups during handshakes indexedBySNI[sniName] = append(indexedBySNI[sniName], p) } } @@ -112,32 +114,79 @@ func (cp ConnectionPolicies) TLSConfig(_ caddy.Context) *tls.Config { } } - return &tls.Config{ - MinVersion: tls.VersionTLS12, - GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { - // filter policies by SNI first, if possible, to speed things up - // when there may be lots of policies - possiblePolicies := cp - if indexedPolicies, ok := indexedBySNI[hello.ServerName]; ok { - possiblePolicies = indexedPolicies - } + getConfigForClient := func(hello *tls.ClientHelloInfo) (*tls.Config, error) { + // filter policies by SNI first, if possible, to speed things up + // when there may be lots of policies + possiblePolicies := cp + if indexedPolicies, ok := indexedBySNI[hello.ServerName]; ok { + possiblePolicies = indexedPolicies + } - policyLoop: - for _, pol := range possiblePolicies { - for _, matcher := range pol.matchers { - if !matcher.Match(hello) { - continue policyLoop + policyLoop: + for _, pol := range possiblePolicies { + for _, matcher := range pol.matchers { + if !matcher.Match(hello) { + continue policyLoop + } + } + if pol.Drop { + return nil, fmt.Errorf("dropping connection") + } + return pol.TLSConfig, nil + } + + return nil, fmt.Errorf("no server TLS configuration available for ClientHello: %+v", hello) + } + + tlsCfg := &tls.Config{ + MinVersion: tls.VersionTLS12, + GetConfigForClient: getConfigForClient, + } + + // enable ECH, if configured + if tlsAppIface, err := ctx.AppIfConfigured("tls"); err == nil { + tlsApp := tlsAppIface.(*TLS) + + if tlsApp.EncryptedClientHello != nil && len(tlsApp.EncryptedClientHello.configs) > 0 { + // if no publication was configured, we apply ECH to all server names by default, + // but the TLS app needs to know what they are in this case, since they don't appear + // in its config (remember, TLS connection policies are used by *other* apps to + // run TLS servers) -- we skip names with placeholders + if tlsApp.EncryptedClientHello.Publication == nil { + var echNames []string + repl := caddy.NewReplacer() + for _, p := range cp { + for _, m := range p.matchers { + if sni, ok := m.(MatchServerName); ok { + for _, name := range sni { + finalName := strings.ToLower(repl.ReplaceAll(name, "")) + echNames = append(echNames, finalName) + } + } } } - if pol.Drop { - return nil, fmt.Errorf("dropping connection") - } - return pol.TLSConfig, nil + tlsApp.RegisterServerNames(echNames) } - return nil, fmt.Errorf("no server TLS configuration available for ClientHello: %+v", hello) - }, + // TODO: Ideally, ECH keys should be rotated. However, as of Go 1.24, the std lib implementation + // does not support safely modifying the tls.Config's EncryptedClientHelloKeys field. + // So, we implement static ECH keys temporarily. See https://github.com/golang/go/issues/71920. + // Revisit this after Go 1.25 is released and implement key rotation. + var stdECHKeys []tls.EncryptedClientHelloKey + for _, echConfigs := range tlsApp.EncryptedClientHello.configs { + for _, c := range echConfigs { + stdECHKeys = append(stdECHKeys, tls.EncryptedClientHelloKey{ + Config: c.configBin, + PrivateKey: c.privKeyBin, + SendAsRetry: c.sendAsRetry, + }) + } + } + tlsCfg.EncryptedClientHelloKeys = stdECHKeys + } } + + return tlsCfg } // ConnectionPolicy specifies the logic for handling a TLS handshake. @@ -321,13 +370,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { } // ensure ALPN includes the ACME TLS-ALPN protocol - var alpnFound bool - for _, a := range p.ALPN { - if a == acmez.ACMETLS1Protocol { - alpnFound = true - break - } - } + alpnFound := slices.Contains(p.ALPN, acmez.ACMETLS1Protocol) if !alpnFound && (cfg.NextProtos == nil || len(cfg.NextProtos) > 0) { cfg.NextProtos = append(cfg.NextProtos, acmez.ACMETLS1Protocol) } @@ -351,6 +394,20 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { if err := p.ClientAuthentication.ConfigureTLSConfig(cfg); err != nil { return fmt.Errorf("configuring TLS client authentication: %v", err) } + + // Prevent privilege escalation in case multiple vhosts are configured for + // this TLS server; we could potentially figure out if that's the case, but + // that might be complex to get right every time. Actually, two proper + // solutions could leave tickets enabled, but I am not sure how to do them + // properly without significant time investment; there may be new Go + // APIs that alloaw this (Wrap/UnwrapSession?) but I do not know how to use + // them at this time. TODO: one of these is a possible future enhancement: + // A) Prevent resumptions across server identities (certificates): binding the ticket to the + // certificate we would serve in a full handshake, or even bind a ticket to the exact SNI + // it was issued under (though there are proposals for session resumption across hostnames). + // B) Prevent resumptions falsely authenticating a client: include the realm in the ticket, + // so that it can be validated upon resumption. + cfg.SessionTicketsDisabled = true } if p.InsecureSecretsLog != "" { @@ -358,7 +415,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { if err != nil { return err } - filename, err = filepath.Abs(filename) + filename, err = caddy.FastAbs(filename) if err != nil { return err } @@ -396,9 +453,18 @@ func (p ConnectionPolicy) SettingsEmpty() bool { p.ProtocolMax == "" && p.ClientAuthentication == nil && p.DefaultSNI == "" && + p.FallbackSNI == "" && p.InsecureSecretsLog == "" } +// SettingsEqual returns true if p's settings (fields +// except the matchers) are the same as q. +func (p ConnectionPolicy) SettingsEqual(q ConnectionPolicy) bool { + p.MatchersRaw = nil + q.MatchersRaw = nil + return reflect.DeepEqual(p, q) +} + // UnmarshalCaddyfile sets up the ConnectionPolicy from Caddyfile tokens. Syntax: // // connection_policy { @@ -535,21 +601,21 @@ type ClientAuthentication struct { CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"` ca CA - // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.inline` module instead. + // Deprecated: Use the `ca` field with the `tls.ca_pool.source.inline` module instead. // A list of base64 DER-encoded CA certificates // against which to validate client certificates. // Client certs which are not signed by any of // these CAs will be rejected. TrustedCACerts []string `json:"trusted_ca_certs,omitempty"` - // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.file` module instead. + // Deprecated: Use the `ca` field with the `tls.ca_pool.source.file` module instead. // TrustedCACertPEMFiles is a list of PEM file names // from which to load certificates of trusted CAs. // Client certificates which are not signed by any of // these CA certificates will be rejected. TrustedCACertPEMFiles []string `json:"trusted_ca_certs_pem_files,omitempty"` - // DEPRECATED: This field is deprecated and will be removed in + // Deprecated: This field is deprecated and will be removed in // a future version. Please use the `validators` field instead // with the tls.client_auth.verifier.leaf module instead. // @@ -736,10 +802,14 @@ func (clientauth *ClientAuthentication) provision(ctx caddy.Context) error { // if we have TrustedCACerts explicitly set, create an 'inline' CA and return if len(clientauth.TrustedCACerts) > 0 { - clientauth.ca = InlineCAPool{ + caPool := InlineCAPool{ TrustedCACerts: clientauth.TrustedCACerts, } - return nil + err := caPool.Provision(ctx) + if err != nil { + return nil + } + clientauth.ca = caPool } // if we don't have any CARaw set, there's not much work to do @@ -871,22 +941,13 @@ func setDefaultTLSParams(cfg *tls.Config) { cfg.CipherSuites = append([]uint16{tls.TLS_FALLBACK_SCSV}, cfg.CipherSuites...) if len(cfg.CurvePreferences) == 0 { - // We would want to write - // - // cfg.CurvePreferences = defaultCurves - // - // but that would disable the post-quantum key agreement X25519Kyber768 - // supported in Go 1.23, for which the CurveID is not exported. - // Instead, we'll set CurvePreferences to nil, which will enable PQC. - // See https://github.com/caddyserver/caddy/issues/6540 - cfg.CurvePreferences = nil + cfg.CurvePreferences = defaultCurves } - if cfg.MinVersion == 0 { - cfg.MinVersion = tls.VersionTLS12 - } - if cfg.MaxVersion == 0 { - cfg.MaxVersion = tls.VersionTLS13 + // crypto/tls docs: + // "If EncryptedClientHelloKeys is set, MinVersion, if set, must be VersionTLS13." + if cfg.EncryptedClientHelloKeys != nil && cfg.MinVersion != 0 && cfg.MinVersion < tls.VersionTLS13 { + cfg.MinVersion = tls.VersionTLS13 } } @@ -928,6 +989,48 @@ func (l *LeafCertClientAuth) Provision(ctx caddy.Context) error { return nil } +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (l *LeafCertClientAuth) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.NextArg() + + // accommodate the use of one-liners + if d.CountRemainingArgs() > 1 { + d.NextArg() + modName := d.Val() + mod, err := caddyfile.UnmarshalModule(d, "tls.leaf_cert_loader."+modName) + if err != nil { + return d.WrapErr(err) + } + vMod, ok := mod.(LeafCertificateLoader) + if !ok { + return fmt.Errorf("leaf module '%s' is not a leaf certificate loader", vMod) + } + l.LeafCertificateLoadersRaw = append( + l.LeafCertificateLoadersRaw, + caddyconfig.JSONModuleObject(vMod, "loader", modName, nil), + ) + return nil + } + + // accommodate the use of nested blocks + for nesting := d.Nesting(); d.NextBlock(nesting); { + modName := d.Val() + mod, err := caddyfile.UnmarshalModule(d, "tls.leaf_cert_loader."+modName) + if err != nil { + return d.WrapErr(err) + } + vMod, ok := mod.(LeafCertificateLoader) + if !ok { + return fmt.Errorf("leaf module '%s' is not a leaf certificate loader", vMod) + } + l.LeafCertificateLoadersRaw = append( + l.LeafCertificateLoadersRaw, + caddyconfig.JSONModuleObject(vMod, "loader", modName, nil), + ) + } + return nil +} + func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error { if len(rawCerts) == 0 { return fmt.Errorf("no client certificate provided") @@ -938,10 +1041,8 @@ func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x5 return fmt.Errorf("can't parse the given certificate: %s", err.Error()) } - for _, trustedLeafCert := range l.trustedLeafCerts { - if remoteLeafCert.Equal(trustedLeafCert) { - return nil - } + if slices.ContainsFunc(l.trustedLeafCerts, remoteLeafCert.Equal) { + return nil } return fmt.Errorf("client leaf certificate failed validation") @@ -991,6 +1092,7 @@ var secretsLogPool = caddy.NewUsagePool() var ( _ caddyfile.Unmarshaler = (*ClientAuthentication)(nil) _ caddyfile.Unmarshaler = (*ConnectionPolicy)(nil) + _ caddyfile.Unmarshaler = (*LeafCertClientAuth)(nil) ) // ParseCaddyfileNestedMatcherSet parses the Caddyfile tokens for a nested diff --git a/modules/caddytls/connpolicy_test.go b/modules/caddytls/connpolicy_test.go index 0caed2899..82ecbc40d 100644 --- a/modules/caddytls/connpolicy_test.go +++ b/modules/caddytls/connpolicy_test.go @@ -20,6 +20,7 @@ import ( "reflect" "testing" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) @@ -278,3 +279,49 @@ func TestClientAuthenticationUnmarshalCaddyfileWithDirectiveName(t *testing.T) { }) } } + +func TestClientAuthenticationProvision(t *testing.T) { + tests := []struct { + name string + ca ClientAuthentication + wantErr bool + }{ + { + name: "specifying both 'CARaw' and 'TrustedCACerts' produces an error", + ca: ClientAuthentication{ + CARaw: json.RawMessage(`{"provider":"inline","trusted_ca_certs":["foo"]}`), + TrustedCACerts: []string{"foo"}, + }, + wantErr: true, + }, + { + name: "specifying both 'CARaw' and 'TrustedCACertPEMFiles' produces an error", + ca: ClientAuthentication{ + CARaw: json.RawMessage(`{"provider":"inline","trusted_ca_certs":["foo"]}`), + TrustedCACertPEMFiles: []string{"foo"}, + }, + wantErr: true, + }, + { + name: "setting 'TrustedCACerts' provisions the cert pool", + ca: ClientAuthentication{ + TrustedCACerts: []string{test_der_1}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.ca.provision(caddy.Context{}) + if (err != nil) != tt.wantErr { + t.Errorf("ClientAuthentication.provision() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if tt.ca.ca.CertPool() == nil { + t.Error("CertPool is nil, expected non-nil value") + } + } + }) + } +} diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go new file mode 100644 index 000000000..1b3bacbd2 --- /dev/null +++ b/modules/caddytls/ech.go @@ -0,0 +1,1040 @@ +package caddytls + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/fs" + weakrand "math/rand/v2" + "path" + "strconv" + "strings" + "time" + + "github.com/caddyserver/certmagic" + "github.com/cloudflare/circl/hpke" + "github.com/cloudflare/circl/kem" + "github.com/libdns/libdns" + "go.uber.org/zap" + "golang.org/x/crypto/cryptobyte" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(ECHDNSPublisher{}) +} + +// ECH enables Encrypted ClientHello (ECH) and configures its management. +// +// ECH helps protect site names (also called "server names" or "domain names" +// or "SNI"), which are normally sent over plaintext when establishing a TLS +// connection. With ECH, the true ClientHello is encrypted and wrapped by an +// "outer" ClientHello that uses a more generic, shared server name that is +// publicly known. +// +// Clients need to know which public name (and other parameters) to use when +// connecting to a site with ECH, and the methods for this vary; however, +// major browsers support reading ECH configurations from DNS records (which +// is typically only secure when DNS-over-HTTPS or DNS-over-TLS is enabled in +// the client). Caddy has the ability to automatically publish ECH configs to +// DNS records if a DNS provider is configured either in the TLS app or with +// each individual publication config object. (Requires a custom build with a +// DNS provider module.) +// +// ECH requires at least TLS 1.3, so any TLS connection policies with ECH +// applied will automatically upgrade the minimum TLS version to 1.3, even if +// configured to a lower version. +// +// Note that, as of Caddy 2.10.0 (~March 2025), ECH keys are not automatically +// rotated due to a limitation in the Go standard library (see +// https://github.com/golang/go/issues/71920). This should be resolved when +// Go 1.25 is released (~Aug. 2025), and Caddy will be updated to automatically +// rotate ECH keys/configs at that point. +// +// EXPERIMENTAL: Subject to change. +type ECH struct { + // The list of ECH configurations for which to automatically generate + // and rotate keys. At least one is required to enable ECH. + // + // It is strongly recommended to use as few ECH configs as possible + // to maximize the size of your anonymity set (see the ECH specification + // for a definition). Typically, each server should have only one public + // name, i.e. one config in this list. + Configs []ECHConfiguration `json:"configs,omitempty"` + + // Publication describes ways to publish ECH configs for clients to + // discover and use. Without publication, most clients will not use + // ECH at all, and those that do will suffer degraded performance. + // + // Most major browsers support ECH by way of publication to HTTPS + // DNS RRs. (This also typically requires that they use DoH or DoT.) + Publication []*ECHPublication `json:"publication,omitempty"` + + // map of public_name to list of configs + configs map[string][]echConfig +} + +// Provision loads or creates ECH configs and returns outer names (for certificate +// management), but does not publish any ECH configs. The DNS module is used as +// a default for later publishing if needed. +func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) { + logger := ctx.Logger().Named("ech") + + // set up publication modules before we need to obtain a lock in storage, + // since this is strictly internal and doesn't require synchronization + for i, pub := range ech.Publication { + mods, err := ctx.LoadModule(pub, "PublishersRaw") + if err != nil { + return nil, fmt.Errorf("loading ECH publication modules: %v", err) + } + for _, modIface := range mods.(map[string]any) { + ech.Publication[i].publishers = append(ech.Publication[i].publishers, modIface.(ECHPublisher)) + } + } + + // the rest of provisioning needs an exclusive lock so that instances aren't + // stepping on each other when setting up ECH configs + storage := ctx.Storage() + const echLockName = "ech_provision" + if err := storage.Lock(ctx, echLockName); err != nil { + return nil, err + } + defer func() { + if err := storage.Unlock(ctx, echLockName); err != nil { + logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err)) + } + }() + + var outerNames []string //nolint:prealloc // (FALSE POSITIVE - see https://github.com/alexkohler/prealloc/issues/30) + + // start by loading all the existing configs (even the older ones on the way out, + // since some clients may still be using them if they haven't yet picked up on the + // new configs) + cfgKeys, err := storage.List(ctx, echConfigsKey, false) + if err != nil && !errors.Is(err, fs.ErrNotExist) { // OK if dir doesn't exist; it will be created + return nil, err + } + for _, cfgKey := range cfgKeys { + cfg, err := loadECHConfig(ctx, path.Base(cfgKey)) + if err != nil { + return nil, err + } + // if any part of the config's folder was corrupted, the load function will + // clean it up and not return an error, since configs are immutable and + // fairly ephemeral... so just check that we actually got a populated config + if cfg.configBin == nil || cfg.privKeyBin == nil { + continue + } + logger.Debug("loaded ECH config", + zap.String("public_name", cfg.RawPublicName), + zap.Uint8("id", cfg.ConfigID)) + ech.configs[cfg.RawPublicName] = append(ech.configs[cfg.RawPublicName], cfg) + outerNames = append(outerNames, cfg.RawPublicName) + } + + // all existing configs are now loaded; see if we need to make any new ones + // based on the input configuration, and also mark the most recent one(s) as + // current/active, so they can be used for ECH retries + for _, cfg := range ech.Configs { + publicName := strings.ToLower(strings.TrimSpace(cfg.PublicName)) + + if list, ok := ech.configs[publicName]; ok && len(list) > 0 { + // at least one config with this public name was loaded, so find the + // most recent one and mark it as active to be used with retries + var mostRecentDate time.Time + var mostRecentIdx int + for i, c := range list { + if mostRecentDate.IsZero() || c.meta.Created.After(mostRecentDate) { + mostRecentDate = c.meta.Created + mostRecentIdx = i + } + } + list[mostRecentIdx].sendAsRetry = true + } else { + // no config with this public name was loaded, so create one + echCfg, err := generateAndStoreECHConfig(ctx, publicName) + if err != nil { + return nil, err + } + logger.Debug("generated new ECH config", + zap.String("public_name", echCfg.RawPublicName), + zap.Uint8("id", echCfg.ConfigID)) + ech.configs[publicName] = append(ech.configs[publicName], echCfg) + outerNames = append(outerNames, publicName) + } + } + + return outerNames, nil +} + +func (t *TLS) publishECHConfigs() error { + logger := t.logger.Named("ech") + + // make publication exclusive, since we don't need to repeat this unnecessarily + storage := t.ctx.Storage() + const echLockName = "ech_publish" + if err := storage.Lock(t.ctx, echLockName); err != nil { + return err + } + defer func() { + if err := storage.Unlock(t.ctx, echLockName); err != nil { + logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err)) + } + }() + + // get the publication config, or use a default if not specified + // (the default publication config should be to publish all ECH + // configs to the app-global DNS provider; if no DNS provider is + // configured, then this whole function is basically a no-op) + publicationList := t.EncryptedClientHello.Publication + if publicationList == nil { + if dnsProv, ok := t.dns.(ECHDNSProvider); ok { + publicationList = []*ECHPublication{ + { + publishers: []ECHPublisher{ + &ECHDNSPublisher{ + provider: dnsProv, + logger: t.logger, + }, + }, + }, + } + } + } + + // for each publication config, build the list of ECH configs to + // publish with it, and figure out which inner names to publish + // to/for, then publish + for _, publication := range publicationList { + // this publication is either configured for specific ECH configs, + // or we just use an implied default of all ECH configs + var echCfgList echConfigList + var configIDs []uint8 // TODO: use IDs or the outer names? + if publication.Configs == nil { + // by default, publish all configs + for _, configs := range t.EncryptedClientHello.configs { + echCfgList = append(echCfgList, configs...) + for _, c := range configs { + configIDs = append(configIDs, c.ConfigID) + } + } + } else { + for _, cfgOuterName := range publication.Configs { + if cfgList, ok := t.EncryptedClientHello.configs[cfgOuterName]; ok { + echCfgList = append(echCfgList, cfgList...) + for _, c := range cfgList { + configIDs = append(configIDs, c.ConfigID) + } + } + } + } + + // marshal the ECH config list as binary for publication + echCfgListBin, err := echCfgList.MarshalBinary() + if err != nil { + return fmt.Errorf("marshaling ECH config list: %v", err) + } + + // now we have our list of ECH configs to publish and the inner names + // to publish for (i.e. the names being protected); iterate each publisher + // and do the publish for any config+name that needs a publish + for _, publisher := range publication.publishers { + publisherKey := publisher.PublisherKey() + + // by default, publish for all (non-outer) server names, unless + // a specific list of names is configured + var serverNamesSet map[string]struct{} + if publication.Domains == nil { + serverNamesSet = make(map[string]struct{}, len(t.serverNames)) + for name := range t.serverNames { + serverNamesSet[name] = struct{}{} + } + } else { + serverNamesSet = make(map[string]struct{}, len(publication.Domains)) + for _, name := range publication.Domains { + serverNamesSet[name] = struct{}{} + } + } + + // remove any domains from the set which have already had all configs in the + // list published by this publisher, to avoid always re-publishing unnecessarily + for configuredInnerName := range serverNamesSet { + allConfigsPublished := true + for _, cfg := range echCfgList { + // TODO: Potentially utilize the timestamp (map value) for recent-enough publication, instead of just checking for existence + if _, ok := cfg.meta.Publications[publisherKey][configuredInnerName]; !ok { + allConfigsPublished = false + break + } + } + if allConfigsPublished { + delete(serverNamesSet, configuredInnerName) + } + } + + // if all the (inner) domains have had this ECH config list published + // by this publisher, then try the next publication config + if len(serverNamesSet) == 0 { + logger.Debug("ECH config list already published by publisher for associated domains (or no domains to publish for)", + zap.Uint8s("config_ids", configIDs), + zap.String("publisher", publisherKey)) + continue + } + + // convert the set of names to a slice + dnsNamesToPublish := make([]string, 0, len(serverNamesSet)) + for name := range serverNamesSet { + dnsNamesToPublish = append(dnsNamesToPublish, name) + } + + logger.Debug("publishing ECH config list", + zap.String("publisher", publisherKey), + zap.Strings("domains", dnsNamesToPublish), + zap.Uint8s("config_ids", configIDs)) + + // publish this ECH config list with this publisher + pubTime := time.Now() + err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin) + + var publishErrs PublishECHConfigListErrors + if errors.As(err, &publishErrs) { + // at least a partial failure, maybe a complete failure, but we can + // log each error by domain + for innerName, domainErr := range publishErrs { + t.logger.Error("failed to publish ECH configuration list", + zap.String("publisher", publisherKey), + zap.String("domain", innerName), + zap.Uint8s("config_ids", configIDs), + zap.Error(domainErr)) + } + } else if err != nil { + // generic error; assume the entire thing failed, I guess + t.logger.Error("failed publishing ECH configuration list", + zap.String("publisher", publisherKey), + zap.Strings("domains", dnsNamesToPublish), + zap.Uint8s("config_ids", configIDs), + zap.Error(err)) + } + + if err == nil || (len(publishErrs) > 0 && len(publishErrs) < len(dnsNamesToPublish)) { + // if publication for at least some domains succeeded, we should update our publication + // state for those domains to avoid unnecessarily republishing every time + someAll := "all" + if len(publishErrs) > 0 { + someAll = "some" + } + // make a list of names that published successfully with this publisher + // so that we update only their state in storage, not the failed ones + var successNames []string + for _, name := range dnsNamesToPublish { + if _, ok := publishErrs[name]; !ok { + successNames = append(successNames, name) + } + } + t.logger.Info("successfully published ECH configuration list for "+someAll+" domains", + zap.String("publisher", publisherKey), + zap.Strings("domains", successNames), + zap.Uint8s("config_ids", configIDs)) + + for _, cfg := range echCfgList { + if cfg.meta.Publications == nil { + cfg.meta.Publications = make(publicationHistory) + } + if _, ok := cfg.meta.Publications[publisherKey]; !ok { + cfg.meta.Publications[publisherKey] = make(map[string]time.Time) + } + for _, name := range successNames { + cfg.meta.Publications[publisherKey][name] = pubTime + } + metaBytes, err := json.Marshal(cfg.meta) + if err != nil { + return fmt.Errorf("marshaling ECH config metadata: %v", err) + } + metaKey := path.Join(echConfigsKey, strconv.Itoa(int(cfg.ConfigID)), "meta.json") + if err := t.ctx.Storage().Store(t.ctx, metaKey, metaBytes); err != nil { + return fmt.Errorf("storing updated ECH config metadata: %v", err) + } + } + } else { + t.logger.Error("all domains failed to publish ECH configuration list (see earlier errors)", + zap.String("publisher", publisherKey), + zap.Strings("domains", dnsNamesToPublish), + zap.Uint8s("config_ids", configIDs)) + } + } + } + + return nil +} + +// loadECHConfig loads the config from storage with the given configID. +// An error is not actually returned in some cases the config fails to +// load because in some cases it just means the config ID folder has +// been cleaned up in storage, maybe due to an incomplete set of keys +// or corrupted contents; in any case, the only rectification is to +// delete it and make new keys (an error IS returned if deleting the +// corrupted keys fails, for example). Check the returned echConfig for +// non-nil privKeyBin and configBin values before using. +func loadECHConfig(ctx caddy.Context, configID string) (echConfig, error) { + storage := ctx.Storage() + logger := ctx.Logger() + + cfgIDKey := path.Join(echConfigsKey, configID) + keyKey := path.Join(cfgIDKey, "key.bin") + configKey := path.Join(cfgIDKey, "config.bin") + metaKey := path.Join(cfgIDKey, "meta.json") + + // if loading anything fails, might as well delete this folder and free up + // the config ID; spec is designed to rotate configs frequently anyway + // (I consider it a more serious error if we can't clean up the folder, + // since leaving stray storage keys is confusing) + privKeyBytes, err := storage.Load(ctx, keyKey) + if err != nil { + delErr := storage.Delete(ctx, cfgIDKey) + if delErr != nil { + return echConfig{}, fmt.Errorf("error loading private key (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr) + } + logger.Warn("could not load ECH private key; deleting its config folder", + zap.String("config_id", configID), + zap.Error(err)) + return echConfig{}, nil + } + echConfigBytes, err := storage.Load(ctx, configKey) + if err != nil { + delErr := storage.Delete(ctx, cfgIDKey) + if delErr != nil { + return echConfig{}, fmt.Errorf("error loading ECH config (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr) + } + logger.Warn("could not load ECH config; deleting its config folder", + zap.String("config_id", configID), + zap.Error(err)) + return echConfig{}, nil + } + var cfg echConfig + if err := cfg.UnmarshalBinary(echConfigBytes); err != nil { + delErr := storage.Delete(ctx, cfgIDKey) + if delErr != nil { + return echConfig{}, fmt.Errorf("error loading ECH config (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr) + } + logger.Warn("could not load ECH config; deleted its config folder", + zap.String("config_id", configID), + zap.Error(err)) + return echConfig{}, nil + } + metaBytes, err := storage.Load(ctx, metaKey) + if errors.Is(err, fs.ErrNotExist) { + logger.Warn("ECH config metadata file missing; will recreate at next publication", + zap.String("config_id", configID), + zap.Error(err)) + } else if err != nil { + delErr := storage.Delete(ctx, cfgIDKey) + if delErr != nil { + return echConfig{}, fmt.Errorf("error loading ECH config metadata (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr) + } + logger.Warn("could not load ECH config metadata; deleted its folder", + zap.String("config_id", configID), + zap.Error(err)) + return echConfig{}, nil + } + var meta echConfigMeta + if len(metaBytes) > 0 { + if err := json.Unmarshal(metaBytes, &meta); err != nil { + // even though it's just metadata, reset the whole config since we can't reliably maintain it + delErr := storage.Delete(ctx, cfgIDKey) + if delErr != nil { + return echConfig{}, fmt.Errorf("error decoding ECH metadata (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr) + } + logger.Warn("could not JSON-decode ECH metadata; deleted its config folder", + zap.String("config_id", configID), + zap.Error(err)) + return echConfig{}, nil + } + } + + cfg.privKeyBin = privKeyBytes + cfg.configBin = echConfigBytes + cfg.meta = meta + + return cfg, nil +} + +func generateAndStoreECHConfig(ctx caddy.Context, publicName string) (echConfig, error) { + // Go currently has very strict requirements for server-side ECH configs, + // to quote the Go 1.24 godoc (with typos of AEAD IDs corrected): + // + // "Config should be a marshalled ECHConfig associated with PrivateKey. This + // must match the config provided to clients byte-for-byte. The config + // should only specify the DHKEM(X25519, HKDF-SHA256) KEM ID (0x0020), the + // HKDF-SHA256 KDF ID (0x0001), and a subset of the following AEAD IDs: + // AES-128-GCM (0x0001), AES-256-GCM (0x0002), ChaCha20Poly1305 (0x0003)." + // + // So we need to be sure we generate a config within these parameters + // so the Go TLS server can use it. + + // generate a key pair + const kemChoice = hpke.KEM_X25519_HKDF_SHA256 + publicKey, privateKey, err := kemChoice.Scheme().GenerateKeyPair() + if err != nil { + return echConfig{}, err + } + + // find an available config ID + configID, err := newECHConfigID(ctx) + if err != nil { + return echConfig{}, fmt.Errorf("generating unique config ID: %v", err) + } + + echCfg := echConfig{ + PublicKey: publicKey, + Version: draftTLSESNI22, + ConfigID: configID, + RawPublicName: publicName, + KEMID: kemChoice, + CipherSuites: []hpkeSymmetricCipherSuite{ + { + KDFID: hpke.KDF_HKDF_SHA256, + AEADID: hpke.AEAD_AES128GCM, + }, + { + KDFID: hpke.KDF_HKDF_SHA256, + AEADID: hpke.AEAD_AES256GCM, + }, + { + KDFID: hpke.KDF_HKDF_SHA256, + AEADID: hpke.AEAD_ChaCha20Poly1305, + }, + }, + sendAsRetry: true, + } + meta := echConfigMeta{ + Created: time.Now(), + } + + privKeyBytes, err := privateKey.MarshalBinary() + if err != nil { + return echConfig{}, fmt.Errorf("marshaling ECH private key: %v", err) + } + echConfigBytes, err := echCfg.MarshalBinary() + if err != nil { + return echConfig{}, fmt.Errorf("marshaling ECH config: %v", err) + } + metaBytes, err := json.Marshal(meta) + if err != nil { + return echConfig{}, fmt.Errorf("marshaling ECH config metadata: %v", err) + } + + parentKey := path.Join(echConfigsKey, strconv.Itoa(int(configID))) + keyKey := path.Join(parentKey, "key.bin") + configKey := path.Join(parentKey, "config.bin") + metaKey := path.Join(parentKey, "meta.json") + + if err := ctx.Storage().Store(ctx, keyKey, privKeyBytes); err != nil { + return echConfig{}, fmt.Errorf("storing ECH private key: %v", err) + } + if err := ctx.Storage().Store(ctx, configKey, echConfigBytes); err != nil { + return echConfig{}, fmt.Errorf("storing ECH config: %v", err) + } + if err := ctx.Storage().Store(ctx, metaKey, metaBytes); err != nil { + return echConfig{}, fmt.Errorf("storing ECH config metadata: %v", err) + } + + echCfg.privKeyBin = privKeyBytes + echCfg.configBin = echConfigBytes // this contains the public key + echCfg.meta = meta + + return echCfg, nil +} + +// ECH represents an Encrypted ClientHello configuration. +// +// EXPERIMENTAL: Subject to change. +type ECHConfiguration struct { + // The public server name (SNI) that will be used in the outer ClientHello. + // This should be a domain name for which this server is authoritative, + // because Caddy will try to provision a certificate for this name. As an + // outer SNI, it is never used for application data (HTTPS, etc.), but it + // is necessary for enabling clients to connect securely in some cases. + // If this field is empty or missing, or if Caddy cannot get a certificate + // for this domain (e.g. the domain's DNS records do not point to this server), + // client reliability becomes brittle, and you risk coercing clients to expose + // true server names in plaintext, which compromises both the privacy of the + // server and makes clients more vulnerable. + PublicName string `json:"public_name"` +} + +// ECHPublication configures publication of ECH config(s). It pairs a list +// of ECH configs with the list of domains they are assigned to protect, and +// describes how to publish those configs for those domains. +// +// Most servers will have only a single publication config, unless their +// domains are spread across multiple DNS providers or require different +// methods of publication. +// +// EXPERIMENTAL: Subject to change. +type ECHPublication struct { + // The list of ECH configurations to publish, identified by public name. + // If not set, all configs will be included for publication by default. + // + // It is generally advised to maximize the size of your anonymity set, + // which implies using as few public names as possible for your sites. + // Usually, only a single public name is used to protect all the sites + // for a server + // + // EXPERIMENTAL: This field may be renamed or have its structure changed. + Configs []string `json:"configs,omitempty"` + + // The list of ("inner") domain names which are protected with the associated + // ECH configurations. + // + // If not set, all server names registered with the TLS module will be + // added to this list implicitly. (This registration is done automatically + // by other Caddy apps that use the TLS module. They should register their + // configured server names for this purpose. For example, the HTTP server + // registers the hostnames for which it applies automatic HTTPS. This is + // not something you, the user, have to do.) Most servers + // + // Names in this list should not appear in any other publication config + // object with the same publishers, since the publications will likely + // overwrite each other. + // + // NOTE: In order to publish ECH configs for domains configured for + // On-Demand TLS that are not explicitly enumerated elsewhere in the + // config, those domain names will have to be listed here. The only + // time Caddy knows which domains it is serving with On-Demand TLS is + // handshake-time, which is too late for publishing ECH configs; it + // means the first connections would not protect the server names, + // revealing that information to observers, and thus defeating the + // purpose of ECH. Hence the need to list them here so Caddy can + // proactively publish ECH configs before clients connect with those + // server names in plaintext. + Domains []string `json:"domains,omitempty"` + + // How to publish the ECH configurations so clients can know to use + // ECH to connect more securely to the server. + PublishersRaw caddy.ModuleMap `json:"publishers,omitempty" caddy:"namespace=tls.ech.publishers"` + publishers []ECHPublisher +} + +// ECHDNSProvider can service DNS entries for ECH purposes. +type ECHDNSProvider interface { + libdns.RecordGetter + libdns.RecordSetter +} + +// ECHDNSPublisher configures how to publish an ECH configuration to +// DNS records for the specified domains. +// +// EXPERIMENTAL: Subject to change. +type ECHDNSPublisher struct { + // The DNS provider module which will establish the HTTPS record(s). + ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"` + provider ECHDNSProvider + + logger *zap.Logger +} + +// CaddyModule returns the Caddy module information. +func (ECHDNSPublisher) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.ech.publishers.dns", + New: func() caddy.Module { return new(ECHDNSPublisher) }, + } +} + +func (dnsPub *ECHDNSPublisher) Provision(ctx caddy.Context) error { + dnsProvMod, err := ctx.LoadModule(dnsPub, "ProviderRaw") + if err != nil { + return fmt.Errorf("loading ECH DNS provider module: %v", err) + } + prov, ok := dnsProvMod.(ECHDNSProvider) + if !ok { + return fmt.Errorf("ECH DNS provider module is not an ECH DNS Provider: %v", err) + } + dnsPub.provider = prov + dnsPub.logger = ctx.Logger() + return nil +} + +// PublisherKey returns the name of the DNS provider module. +// We intentionally omit specific provider configuration (or a hash thereof, +// since the config is likely sensitive, potentially containing an API key) +// because it is unlikely that specific configuration, such as an API key, +// is relevant to unique key use as an ECH config publisher. +func (dnsPub ECHDNSPublisher) PublisherKey() string { + return string(dnsPub.provider.(caddy.Module).CaddyModule().ID) +} + +// PublishECHConfigList publishes the given ECH config list (as binary) to the given DNS names. +// If there is an error, it may be of type PublishECHConfigListErrors, detailing +// potentially multiple errors keyed by associated innerName. +func (dnsPub *ECHDNSPublisher) PublishECHConfigList(ctx context.Context, innerNames []string, configListBin []byte) error { + nameservers := certmagic.RecursiveNameservers(nil) // TODO: we could make resolvers configurable + + errs := make(PublishECHConfigListErrors) + +nextName: + for _, domain := range innerNames { + zone, err := certmagic.FindZoneByFQDN(ctx, dnsPub.logger, domain, nameservers) + if err != nil { + errs[domain] = fmt.Errorf("could not determine zone for domain: %w (domain=%s nameservers=%v)", err, domain, nameservers) + continue + } + + relName := libdns.RelativeName(domain+".", zone) + + // get existing records for this domain; we need to make sure another + // record exists for it so we don't accidentally trample a wildcard; we + // also want to get any HTTPS record that may already exist for it so + // we can augment the ech SvcParamKey with any other existing SvcParams + recs, err := dnsPub.provider.GetRecords(ctx, zone) + if err != nil { + errs[domain] = fmt.Errorf("unable to get existing DNS records to publish ECH data to HTTPS DNS record: %w", err) + continue + } + var httpsRec libdns.ServiceBinding + var nameHasExistingRecord bool + for _, rec := range recs { + rr := rec.RR() + if rr.Name == relName { + // CNAME records are exclusive of all other records, so we cannot publish an HTTPS + // record for a domain that is CNAME'd. See #6922. + if rr.Type == "CNAME" { + dnsPub.logger.Warn("domain has CNAME record, so unable to publish ECH data to HTTPS record", + zap.String("domain", domain), + zap.String("cname_value", rr.Data)) + continue nextName + } + nameHasExistingRecord = true + if svcb, ok := rec.(libdns.ServiceBinding); ok && svcb.Scheme == "https" { + if svcb.Target == "" || svcb.Target == "." { + httpsRec = svcb + break + } + } + } + } + if !nameHasExistingRecord { + // Turns out if you publish a DNS record for a name that doesn't have any DNS record yet, + // any wildcard records won't apply for the name anymore, meaning if a wildcard A/AAAA record + // is used to resolve the domain to a server, publishing an HTTPS record could break resolution! + // In theory, this should be a non-issue, at least for A/AAAA records, if the HTTPS record + // includes ipv[4|6]hint SvcParamKeys, + dnsPub.logger.Warn("domain does not have any existing records, so skipping publication of HTTPS record", + zap.String("domain", domain), + zap.String("relative_name", relName), + zap.String("zone", zone)) + continue + } + params := httpsRec.Params + if params == nil { + params = make(libdns.SvcParams) + } + + // overwrite only the "ech" SvcParamKey + params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)} + + // publish record + _, err = dnsPub.provider.SetRecords(ctx, zone, []libdns.Record{ + libdns.ServiceBinding{ + // HTTPS and SVCB RRs: RFC 9460 (https://www.rfc-editor.org/rfc/rfc9460) + Scheme: "https", + Name: relName, + TTL: 5 * time.Minute, // TODO: low hard-coded value only temporary; change to a higher value once more field-tested and key rotation is implemented + Priority: 2, // allows a manual override with priority 1 + Target: ".", + Params: params, + }, + }) + if err != nil { + errs[domain] = fmt.Errorf("unable to publish ECH data to HTTPS DNS record: %w (zone=%s dns_record_name=%s)", err, zone, relName) + continue + } + } + + if len(errs) > 0 { + return errs + } + return nil +} + +// echConfig represents an ECHConfig from the specification, +// [draft-ietf-tls-esni-22](https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html). +type echConfig struct { + // "The version of ECH for which this configuration is used. + // The version is the same as the code point for the + // encrypted_client_hello extension. Clients MUST ignore any + // ECHConfig structure with a version they do not support." + Version uint16 + + // The "length" and "contents" fields defined next in the + // structure are implicitly taken care of by cryptobyte + // when encoding the following fields: + + // HpkeKeyConfig fields: + ConfigID uint8 + KEMID hpke.KEM + PublicKey kem.PublicKey + CipherSuites []hpkeSymmetricCipherSuite + + // ECHConfigContents fields: + MaxNameLength uint8 + RawPublicName string + RawExtensions []byte + + // these fields are not part of the spec, but are here for + // our use when setting up TLS servers or maintenance + configBin []byte + privKeyBin []byte + meta echConfigMeta + sendAsRetry bool +} + +func (echCfg echConfig) MarshalBinary() ([]byte, error) { + var b cryptobyte.Builder + if err := echCfg.marshalBinary(&b); err != nil { + return nil, err + } + return b.Bytes() +} + +// UnmarshalBinary decodes the data back into an ECH config. +// +// Borrowed from github.com/OmarTariq612/goech with modifications. +// Original code: Copyright (c) 2023 Omar Tariq AbdEl-Raziq +func (echCfg *echConfig) UnmarshalBinary(data []byte) error { + var content cryptobyte.String + b := cryptobyte.String(data) + + if !b.ReadUint16(&echCfg.Version) { + return errInvalidLen + } + if echCfg.Version != draftTLSESNI22 { + return fmt.Errorf("supported version must be %d: got %d", draftTLSESNI22, echCfg.Version) + } + + if !b.ReadUint16LengthPrefixed(&content) || !b.Empty() { + return errInvalidLen + } + + var t cryptobyte.String + var pk []byte + + if !content.ReadUint8(&echCfg.ConfigID) || + !content.ReadUint16((*uint16)(&echCfg.KEMID)) || + !content.ReadUint16LengthPrefixed(&t) || + !t.ReadBytes(&pk, len(t)) || + !content.ReadUint16LengthPrefixed(&t) || + len(t)%4 != 0 /* the length of (KDFs and AEADs) must be divisible by 4 */ { + return errInvalidLen + } + + if !echCfg.KEMID.IsValid() { + return fmt.Errorf("invalid KEM ID: %d", echCfg.KEMID) + } + + var err error + if echCfg.PublicKey, err = echCfg.KEMID.Scheme().UnmarshalBinaryPublicKey(pk); err != nil { + return fmt.Errorf("parsing public_key: %w", err) + } + + echCfg.CipherSuites = echCfg.CipherSuites[:0] + + for !t.Empty() { + var hpkeKDF, hpkeAEAD uint16 + if !t.ReadUint16(&hpkeKDF) || !t.ReadUint16(&hpkeAEAD) { + // we have already checked that the length is divisible by 4 + panic("this must not happen") + } + if !hpke.KDF(hpkeKDF).IsValid() { + return fmt.Errorf("invalid KDF ID: %d", hpkeKDF) + } + if !hpke.AEAD(hpkeAEAD).IsValid() { + return fmt.Errorf("invalid AEAD ID: %d", hpkeAEAD) + } + echCfg.CipherSuites = append(echCfg.CipherSuites, hpkeSymmetricCipherSuite{ + KDFID: hpke.KDF(hpkeKDF), + AEADID: hpke.AEAD(hpkeAEAD), + }) + } + + var rawPublicName []byte + if !content.ReadUint8(&echCfg.MaxNameLength) || + !content.ReadUint8LengthPrefixed(&t) || + !t.ReadBytes(&rawPublicName, len(t)) || + !content.ReadUint16LengthPrefixed(&t) || + !t.ReadBytes(&echCfg.RawExtensions, len(t)) || + !content.Empty() { + return errInvalidLen + } + echCfg.RawPublicName = string(rawPublicName) + + return nil +} + +var errInvalidLen = errors.New("invalid length") + +// marshalBinary writes this config to the cryptobyte builder. If there is an error, +// it will occur before any writes have happened. +func (echCfg echConfig) marshalBinary(b *cryptobyte.Builder) error { + pk, err := echCfg.PublicKey.MarshalBinary() + if err != nil { + return err + } + if l := len(echCfg.RawPublicName); l == 0 || l > 255 { + return fmt.Errorf("public name length (%d) must be in the range 1-255", l) + } + + b.AddUint16(echCfg.Version) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { // "length" field + b.AddUint8(echCfg.ConfigID) + b.AddUint16(uint16(echCfg.KEMID)) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(pk) + }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + for _, cs := range echCfg.CipherSuites { + b.AddUint16(uint16(cs.KDFID)) + b.AddUint16(uint16(cs.AEADID)) + } + }) + b.AddUint8(uint8(min(len(echCfg.RawPublicName)+16, 255))) + b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes([]byte(echCfg.RawPublicName)) + }) + b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) { + child.AddBytes(echCfg.RawExtensions) + }) + }) + + return nil +} + +type hpkeSymmetricCipherSuite struct { + KDFID hpke.KDF + AEADID hpke.AEAD +} + +type echConfigList []echConfig + +func (cl echConfigList) MarshalBinary() ([]byte, error) { + var b cryptobyte.Builder + var err error + + // the list's length prefixes the list, as with most opaque values + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + for _, cfg := range cl { + if err = cfg.marshalBinary(b); err != nil { + break + } + } + }) + if err != nil { + return nil, err + } + + return b.Bytes() +} + +func newECHConfigID(ctx caddy.Context) (uint8, error) { + // uint8 can be 0-255 inclusive + const uint8Range = 256 + + // avoid repeating storage checks + tried := make([]bool, uint8Range) + + // Try to find an available number with random rejection sampling; + // i.e. choose a random number and see if it's already taken. + // The hard limit on how many times we try to find an available + // number is flexible... in theory, assuming uniform distribution, + // 256 attempts should make each possible value show up exactly + // once, but obviously that won't be the case. We can try more + // times to try to ensure that every number gets a chance, which + // is especially useful if few are available, or we can lower it + // if we assume we should have found an available value by then + // and want to limit runtime; for now I choose the middle ground + // and just try as many times as there are possible values. + for i := 0; i < uint8Range && ctx.Err() == nil; i++ { + num := uint8(weakrand.N(uint8Range)) //nolint:gosec + + // don't try the same number a second time + if tried[num] { + continue + } + tried[num] = true + + // check to see if any of the subkeys use this config ID + numStr := strconv.Itoa(int(num)) + trialPath := path.Join(echConfigsKey, numStr) + if ctx.Storage().Exists(ctx, trialPath) { + continue + } + + return num, nil + } + + if err := ctx.Err(); err != nil { + return 0, err + } + + return 0, fmt.Errorf("depleted attempts to find an available config_id") +} + +// ECHPublisher is an interface for publishing ECHConfigList values +// so that they can be used by clients. +type ECHPublisher interface { + // Returns a key that is unique to this publisher and its configuration. + // A publisher's ID combined with its config is a valid key. + // It is used to prevent duplicating publications. + PublisherKey() string + + // Publishes the ECH config list (as binary) for the given innerNames. Some + // publishers may not need a list of inner/protected names, and can ignore the + // argument; most, however, will want to use it to know which inner names are + // to be associated with the given ECH config list. + // + // Implementations should return an error of type PublishECHConfigListErrors + // when relevant to key errors to their associated innerName, but should never + // return a non-nil PublishECHConfigListErrors when its length is 0. + PublishECHConfigList(ctx context.Context, innerNames []string, echConfigList []byte) error +} + +// PublishECHConfigListErrors is returned by ECHPublishers to describe one or more +// errors publishing an ECH config list from PublishECHConfigList. A non-nil, empty +// value of this type should never be returned. +// nolint:errname // The linter wants "Error" convention, but this is a multi-error type. +type PublishECHConfigListErrors map[string]error + +func (p PublishECHConfigListErrors) Error() string { + var sb strings.Builder + for innerName, err := range p { + if sb.Len() > 0 { + sb.WriteString("; ") + } + sb.WriteString(innerName) + sb.WriteString(": ") + sb.WriteString(err.Error()) + } + return sb.String() +} + +type echConfigMeta struct { + Created time.Time `json:"created"` + Publications publicationHistory `json:"publications"` +} + +// publicationHistory is a map of publisher key to +// map of inner name to timestamp +type publicationHistory map[string]map[string]time.Time + +// The key prefix when putting ECH configs in storage. After this +// comes the config ID. +const echConfigsKey = "ech/configs" + +// https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html +const draftTLSESNI22 = 0xfe0d + +// Interface guard +var _ ECHPublisher = (*ECHDNSPublisher)(nil) diff --git a/modules/caddytls/internalissuer.go b/modules/caddytls/internalissuer.go index b55757af5..be779757a 100644 --- a/modules/caddytls/internalissuer.go +++ b/modules/caddytls/internalissuer.go @@ -178,6 +178,9 @@ func (iss *InternalIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } iss.SignWithRoot = true + + default: + return d.Errf("unrecognized subdirective '%s'", d.Val()) } } return nil diff --git a/modules/caddytls/leaffileloader.go b/modules/caddytls/leaffileloader.go index 1d3f3a3e5..c2177ab55 100644 --- a/modules/caddytls/leaffileloader.go +++ b/modules/caddytls/leaffileloader.go @@ -21,6 +21,7 @@ import ( "os" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { @@ -32,6 +33,14 @@ type LeafFileLoader struct { Files []string `json:"files,omitempty"` } +// CaddyModule returns the Caddy module information. +func (LeafFileLoader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.leaf_cert_loader.file", + New: func() caddy.Module { return new(LeafFileLoader) }, + } +} + // Provision implements caddy.Provisioner. func (fl *LeafFileLoader) Provision(ctx caddy.Context) error { repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) @@ -44,12 +53,11 @@ func (fl *LeafFileLoader) Provision(ctx caddy.Context) error { return nil } -// CaddyModule returns the Caddy module information. -func (LeafFileLoader) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "tls.leaf_cert_loader.file", - New: func() caddy.Module { return new(LeafFileLoader) }, - } +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (fl *LeafFileLoader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.NextArg() + fl.Files = append(fl.Files, d.RemainingArgs()...) + return nil } // LoadLeafCertificates returns the certificates to be loaded by fl. @@ -96,4 +104,5 @@ func convertPEMFilesToDERBytes(filename string) ([]byte, error) { var ( _ LeafCertificateLoader = (*LeafFileLoader)(nil) _ caddy.Provisioner = (*LeafFileLoader)(nil) + _ caddyfile.Unmarshaler = (*LeafFileLoader)(nil) ) diff --git a/modules/caddytls/leaffolderloader.go b/modules/caddytls/leaffolderloader.go index 5c7b06e76..20f5aa82c 100644 --- a/modules/caddytls/leaffolderloader.go +++ b/modules/caddytls/leaffolderloader.go @@ -22,6 +22,7 @@ import ( "strings" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { @@ -55,6 +56,13 @@ func (fl *LeafFolderLoader) Provision(ctx caddy.Context) error { return nil } +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (fl *LeafFolderLoader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.NextArg() + fl.Folders = append(fl.Folders, d.RemainingArgs()...) + return nil +} + // LoadLeafCertificates loads all the leaf certificates in the directories // listed in fl from all files ending with .pem. func (fl LeafFolderLoader) LoadLeafCertificates() ([]*x509.Certificate, error) { @@ -94,4 +102,5 @@ func (fl LeafFolderLoader) LoadLeafCertificates() ([]*x509.Certificate, error) { var ( _ LeafCertificateLoader = (*LeafFolderLoader)(nil) _ caddy.Provisioner = (*LeafFolderLoader)(nil) + _ caddyfile.Unmarshaler = (*LeafFolderLoader)(nil) ) diff --git a/modules/caddytls/leafpemloader.go b/modules/caddytls/leafpemloader.go index 28467ccf2..de24bcc9f 100644 --- a/modules/caddytls/leafpemloader.go +++ b/modules/caddytls/leafpemloader.go @@ -19,6 +19,7 @@ import ( "fmt" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { @@ -52,6 +53,13 @@ func (LeafPEMLoader) CaddyModule() caddy.ModuleInfo { } } +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (fl *LeafPEMLoader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.NextArg() + fl.Certificates = append(fl.Certificates, d.RemainingArgs()...) + return nil +} + // LoadLeafCertificates returns the certificates contained in pl. func (pl LeafPEMLoader) LoadLeafCertificates() ([]*x509.Certificate, error) { certs := make([]*x509.Certificate, 0, len(pl.Certificates)) diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index a74d6ea80..dfbec94cc 100644 --- a/modules/caddytls/matchers.go +++ b/modules/caddytls/matchers.go @@ -15,6 +15,7 @@ package caddytls import ( + "context" "crypto/tls" "fmt" "net" @@ -55,7 +56,7 @@ func (MatchServerName) CaddyModule() caddy.ModuleInfo { // Match matches hello based on SNI. func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool { - repl := caddy.NewReplacer() + var repl *caddy.Replacer // caddytls.TestServerNameMatcher calls this function without any context if ctx := hello.Context(); ctx != nil { // In some situations the existing context may have no replacer @@ -64,6 +65,10 @@ func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool { } } + if repl == nil { + repl = caddy.NewReplacer() + } + for _, name := range m { rs := repl.ReplaceAll(name, "") if certmagic.MatchWildcard(hello.ServerName, rs) { @@ -224,15 +229,28 @@ func (MatchServerNameRE) CaddyModule() caddy.ModuleInfo { // Match matches hello based on SNI using a regular expression. func (m MatchServerNameRE) Match(hello *tls.ClientHelloInfo) bool { - repl := caddy.NewReplacer() - // caddytls.TestServerNameMatcher calls this function without any context - if ctx := hello.Context(); ctx != nil { + // Note: caddytls.TestServerNameMatcher calls this function without any context + ctx := hello.Context() + if ctx == nil { + // layer4.Connection implements GetContext() to pass its context here, + // since hello.Context() returns nil + if mayHaveContext, ok := hello.Conn.(interface{ GetContext() context.Context }); ok { + ctx = mayHaveContext.GetContext() + } + } + + var repl *caddy.Replacer + if ctx != nil { // In some situations the existing context may have no replacer if replAny := ctx.Value(caddy.ReplacerCtxKey); replAny != nil { repl = replAny.(*caddy.Replacer) } } + if repl == nil { + repl = caddy.NewReplacer() + } + return m.MatchRegexp.Match(hello.ServerName, repl) } diff --git a/modules/caddytls/ondemand.go b/modules/caddytls/ondemand.go index 89abfe03f..0970234ce 100644 --- a/modules/caddytls/ondemand.go +++ b/modules/caddytls/ondemand.go @@ -38,12 +38,11 @@ func init() { // OnDemandConfig configures on-demand TLS, for obtaining // needed certificates at handshake-time. Because this -// feature can easily be abused, you should use this to -// establish rate limits and/or an internal endpoint that -// Caddy can "ask" if it should be allowed to manage -// certificates for a given hostname. +// feature can easily be abused, Caddy must ask permission +// to your application whether a particular domain is allowed +// to have a certificate issued for it. type OnDemandConfig struct { - // DEPRECATED. WILL BE REMOVED SOON. Use 'permission' instead. + // Deprecated. WILL BE REMOVED SOON. Use 'permission' instead with the `http` module. Ask string `json:"ask,omitempty"` // REQUIRED. A module that will determine whether a @@ -51,25 +50,6 @@ type OnDemandConfig struct { // or obtained from an issuer on demand. PermissionRaw json.RawMessage `json:"permission,omitempty" caddy:"namespace=tls.permission inline_key=module"` permission OnDemandPermission - - // DEPRECATED. An optional rate limit to throttle - // the checking of storage and the issuance of - // certificates from handshakes if not already in - // storage. WILL BE REMOVED IN A FUTURE RELEASE. - RateLimit *RateLimit `json:"rate_limit,omitempty"` -} - -// DEPRECATED. WILL LIKELY BE REMOVED SOON. -// Instead of using this rate limiter, use a proper tool such as a -// level 3 or 4 firewall and/or a permission module to apply rate limits. -type RateLimit struct { - // A duration value. Storage may be checked and a certificate may be - // obtained 'burst' times during this interval. - Interval caddy.Duration `json:"interval,omitempty"` - - // How many times during an interval storage can be checked or a - // certificate can be obtained. - Burst int `json:"burst,omitempty"` } // OnDemandPermission is a type that can give permission for @@ -195,8 +175,7 @@ var ErrPermissionDenied = errors.New("certificate not allowed by permission modu // These perpetual values are used for on-demand TLS. var ( - onDemandRateLimiter = certmagic.NewRateLimiter(0, 0) - onDemandAskClient = &http.Client{ + onDemandAskClient = &http.Client{ Timeout: 10 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return fmt.Errorf("following http redirects is not allowed") diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index f04beb2ee..7b49c0208 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -20,16 +20,20 @@ import ( "encoding/json" "fmt" "log" + "net" "net/http" "runtime/debug" + "strings" "sync" "time" "github.com/caddyserver/certmagic" + "github.com/libdns/libdns" "go.uber.org/zap" "go.uber.org/zap/zapcore" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/internal" "github.com/caddyserver/caddy/v2/modules/caddyevents" ) @@ -52,8 +56,10 @@ type TLS struct { // // The "automate" certificate loader module can be used to // specify a list of subjects that need certificates to be - // managed automatically. The first matching automation - // policy will be applied to manage the certificate(s). + // managed automatically, including subdomains that may + // already be covered by a managed wildcard certificate. + // The first matching automation policy will be used + // to manage automated certificate(s). // // All loaded certificates get pooled // into the same cache and may be used to complete TLS @@ -79,6 +85,7 @@ type TLS struct { // Disabling OCSP stapling puts clients at greater risk, reduces their // privacy, and usually lowers client performance. It is NOT recommended // to disable this unless you are able to justify the costs. + // // EXPERIMENTAL. Subject to change. DisableOCSPStapling bool `json:"disable_ocsp_stapling,omitempty"` @@ -89,17 +96,46 @@ type TLS struct { // // Disabling these checks should only be done when the storage // can be trusted to have enough capacity and no other problems. + // // EXPERIMENTAL. Subject to change. DisableStorageCheck bool `json:"disable_storage_check,omitempty"` + // Disables the automatic cleanup of the storage backend. + // This is useful when TLS is not being used to store certificates + // and the user wants run their server in a read-only mode. + // + // Storage cleaning creates two files: instance.uuid and last_clean.json. + // The instance.uuid file is used to identify the instance of Caddy + // in a cluster. The last_clean.json file is used to store the last + // time the storage was cleaned. + // + // EXPERIMENTAL. Subject to change. + DisableStorageClean bool `json:"disable_storage_clean,omitempty"` + + // Enable Encrypted ClientHello (ECH). ECH protects the server name + // (SNI) and other sensitive parameters of a normally-plaintext TLS + // ClientHello during a handshake. + // + // EXPERIMENTAL: Subject to change. + EncryptedClientHello *ECH `json:"encrypted_client_hello,omitempty"` + + // The default DNS provider module to use when a DNS module is needed. + // + // EXPERIMENTAL: Subject to change. + DNSRaw json.RawMessage `json:"dns,omitempty" caddy:"namespace=dns.providers inline_key=name"` + dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.) + certificateLoaders []CertificateLoader - automateNames []string + automateNames map[string]struct{} ctx caddy.Context storageCleanTicker *time.Ticker storageCleanStop chan struct{} logger *zap.Logger events *caddyevents.App + serverNames map[string]struct{} + serverNamesMu *sync.Mutex + // set of subjects with managed certificates, // and hashes of manually-loaded certificates // (managing's value is an optional issuer key, for distinction) @@ -125,6 +161,29 @@ func (t *TLS) Provision(ctx caddy.Context) error { t.logger = ctx.Logger() repl := caddy.NewReplacer() t.managing, t.loaded = make(map[string]string), make(map[string]string) + t.serverNames = make(map[string]struct{}) + t.serverNamesMu = new(sync.Mutex) + + // set up default DNS module, if any, and make sure it implements all the + // common libdns interfaces, since it could be used for a variety of things + // (do this before provisioning other modules, since they may rely on this) + if len(t.DNSRaw) > 0 { + dnsMod, err := ctx.LoadModule(t, "DNSRaw") + if err != nil { + return fmt.Errorf("loading overall DNS provider module: %v", err) + } + switch dnsMod.(type) { + case interface { + libdns.RecordAppender + libdns.RecordDeleter + libdns.RecordGetter + libdns.RecordSetter + }: + default: + return fmt.Errorf("DNS module does not implement the most common libdns interfaces: %T", dnsMod) + } + t.dns = dnsMod + } // set up a new certificate cache; this (re)loads all certificates cacheOpts := certmagic.CacheOptions{ @@ -162,12 +221,13 @@ func (t *TLS) Provision(ctx caddy.Context) error { // special case; these will be loaded in later using our automation facilities, // which we want to avoid doing during provisioning if automateNames, ok := modIface.(*AutomateLoader); ok && automateNames != nil { - repl := caddy.NewReplacer() - subjects := make([]string, len(*automateNames)) - for i, sub := range *automateNames { - subjects[i] = repl.ReplaceAll(sub, "") + if t.automateNames == nil { + t.automateNames = make(map[string]struct{}) + } + repl := caddy.NewReplacer() + for _, sub := range *automateNames { + t.automateNames[repl.ReplaceAll(sub, "")] = struct{}{} } - t.automateNames = subjects } else { return fmt.Errorf("loading certificates with 'automate' requires array of strings, got: %T", modIface) } @@ -176,85 +236,8 @@ func (t *TLS) Provision(ctx caddy.Context) error { t.certificateLoaders = append(t.certificateLoaders, modIface.(CertificateLoader)) } - // on-demand permission module - if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.PermissionRaw != nil { - if t.Automation.OnDemand.Ask != "" { - return fmt.Errorf("on-demand TLS config conflict: both 'ask' endpoint and a 'permission' module are specified; 'ask' is deprecated, so use only the permission module") - } - val, err := ctx.LoadModule(t.Automation.OnDemand, "PermissionRaw") - if err != nil { - return fmt.Errorf("loading on-demand TLS permission module: %v", err) - } - t.Automation.OnDemand.permission = val.(OnDemandPermission) - } - - // on-demand rate limiting (TODO: deprecated, and should be removed later; rate limiting is ineffective now that permission modules are required) - if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.RateLimit != nil { - t.logger.Warn("DEPRECATED: on_demand.rate_limit will be removed in a future release; use permission modules or external certificate managers instead") - onDemandRateLimiter.SetMaxEvents(t.Automation.OnDemand.RateLimit.Burst) - onDemandRateLimiter.SetWindow(time.Duration(t.Automation.OnDemand.RateLimit.Interval)) - } else { - // remove any existing rate limiter - onDemandRateLimiter.SetWindow(0) - onDemandRateLimiter.SetMaxEvents(0) - } - - // run replacer on ask URL (for environment variables) -- return errors to prevent surprises (#5036) - if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.Ask != "" { - t.Automation.OnDemand.Ask, err = repl.ReplaceOrErr(t.Automation.OnDemand.Ask, true, true) - if err != nil { - return fmt.Errorf("preparing 'ask' endpoint: %v", err) - } - perm := PermissionByHTTP{ - Endpoint: t.Automation.OnDemand.Ask, - } - if err := perm.Provision(ctx); err != nil { - return fmt.Errorf("provisioning 'ask' module: %v", err) - } - t.Automation.OnDemand.permission = perm - } - - // automation/management policies - if t.Automation == nil { - t.Automation = new(AutomationConfig) - } - t.Automation.defaultPublicAutomationPolicy = new(AutomationPolicy) - err = t.Automation.defaultPublicAutomationPolicy.Provision(t) - if err != nil { - return fmt.Errorf("provisioning default public automation policy: %v", err) - } - for _, n := range t.automateNames { - // if any names specified by the "automate" loader do not qualify for a public - // certificate, we should initialize a default internal automation policy - // (but we don't want to do this unnecessarily, since it may prompt for password!) - if certmagic.SubjectQualifiesForPublicCert(n) { - continue - } - t.Automation.defaultInternalAutomationPolicy = &AutomationPolicy{ - IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)}, - } - err = t.Automation.defaultInternalAutomationPolicy.Provision(t) - if err != nil { - return fmt.Errorf("provisioning default internal automation policy: %v", err) - } - break - } - for i, ap := range t.Automation.Policies { - err := ap.Provision(t) - if err != nil { - return fmt.Errorf("provisioning automation policy %d: %v", i, err) - } - } - - // session ticket ephemeral keys (STEK) service and provider - if t.SessionTickets != nil { - err := t.SessionTickets.provision(ctx) - if err != nil { - return fmt.Errorf("provisioning session tickets configuration: %v", err) - } - } - - // load manual/static (unmanaged) certificates - we do this in + // using the certificate loaders we just initialized, load + // manual/static (unmanaged) certificates - we do this in // provision so that other apps (such as http) can know which // certificates have been manually loaded, and also so that // commands like validate can be a better test @@ -283,6 +266,95 @@ func (t *TLS) Provision(ctx caddy.Context) error { } } + // on-demand permission module + if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.PermissionRaw != nil { + if t.Automation.OnDemand.Ask != "" { + return fmt.Errorf("on-demand TLS config conflict: both 'ask' endpoint and a 'permission' module are specified; 'ask' is deprecated, so use only the permission module") + } + val, err := ctx.LoadModule(t.Automation.OnDemand, "PermissionRaw") + if err != nil { + return fmt.Errorf("loading on-demand TLS permission module: %v", err) + } + t.Automation.OnDemand.permission = val.(OnDemandPermission) + } + + // automation/management policies + if t.Automation == nil { + t.Automation = new(AutomationConfig) + } + t.Automation.defaultPublicAutomationPolicy = new(AutomationPolicy) + err = t.Automation.defaultPublicAutomationPolicy.Provision(t) + if err != nil { + return fmt.Errorf("provisioning default public automation policy: %v", err) + } + for n := range t.automateNames { + // if any names specified by the "automate" loader do not qualify for a public + // certificate, we should initialize a default internal automation policy + // (but we don't want to do this unnecessarily, since it may prompt for password!) + if certmagic.SubjectQualifiesForPublicCert(n) { + continue + } + t.Automation.defaultInternalAutomationPolicy = &AutomationPolicy{ + IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)}, + } + err = t.Automation.defaultInternalAutomationPolicy.Provision(t) + if err != nil { + return fmt.Errorf("provisioning default internal automation policy: %v", err) + } + break + } + for i, ap := range t.Automation.Policies { + err := ap.Provision(t) + if err != nil { + return fmt.Errorf("provisioning automation policy %d: %v", i, err) + } + } + + // run replacer on ask URL (for environment variables) -- return errors to prevent surprises (#5036) + if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.Ask != "" { + t.Automation.OnDemand.Ask, err = repl.ReplaceOrErr(t.Automation.OnDemand.Ask, true, true) + if err != nil { + return fmt.Errorf("preparing 'ask' endpoint: %v", err) + } + perm := PermissionByHTTP{ + Endpoint: t.Automation.OnDemand.Ask, + } + if err := perm.Provision(ctx); err != nil { + return fmt.Errorf("provisioning 'ask' module: %v", err) + } + t.Automation.OnDemand.permission = perm + } + + // session ticket ephemeral keys (STEK) service and provider + if t.SessionTickets != nil { + err := t.SessionTickets.provision(ctx) + if err != nil { + return fmt.Errorf("provisioning session tickets configuration: %v", err) + } + } + + // ECH (Encrypted ClientHello) initialization + if t.EncryptedClientHello != nil { + t.EncryptedClientHello.configs = make(map[string][]echConfig) + outerNames, err := t.EncryptedClientHello.Provision(ctx) + if err != nil { + return fmt.Errorf("provisioning Encrypted ClientHello components: %v", err) + } + + // outer names should have certificates to reduce client brittleness + for _, outerName := range outerNames { + if outerName == "" { + continue + } + if !t.HasCertificateForSubject(outerName) { + if t.automateNames == nil { + t.automateNames = make(map[string]struct{}) + } + t.automateNames[outerName] = struct{}{} + } + } + } + return nil } @@ -339,7 +411,21 @@ func (t *TLS) Start() error { return fmt.Errorf("automate: managing %v: %v", t.automateNames, err) } - t.keepStorageClean() + // publish ECH configs in the background; does not need to block + // server startup, as it could take a while + if t.EncryptedClientHello != nil { + go func() { + if err := t.publishECHConfigs(); err != nil { + t.logger.Named("ech").Error("publication(s) failed", zap.Error(err)) + } + }() + } + + if !t.DisableStorageClean { + // start the storage cleaner goroutine and ticker, + // which cleans out expired certificates and more + t.keepStorageClean() + } return nil } @@ -373,7 +459,8 @@ func (t *TLS) Cleanup() error { // app instance (which is being stopped) that are not managed or loaded by the // new app instance (which just started), and remove them from the cache var noLongerManaged []certmagic.SubjectIssuer - var reManage, noLongerLoaded []string + var noLongerLoaded []string + reManage := make(map[string]struct{}) for subj, currentIssuerKey := range t.managing { // It's a bit nuanced: managed certs can sometimes be different enough that we have to // swap them out for a different one, even if they are for the same subject/domain. @@ -391,7 +478,7 @@ func (t *TLS) Cleanup() error { // then, if the next app is managing a cert for this name, but with a different issuer, re-manage it if ok && nextIssuerKey != currentIssuerKey { - reManage = append(reManage, subj) + reManage[subj] = struct{}{} } } } @@ -412,33 +499,52 @@ func (t *TLS) Cleanup() error { if err := nextTLSApp.Manage(reManage); err != nil { if c := t.logger.Check(zapcore.ErrorLevel, "re-managing unloaded certificates with new config"); c != nil { c.Write( - zap.Strings("subjects", reManage), + zap.Strings("subjects", internal.MaxSizeSubjectsListForLog(reManage, 1000)), zap.Error(err), ) } } } else { - // no more TLS app running, so delete in-memory cert cache - certCache.Stop() - certCacheMu.Lock() - certCache = nil - certCacheMu.Unlock() + // no more TLS app running, so delete in-memory cert cache, if it was created yet + certCacheMu.RLock() + hasCache := certCache != nil + certCacheMu.RUnlock() + if hasCache { + certCache.Stop() + certCacheMu.Lock() + certCache = nil + certCacheMu.Unlock() + } } return nil } -// Manage immediately begins managing names according to the -// matching automation policy. -func (t *TLS) Manage(names []string) error { +// Manage immediately begins managing subjects according to the +// matching automation policy. The subjects are given in a map +// to prevent duplication and also because quick lookups are +// needed to assess wildcard coverage, if any, depending on +// certain config parameters (with lots of subjects, computing +// wildcard coverage over a slice can be highly inefficient). +func (t *TLS) Manage(subjects map[string]struct{}) error { // for a large number of names, we can be more memory-efficient // by making only one certmagic.Config for all the names that // use that config, rather than calling ManageAsync once for // every name; so first, bin names by AutomationPolicy policyToNames := make(map[*AutomationPolicy][]string) - for _, name := range names { - ap := t.getAutomationPolicyForName(name) - policyToNames[ap] = append(policyToNames[ap], name) + for subj := range subjects { + ap := t.getAutomationPolicyForName(subj) + // by default, if a wildcard that covers the subj is also being + // managed, either by a previous call to Manage or by this one, + // prefer using that over individual certs for its subdomains; + // but users can disable this and force getting a certificate for + // subdomains by adding the name to the 'automate' cert loader + if t.managingWildcardFor(subj, subjects) { + if _, ok := t.automateNames[subj]; !ok { + continue + } + } + policyToNames[ap] = append(policyToNames[ap], subj) } // now that names are grouped by policy, we can simply make one @@ -449,7 +555,7 @@ func (t *TLS) Manage(names []string) error { if err != nil { const maxNamesToDisplay = 100 if len(names) > maxNamesToDisplay { - names = append(names[:maxNamesToDisplay], fmt.Sprintf("(%d more...)", len(names)-maxNamesToDisplay)) + names = append(names[:maxNamesToDisplay], fmt.Sprintf("(and %d more...)", len(names)-maxNamesToDisplay)) } return fmt.Errorf("automate: manage %v: %v", names, err) } @@ -474,6 +580,66 @@ func (t *TLS) Manage(names []string) error { return nil } +// managingWildcardFor returns true if the app is managing a certificate that covers that +// subject name (including consideration of wildcards), either from its internal list of +// names that it IS managing certs for, or from the otherSubjsToManage which includes names +// that WILL be managed. +func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]struct{}) bool { + // TODO: we could also consider manually-loaded certs using t.HasCertificateForSubject(), + // but that does not account for how manually-loaded certs may be restricted as to which + // hostnames or ClientHellos they can be used with by tags, etc; I don't *think* anyone + // necessarily wants this anyway, but I thought I'd note this here for now (if we did + // consider manually-loaded certs, we'd probably want to rename the method since it + // wouldn't be just about managed certs anymore) + + // IP addresses must match exactly + if ip := net.ParseIP(subj); ip != nil { + _, managing := t.managing[subj] + return managing + } + + // replace labels of the domain with wildcards until we get a match + labels := strings.Split(subj, ".") + for i := range labels { + if labels[i] == "*" { + continue + } + labels[i] = "*" + candidate := strings.Join(labels, ".") + if _, ok := t.managing[candidate]; ok { + return true + } + if _, ok := otherSubjsToManage[candidate]; ok { + return true + } + } + + return false +} + +// RegisterServerNames registers the provided DNS names with the TLS app. +// This is currently used to auto-publish Encrypted ClientHello (ECH) +// configurations, if enabled. Use of this function by apps using the TLS +// app removes the need for the user to redundantly specify domain names +// in their configuration. This function separates hostname and port +// (keeping only the hotsname) and filters IP addresses, which can't be +// used with ECH. +// +// EXPERIMENTAL: This function and its semantics/behavior are subject to change. +func (t *TLS) RegisterServerNames(dnsNames []string) { + t.serverNamesMu.Lock() + for _, name := range dnsNames { + host, _, err := net.SplitHostPort(name) + if err != nil { + host = name + } + if strings.TrimSpace(host) != "" && !certmagic.SubjectIsIP(host) { + t.serverNames[strings.ToLower(host)] = struct{}{} + } + } + t.serverNamesMu.Unlock() +} + // HandleHTTPChallenge ensures that the ACME HTTP challenge or ZeroSSL HTTP // validation request is handled for the certificate named by r.Host, if it // is an HTTP challenge request. It requires that the automation policy for diff --git a/modules/caddytls/values.go b/modules/caddytls/values.go index 20fe45ff8..3198ffa04 100644 --- a/modules/caddytls/values.go +++ b/modules/caddytls/values.go @@ -81,13 +81,15 @@ func getOptimalDefaultCipherSuites() []uint16 { return defaultCipherSuitesWithoutAESNI } -// SupportedCurves is the unordered map of supported curves. +// SupportedCurves is the unordered map of supported curves +// or key exchange mechanisms ("curves" traditionally). // https://golang.org/pkg/crypto/tls/#CurveID var SupportedCurves = map[string]tls.CurveID{ - "x25519": tls.X25519, - "secp256r1": tls.CurveP256, - "secp384r1": tls.CurveP384, - "secp521r1": tls.CurveP521, + "x25519mlkem768": tls.X25519MLKEM768, + "x25519": tls.X25519, + "secp256r1": tls.CurveP256, + "secp384r1": tls.CurveP384, + "secp521r1": tls.CurveP521, } // supportedCertKeyTypes is all the key types that are supported @@ -100,20 +102,16 @@ var supportedCertKeyTypes = map[string]certmagic.KeyType{ "ed25519": certmagic.ED25519, } -// defaultCurves is the list of only the curves we want to use -// by default, in descending order of preference. +// defaultCurves is the list of only the curves or key exchange +// mechanisms we want to use by default. The order is irrelevant. // -// This list should only include curves which are fast by design -// (e.g. X25519) and those for which an optimized assembly +// This list should only include mechanisms which are fast by +// design (e.g. X25519) and those for which an optimized assembly // implementation exists (e.g. P256). The latter ones can be // found here: // https://github.com/golang/go/tree/master/src/crypto/elliptic -// -// Temporily we ignore these default, to take advantage of X25519Kyber768 -// in Go's defaults (X25519Kyber768, X25519, P-256, P-384, P-521), which -// isn't exported. See https://github.com/caddyserver/caddy/issues/6540 -// nolint:unused var defaultCurves = []tls.CurveID{ + tls.X25519MLKEM768, tls.X25519, tls.CurveP256, } diff --git a/modules/internal/network/networkproxy.go b/modules/internal/network/networkproxy.go new file mode 100644 index 000000000..f9deeb43a --- /dev/null +++ b/modules/internal/network/networkproxy.go @@ -0,0 +1,144 @@ +package network + +import ( + "errors" + "net/http" + "net/url" + "strings" + + "go.uber.org/zap" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" +) + +func init() { + caddy.RegisterModule(ProxyFromURL{}) + caddy.RegisterModule(ProxyFromNone{}) +} + +// The "url" proxy source uses the defined URL as the proxy +type ProxyFromURL struct { + URL string `json:"url"` + + ctx caddy.Context + logger *zap.Logger +} + +// CaddyModule implements Module. +func (p ProxyFromURL) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "caddy.network_proxy.url", + New: func() caddy.Module { + return &ProxyFromURL{} + }, + } +} + +func (p *ProxyFromURL) Provision(ctx caddy.Context) error { + p.ctx = ctx + p.logger = ctx.Logger() + return nil +} + +// Validate implements Validator. +func (p ProxyFromURL) Validate() error { + if _, err := url.Parse(p.URL); err != nil { + return err + } + return nil +} + +// ProxyFunc implements ProxyFuncProducer. +func (p ProxyFromURL) ProxyFunc() func(*http.Request) (*url.URL, error) { + if strings.Contains(p.URL, "{") && strings.Contains(p.URL, "}") { + // courtesy of @ImpostorKeanu: https://github.com/caddyserver/caddy/pull/6397 + return func(r *http.Request) (*url.URL, error) { + // retrieve the replacer from context. + repl, ok := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + err := errors.New("failed to obtain replacer from request") + p.logger.Error(err.Error()) + return nil, err + } + + // apply placeholders to the value + // note: h.ForwardProxyURL should never be empty at this point + s := repl.ReplaceAll(p.URL, "") + if s == "" { + p.logger.Error("network_proxy URL was empty after applying placeholders", + zap.String("initial_value", p.URL), + zap.String("final_value", s), + zap.String("hint", "check for invalid placeholders")) + return nil, errors.New("empty value for network_proxy URL") + } + + // parse the url + pUrl, err := url.Parse(s) + if err != nil { + p.logger.Warn("failed to derive transport proxy from network_proxy URL") + pUrl = nil + } else if pUrl.Host == "" || strings.Split("", pUrl.Host)[0] == ":" { + // url.Parse does not return an error on these values: + // + // - http://:80 + // - pUrl.Host == ":80" + // - /some/path + // - pUrl.Host == "" + // + // Super edge cases, but humans are human. + err = errors.New("supplied network_proxy URL is missing a host value") + pUrl = nil + } else { + p.logger.Debug("setting transport proxy url", zap.String("url", s)) + } + + return pUrl, err + } + } + return func(r *http.Request) (*url.URL, error) { + return url.Parse(p.URL) + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (p *ProxyFromURL) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() + d.Next() + p.URL = d.Val() + return nil +} + +// The "none" proxy source module disables the use of network proxy. +type ProxyFromNone struct{} + +func (p ProxyFromNone) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "caddy.network_proxy.none", + New: func() caddy.Module { + return &ProxyFromNone{} + }, + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (p ProxyFromNone) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + return nil +} + +// ProxyFunc implements ProxyFuncProducer. +func (p ProxyFromNone) ProxyFunc() func(*http.Request) (*url.URL, error) { + return nil +} + +var ( + _ caddy.Module = ProxyFromURL{} + _ caddy.Provisioner = (*ProxyFromURL)(nil) + _ caddy.Validator = ProxyFromURL{} + _ caddy.ProxyFuncProducer = ProxyFromURL{} + _ caddyfile.Unmarshaler = (*ProxyFromURL)(nil) + + _ caddy.Module = ProxyFromNone{} + _ caddy.ProxyFuncProducer = ProxyFromNone{} + _ caddyfile.Unmarshaler = ProxyFromNone{} +) diff --git a/modules/logging/filewriter.go b/modules/logging/filewriter.go index 44c0feb67..0999bbfb2 100644 --- a/modules/logging/filewriter.go +++ b/modules/logging/filewriter.go @@ -133,7 +133,7 @@ func (fw *FileWriter) Provision(ctx caddy.Context) error { } func (fw FileWriter) String() string { - fpath, err := filepath.Abs(fw.Filename) + fpath, err := caddy.FastAbs(fw.Filename) if err == nil { return fpath } @@ -147,51 +147,68 @@ func (fw FileWriter) WriterKey() string { // OpenWriter opens a new file writer. func (fw FileWriter) OpenWriter() (io.WriteCloser, error) { - if fw.Mode == 0 { - fw.Mode = 0o600 + modeIfCreating := os.FileMode(fw.Mode) + if modeIfCreating == 0 { + modeIfCreating = 0o600 } - // roll log files by default - if fw.Roll == nil || *fw.Roll { - if fw.RollSizeMB == 0 { - fw.RollSizeMB = 100 - } - if fw.RollCompress == nil { - compress := true - fw.RollCompress = &compress - } - if fw.RollKeep == 0 { - fw.RollKeep = 10 - } - if fw.RollKeepDays == 0 { - fw.RollKeepDays = 90 - } + // roll log files as a sensible default to avoid disk space exhaustion + roll := fw.Roll == nil || *fw.Roll - // create the file if it does not exist with the right mode. - // lumberjack will reuse the file mode across log rotation. - f_tmp, err := os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(fw.Mode)) + // create the file if it does not exist; create with the configured mode, or default + // to restrictive if not set. (lumberjack will reuse the file mode across log rotation) + if err := os.MkdirAll(filepath.Dir(fw.Filename), 0o700); err != nil { + return nil, err + } + file, err := os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, modeIfCreating) + if err != nil { + return nil, err + } + info, err := file.Stat() + if roll { + file.Close() // lumberjack will reopen it on its own + } + + // Ensure already existing files have the right mode, since OpenFile will not set the mode in such case. + if configuredMode := os.FileMode(fw.Mode); configuredMode != 0 { if err != nil { - return nil, err + return nil, fmt.Errorf("unable to stat log file to see if we need to set permissions: %v", err) } - f_tmp.Close() - // ensure already existing files have the right mode, - // since OpenFile will not set the mode in such case. - if err = os.Chmod(fw.Filename, os.FileMode(fw.Mode)); err != nil { - return nil, err + // only chmod if the configured mode is different + if info.Mode()&os.ModePerm != configuredMode&os.ModePerm { + if err = os.Chmod(fw.Filename, configuredMode); err != nil { + return nil, err + } } - - return &lumberjack.Logger{ - Filename: fw.Filename, - MaxSize: fw.RollSizeMB, - MaxAge: fw.RollKeepDays, - MaxBackups: fw.RollKeep, - LocalTime: fw.RollLocalTime, - Compress: *fw.RollCompress, - }, nil } - // otherwise just open a regular file - return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(fw.Mode)) + // if not rolling, then the plain file handle is all we need + if !roll { + return file, nil + } + + // otherwise, return a rolling log + if fw.RollSizeMB == 0 { + fw.RollSizeMB = 100 + } + if fw.RollCompress == nil { + compress := true + fw.RollCompress = &compress + } + if fw.RollKeep == 0 { + fw.RollKeep = 10 + } + if fw.RollKeepDays == 0 { + fw.RollKeepDays = 90 + } + return &lumberjack.Logger{ + Filename: fw.Filename, + MaxSize: fw.RollSizeMB, + MaxAge: fw.RollKeepDays, + MaxBackups: fw.RollKeep, + LocalTime: fw.RollLocalTime, + Compress: *fw.RollCompress, + }, nil } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: @@ -296,6 +313,9 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.Errf("negative roll_keep_for duration: %v", keepFor) } fw.RollKeepDays = int(math.Ceil(keepFor.Hours() / 24)) + + default: + return d.Errf("unrecognized subdirective '%s'", d.Val()) } } return nil diff --git a/modules/logging/filewriter_test.go b/modules/logging/filewriter_test.go index 0c54a6590..2a246156c 100644 --- a/modules/logging/filewriter_test.go +++ b/modules/logging/filewriter_test.go @@ -20,6 +20,7 @@ import ( "encoding/json" "os" "path" + "path/filepath" "syscall" "testing" @@ -77,7 +78,7 @@ func TestFileCreationMode(t *testing.T) { t.Fatalf("failed to create tempdir: %v", err) } defer os.RemoveAll(dir) - fpath := path.Join(dir, "test.log") + fpath := filepath.Join(dir, "test.log") tt.fw.Filename = fpath logger, err := tt.fw.OpenWriter() @@ -92,7 +93,7 @@ func TestFileCreationMode(t *testing.T) { } if st.Mode() != tt.wantMode { - t.Errorf("file mode is %v, want %v", st.Mode(), tt.wantMode) + t.Errorf("%s: file mode is %v, want %v", tt.name, st.Mode(), tt.wantMode) } }) } @@ -316,7 +317,7 @@ func TestFileModeToJSON(t *testing.T) { }{ { name: "none zero", - mode: 0644, + mode: 0o644, want: `"0644"`, wantErr: false, }, @@ -357,7 +358,7 @@ func TestFileModeModification(t *testing.T) { defer os.RemoveAll(dir) fpath := path.Join(dir, "test.log") - f_tmp, err := os.OpenFile(fpath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(0600)) + f_tmp, err := os.OpenFile(fpath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(0o600)) if err != nil { t.Fatalf("failed to create test file: %v", err) } diff --git a/modules/logging/filters.go b/modules/logging/filters.go index 79d908fca..4c74bb95b 100644 --- a/modules/logging/filters.go +++ b/modules/logging/filters.go @@ -255,7 +255,7 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field { func (m IPMaskFilter) mask(s string) string { output := "" - for _, value := range strings.Split(s, ",") { + for value := range strings.SplitSeq(s, ",") { value = strings.TrimSpace(value) host, port, err := net.SplitHostPort(value) if err != nil { diff --git a/modules/logging/filters_test.go b/modules/logging/filters_test.go index 8f7ba0d70..a929617d7 100644 --- a/modules/logging/filters_test.go +++ b/modules/logging/filters_test.go @@ -3,9 +3,10 @@ package logging import ( "testing" + "go.uber.org/zap/zapcore" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" - "go.uber.org/zap/zapcore" ) func TestIPMaskSingleValue(t *testing.T) { diff --git a/modules/logging/netwriter.go b/modules/logging/netwriter.go index dc2b0922c..7d8481e3c 100644 --- a/modules/logging/netwriter.go +++ b/modules/logging/netwriter.go @@ -145,6 +145,9 @@ func (nw *NetWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } nw.SoftStart = true + + default: + return d.Errf("unrecognized subdirective '%s'", d.Val()) } } return nil diff --git a/modules/metrics/adminmetrics.go b/modules/metrics/adminmetrics.go index 1cf398e4c..1e3a841dd 100644 --- a/modules/metrics/adminmetrics.go +++ b/modules/metrics/adminmetrics.go @@ -15,8 +15,11 @@ package metrics import ( + "errors" "net/http" + "github.com/prometheus/client_golang/prometheus" + "github.com/caddyserver/caddy/v2" ) @@ -29,7 +32,11 @@ func init() { // is permanently mounted to the admin API endpoint at "/metrics". // See the Metrics module for a configurable endpoint that is usable if the // Admin API is disabled. -type AdminMetrics struct{} +type AdminMetrics struct { + registry *prometheus.Registry + + metricsHandler http.Handler +} // CaddyModule returns the Caddy module information. func (AdminMetrics) CaddyModule() caddy.ModuleInfo { @@ -39,17 +46,28 @@ func (AdminMetrics) CaddyModule() caddy.ModuleInfo { } } +// Provision - +func (m *AdminMetrics) Provision(ctx caddy.Context) error { + m.registry = ctx.GetMetricsRegistry() + if m.registry == nil { + return errors.New("no metrics registry found") + } + m.metricsHandler = createMetricsHandler(nil, false, m.registry) + return nil +} + // Routes returns a route for the /metrics endpoint. func (m *AdminMetrics) Routes() []caddy.AdminRoute { - metricsHandler := createMetricsHandler(nil, false) - h := caddy.AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error { - metricsHandler.ServeHTTP(w, r) - return nil - }) - return []caddy.AdminRoute{{Pattern: "/metrics", Handler: h}} + return []caddy.AdminRoute{{Pattern: "/metrics", Handler: caddy.AdminHandlerFunc(m.serveHTTP)}} +} + +func (m *AdminMetrics) serveHTTP(w http.ResponseWriter, r *http.Request) error { + m.metricsHandler.ServeHTTP(w, r) + return nil } // Interface guards var ( + _ caddy.Provisioner = (*AdminMetrics)(nil) _ caddy.AdminRouter = (*AdminMetrics)(nil) ) diff --git a/modules/metrics/metrics.go b/modules/metrics/metrics.go index dc6196a15..42b30d88d 100644 --- a/modules/metrics/metrics.go +++ b/modules/metrics/metrics.go @@ -15,6 +15,7 @@ package metrics import ( + "errors" "net/http" "github.com/prometheus/client_golang/prometheus" @@ -62,7 +63,11 @@ func (l *zapLogger) Println(v ...any) { // Provision sets up m. func (m *Metrics) Provision(ctx caddy.Context) error { log := ctx.Logger() - m.metricsHandler = createMetricsHandler(&zapLogger{log}, !m.DisableOpenMetrics) + registry := ctx.GetMetricsRegistry() + if registry == nil { + return errors.New("no metrics registry found") + } + m.metricsHandler = createMetricsHandler(&zapLogger{log}, !m.DisableOpenMetrics, registry) return nil } @@ -107,9 +112,9 @@ var ( _ caddyfile.Unmarshaler = (*Metrics)(nil) ) -func createMetricsHandler(logger promhttp.Logger, enableOpenMetrics bool) http.Handler { - return promhttp.InstrumentMetricHandler(prometheus.DefaultRegisterer, - promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{ +func createMetricsHandler(logger promhttp.Logger, enableOpenMetrics bool, registry *prometheus.Registry) http.Handler { + return promhttp.InstrumentMetricHandler(registry, + promhttp.HandlerFor(registry, promhttp.HandlerOpts{ // will only log errors if logger is non-nil ErrorLog: logger, diff --git a/replacer.go b/replacer.go index 297dd935c..1a2aa5771 100644 --- a/replacer.go +++ b/replacer.go @@ -335,7 +335,7 @@ type replacementProvider interface { replace(key string) (any, bool) } -// fileReplacementsProvider handles {file.*} replacements, +// fileReplacementProvider handles {file.*} replacements, // reading a file from disk and replacing with its contents. type fileReplacementProvider struct{} @@ -360,7 +360,7 @@ func (f fileReplacementProvider) replace(key string) (any, bool) { return string(body), true } -// globalDefaultReplacementsProvider handles replacements +// globalDefaultReplacementProvider handles replacements // that can be used in any context, such as system variables, // time, or environment variables. type globalDefaultReplacementProvider struct{} diff --git a/replacer_test.go b/replacer_test.go index 1c1a7048f..4f20bede3 100644 --- a/replacer_test.go +++ b/replacer_test.go @@ -516,7 +516,7 @@ func BenchmarkReplacer(b *testing.B) { }, } { b.Run(bm.name, func(b *testing.B) { - for i := 0; i < b.N; i++ { + for b.Loop() { rep.ReplaceAll(bm.input, bm.empty) } }) diff --git a/sigtrap_posix.go b/sigtrap_posix.go index 7033f1635..2c6306121 100644 --- a/sigtrap_posix.go +++ b/sigtrap_posix.go @@ -28,6 +28,10 @@ import ( // trapSignalsPosix captures POSIX-only signals. func trapSignalsPosix() { + // Ignore all SIGPIPE signals to prevent weird issues with systemd: https://github.com/dunglas/frankenphp/issues/1020 + // Docker/Moby has a similar hack: https://github.com/moby/moby/blob/d828b032a87606ae34267e349bf7f7ccb1f6495a/cmd/dockerd/docker.go#L87-L90 + signal.Ignore(syscall.SIGPIPE) + go func() { sigchan := make(chan os.Signal, 1) signal.Notify(sigchan, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)