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 557b4bac1..6f13031b9 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -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 5f3e98db6..50501a0f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,9 +13,13 @@ on: - 2.* env: + GOFLAGS: '-tags=nobadger,nomysql,nopgx' # https://github.com/actions/setup-go/issues/491 GOTOOLCHAIN: local +permissions: + contents: read + jobs: test: strategy: @@ -27,13 +31,13 @@ jobs: - mac - windows go: - - '1.24' + - '1.25' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.24' - GO_SEMVER: '~1.24.1' + - 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@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ matrix.GO_SEMVER }} check-latest: true @@ -99,7 +111,7 @@ jobs: env: CGO_ENABLED: 0 run: | - go build -tags nobadger,nomysql,nopgx -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,nomysql,nopgx -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 == '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@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + allowed-endpoints: ci-s390x.caddyserver.com:22 + - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - 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,nomysql,nopgx -v ./... + CGO_ENABLED=0 go test -p 1 -v ./... exit_code=$? if ((exit_code == 0)); then break @@ -194,25 +215,33 @@ 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@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: goreleaser/goreleaser-action@v6 + - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 with: version: latest args: check - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: - go-version: "~1.24" + 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@v6 + - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 with: version: latest args: build --single-target --snapshot diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml index 372cc7652..8aa9eaf59 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -11,9 +11,14 @@ on: - 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: @@ -31,22 +36,30 @@ jobs: - 'darwin' - 'netbsd' go: - - '1.24' + - '1.25' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.24' - GO_SEMVER: '~1.24.1' + - 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@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.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,nomysql,nopgx -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 c5c89b502..849188c64 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -44,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@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: - go-version: '~1.24' + egress-policy: audit + + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.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 @@ -62,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@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: - go-version-input: '~1.24.1' + 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@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + + - name: 'Checkout Repository' + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: 'Dependency Review' + uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0 + 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 b508ba468..397df5ea2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,9 @@ env: # https://github.com/actions/setup-go/issues/491 GOTOOLCHAIN: local +permissions: + contents: read + jobs: release: name: Release @@ -17,13 +20,13 @@ jobs: os: - ubuntu-latest go: - - '1.24' + - '1.25' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.24' - GO_SEMVER: '~1.24.1' + - go: '1.25' + GO_SEMVER: '~1.25.0' runs-on: ${{ matrix.os }} # https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233 @@ -35,19 +38,24 @@ jobs: contents: write steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 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 @@ -101,11 +109,11 @@ jobs: git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1 - name: Install Cosign - uses: sigstore/cosign-installer@main + uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # main - name: Cosign version run: cosign version - name: Install Syft - uses: anchore/sbom-action/download-syft@main + uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # main - name: Syft version run: syft version - name: Install xcaddy @@ -114,7 +122,7 @@ jobs: 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@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.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..8afc5c35e 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@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + - name: Trigger event on caddyserver/dist - uses: peter-evans/repository-dispatch@v3 + uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.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@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.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..bb49f935d --- /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@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + + - name: "Checkout code" + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + 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@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # 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/.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 4bebaafdb..4c091f714 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@

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

+
@caddyserver on Twitter @@ -88,7 +89,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i Requirements: -- [Go 1.24.0 or newer](https://golang.org/dl/) +- [Go 1.25.0 or newer](https://golang.org/dl/) ### For development diff --git a/admin.go b/admin.go index 6df5a23f7..6ccec41e7 100644 --- a/admin.go +++ b/admin.go @@ -221,7 +221,8 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Co 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 } @@ -310,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{}{} } } @@ -427,6 +424,13 @@ func replaceLocalAdminServer(cfg *Config, ctx Context) error { 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 { return err @@ -548,6 +552,13 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error { // because we are using TLS authentication instead 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 clientCertPool := x509.NewCertPool() @@ -935,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() } @@ -1018,6 +1029,13 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error { return err } + // If this request changed the config, clear the last + // config info we have stored, if it is different from + // the original source. + ClearLastConfigIfDifferent( + r.Header.Get("Caddy-Config-Source-File"), + r.Header.Get("Caddy-Config-Source-Adapter")) + default: return APIError{ HTTPStatus: http.StatusMethodNotAllowed, diff --git a/admin_test.go b/admin_test.go index b00cfaae2..92dd43a5c 100644 --- a/admin_test.go +++ b/admin_test.go @@ -19,6 +19,7 @@ import ( "crypto/x509" "encoding/json" "fmt" + "maps" "net/http" "net/http/httptest" "reflect" @@ -148,11 +149,9 @@ func TestLoadConcurrent(t *testing.T) { var wg sync.WaitGroup for i := 0; i < 100; i++ { - wg.Add(1) - go func() { + wg.Go(func() { _ = Load(testCfg, true) - wg.Done() - }() + }) } wg.Wait() } @@ -206,7 +205,7 @@ func TestETags(t *testing.T) { } func BenchmarkLoad(b *testing.B) { - for i := 0; i < b.N; i++ { + for b.Loop() { Load(testCfg, true) } } @@ -335,9 +334,7 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) { func testGetMetricValue(labels map[string]string) float64 { promLabels := prometheus.Labels{} - for k, v := range labels { - promLabels[k] = v - } + maps.Copy(promLabels, labels) metric, err := adminMetrics.requestErrors.GetMetricWith(promLabels) if err != nil { @@ -377,9 +374,7 @@ func (m *mockModule) CaddyModule() ModuleInfo { func TestNewAdminHandlerRouterRegistration(t *testing.T) { originalModules := make(map[string]ModuleInfo) - for k, v := range modules { - originalModules[k] = v - } + maps.Copy(originalModules, modules) defer func() { modules = originalModules }() @@ -479,9 +474,7 @@ func TestAdminRouterProvisioning(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { originalModules := make(map[string]ModuleInfo) - for k, v := range modules { - originalModules[k] = v - } + maps.Copy(originalModules, modules) defer func() { modules = originalModules }() @@ -531,6 +524,7 @@ func TestAdminRouterProvisioning(t *testing.T) { } 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 @@ -543,12 +537,8 @@ func TestAllowedOriginsUnixSocket(t *testing.T) { Network: "unix", Host: "/tmp/caddy.sock", }, - origins: nil, // default origins - expectOrigins: []string{ - "", // empty host as per RFC 2616 - "127.0.0.1", - "::1", - }, + origins: nil, // default origins + expectOrigins: []string{}, }, { name: "unix socket with custom origins", @@ -578,7 +568,7 @@ func TestAllowedOriginsUnixSocket(t *testing.T) { }, } - for _, test := range tests { + for i, test := range tests { t.Run(test.name, func(t *testing.T) { admin := AdminConfig{ Origins: test.origins, @@ -592,7 +582,7 @@ func TestAllowedOriginsUnixSocket(t *testing.T) { } if len(gotOrigins) != len(test.expectOrigins) { - t.Errorf("Expected %d origins but got %d", len(test.expectOrigins), len(gotOrigins)) + t.Errorf("%d: Expected %d origins but got %d", i, len(test.expectOrigins), len(gotOrigins)) return } @@ -607,7 +597,7 @@ func TestAllowedOriginsUnixSocket(t *testing.T) { } if !reflect.DeepEqual(expectMap, gotMap) { - t.Errorf("Origins mismatch.\nExpected: %v\nGot: %v", test.expectOrigins, gotOrigins) + t.Errorf("%d: Origins mismatch.\nExpected: %v\nGot: %v", i, test.expectOrigins, gotOrigins) } }) } @@ -777,9 +767,7 @@ func (m *mockIssuerModule) CaddyModule() ModuleInfo { func TestManageIdentity(t *testing.T) { originalModules := make(map[string]ModuleInfo) - for k, v := range modules { - originalModules[k] = v - } + maps.Copy(originalModules, modules) defer func() { modules = originalModules }() diff --git a/caddy.go b/caddy.go index 758b0b2f6..5f71d8e8b 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. @@ -407,11 +411,23 @@ 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) if err != nil { - globalMetrics.configSuccess.Set(0) return ctx, err } @@ -437,14 +453,18 @@ func run(newCfg *Config, start bool) (Context, error) { return nil }() if err != nil { - globalMetrics.configSuccess.Set(0) 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 @@ -500,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, ctx) - 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 { @@ -539,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 { @@ -696,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() @@ -951,11 +975,11 @@ func Version() (simple, full string) { if CustomVersion != "" { full = CustomVersion simple = CustomVersion - return + return simple, full } full = "unknown" simple = "unknown" - return + return simple, full } // find the Caddy module in the dependency list for _, dep := range bi.Deps { @@ -1035,9 +1059,101 @@ func Version() (simple, full string) { } } - return + return simple, full } +// 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. @@ -1081,6 +1197,91 @@ var ( rawCfgMu sync.RWMutex ) +// lastConfigFile and lastConfigAdapter remember the source config +// file and adapter used when Caddy was started via the CLI "run" command. +// These are consulted by the SIGUSR1 handler to attempt reloading from +// the same source. They are intentionally not set for other entrypoints +// such as "caddy start" or subcommands like file-server. +var ( + lastConfigMu sync.RWMutex + lastConfigFile string + lastConfigAdapter string +) + +// reloadFromSourceFunc is the type of stored callback +// which is called when we receive a SIGUSR1 signal. +type reloadFromSourceFunc func(file, adapter string) error + +// reloadFromSourceCallback is the stored callback +// which is called when we receive a SIGUSR1 signal. +var reloadFromSourceCallback reloadFromSourceFunc + +// errReloadFromSourceUnavailable is returned when no reload-from-source callback is set. +var errReloadFromSourceUnavailable = errors.New("reload from source unavailable in this process") //nolint:unused + +// SetLastConfig records the given source file and adapter as the +// last-known external configuration source. Intended to be called +// only when starting via "caddy run --config --adapter ". +func SetLastConfig(file, adapter string, fn reloadFromSourceFunc) { + lastConfigMu.Lock() + lastConfigFile = file + lastConfigAdapter = adapter + reloadFromSourceCallback = fn + lastConfigMu.Unlock() +} + +// ClearLastConfigIfDifferent clears the recorded last-config if the provided +// source file/adapter do not match the recorded last-config. If both srcFile +// and srcAdapter are empty, the last-config is cleared. +func ClearLastConfigIfDifferent(srcFile, srcAdapter string) { + if (srcFile != "" || srcAdapter != "") && lastConfigMatches(srcFile, srcAdapter) { + return + } + SetLastConfig("", "", nil) +} + +// getLastConfig returns the last-known config file and adapter. +func getLastConfig() (file, adapter string, fn reloadFromSourceFunc) { + lastConfigMu.RLock() + f, a, cb := lastConfigFile, lastConfigAdapter, reloadFromSourceCallback + lastConfigMu.RUnlock() + return f, a, cb +} + +// lastConfigMatches returns true if the provided source file and/or adapter +// matches the recorded last-config. Matching rules (in priority order): +// 1. If srcAdapter is provided and differs from the recorded adapter, no match. +// 2. If srcFile exactly equals the recorded file, match. +// 3. If both sides can be made absolute and equal, match. +// 4. If basenames are equal, match. +func lastConfigMatches(srcFile, srcAdapter string) bool { + lf, la, _ := getLastConfig() + + // If adapter is provided, it must match. + if srcAdapter != "" && srcAdapter != la { + return false + } + + // Quick equality check. + if srcFile == lf { + return true + } + + // Try absolute path comparison. + sAbs, sErr := filepath.Abs(srcFile) + lAbs, lErr := filepath.Abs(lf) + if sErr == nil && lErr == nil && sAbs == lAbs { + return true + } + + // Final fallback: basename equality. + if filepath.Base(srcFile) == filepath.Base(lf) { + return true + } + + return false +} + // errSameConfig is returned if the new config is the same // as the old one. This isn't usually an actual, actionable // error; it's mostly a sentinel value. 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 d04a1ac46..8439f3731 100644 --- a/caddyconfig/caddyfile/parse.go +++ b/caddyconfig/caddyfile/parse.go @@ -379,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 } } @@ -538,29 +533,24 @@ func (p *parser) doImport(nesting int) error { } // if it is {block}, we substitute with all tokens in the block // if it is {blocks.*}, we substitute with the tokens in the mapping for the * - var skip bool var tokensToAdd []Token + foundBlockDirective := false switch { case token.Text == "{block}": + foundBlockDirective = true tokensToAdd = blockTokens case strings.HasPrefix(token.Text, "{blocks.") && strings.HasSuffix(token.Text, "}"): + foundBlockDirective = true // {blocks.foo.bar} will be extracted to key `foo.bar` blockKey := strings.TrimPrefix(strings.TrimSuffix(token.Text, "}"), "{blocks.") val, ok := blockMapping[blockKey] if ok { tokensToAdd = val } - default: - skip = true } - if !skip { - if len(tokensToAdd) == 0 { - // if there is no content in the snippet block, don't do any replacement - // this allows snippets which contained {block}/{block.*} before this change to continue functioning as normal - tokensCopy = append(tokensCopy, token) - } else { - tokensCopy = append(tokensCopy, tokensToAdd...) - } + + if foundBlockDirective { + tokensCopy = append(tokensCopy, tokensToAdd...) continue } diff --git a/caddyconfig/caddyfile/parse_test.go b/caddyconfig/caddyfile/parse_test.go index d3fada4e0..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" ) @@ -884,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/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 3fc08b2c8..061aaa48b 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -15,6 +15,7 @@ package httpcaddyfile import ( + "encoding/json" "fmt" "html" "net/http" @@ -90,7 +91,7 @@ func parseBind(h Helper) ([]ConfigValue, error) { // curves // client_auth { // mode [request|require|verify_if_given|require_and_verify] -// trust_pool [...] +// trust_pool [...] // trusted_leaf_cert // trusted_leaf_cert_file // } @@ -129,6 +130,9 @@ func parseTLS(h Helper) ([]ConfigValue, error) { var reusePrivateKeys bool var forceAutomate bool + // Track which DNS challenge options are set + var dnsOptionsSet []string + firstLine := h.RemainingArgs() switch len(firstLine) { case 0: @@ -349,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": @@ -370,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": @@ -397,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": @@ -418,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": @@ -434,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": @@ -469,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 || h.Option("acme_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() @@ -843,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{ { @@ -1160,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/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 ae6f5ddee..3dcd3ea5b 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -633,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 } } @@ -706,16 +700,6 @@ func (st *ServerType) serversFromPairings( return specificity(iLongestHost) > specificity(jLongestHost) }) - // collect all hosts that have a wildcard in them - wildcardHosts := []string{} - for _, sblock := range p.serverBlocks { - for _, addr := range sblock.parsedKeys { - if strings.HasPrefix(addr.Host, "*.") { - wildcardHosts = append(wildcardHosts, addr.Host[2:]) - } - } - } - var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled @@ -801,7 +785,13 @@ func (st *ServerType) serversFromPairings( cp.FallbackSNI = fallbackSNI } - // only append this policy if it actually changes something + // 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 @@ -841,18 +831,6 @@ func (st *ServerType) serversFromPairings( addressQualifiesForTLS = true } - // If prefer wildcard is enabled, then we add hosts that are - // already covered by the wildcard to the skip list - if addressQualifiesForTLS && srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard { - 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.SkipCerts = append(srv.AutoHTTPS.SkipCerts, addr.Host) - } - } - // 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 && @@ -1083,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) { @@ -1189,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 } diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index e48a52577..336c6999f 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -458,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'") } @@ -557,8 +555,14 @@ func parseOptPreferredChains(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptDNS(d *caddyfile.Dispenser, _ any) (any, error) { d.Next() // consume option name + optName := d.Val() - if !d.Next() { // get DNS module name + // 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() diff --git a/caddyconfig/httpcaddyfile/pkiapp.go b/caddyconfig/httpcaddyfile/pkiapp.go index c57263baf..25b6c221c 100644 --- a/caddyconfig/httpcaddyfile/pkiapp.go +++ b/caddyconfig/httpcaddyfile/pkiapp.go @@ -15,6 +15,8 @@ package httpcaddyfile import ( + "slices" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" @@ -178,6 +180,15 @@ func (st ServerType) buildPKIApp( if _, ok := options["skip_install_trust"]; ok { skipInstallTrust = true } + + // check if auto_https is off - in that case we should not create + // any PKI infrastructure even with skip_install_trust directive + autoHTTPS := []string{} + if ah, ok := options["auto_https"].([]string); ok { + autoHTTPS = ah + } + autoHTTPSOff := slices.Contains(autoHTTPS, "off") + falseBool := false // Load the PKI app configured via global options @@ -218,7 +229,8 @@ func (st ServerType) buildPKIApp( // if there was no CAs defined in any of the servers, // and we were requested to not install trust, then // add one for the default/local CA to do so - if len(pkiApp.CAs) == 0 && skipInstallTrust { + // only if auto_https is not completely disabled + if len(pkiApp.CAs) == 0 && skipInstallTrust && !autoHTTPSOff { ca := new(caddypki.CA) ca.ID = caddypki.DefaultCAID ca.InstallTrust = &falseBool diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go index d60ce51a9..9431f1aed 100644 --- a/caddyconfig/httpcaddyfile/serveroptions.go +++ b/caddyconfig/httpcaddyfile/serveroptions.go @@ -18,6 +18,7 @@ import ( "encoding/json" "fmt" "slices" + "strconv" "github.com/dustin/go-humanize" @@ -42,12 +43,15 @@ type serverOptions struct { WriteTimeout caddy.Duration IdleTimeout caddy.Duration KeepAliveInterval caddy.Duration + KeepAliveIdle caddy.Duration + KeepAliveCount int MaxHeaderBytes int EnableFullDuplex bool Protocols []string StrictSNIHost *bool TrustedProxiesRaw json.RawMessage TrustedProxiesStrict int + TrustedProxiesUnix bool ClientIPHeaders []string ShouldLogCredentials bool Metrics *caddyhttp.Metrics @@ -142,6 +146,7 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { return nil, d.Errf("unrecognized timeouts option '%s'", d.Val()) } } + case "keepalive_interval": if !d.NextArg() { return nil, d.ArgErr() @@ -152,6 +157,26 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { } serverOpts.KeepAliveInterval = caddy.Duration(dur) + case "keepalive_idle": + if !d.NextArg() { + return nil, d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return nil, d.Errf("parsing keepalive idle duration: %v", err) + } + serverOpts.KeepAliveIdle = caddy.Duration(dur) + + case "keepalive_count": + if !d.NextArg() { + return nil, d.ArgErr() + } + cnt, err := strconv.ParseInt(d.Val(), 10, 32) + if err != nil { + return nil, d.Errf("parsing keepalive count int: %v", err) + } + serverOpts.KeepAliveCount = int(cnt) + case "max_header_size": var sizeStr string if !d.AllArgs(&sizeStr) { @@ -227,6 +252,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { } serverOpts.TrustedProxiesStrict = 1 + case "trusted_proxies_unix": + if d.NextArg() { + return nil, d.ArgErr() + } + serverOpts.TrustedProxiesUnix = true + case "client_ip_headers": headers := d.RemainingArgs() for _, header := range headers { @@ -309,6 +340,8 @@ func applyServerOptions( server.WriteTimeout = opts.WriteTimeout server.IdleTimeout = opts.IdleTimeout server.KeepAliveInterval = opts.KeepAliveInterval + server.KeepAliveIdle = opts.KeepAliveIdle + server.KeepAliveCount = opts.KeepAliveCount server.MaxHeaderBytes = opts.MaxHeaderBytes server.EnableFullDuplex = opts.EnableFullDuplex server.Protocols = opts.Protocols @@ -316,6 +349,7 @@ func applyServerOptions( server.TrustedProxiesRaw = opts.TrustedProxiesRaw server.ClientIPHeaders = opts.ClientIPHeaders server.TrustedProxiesStrict = opts.TrustedProxiesStrict + server.TrustedProxiesUnix = opts.TrustedProxiesUnix server.Metrics = opts.Metrics if opts.ShouldLogCredentials { if server.Logs == nil { diff --git a/caddyconfig/httpcaddyfile/shorthands.go b/caddyconfig/httpcaddyfile/shorthands.go index ca6e4f92c..bf612e092 100644 --- a/caddyconfig/httpcaddyfile/shorthands.go +++ b/caddyconfig/httpcaddyfile/shorthands.go @@ -64,10 +64,13 @@ func placeholderShorthands() []string { "{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}", diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 8a21ca038..30948f84f 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -92,11 +92,9 @@ func (st ServerType) buildTLSApp( tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP) } - // collect all hosts that have a wildcard in them, and arent HTTP - wildcardHosts := []string{} - // hosts that have been explicitly marked to be automated, - // even if covered by another wildcard - forcedAutomatedNames := make(map[string]struct{}) + 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 { @@ -153,7 +151,7 @@ func (st ServerType) buildTLSApp( ap.OnDemand = true } - // collect hosts that are forced to be automated + // 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{}{} @@ -340,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) @@ -375,7 +373,9 @@ func (st ServerType) buildTLSApp( return nil, warnings, err } for _, cfg := range ech.Configs { - ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName) + if cfg.PublicName != "" { + ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName) + } } if tlsApp.Automation == nil { tlsApp.Automation = new(caddytls.AutomationConfig) @@ -464,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 @@ -549,11 +549,12 @@ 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"] globalHTTPPort, globalHTTPSPort := options["http_port"], options["https_port"] + globalDefaultBind := options["default_bind"] if globalEmail != nil && acmeIssuer.Email == "" { acmeIssuer.Email = globalEmail.(string) @@ -564,11 +565,21 @@ 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 && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil || acmeIssuer.Challenges.DNS.ProviderRaw == nil) { + globalDNS := options["dns"] + if globalDNS == nil && globalACMEDNS == nil { + return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option") + } + if acmeIssuer.Challenges == nil { + acmeIssuer.Challenges = new(caddytls.ChallengesConfig) + } + if acmeIssuer.Challenges.DNS == nil { + acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) + } + // If global `dns` is set, do NOT set provider in issuer, just set empty dns config + if globalDNS == nil && acmeIssuer.Challenges.DNS.ProviderRaw == nil { + // Set a global DNS provider if `acme_dns` is set and `dns` is NOT set + acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil) } } if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil { @@ -596,6 +607,20 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e } acmeIssuer.Challenges.TLSALPN.AlternatePort = globalHTTPSPort.(int) } + // If BindHost is still unset, fall back to the first default_bind address if set + // This avoids binding the automation policy to the wildcard socket, which is unexpected behavior when a more selective socket is specified via default_bind + // In BSD it is valid to bind to the wildcard socket even though a more selective socket is already open (still unexpected behavior by the caller though) + // In Linux the same call will error with EADDRINUSE whenever the listener for the automation policy is opened + if acmeIssuer.Challenges == nil || (acmeIssuer.Challenges.DNS == nil && acmeIssuer.Challenges.BindHost == "") { + if defBinds, ok := globalDefaultBind.([]ConfigValue); ok && len(defBinds) > 0 { + if abp, ok := defBinds[0].Value.(addressesWithProtocols); ok && len(abp.addresses) > 0 { + if acmeIssuer.Challenges == nil { + acmeIssuer.Challenges = new(caddytls.ChallengesConfig) + } + acmeIssuer.Challenges.BindHost = abp.addresses[0] + } + } + } if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 { acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration) } @@ -616,12 +641,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 } @@ -643,6 +674,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 diff --git a/caddyconfig/load.go b/caddyconfig/load.go index 9f5cda905..9422d2fbb 100644 --- a/caddyconfig/load.go +++ b/caddyconfig/load.go @@ -121,6 +121,13 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error { } } + // If this request changed the config, clear the last + // config info we have stored, if it is different from + // the original source. + caddy.ClearLastConfigIfDifferent( + r.Header.Get("Caddy-Config-Source-File"), + r.Header.Get("Caddy-Config-Source-Adapter")) + caddy.Log().Named("admin.api").Info("load complete") return nil diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 623c45e5e..7b56bb281 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -281,7 +281,7 @@ func validateTestPrerequisites(tc *Tester) error { tc.t.Cleanup(func() { os.Remove(f.Name()) }) - if _, err := f.WriteString(fmt.Sprintf(initConfig, tc.config.AdminPort)); err != nil { + if _, err := fmt.Fprintf(f, initConfig, tc.config.AdminPort); err != nil { return err } diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go index d7e4c296d..f10aef6a8 100644 --- a/caddytest/integration/acme_test.go +++ b/caddytest/integration/acme_test.go @@ -12,13 +12,14 @@ import ( "strings" "testing" - "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/caddytest" "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 diff --git a/caddytest/integration/acmeserver_test.go b/caddytest/integration/acmeserver_test.go index ca5845f87..d6a9ba005 100644 --- a/caddytest/integration/acmeserver_test.go +++ b/caddytest/integration/acmeserver_test.go @@ -9,11 +9,12 @@ import ( "strings" "testing" - "github.com/caddyserver/caddy/v2/caddytest" "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) { 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..3f43a082a --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/acme_dns_configured.caddyfiletest @@ -0,0 +1,69 @@ +{ + 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": { + "argument": "foo", + "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/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/auto_https_prefer_wildcard.caddyfiletest b/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest deleted file mode 100644 index 04f2c4665..000000000 --- a/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest +++ /dev/null @@ -1,109 +0,0 @@ -{ - auto_https prefer_wildcard -} - -*.example.com { - tls { - dns mock - } - respond "fallback" -} - -foo.example.com { - respond "foo" -} ----------- -{ - "apps": { - "http": { - "servers": { - "srv0": { - "listen": [ - ":443" - ], - "routes": [ - { - "match": [ - { - "host": [ - "foo.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "foo", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - }, - { - "match": [ - { - "host": [ - "*.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "fallback", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - } - ], - "automatic_https": { - "skip_certificates": [ - "foo.example.com" - ], - "prefer_wildcard": true - } - } - } - }, - "tls": { - "automation": { - "policies": [ - { - "subjects": [ - "*.example.com" - ], - "issuers": [ - { - "challenges": { - "dns": { - "provider": { - "name": "mock" - } - } - }, - "module": "acme" - } - ] - } - ] - } - } - } -} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard_multi.caddyfiletest b/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard_multi.caddyfiletest deleted file mode 100644 index 4f8c26a5d..000000000 --- a/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard_multi.caddyfiletest +++ /dev/null @@ -1,268 +0,0 @@ -{ - auto_https prefer_wildcard -} - -# Covers two domains -*.one.example.com { - tls { - dns mock - } - respond "one fallback" -} - -# Is covered, should not get its own AP -foo.one.example.com { - respond "foo one" -} - -# This one has its own tls config so it doesn't get covered (escape hatch) -bar.one.example.com { - respond "bar one" - tls bar@bar.com -} - -# Covers nothing but AP gets consolidated with the first -*.two.example.com { - tls { - dns mock - } - respond "two fallback" -} - -# Is HTTP so it should not cover -http://*.three.example.com { - respond "three fallback" -} - -# Has no wildcard coverage so it gets an AP -foo.three.example.com { - respond "foo three" -} ----------- -{ - "apps": { - "http": { - "servers": { - "srv0": { - "listen": [ - ":443" - ], - "routes": [ - { - "match": [ - { - "host": [ - "foo.three.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "foo three", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - }, - { - "match": [ - { - "host": [ - "foo.one.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "foo one", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - }, - { - "match": [ - { - "host": [ - "bar.one.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "bar one", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - }, - { - "match": [ - { - "host": [ - "*.one.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "one fallback", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - }, - { - "match": [ - { - "host": [ - "*.two.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "two fallback", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - } - ], - "automatic_https": { - "skip_certificates": [ - "foo.one.example.com", - "bar.one.example.com" - ], - "prefer_wildcard": true - } - }, - "srv1": { - "listen": [ - ":80" - ], - "routes": [ - { - "match": [ - { - "host": [ - "*.three.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "three fallback", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - } - ], - "automatic_https": { - "prefer_wildcard": true - } - } - } - }, - "tls": { - "automation": { - "policies": [ - { - "subjects": [ - "foo.three.example.com" - ] - }, - { - "subjects": [ - "bar.one.example.com" - ], - "issuers": [ - { - "email": "bar@bar.com", - "module": "acme" - }, - { - "ca": "https://acme.zerossl.com/v2/DV90", - "email": "bar@bar.com", - "module": "acme" - } - ] - }, - { - "subjects": [ - "*.one.example.com", - "*.two.example.com" - ], - "issuers": [ - { - "challenges": { - "dns": { - "provider": { - "name": "mock" - } - } - }, - "module": "acme" - } - ] - } - ] - } - } - } -} \ 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/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/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/global_server_options_single.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_server_options_single.caddyfiletest index 2f3306fd9..6b2ffaec4 100644 --- a/caddytest/integration/caddyfile_adapt/global_server_options_single.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/global_server_options_single.caddyfiletest @@ -18,6 +18,9 @@ trusted_proxies static private_ranges client_ip_headers Custom-Real-Client-IP X-Forwarded-For client_ip_headers A-Third-One + keepalive_interval 20s + keepalive_idle 20s + keepalive_count 10 } } @@ -45,6 +48,9 @@ foo.com { "read_header_timeout": 30000000000, "write_timeout": 30000000000, "idle_timeout": 30000000000, + "keepalive_interval": 20000000000, + "keepalive_idle": 20000000000, + "keepalive_count": 10, "max_header_bytes": 100000000, "enable_full_duplex": true, "routes": [ @@ -89,4 +95,4 @@ foo.com { } } } -} \ No newline at end of file +} 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_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 <headers>Authorization regexp "Bearer\s+([A-Za-z0-9_-]+)" "Bearer [REDACTED]" + request>headers>Authorization regexp "Basic\s+([A-Za-z0-9+/=]+)" "Basic [REDACTED]" + request>headers>Authorization regexp "token=([^&\s]+)" "token=[REDACTED]" + + # Single regexp filter - this should continue to work as before + request>headers>Cookie regexp "sessionid=[^;]+" "sessionid=[REDACTED]" + + # Mixed filters (non-regexp) - these should work normally + request>headers>Server delete + request>remote_ip ip_mask { + ipv4 24 + ipv6 32 + } + } +} +---------- +{ + "logging": { + "logs": { + "default": { + "exclude": [ + "http.log.access.log0" + ] + }, + "log0": { + "writer": { + "output": "stdout" + }, + "encoder": { + "fields": { + "request\u003eheaders\u003eAuthorization": { + "filter": "multi_regexp", + "operations": [ + { + "regexp": "Bearer\\s+([A-Za-z0-9_-]+)", + "value": "Bearer [REDACTED]" + }, + { + "regexp": "Basic\\s+([A-Za-z0-9+/=]+)", + "value": "Basic [REDACTED]" + }, + { + "regexp": "token=([^\u0026\\s]+)", + "value": "token=[REDACTED]" + } + ] + }, + "request\u003eheaders\u003eCookie": { + "filter": "regexp", + "regexp": "sessionid=[^;]+", + "value": "sessionid=[REDACTED]" + }, + "request\u003eheaders\u003eServer": { + "filter": "delete" + }, + "request\u003eremote_ip": { + "filter": "ip_mask", + "ipv4_cidr": 24, + "ipv6_cidr": 32 + } + }, + "format": "filter", + "wrap": { + "format": "console" + } + }, + "include": [ + "http.log.access.log0" + ] + } + } + }, + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "logs": { + "default_logger_name": "log0" + } + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/matcher_outside_site_block.caddyfiletest b/caddytest/integration/caddyfile_adapt/matcher_outside_site_block.caddyfiletest new file mode 100644 index 000000000..04590c6c2 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/matcher_outside_site_block.caddyfiletest @@ -0,0 +1,9 @@ +@foo { + path /foo +} + +handle { + respond "should not work" +} +---------- +request matchers may not be defined globally, they must be in a site block; found @foo, at Caddyfile:1 \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_forward_proxy_url.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_forward_proxy_url.txt new file mode 100644 index 000000000..9fc445283 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_forward_proxy_url.txt @@ -0,0 +1,41 @@ +:8884 +reverse_proxy 127.0.0.1:65535 { + transport http { + forward_proxy_url http://localhost:8080 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8884" + ], + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "network_proxy": { + "from": "url", + "url": "http://localhost:8080" + }, + "protocol": "http" + }, + "upstreams": [ + { + "dial": "127.0.0.1:65535" + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_none_proxy.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_none_proxy.txt new file mode 100644 index 000000000..3805448d9 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_none_proxy.txt @@ -0,0 +1,40 @@ +:8884 +reverse_proxy 127.0.0.1:65535 { + transport http { + network_proxy none + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8884" + ], + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "network_proxy": { + "from": "none" + }, + "protocol": "http" + }, + "upstreams": [ + { + "dial": "127.0.0.1:65535" + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_url_proxy.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_url_proxy.txt new file mode 100644 index 000000000..9397458e9 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_url_proxy.txt @@ -0,0 +1,41 @@ +:8884 +reverse_proxy 127.0.0.1:65535 { + transport http { + network_proxy url http://localhost:8080 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8884" + ], + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "network_proxy": { + "from": "url", + "url": "http://localhost:8080" + }, + "protocol": "http" + }, + "upstreams": [ + { + "dial": "127.0.0.1:65535" + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_trusted_proxies_unix.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_trusted_proxies_unix.caddyfiletest new file mode 100644 index 000000000..8f7175124 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_trusted_proxies_unix.caddyfiletest @@ -0,0 +1,59 @@ +{ + servers { + trusted_proxies_unix + } +} + +example.com { + reverse_proxy https://local:8080 +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "protocol": "http", + "tls": {} + }, + "upstreams": [ + { + "dial": "local:8080" + } + ] + } + ] + } + ] + } + ], + "terminal": true + } + ], + "trusted_proxies_unix": true + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/site_address_invalid_port.caddyfiletest b/caddytest/integration/caddyfile_adapt/site_address_invalid_port.caddyfiletest new file mode 100644 index 000000000..3b8e2f596 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/site_address_invalid_port.caddyfiletest @@ -0,0 +1,7 @@ +:70000 + +handle { + respond "should not work" +} +---------- +port 70000 is out of range \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/site_address_negative_port.caddyfiletest b/caddytest/integration/caddyfile_adapt/site_address_negative_port.caddyfiletest new file mode 100644 index 000000000..b67849868 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/site_address_negative_port.caddyfiletest @@ -0,0 +1,7 @@ +:-1 + +handle { + respond "should not work" +} +---------- +port -1 is out of range \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/site_address_unsupported_scheme.caddyfiletest b/caddytest/integration/caddyfile_adapt/site_address_unsupported_scheme.caddyfiletest new file mode 100644 index 000000000..8616504e5 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/site_address_unsupported_scheme.caddyfiletest @@ -0,0 +1,7 @@ +foo://example.com + +handle { + respond "hello" +} +---------- +unsupported URL scheme foo:// \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/site_address_wss_invalid_port.caddyfiletest b/caddytest/integration/caddyfile_adapt/site_address_wss_invalid_port.caddyfiletest new file mode 100644 index 000000000..4a9060988 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/site_address_wss_invalid_port.caddyfiletest @@ -0,0 +1,7 @@ +wss://example.com:70000 + +handle { + respond "should not work" +} +---------- +port 70000 is out of range \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/site_address_wss_scheme.caddyfiletest b/caddytest/integration/caddyfile_adapt/site_address_wss_scheme.caddyfiletest new file mode 100644 index 000000000..d1051c071 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/site_address_wss_scheme.caddyfiletest @@ -0,0 +1,7 @@ +wss://example.com + +handle { + respond "hello" +} +---------- +the scheme wss:// is only supported in browsers; use https:// instead \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest index 4eb6c4f1c..623bafd70 100644 --- a/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest @@ -131,13 +131,7 @@ shadowed.example.com { { "match": { "sni": [ - "automated1.example.com" - ] - } - }, - { - "match": { - "sni": [ + "automated1.example.com", "automated2.example.com" ] } diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_file_loader_block.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_file_loader_block.caddyfiletest new file mode 100644 index 000000000..bc71456b8 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_file_loader_block.caddyfiletest @@ -0,0 +1,87 @@ +localhost + +respond "hello from localhost" +tls { + client_auth { + mode request + trust_pool inline { + trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ== + } + verifier leaf { + file ../caddy.ca.cer + } + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "hello from localhost", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "localhost" + ] + }, + "client_authentication": { + "ca": { + "provider": "inline", + "trusted_ca_certs": [ + "MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==" + ] + }, + "verifiers": [ + { + "leaf_certs_loaders": [ + { + "files": [ + "../caddy.ca.cer" + ], + "loader": "file" + } + ], + "verifier": "leaf" + } + ], + "mode": "request" + } + }, + {} + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_file_loader_inline.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_file_loader_inline.caddyfiletest new file mode 100644 index 000000000..baca2433a --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_file_loader_inline.caddyfiletest @@ -0,0 +1,85 @@ +localhost + +respond "hello from localhost" +tls { + client_auth { + mode request + trust_pool inline { + trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ== + } + verifier leaf file ../caddy.ca.cer + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "hello from localhost", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "localhost" + ] + }, + "client_authentication": { + "ca": { + "provider": "inline", + "trusted_ca_certs": [ + "MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==" + ] + }, + "verifiers": [ + { + "leaf_certs_loaders": [ + { + "files": [ + "../caddy.ca.cer" + ], + "loader": "file" + } + ], + "verifier": "leaf" + } + ], + "mode": "request" + } + }, + {} + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_file_loader_multi-in-block.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_file_loader_multi-in-block.caddyfiletest new file mode 100644 index 000000000..1fe22a317 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_file_loader_multi-in-block.caddyfiletest @@ -0,0 +1,94 @@ +localhost + +respond "hello from localhost" +tls { + client_auth { + mode request + trust_pool inline { + trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ== + } + verifier leaf { + file ../caddy.ca.cer + file ../caddy.ca.cer + } + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "hello from localhost", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "localhost" + ] + }, + "client_authentication": { + "ca": { + "provider": "inline", + "trusted_ca_certs": [ + "MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==" + ] + }, + "verifiers": [ + { + "leaf_certs_loaders": [ + { + "files": [ + "../caddy.ca.cer" + ], + "loader": "file" + }, + { + "files": [ + "../caddy.ca.cer" + ], + "loader": "file" + } + ], + "verifier": "leaf" + } + ], + "mode": "request" + } + }, + {} + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_folder_loader_block.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_folder_loader_block.caddyfiletest new file mode 100644 index 000000000..ee9be71aa --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_folder_loader_block.caddyfiletest @@ -0,0 +1,87 @@ +localhost + +respond "hello from localhost" +tls { + client_auth { + mode request + trust_pool inline { + trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ== + } + verifier leaf { + folder ../ + } + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "hello from localhost", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "localhost" + ] + }, + "client_authentication": { + "ca": { + "provider": "inline", + "trusted_ca_certs": [ + "MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==" + ] + }, + "verifiers": [ + { + "leaf_certs_loaders": [ + { + "folders": [ + "../" + ], + "loader": "folder" + } + ], + "verifier": "leaf" + } + ], + "mode": "request" + } + }, + {} + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_folder_loader_inline.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_folder_loader_inline.caddyfiletest new file mode 100644 index 000000000..b6c4b8727 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_folder_loader_inline.caddyfiletest @@ -0,0 +1,85 @@ +localhost + +respond "hello from localhost" +tls { + client_auth { + mode request + trust_pool inline { + trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ== + } + verifier leaf folder ../ + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "hello from localhost", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "localhost" + ] + }, + "client_authentication": { + "ca": { + "provider": "inline", + "trusted_ca_certs": [ + "MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==" + ] + }, + "verifiers": [ + { + "leaf_certs_loaders": [ + { + "folders": [ + "../" + ], + "loader": "folder" + } + ], + "verifier": "leaf" + } + ], + "mode": "request" + } + }, + {} + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_folder_loader_multi-in-block.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_folder_loader_multi-in-block.caddyfiletest new file mode 100644 index 000000000..dd5663d8d --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_leaf_verifier_folder_loader_multi-in-block.caddyfiletest @@ -0,0 +1,94 @@ +localhost + +respond "hello from localhost" +tls { + client_auth { + mode request + trust_pool inline { + trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ== + } + verifier leaf { + folder ../ + folder ../ + } + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "hello from localhost", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "localhost" + ] + }, + "client_authentication": { + "ca": { + "provider": "inline", + "trusted_ca_certs": [ + "MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==" + ] + }, + "verifiers": [ + { + "leaf_certs_loaders": [ + { + "folders": [ + "../" + ], + "loader": "folder" + }, + { + "folders": [ + "../" + ], + "loader": "folder" + } + ], + "verifier": "leaf" + } + ], + "mode": "request" + } + }, + {} + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_dns_multiple_options_without_provider.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_dns_multiple_options_without_provider.caddyfiletest new file mode 100644 index 000000000..235089c39 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_dns_multiple_options_without_provider.caddyfiletest @@ -0,0 +1,9 @@ +localhost + +tls { + propagation_delay 10s + dns_ttl 5m +} + +---------- +parsing caddyfile tokens for 'tls': setting DNS challenge options [propagation_delay, dns_ttl] requires a DNS provider (set with the 'dns' subdirective or 'acme_dns' global option), at Caddyfile:6 \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_dns_override_acme_dns.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_dns_override_acme_dns.caddyfiletest new file mode 100644 index 000000000..2f7c6096a --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_dns_override_acme_dns.caddyfiletest @@ -0,0 +1,79 @@ +{ + acme_dns mock foo +} + +localhost { + tls { + dns mock bar + resolvers 8.8.8.8 8.8.4.4 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "subjects": [ + "localhost" + ], + "issuers": [ + { + "challenges": { + "dns": { + "provider": { + "argument": "bar", + "name": "mock" + }, + "resolvers": [ + "8.8.8.8", + "8.8.4.4" + ] + } + }, + "module": "acme" + } + ] + }, + { + "issuers": [ + { + "challenges": { + "dns": { + "provider": { + "argument": "foo", + "name": "mock" + } + } + }, + "module": "acme" + } + ] + } + ] + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/tls_dns_override_global_dns.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_dns_override_global_dns.caddyfiletest new file mode 100644 index 000000000..fba67b566 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_dns_override_global_dns.caddyfiletest @@ -0,0 +1,68 @@ +{ + dns mock foo +} + +localhost { + tls { + dns mock bar + resolvers 8.8.8.8 8.8.4.4 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "subjects": [ + "localhost" + ], + "issuers": [ + { + "challenges": { + "dns": { + "provider": { + "argument": "bar", + "name": "mock" + }, + "resolvers": [ + "8.8.8.8", + "8.8.4.4" + ] + } + }, + "module": "acme" + } + ] + } + ] + }, + "dns": { + "argument": "foo", + "name": "mock" + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/tls_dns_propagation_timeout_without_provider.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_dns_propagation_timeout_without_provider.caddyfiletest new file mode 100644 index 000000000..48602135a --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_dns_propagation_timeout_without_provider.caddyfiletest @@ -0,0 +1,7 @@ +:443 { + tls { + propagation_timeout 30s + } +} +---------- +parsing caddyfile tokens for 'tls': setting DNS challenge options [propagation_timeout] requires a DNS provider (set with the 'dns' subdirective or 'acme_dns' global option), at Caddyfile:4 \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_dns_propagation_without_provider.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_dns_propagation_without_provider.caddyfiletest new file mode 100644 index 000000000..6ab6e3236 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_dns_propagation_without_provider.caddyfiletest @@ -0,0 +1,7 @@ +:443 { + tls { + propagation_delay 30s + } +} +---------- +parsing caddyfile tokens for 'tls': setting DNS challenge options [propagation_delay] requires a DNS provider (set with the 'dns' subdirective or 'acme_dns' global option), at Caddyfile:4 \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_dns_resolvers_with_global_provider.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_dns_resolvers_with_global_provider.caddyfiletest new file mode 100644 index 000000000..0292e8d07 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_dns_resolvers_with_global_provider.caddyfiletest @@ -0,0 +1,76 @@ +{ + acme_dns mock +} + +localhost { + tls { + resolvers 8.8.8.8 8.8.4.4 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "subjects": [ + "localhost" + ], + "issuers": [ + { + "challenges": { + "dns": { + "provider": { + "name": "mock" + }, + "resolvers": [ + "8.8.8.8", + "8.8.4.4" + ] + } + }, + "module": "acme" + } + ] + }, + { + "issuers": [ + { + "challenges": { + "dns": { + "provider": { + "name": "mock" + } + } + }, + "module": "acme" + } + ] + } + ] + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest index c452bf79f..6d7c007bd 100644 --- a/caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest @@ -2,6 +2,7 @@ localhost respond "hello from localhost" tls { + dns mock dns_ttl 5m10s } ---------- @@ -54,6 +55,9 @@ tls { { "challenges": { "dns": { + "provider": { + "name": "mock" + }, "ttl": 310000000000 } }, diff --git a/caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest index 43ec9774b..f45959b5c 100644 --- a/caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest @@ -2,6 +2,7 @@ localhost respond "hello from localhost" tls { + dns mock propagation_delay 5m10s propagation_timeout 10m20s } @@ -56,7 +57,10 @@ tls { "challenges": { "dns": { "propagation_delay": 310000000000, - "propagation_timeout": 620000000000 + "propagation_timeout": 620000000000, + "provider": { + "name": "mock" + } } }, "module": "acme" diff --git a/caddytest/integration/caddyfile_adapt_test.go b/caddytest/integration/caddyfile_adapt_test.go index 0d9f0fa47..9bc6af4b6 100644 --- a/caddytest/integration/caddyfile_adapt_test.go +++ b/caddytest/integration/caddyfile_adapt_test.go @@ -9,8 +9,8 @@ import ( "strings" "testing" + "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddytest" - _ "github.com/caddyserver/caddy/v2/internal/testmocks" ) @@ -28,30 +28,48 @@ func TestCaddyfileAdaptToJSON(t *testing.T) { if f.IsDir() { continue } - - // read the test file filename := f.Name() - data, err := os.ReadFile("./caddyfile_adapt/" + filename) - if err != nil { - t.Errorf("failed to read %s dir: %s", filename, err) - } - // split the Caddyfile (first) and JSON (second) parts - // (append newline to Caddyfile to match formatter expectations) - parts := strings.Split(string(data), "----------") - caddyfile, json := strings.TrimSpace(parts[0])+"\n", strings.TrimSpace(parts[1]) + // run each file as a subtest, so that we can see which one fails more easily + t.Run(filename, func(t *testing.T) { + // read the test file + data, err := os.ReadFile("./caddyfile_adapt/" + filename) + if err != nil { + t.Errorf("failed to read %s dir: %s", filename, err) + } - // replace windows newlines in the json with unix newlines - json = winNewlines.ReplaceAllString(json, "\n") + // split the Caddyfile (first) and JSON (second) parts + // (append newline to Caddyfile to match formatter expectations) + parts := strings.Split(string(data), "----------") + caddyfile, expected := strings.TrimSpace(parts[0])+"\n", strings.TrimSpace(parts[1]) - // replace os-specific default path for file_server's hide field - replacePath, _ := jsonMod.Marshal(fmt.Sprint(".", string(filepath.Separator), "Caddyfile")) - json = strings.ReplaceAll(json, `"./Caddyfile"`, string(replacePath)) + // replace windows newlines in the json with unix newlines + expected = winNewlines.ReplaceAllString(expected, "\n") - // run the test - ok := caddytest.CompareAdapt(t, filename, caddyfile, "caddyfile", json) - if !ok { - t.Errorf("failed to adapt %s", filename) - } + // replace os-specific default path for file_server's hide field + replacePath, _ := jsonMod.Marshal(fmt.Sprint(".", string(filepath.Separator), "Caddyfile")) + expected = strings.ReplaceAll(expected, `"./Caddyfile"`, string(replacePath)) + + // if the expected output is JSON, compare it + if len(expected) > 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/h2listener_test.go b/caddytest/integration/h2listener_test.go new file mode 100644 index 000000000..451c925ba --- /dev/null +++ b/caddytest/integration/h2listener_test.go @@ -0,0 +1,129 @@ +package integration + +import ( + "fmt" + "net/http" + "slices" + "strings" + "testing" + + "github.com/caddyserver/caddy/v2/caddytest" +) + +func newH2ListenerWithVersionsWithTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester { + const baseConfig = ` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + servers :9443 { + protocols %s + } + } + localhost { + respond "{http.request.tls.proto} {http.request.proto}" + } + ` + tester := caddytest.NewTester(t) + tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile") + + tr := tester.Client.Transport.(*http.Transport) + tr.TLSClientConfig.NextProtos = clientVersions + tr.Protocols = new(http.Protocols) + if slices.Contains(clientVersions, "h2") { + tr.ForceAttemptHTTP2 = true + tr.Protocols.SetHTTP2(true) + } + if !slices.Contains(clientVersions, "http/1.1") { + tr.Protocols.SetHTTP1(false) + } + + return tester +} + +func TestH2ListenerWithTLS(t *testing.T) { + tests := []struct { + serverVersions []string + clientVersions []string + expectedBody string + failed bool + }{ + {[]string{"h2"}, []string{"h2"}, "h2 HTTP/2.0", false}, + {[]string{"h2"}, []string{"http/1.1"}, "", true}, + {[]string{"h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false}, + {[]string{"h1"}, []string{"h2"}, "", true}, + {[]string{"h2", "h1"}, []string{"h2"}, "h2 HTTP/2.0", false}, + {[]string{"h2", "h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false}, + } + for _, tc := range tests { + tester := newH2ListenerWithVersionsWithTLSTester(t, tc.serverVersions, tc.clientVersions) + t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions) + if tc.failed { + resp, err := tester.Client.Get("https://localhost:9443") + if err == nil { + t.Errorf("unexpected response: %d", resp.StatusCode) + } + } else { + tester.AssertGetResponse("https://localhost:9443", 200, tc.expectedBody) + } + } +} + +func newH2ListenerWithVersionsWithoutTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester { + const baseConfig = ` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + servers :9080 { + protocols %s + } + } + http://localhost { + respond "{http.request.proto}" + } + ` + tester := caddytest.NewTester(t) + tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile") + + tr := tester.Client.Transport.(*http.Transport) + tr.Protocols = new(http.Protocols) + if slices.Contains(clientVersions, "h2c") { + tr.Protocols.SetHTTP1(false) + tr.Protocols.SetUnencryptedHTTP2(true) + } else if slices.Contains(clientVersions, "http/1.1") { + tr.Protocols.SetHTTP1(true) + tr.Protocols.SetUnencryptedHTTP2(false) + } + + return tester +} + +func TestH2ListenerWithoutTLS(t *testing.T) { + tests := []struct { + serverVersions []string + clientVersions []string + expectedBody string + failed bool + }{ + {[]string{"h2c"}, []string{"h2c"}, "HTTP/2.0", false}, + {[]string{"h2c"}, []string{"http/1.1"}, "", true}, + {[]string{"h1"}, []string{"http/1.1"}, "HTTP/1.1", false}, + {[]string{"h1"}, []string{"h2c"}, "", true}, + {[]string{"h2c", "h1"}, []string{"h2c"}, "HTTP/2.0", false}, + {[]string{"h2c", "h1"}, []string{"http/1.1"}, "HTTP/1.1", false}, + } + for _, tc := range tests { + tester := newH2ListenerWithVersionsWithoutTLSTester(t, tc.serverVersions, tc.clientVersions) + t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions) + if tc.failed { + resp, err := tester.Client.Get("http://localhost:9080") + if err == nil { + t.Errorf("unexpected response: %d", resp.StatusCode) + } + } else { + tester.AssertGetResponse("http://localhost:9080", 200, tc.expectedBody) + } + } +} diff --git a/caddytest/integration/mockdns_test.go b/caddytest/integration/mockdns_test.go index 615116a3a..e55a6df58 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() { @@ -14,7 +15,9 @@ func init() { } // MockDNSProvider is a mock DNS provider, for testing config with DNS modules. -type MockDNSProvider struct{} +type MockDNSProvider struct { + Argument string `json:"argument,omitempty"` // optional argument useful for testing +} // CaddyModule returns the Caddy module information. func (MockDNSProvider) CaddyModule() caddy.ModuleInfo { @@ -30,7 +33,15 @@ func (MockDNSProvider) Provision(ctx caddy.Context) error { } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. -func (MockDNSProvider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { +func (p *MockDNSProvider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume directive name + + if d.NextArg() { + p.Argument = d.Val() + } + if d.NextArg() { + return d.Errf("unexpected argument '%s'", d.Val()) + } return nil } @@ -55,7 +66,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/commandfuncs.go b/cmd/commandfuncs.go index 5127c0f90..75d114992 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -24,6 +24,7 @@ import ( "io" "io/fs" "log" + "maps" "net" "net/http" "os" @@ -171,9 +172,19 @@ func cmdStart(fl Flags) (int, error) { func cmdRun(fl Flags) (int, error) { caddy.TrapSignals() - logger := caddy.Log() + // set up buffered logging for early startup + // so that we can hold onto logs until after + // the config is loaded (or fails to load) + // so that we can write the logs to the user's + // configured output. we must be sure to flush + // on any error before the config is loaded. + logger, defaultLogger, logBuffer := caddy.BufferedLog() + undoMaxProcs := setResourceLimits(logger) defer undoMaxProcs() + // release the local reference to the undo function so it can be GC'd; + // the deferred call above has already captured the actual function value. + undoMaxProcs = nil //nolint:ineffassign,wastedassign configFlag := fl.String("config") configAdapterFlag := fl.String("adapter") @@ -186,6 +197,7 @@ func cmdRun(fl Flags) (int, error) { // load all additional envs as soon as possible err := handleEnvFileFlag(fl) if err != nil { + logBuffer.FlushTo(defaultLogger) return caddy.ExitCodeFailedStartup, err } @@ -203,6 +215,7 @@ func cmdRun(fl Flags) (int, error) { logger.Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath)) resumeFlag = false } else if err != nil { + logBuffer.FlushTo(defaultLogger) return caddy.ExitCodeFailedStartup, err } else { if configFlag == "" { @@ -218,9 +231,11 @@ func cmdRun(fl Flags) (int, error) { } // we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive var configFile string + var adapterUsed string if !resumeFlag { - config, configFile, err = LoadConfig(configFlag, configAdapterFlag) + config, configFile, adapterUsed, err = LoadConfig(configFlag, configAdapterFlag) if err != nil { + logBuffer.FlushTo(defaultLogger) return caddy.ExitCodeFailedStartup, err } } @@ -235,11 +250,35 @@ func cmdRun(fl Flags) (int, error) { } } + // If we have a source config file (we're running via 'caddy run --config ...'), + // record it so SIGUSR1 can reload from the same file. Also provide a callback + // that knows how to load/adapt that source when requested by the main process. + if configFile != "" { + caddy.SetLastConfig(configFile, adapterUsed, func(file, adapter string) error { + cfg, _, _, err := LoadConfig(file, adapter) + if err != nil { + return err + } + return caddy.Load(cfg, true) + }) + } + // run the initial config err = caddy.Load(config, true) if err != nil { + logBuffer.FlushTo(defaultLogger) return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err) } + // release the reference to the config so it can be GC'd + config = nil //nolint:ineffassign,wastedassign + + // at this stage the config will have replaced the + // default logger to the configured one, so we can + // log normally, now that the config is running. + // also clear our ref to the buffer so it can get GC'd + logger = caddy.Log() + defaultLogger = nil //nolint:ineffassign,wastedassign + logBuffer = nil //nolint:wastedassign,ineffassign logger.Info("serving initial configuration") // if we are to report to another process the successful start @@ -255,18 +294,22 @@ func cmdRun(fl Flags) (int, error) { return caddy.ExitCodeFailedStartup, fmt.Errorf("dialing confirmation address: %v", err) } - defer conn.Close() _, err = conn.Write(confirmationBytes) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("writing confirmation bytes to %s: %v", pingbackFlag, err) } + // close (non-defer because we `select {}` below) + // and release references so they can be GC'd + conn.Close() + confirmationBytes = nil //nolint:ineffassign,wastedassign + conn = nil //nolint:wastedassign,ineffassign } // if enabled, reload config file automatically on changes // (this better only be used in dev!) if watchFlag { - go watchConfigFile(configFile, configAdapterFlag) + go watchConfigFile(configFile, adapterUsed) } // warn if the environment does not provide enough information about the disk @@ -288,6 +331,9 @@ func cmdRun(fl Flags) (int, error) { } } + // release the last local logger reference + logger = nil //nolint:wastedassign,ineffassign + select {} } @@ -318,7 +364,7 @@ func cmdReload(fl Flags) (int, error) { forceFlag := fl.Bool("force") // get the config in caddy's native format - config, configFile, err := LoadConfig(configFlag, configAdapterFlag) + config, configFile, adapterUsed, err := LoadConfig(configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } @@ -336,6 +382,10 @@ func cmdReload(fl Flags) (int, error) { if forceFlag { headers.Set("Cache-Control", "must-revalidate") } + // Provide the source file/adapter to the running process so it can + // preserve its last-config knowledge if this reload came from the same source. + headers.Set("Caddy-Config-Source-File", configFile) + headers.Set("Caddy-Config-Source-Adapter", adapterUsed) resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config)) if err != nil { @@ -440,16 +490,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) @@ -468,13 +522,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 { @@ -540,7 +600,7 @@ func cmdValidateConfig(fl Flags) (int, error) { fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)") } - input, _, err := LoadConfig(configFlag, adapterFlag) + input, _, _, err := LoadConfig(configFlag, adapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } @@ -703,9 +763,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 @@ -757,7 +815,7 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA loadedConfig := config if len(loadedConfig) == 0 { // get the config in caddy's native format - loadedConfig, loadedConfigFile, err = LoadConfig(configFile, configAdapter) + loadedConfig, loadedConfigFile, _, err = LoadConfig(configFile, configAdapter) if err != nil { return "", err } diff --git a/cmd/commands.go b/cmd/commands.go index 259dd358f..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,7 +392,7 @@ 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. `, @@ -441,7 +450,7 @@ EXPERIMENTAL: May be changed or removed. }) defaultFactory.Use(func(rootCmd *cobra.Command) { - rootCmd.AddCommand(caddyCmdToCobra(Command{ + manpageCommand := Command{ Name: "manpage", Usage: "--directory ", Short: "Generates the manual pages for Caddy commands", @@ -471,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: @@ -516,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() }) } @@ -552,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") } @@ -570,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 87fa9fb95..411f4545d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -100,7 +100,12 @@ func handlePingbackConn(conn net.Conn, expect []byte) error { // there is no config available. It prints any warnings to stderr, // and returns the resulting JSON config bytes along with // the name of the loaded config file (if any). -func LoadConfig(configFile, adapterName string) ([]byte, string, error) { +// The return values are: +// - config bytes (nil if no config) +// - config file used ("" if none) +// - adapter used ("" if none) +// - error, if any +func LoadConfig(configFile, adapterName string) ([]byte, string, string, error) { return loadConfigWithLogger(caddy.Log(), configFile, adapterName) } @@ -138,7 +143,7 @@ func isCaddyfile(configFile, adapterName string) (bool, error) { return false, nil } -func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, error) { +func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, string, error) { // if no logger is provided, use a nop logger // just so we don't have to check for nil if logger == nil { @@ -147,7 +152,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([ // specifying an adapter without a config file is ambiguous if adapterName != "" && configFile == "" { - return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)") + return nil, "", "", fmt.Errorf("cannot adapt config without config file (use --config)") } // load initial config and adapter @@ -158,13 +163,13 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([ if configFile == "-" { config, err = io.ReadAll(os.Stdin) if err != nil { - return nil, "", fmt.Errorf("reading config from stdin: %v", err) + return nil, "", "", fmt.Errorf("reading config from stdin: %v", err) } logger.Info("using config from stdin") } else { config, err = os.ReadFile(configFile) if err != nil { - return nil, "", fmt.Errorf("reading config from file: %v", err) + return nil, "", "", fmt.Errorf("reading config from file: %v", err) } logger.Info("using config from file", zap.String("file", configFile)) } @@ -179,7 +184,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([ cfgAdapter = nil } else if err != nil { // default Caddyfile exists, but error reading it - return nil, "", fmt.Errorf("reading default Caddyfile: %v", err) + return nil, "", "", fmt.Errorf("reading default Caddyfile: %v", err) } else { // success reading default Caddyfile configFile = "Caddyfile" @@ -191,14 +196,14 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([ if yes, err := isCaddyfile(configFile, adapterName); yes { adapterName = "caddyfile" } else if err != nil { - return nil, "", err + return nil, "", "", err } // load config adapter if adapterName != "" { cfgAdapter = caddyconfig.GetAdapter(adapterName) if cfgAdapter == nil { - return nil, "", fmt.Errorf("unrecognized config adapter: %s", adapterName) + return nil, "", "", fmt.Errorf("unrecognized config adapter: %s", adapterName) } } @@ -208,7 +213,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([ "filename": configFile, }) if err != nil { - return nil, "", fmt.Errorf("adapting config using %s: %v", adapterName, err) + return nil, "", "", fmt.Errorf("adapting config using %s: %v", adapterName, err) } logger.Info("adapted config to JSON", zap.String("adapter", adapterName)) for _, warn := range warnings { @@ -226,11 +231,11 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([ // validate that the config is at least valid JSON err = json.Unmarshal(config, new(any)) if err != nil { - return nil, "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err) + return nil, "", "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err) } } - return config, configFile, nil + return config, configFile, adapterName, nil } // watchConfigFile watches the config file at filename for changes @@ -256,7 +261,7 @@ func watchConfigFile(filename, adapterName string) { } // get current config - lastCfg, _, err := loadConfigWithLogger(nil, filename, adapterName) + lastCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName) if err != nil { logger().Error("unable to load latest config", zap.Error(err)) return @@ -268,7 +273,7 @@ func watchConfigFile(filename, adapterName string) { //nolint:staticcheck for range time.Tick(1 * time.Second) { // get current config - newCfg, _, err := loadConfigWithLogger(nil, filename, adapterName) + newCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName) if err != nil { logger().Error("unable to load latest config", zap.Error(err)) return @@ -418,7 +423,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 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 695232001..4d0ff0680 100644 --- a/cmd/packagesfuncs.go +++ b/cmd/packagesfuncs.go @@ -62,7 +62,7 @@ func splitModule(arg string) (module, version string, err error) { err = fmt.Errorf("module name is required") } - return + return module, version, err } func cmdAddPackage(fl Flags) (int, error) { @@ -84,7 +84,7 @@ func cmdAddPackage(fl Flags) (int, error) { 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) { + if _, ok := pluginPkgs[module]; ok && (version == "" || pluginPkgs[module].Version == version) { return caddy.ExitCodeFailedStartup, fmt.Errorf("package is already added") } pluginPkgs[module] = pluginPackage{Version: version, Path: module} @@ -217,7 +217,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) { bi, ok := debug.ReadBuildInfo() if !ok { err = fmt.Errorf("no build info") - return + return standard, nonstandard, unknown, err } for _, modID := range caddy.Modules() { @@ -260,7 +260,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) { nonstandard = append(nonstandard, caddyModGoMod) } } - return + return standard, nonstandard, unknown, err } func listModules(path string) error { diff --git a/cmd/storagefuncs.go b/cmd/storagefuncs.go index 3c4219719..5606fe4ae 100644 --- a/cmd/storagefuncs.go +++ b/cmd/storagefuncs.go @@ -36,7 +36,7 @@ type storVal struct { // determineStorage returns the top-level storage module from the given config. // It may return nil even if no error. func determineStorage(configFile string, configAdapter string) (*storVal, error) { - cfg, _, err := LoadConfig(configFile, configAdapter) + cfg, _, _, err := LoadConfig(configFile, configAdapter) if err != nil { return nil, err } diff --git a/context.go b/context.go index 94623df72..4c1139936 100644 --- a/context.go +++ b/context.go @@ -91,14 +91,14 @@ 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 @@ -277,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). @@ -385,6 +393,8 @@ 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 @@ -394,12 +404,17 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error // 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 @@ -414,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 { @@ -429,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 } @@ -471,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 } @@ -495,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 } @@ -561,11 +592,11 @@ 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.WithName(string(mod.CaddyModule().ID)), @@ -600,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/go.mod b/go.mod index 6bd8743e7..70f85aed9 100644 --- a/go.mod +++ b/go.mod @@ -1,84 +1,101 @@ module github.com/caddyserver/caddy/v2 -go 1.24 +go 1.25 require ( - github.com/BurntSushi/toml v1.4.0 - github.com/KimMachineGun/automemlimit v0.7.1 + github.com/BurntSushi/toml v1.5.0 + github.com/DeRuina/timberjack v1.3.8 + github.com/KimMachineGun/automemlimit v0.7.4 github.com/Masterminds/sprig/v3 v3.3.0 - github.com/alecthomas/chroma/v2 v2.15.0 + github.com/alecthomas/chroma/v2 v2.20.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.22.0 + github.com/caddyserver/certmagic v0.25.0 github.com/caddyserver/zerossl v0.1.3 - github.com/cloudflare/circl v1.6.0 + github.com/cloudflare/circl v1.6.1 github.com/dustin/go-humanize v1.0.1 - github.com/go-chi/chi/v5 v5.2.1 - github.com/google/cel-go v0.24.1 + github.com/go-chi/chi/v5 v5.2.3 + github.com/google/cel-go v0.26.1 github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.18.0 - github.com/klauspost/cpuid/v2 v2.2.10 - github.com/mholt/acmez/v3 v3.1.0 - github.com/prometheus/client_golang v1.19.1 - github.com/quic-go/quic-go v0.50.0 - github.com/smallstep/certificates v0.26.1 - github.com/smallstep/nosql v0.6.1 + github.com/klauspost/cpuid/v2 v2.3.0 + github.com/mholt/acmez/v3 v3.1.4 + github.com/prometheus/client_golang v1.23.2 + github.com/quic-go/quic-go v0.55.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.9.1 - github.com/spf13/pflag v1.0.6 - github.com/stretchr/testify v1.10.0 + github.com/spf13/cobra v1.10.1 + github.com/spf13/pflag v1.0.9 + github.com/stretchr/testify v1.11.1 github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 - github.com/yuin/goldmark v1.7.8 + 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.56.0 - go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 - go.opentelemetry.io/otel v1.31.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 - go.opentelemetry.io/otel/sdk v1.31.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 + go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.0 go.uber.org/zap/exp v0.3.0 - golang.org/x/crypto v0.36.0 - golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 - golang.org/x/net v0.37.0 - golang.org/x/sync v0.12.0 - golang.org/x/term v0.30.0 - golang.org/x/time v0.11.0 - gopkg.in/natefinch/lumberjack.v2 v2.2.1 + golang.org/x/crypto v0.43.0 + golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99 + golang.org/x/net v0.46.0 + golang.org/x/sync v0.17.0 + golang.org/x/term v0.36.0 + golang.org/x/time v0.14.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - cel.dev/expr v0.19.1 // indirect + cel.dev/expr v0.24.0 // indirect + cloud.google.com/go/auth v0.16.4 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.8.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.3 // 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/fxamacker/cbor/v2 v2.8.0 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect - github.com/go-kit/log v0.2.1 // indirect + github.com/go-jose/go-jose/v4 v4.1.1 // 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.22.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.15.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // 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.4 // 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.5.0 // indirect - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/propagators/aws v1.38.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.38.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect + go.opentelemetry.io/contrib/propagators/ot v1.38.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect + golang.org/x/oauth2 v0.31.0 // indirect + google.golang.org/api v0.247.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect ) require ( @@ -87,72 +104,60 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 github.com/chzyer/readline v1.5.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.6 // 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.2.0 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect - github.com/dlclark/regexp2 v1.11.4 // 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.2 // 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.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.3 + github.com/libdns/libdns v1.1.1 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.63 // indirect + github.com/miekg/dns v1.1.68 // 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 - 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.67.1 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.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/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.31.0 // indirect - go.opentelemetry.io/otel/metric v1.31.0 // indirect - go.opentelemetry.io/otel/trace v1.31.0 - go.opentelemetry.io/proto/otlp v1.3.1 // 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.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.step.sm/crypto v0.70.0 go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sys v0.31.0 - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.31.0 // indirect - google.golang.org/grpc v1.67.1 // indirect - google.golang.org/protobuf v1.35.1 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sys v0.37.0 + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.38.0 // indirect + google.golang.org/grpc v1.75.1 // indirect + google.golang.org/protobuf v1.36.10 // indirect howett.net/plist v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 6cf21753d..5c4a2975b 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,23 @@ -cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= -cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +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.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= -cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= -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.4 h1:fXOAIQmkApVvcIn7Pc2+5J8QTMVbUGLscnSVNl11su8= +cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M= +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.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= +cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= +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= @@ -30,14 +30,14 @@ 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/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/KimMachineGun/automemlimit v0.7.1 h1:QcG/0iCOLChjfUweIMC3YL5Xy9C3VBeNmCZHrZfJMBw= -github.com/KimMachineGun/automemlimit v0.7.1/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= +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/DeRuina/timberjack v1.3.8 h1:lLxmRExvZygKSbb27Vp9hS0Tv8mL0WmFbwfRF29nY0Q= +github.com/DeRuina/timberjack v1.3.8/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE= +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.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= @@ -49,56 +49,58 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE 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.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= -github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= +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.38.0 h1:UCRQ5mlqcFk9HJDIqENSLR3wiG1VTWlyUfLDEvY7RxU= +github.com/aws/aws-sdk-go-v2 v1.38.0/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= +github.com/aws/aws-sdk-go-v2/config v1.31.0 h1:9yH0xiY5fUnVNLRWO0AtayqwU1ndriZdN78LlhruJR4= +github.com/aws/aws-sdk-go-v2/config v1.31.0/go.mod h1:VeV3K72nXnhbe4EuxxhzsDc/ByrCSlZwUnWH52Nde/I= +github.com/aws/aws-sdk-go-v2/credentials v1.18.4 h1:IPd0Algf1b+Qy9BcDp0sCUcIWdCQPSzDoMK3a8pcbUM= +github.com/aws/aws-sdk-go-v2/credentials v1.18.4/go.mod h1:nwg78FjH2qvsRM1EVZlX9WuGUJOL5od+0qvm0adEzHk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3 h1:GicIdnekoJsjq9wqnvyi2elW6CGMSYKhdozE7/Svh78= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3/go.mod h1:R7BIi6WNC5mc1kfRM7XM/VHC3uRWkjc396sfabq4iOo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3 h1:o9RnO+YZ4X+kt5Z7Nvcishlz0nksIt2PIzDglLMP0vA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3/go.mod h1:+6aLJzOG1fvMOyzIySYjOFjcguGvVRL68R+uoRencN4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3 h1:joyyUFhiTQQmVK6ImzNU9TQSNRNeD9kOklqTzyk5v6s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3/go.mod h1:+vNIyZQP3b3B1tSLI0lxvrU9cfM7gpdRXMFfm67ZcPc= +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.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3 h1:ieRzyHXypu5ByllM7Sp4hC5f/1Fy5wqxqY0yB85hC7s= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3/go.mod h1:O5ROz8jHiOAKAwx179v+7sHMhfobFVi6nZt8DEyiYoM= +github.com/aws/aws-sdk-go-v2/service/kms v1.44.0 h1:Z95XCqqSnwXr0AY7PgsiOUBhUG2GoDM5getw6RfD1Lg= +github.com/aws/aws-sdk-go-v2/service/kms v1.44.0/go.mod h1:DqcSngL7jJeU1fOzh5Ll5rSvX/MlMV6OZlE4mVdFAQc= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.0 h1:Mc/MKBf2m4VynyJkABoVEN+QzkfLqGj0aiJuEe7cMeM= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.0/go.mod h1:iS5OmxEcN4QIPXARGhavH7S8kETNL11kym6jhoS7IUQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0 h1:6csaS/aJmqZQbKhi1EyEMM7yBW653Wy/B9hnBofW+sw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0/go.mod h1:59qHWaY5B+Rs7HGTuVGaC32m0rdpQ68N8QCN3khYiqs= +github.com/aws/aws-sdk-go-v2/service/sts v1.37.0 h1:MG9VFW43M4A8BYeAfaJJZWrroinxeTi2r3+SnmLQfSA= +github.com/aws/aws-sdk-go-v2/service/sts v1.37.0/go.mod h1:JdeBDPgpJfuS6rU/hNglmOigKhyEZtBmbraLE4GK1J8= +github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= +github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= 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.22.0 h1:hi2skv2jouUw9uQUEyYSTTmqPZPHgf61dOANSIVCLOw= -github.com/caddyserver/certmagic v0.22.0/go.mod h1:Vc0msarAPhOagbDc/SU6M2zbzdwVuZ0lkTh2EqtH4vs= +github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic= +github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA= 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.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/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.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/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.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -113,21 +115,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/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= -github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= -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.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +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= @@ -144,55 +143,41 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WA 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.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= -github.com/dlclark/regexp2 v1.11.4/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= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 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.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= -github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/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.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-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/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/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/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= @@ -206,44 +191,41 @@ 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.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI= -github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8= +github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= +github.com/google/cel-go v0.26.1/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/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.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= 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.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= 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= @@ -252,53 +234,14 @@ github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq 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= @@ -307,51 +250,39 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 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.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -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/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.3 h1:ba30K4ObwMGB/QTmqUxf3H4/GmUrCAIkMWejeGl12v8= -github.com/libdns/libdns v0.2.3/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.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= +github.com/libdns/libdns v1.1.1/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/v3 v3.1.0 h1:RlOx2SSZ8dIAM5GfkMe8TdaxjjkiHTGorlMUt8GeMzg= -github.com/mholt/acmez/v3 v3.1.0/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= +github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ= +github.com/mholt/acmez/v3 v3.1.4/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.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= -github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= +github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= 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= @@ -362,20 +293,18 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx 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= @@ -384,38 +313,31 @@ 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.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 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.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= +github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= 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.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= 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.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo= -github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= 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.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/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/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= @@ -442,25 +364,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= @@ -473,37 +398,36 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU 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.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= 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.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/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= @@ -511,8 +435,8 @@ 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.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= -github.com/yuin/goldmark v1.7.8/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= @@ -521,102 +445,86 @@ 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.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -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.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= -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.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.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 h1:S3+4UwR3Y1tUKklruMwOacAFInNvtuOexz4ZTmJNAyw= +go.opentelemetry.io/contrib/propagators/autoprop v0.63.0/go.mod h1:qpIuOggbbw2T9nKRaO1je/oTRKd4zslAcJonN8LYbTg= +go.opentelemetry.io/contrib/propagators/aws v1.38.0 h1:eRZ7asSbLc5dH7+TBzL6hFKb1dabz0IV51uUUwYRZts= +go.opentelemetry.io/contrib/propagators/aws v1.38.0/go.mod h1:wXqc9NTGcXapBExHBDVLEZlByu6quiQL8w7Tjgv8TCg= +go.opentelemetry.io/contrib/propagators/b3 v1.38.0 h1:uHsCCOSKl0kLrV2dLkFK+8Ywk9iKa/fptkytc6aFFEo= +go.opentelemetry.io/contrib/propagators/b3 v1.38.0/go.mod h1:wMRSZJZcY8ya9mApLLhwIMjqmApy2o/Ml+62lhvxyHU= +go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 h1:nXGeLvT1QtCAhkASkP/ksjkTKZALIaQBIW+JSIw1KIc= +go.opentelemetry.io/contrib/propagators/jaeger v1.38.0/go.mod h1:oMvOXk78ZR3KEuPMBgp/ThAMDy9ku/eyUVztr+3G6Wo= +go.opentelemetry.io/contrib/propagators/ot v1.38.0 h1:k4gSyyohaDXI8F9BDXYC3uO2vr5sRNeQFMsN9Zn0EoI= +go.opentelemetry.io/contrib/propagators/ot v1.38.0/go.mod h1:2hDsuiHRO39SRUMhYGqmj64z/IuMRoxE4bBSFR82Lo8= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.step.sm/crypto v0.70.0 h1:Q9Ft7N637mucyZcHZd1+0VVQJVwDCKqcb9CYcYi7cds= +go.step.sm/crypto v0.70.0/go.mod h1:pzfUhS5/ue7ev64PLlEgXvhx1opwbhFCjkvlhsxVds0= 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.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -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.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 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.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.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -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/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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99 h1:CH0o4/bZX6KIUCjjgjmtNtfM/kXSkTYlzTOB9vZF45g= +golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18= 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-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE= +golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= 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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 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= @@ -624,23 +532,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.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.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +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.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= 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.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= -golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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= @@ -649,25 +557,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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-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= @@ -678,60 +583,62 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc 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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +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.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.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +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.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= 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.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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= 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.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/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.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= -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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 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= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc= +google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM= 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= @@ -741,30 +648,27 @@ 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-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg= -google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= 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.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= @@ -776,7 +680,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/logbuffer.go b/internal/logbuffer.go new file mode 100644 index 000000000..991041bd8 --- /dev/null +++ b/internal/logbuffer.go @@ -0,0 +1,82 @@ +// 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 internal + +import ( + "sync" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// LogBufferCore is a zapcore.Core that buffers log entries in memory. +type LogBufferCore struct { + mu sync.Mutex + entries []zapcore.Entry + fields [][]zapcore.Field + level zapcore.LevelEnabler +} + +type LogBufferCoreInterface interface { + zapcore.Core + FlushTo(*zap.Logger) +} + +func NewLogBufferCore(level zapcore.LevelEnabler) *LogBufferCore { + return &LogBufferCore{ + level: level, + } +} + +func (c *LogBufferCore) Enabled(lvl zapcore.Level) bool { + return c.level.Enabled(lvl) +} + +func (c *LogBufferCore) With(fields []zapcore.Field) zapcore.Core { + return c +} + +func (c *LogBufferCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if c.Enabled(entry.Level) { + return ce.AddCore(entry, c) + } + return ce +} + +func (c *LogBufferCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { + c.mu.Lock() + defer c.mu.Unlock() + c.entries = append(c.entries, entry) + c.fields = append(c.fields, fields) + return nil +} + +func (c *LogBufferCore) Sync() error { return nil } + +// FlushTo flushes buffered logs to the given zap.Logger. +func (c *LogBufferCore) FlushTo(logger *zap.Logger) { + c.mu.Lock() + defer c.mu.Unlock() + for idx, entry := range c.entries { + logger.WithOptions().Check(entry.Level, entry.Message).Write(c.fields[idx]...) + } + c.entries = nil + c.fields = nil +} + +var ( + _ zapcore.Core = (*LogBufferCore)(nil) + _ LogBufferCoreInterface = (*LogBufferCore)(nil) +) 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 1a7051bbf..fba9c3a6b 100644 --- a/listen.go +++ b/listen.go @@ -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)) } @@ -265,14 +261,14 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e if atomic.LoadInt32(&fcpc.closed) == 1 { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { if err = fcpc.SetReadDeadline(time.Time{}); err != nil { - return + return n, addr, err } } } - return + return n, addr, err } - return + return n, addr, err } // Close won't close the underlying socket unless there is no more reference, then listenerPool will close it. diff --git a/listeners.go b/listeners.go index b22df77ba..8a862bacf 100644 --- a/listeners.go +++ b/listeners.go @@ -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) @@ -382,7 +382,7 @@ func SplitNetworkAddress(a string) (network, host, port string, err error) { a = afterSlash if IsUnixNetwork(network) || IsFdNetwork(network) { host = a - return + return network, host, port, err } } @@ -402,7 +402,7 @@ func SplitNetworkAddress(a string) (network, host, port string, err error) { err = errors.Join(firstErr, err) } - return + return network, host, port, err } // JoinNetworkAddress combines network, host, and port into a single @@ -430,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) { @@ -610,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 @@ -626,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 @@ -641,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") } diff --git a/listeners_test.go b/listeners_test.go index 03945308e..a4cadd3aa 100644 --- a/listeners_test.go +++ b/listeners_test.go @@ -30,7 +30,7 @@ func TestSplitNetworkAddress(t *testing.T) { expectErr bool }{ { - input: "", + input: "", expectHost: "", }, { @@ -41,7 +41,7 @@ func TestSplitNetworkAddress(t *testing.T) { input: ":", // empty host & empty port }, { - input: "::", + input: "::", expectHost: "::", }, { @@ -184,9 +184,8 @@ func TestParseNetworkAddress(t *testing.T) { expectErr bool }{ { - input: "", - expectAddr: NetworkAddress{ - }, + input: "", + expectAddr: NetworkAddress{}, }, { input: ":", @@ -311,9 +310,8 @@ func TestParseNetworkAddressWithDefaults(t *testing.T) { expectErr bool }{ { - input: "", - expectAddr: NetworkAddress{ - }, + input: "", + expectAddr: NetworkAddress{}, }, { input: ":", diff --git a/logging.go b/logging.go index ca10beeed..2734b5425 100644 --- a/logging.go +++ b/logging.go @@ -20,6 +20,7 @@ import ( "io" "log" "os" + "slices" "strings" "sync" "time" @@ -27,6 +28,8 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" "golang.org/x/term" + + "github.com/caddyserver/caddy/v2/internal" ) func init() { @@ -161,7 +164,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() @@ -187,6 +192,13 @@ func (logging *Logging) setupNewDefault(ctx Context) error { ) } + // if we had a buffered core, flush its contents ASAP + // before we try to log anything else, so the order of + // logs is preserved + if oldBufferCore, ok := oldDefault.logger.Core().(*internal.LogBufferCore); ok { + oldBufferCore.FlushTo(newDefault.logger) + } + return nil } @@ -490,10 +502,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) } } @@ -772,6 +782,21 @@ func Log() *zap.Logger { return defaultLogger.logger } +// BufferedLog sets the default logger to one that buffers +// logs before a config is loaded. +// Returns the buffered logger, the original default logger +// (for flushing on errors), and the buffer core so that the +// caller can flush the logs after the config is loaded or +// fails to load. +func BufferedLog() (*zap.Logger, *zap.Logger, *internal.LogBufferCore) { + defaultLoggerMu.Lock() + defer defaultLoggerMu.Unlock() + origLogger := defaultLogger.logger + bufferCore := internal.NewLogBufferCore(zap.InfoLevel) + defaultLogger.logger = zap.New(bufferCore) + return defaultLogger.logger, origLogger, bufferCore +} + var ( coloringEnabled = os.Getenv("NO_COLOR") == "" && os.Getenv("TERM") != "xterm-mono" defaultLogger, _ = newDefaultProductionLog() 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/modules.go b/modules.go index 470c25e37..24c452589 100644 --- a/modules.go +++ b/modules.go @@ -18,6 +18,8 @@ import ( "bytes" "encoding/json" "fmt" + "net/http" + "net/url" "reflect" "sort" "strings" @@ -343,9 +345,11 @@ func StrictUnmarshalJSON(data []byte, v any) error { return dec.Decode(v) } +var JSONRawMessageType = reflect.TypeFor[json.RawMessage]() + // isJSONRawMessage returns true if the type is encoding/json.RawMessage. func isJSONRawMessage(typ reflect.Type) bool { - return typ.PkgPath() == "encoding/json" && typ.Name() == "RawMessage" + return typ == JSONRawMessageType } // isModuleMapType returns true if the type is map[string]json.RawMessage. @@ -360,6 +364,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 e78b00f8c..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 } @@ -269,7 +266,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E // 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 cbd168d31..7611285f7 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -28,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" @@ -73,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` @@ -151,8 +150,13 @@ type App struct { 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. @@ -166,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 { @@ -231,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) { @@ -273,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 } } @@ -443,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 { @@ -461,32 +464,44 @@ 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) + if nc, ok := c.(interface{ tlsNetConn() net.Conn }); ok { + getTlsConStateFunc := sync.OnceValue(func() *tls.ConnectionState { + tlsConnState := nc.tlsNetConn().(connectionStater).ConnectionState() + return &tlsConnState + }) + ctx = context.WithValue(ctx, tlsConnectionStateFuncCtxKey, getTlsConStateFunc) + } + return ctx }, } - 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) } @@ -496,11 +511,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 { @@ -531,7 +541,14 @@ func (app *App) Start() error { 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), + Idle: time.Duration(srv.KeepAliveIdle), + Count: srv.KeepAliveCount, + }, + }) if err != nil { return fmt.Errorf("listening on %s: %v", listenAddr.At(portOffset), err) } @@ -560,15 +577,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; @@ -586,11 +601,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 { @@ -703,6 +715,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), @@ -717,31 +734,36 @@ func (app *App) Stop() error { return } + // 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)) + } + } + 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; @@ -768,9 +790,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 diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index dce21a721..05f8a7517 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: @@ -205,7 +187,6 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er // for all the hostnames we found, filter them so we have // a deduplicated list of names for which to obtain certs // (only if cert management not disabled for this server) - var echDomains []string if srv.AutoHTTPS.DisableCerts { logger.Warn("skipping automated certificate management for server because it is disabled", zap.String("server_name", srvName)) } else { @@ -232,14 +213,10 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er } uniqueDomainsForCerts[d] = struct{}{} - echDomains = append(echDomains, d) } } } - // let the TLS server know we have some hostnames that could be protected behind ECH - app.tlsApp.RegisterServerNames(echDomains) - // tell the server to use TLS if it is not already doing so if srv.TLSConnPolicies == nil { srv.TLSConnPolicies = caddytls.ConnectionPolicies{new(caddytls.ConnectionPolicy)} @@ -288,19 +265,26 @@ 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)) + // if all servers have auto_https disabled and no domains need certs, + // skip the rest of the TLS automation setup to avoid creating + // unnecessary PKI infrastructure and automation policies + allServersDisabled := true + for _, srv := range app.Servers { + if srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled { + allServersDisabled = false + break + } + } + + if allServersDisabled && len(uniqueDomainsForCerts) == 0 { + logger.Debug("all servers have automatic HTTPS disabled and no domains need certificates, skipping TLS automation setup") + return nil + } + + // 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 @@ -339,6 +323,7 @@ uniqueDomainsLoop: } if isTailscaleDomain(d) { tailscale = append(tailscale, d) + delete(uniqueDomainsForCerts, d) // not managed by us; handled separately } else if shouldUseInternal(d) { internal = append(internal, d) } @@ -374,7 +359,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)) } @@ -468,6 +453,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)) @@ -770,7 +758,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/argon2id.go b/modules/caddyhttp/caddyauth/argon2id.go new file mode 100644 index 000000000..f1070ce48 --- /dev/null +++ b/modules/caddyhttp/caddyauth/argon2id.go @@ -0,0 +1,188 @@ +// 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 caddyauth + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "fmt" + "strconv" + "strings" + + "golang.org/x/crypto/argon2" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(Argon2idHash{}) +} + +const ( + argon2idName = "argon2id" + defaultArgon2idTime = 1 + defaultArgon2idMemory = 46 * 1024 + defaultArgon2idThreads = 1 + defaultArgon2idKeylen = 32 + defaultSaltLength = 16 +) + +// Argon2idHash implements the Argon2id password hashing. +type Argon2idHash struct { + salt []byte + time uint32 + memory uint32 + threads uint8 + keyLen uint32 +} + +// CaddyModule returns the Caddy module information. +func (Argon2idHash) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "http.authentication.hashes.argon2id", + New: func() caddy.Module { return new(Argon2idHash) }, + } +} + +// Compare checks if the plaintext password matches the given Argon2id hash. +func (Argon2idHash) Compare(hashed, plaintext []byte) (bool, error) { + argHash, storedKey, err := DecodeHash(hashed) + if err != nil { + return false, err + } + + computedKey := argon2.IDKey( + plaintext, + argHash.salt, + argHash.time, + argHash.memory, + argHash.threads, + argHash.keyLen, + ) + + return subtle.ConstantTimeCompare(storedKey, computedKey) == 1, nil +} + +// Hash generates an Argon2id hash of the given plaintext using the configured parameters and salt. +func (b Argon2idHash) Hash(plaintext []byte) ([]byte, error) { + if b.salt == nil { + s, err := generateSalt(defaultSaltLength) + if err != nil { + return nil, err + } + b.salt = s + } + + key := argon2.IDKey( + plaintext, + b.salt, + b.time, + b.memory, + b.threads, + b.keyLen, + ) + + hash := fmt.Sprintf( + "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + argon2.Version, + b.memory, + b.time, + b.threads, + base64.RawStdEncoding.EncodeToString(b.salt), + base64.RawStdEncoding.EncodeToString(key), + ) + + return []byte(hash), nil +} + +// DecodeHash parses an Argon2id PHC string into an Argon2idHash struct and returns the struct along with the derived key. +func DecodeHash(hash []byte) (*Argon2idHash, []byte, error) { + parts := strings.Split(string(hash), "$") + if len(parts) != 6 { + return nil, nil, fmt.Errorf("invalid hash format") + } + + if parts[1] != argon2idName { + return nil, nil, fmt.Errorf("unsupported variant: %s", parts[1]) + } + + version, err := strconv.Atoi(strings.TrimPrefix(parts[2], "v=")) + if err != nil { + return nil, nil, fmt.Errorf("invalid version: %w", err) + } + if version != argon2.Version { + return nil, nil, fmt.Errorf("incompatible version: %d", version) + } + + params := strings.Split(parts[3], ",") + if len(params) != 3 { + return nil, nil, fmt.Errorf("invalid parameters") + } + + mem, err := strconv.ParseUint(strings.TrimPrefix(params[0], "m="), 10, 32) + if err != nil { + return nil, nil, fmt.Errorf("invalid memory parameter: %w", err) + } + + iter, err := strconv.ParseUint(strings.TrimPrefix(params[1], "t="), 10, 32) + if err != nil { + return nil, nil, fmt.Errorf("invalid iterations parameter: %w", err) + } + + threads, err := strconv.ParseUint(strings.TrimPrefix(params[2], "p="), 10, 8) + if err != nil { + return nil, nil, fmt.Errorf("invalid parallelism parameter: %w", err) + } + + salt, err := base64.RawStdEncoding.Strict().DecodeString(parts[4]) + if err != nil { + return nil, nil, fmt.Errorf("decode salt: %w", err) + } + + key, err := base64.RawStdEncoding.Strict().DecodeString(parts[5]) + if err != nil { + return nil, nil, fmt.Errorf("decode key: %w", err) + } + + return &Argon2idHash{ + salt: salt, + time: uint32(iter), + memory: uint32(mem), + threads: uint8(threads), + keyLen: uint32(len(key)), + }, key, nil +} + +// FakeHash returns a constant fake hash for timing attacks mitigation. +func (Argon2idHash) FakeHash() []byte { + // hashed with the following command: + // caddy hash-password --plaintext "antitiming" --algorithm "argon2id" + return []byte("$argon2id$v=19$m=47104,t=1,p=1$P2nzckEdTZ3bxCiBCkRTyA$xQL3Z32eo5jKl7u5tcIsnEKObYiyNZQQf5/4sAau6Pg") +} + +// Interface guards +var ( + _ Comparer = (*Argon2idHash)(nil) + _ Hasher = (*Argon2idHash)(nil) +) + +func generateSalt(length int) ([]byte, error) { + salt := make([]byte, length) + if _, err := rand.Read(salt); err != nil { + return nil, fmt.Errorf("failed to generate salt: %w", err) + } + return salt, 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/hashes.go b/modules/caddyhttp/caddyauth/bcrypt.go similarity index 73% rename from modules/caddyhttp/caddyauth/hashes.go rename to modules/caddyhttp/caddyauth/bcrypt.go index ce3df901e..f6940996e 100644 --- a/modules/caddyhttp/caddyauth/hashes.go +++ b/modules/caddyhttp/caddyauth/bcrypt.go @@ -15,6 +15,8 @@ package caddyauth import ( + "errors" + "golang.org/x/crypto/bcrypt" "github.com/caddyserver/caddy/v2" @@ -24,8 +26,18 @@ func init() { caddy.RegisterModule(BcryptHash{}) } +// defaultBcryptCost cost 14 strikes a solid balance between security, usability, and hardware performance +const ( + bcryptName = "bcrypt" + 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 +50,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 +60,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/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/caddyfile.go b/modules/caddyhttp/caddyauth/caddyfile.go index cc92477e5..99a33aff5 100644 --- a/modules/caddyhttp/caddyauth/caddyfile.go +++ b/modules/caddyhttp/caddyauth/caddyfile.go @@ -51,7 +51,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) var hashName string switch len(args) { case 0: - hashName = "bcrypt" + hashName = bcryptName case 1: hashName = args[0] case 2: @@ -62,8 +62,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) } switch hashName { - case "bcrypt": + case bcryptName: cmp = BcryptHash{} + case argon2idName: + cmp = Argon2idHash{} default: return nil, h.Errf("unrecognized hash algorithm: %s", hashName) } diff --git a/modules/caddyhttp/caddyauth/command.go b/modules/caddyhttp/caddyauth/command.go index c9f440060..e9c513005 100644 --- a/modules/caddyhttp/caddyauth/command.go +++ b/modules/caddyhttp/caddyauth/command.go @@ -32,21 +32,55 @@ import ( func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "hash-password", - Usage: "[--plaintext ] [--algorithm ]", + Usage: "[--plaintext ] [--algorithm ] [--bcrypt-cost ] [--argon2id-time ] [--argon2id-memory ] [--argon2id-threads ] [--argon2id-keylen ]", Short: "Hashes a password and writes base64", Long: ` Convenient way to hash a plaintext password. The resulting hash is written to stdout as a base64 string. ---plaintext, when omitted, will be read from stdin. If -Caddy is attached to a controlling tty, the plaintext will -not be echoed. +--plaintext + The password to hash. If omitted, it will be read from stdin. + If Caddy is attached to a controlling TTY, the input will not be echoed. ---algorithm currently only supports 'bcrypt', and is the default. +--algorithm + Selects the hashing algorithm. Valid options are: + * 'argon2id' (recommended for modern security) + * 'bcrypt' (legacy, slower, configurable cost) + +bcrypt-specific parameters: + +--bcrypt-cost + Sets the bcrypt hashing difficulty. Higher values increase security by + making the hash computation slower and more CPU-intensive. + Must be within the valid range [bcrypt.MinCost, bcrypt.MaxCost]. + If omitted or invalid, the default cost is used. + +Argon2id-specific parameters: + +--argon2id-time + Number of iterations to perform. Increasing this makes + hashing slower and more resistant to brute-force attacks. + +--argon2id-memory + Amount of memory to use during hashing. + Larger values increase resistance to GPU/ASIC attacks. + +--argon2id-threads + Number of CPU threads to use. Increase for faster hashing + on multi-core systems. + +--argon2id-keylen + Length of the resulting hash in bytes. Longer keys increase + security but slightly increase storage size. `, 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().StringP("algorithm", "a", bcryptName, "Name of the hash algorithm") + cmd.Flags().Int("bcrypt-cost", defaultBcryptCost, "Bcrypt hashing cost (only used with 'bcrypt' algorithm)") + cmd.Flags().Uint32("argon2id-time", defaultArgon2idTime, "Number of iterations for Argon2id hashing. Increasing this makes the hash slower and more resistant to brute-force attacks.") + cmd.Flags().Uint32("argon2id-memory", defaultArgon2idMemory, "Memory to use in KiB for Argon2id hashing. Larger values increase resistance to GPU/ASIC attacks.") + cmd.Flags().Uint8("argon2id-threads", defaultArgon2idThreads, "Number of CPU threads to use for Argon2id hashing. Increase for faster hashing on multi-core systems.") + cmd.Flags().Uint32("argon2id-keylen", defaultArgon2idKeylen, "Length of the resulting Argon2id hash in bytes. Longer hashes increase security but slightly increase storage size.") cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdHashPassword) }, }) @@ -57,6 +91,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()) @@ -107,8 +142,34 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) { var hash []byte var hashString string switch algorithm { - case "bcrypt": - hash, err = BcryptHash{}.Hash(plaintext) + case bcryptName: + hash, err = BcryptHash{cost: bcryptCost}.Hash(plaintext) + hashString = string(hash) + case argon2idName: + time, err := fs.GetUint32("argon2id-time") + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id time parameter: %w", err) + } + memory, err := fs.GetUint32("argon2id-memory") + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id memory parameter: %w", err) + } + threads, err := fs.GetUint8("argon2id-threads") + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id threads parameter: %w", err) + } + keyLen, err := fs.GetUint32("argon2id-keylen") + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to get argon2id keylen parameter: %w", err) + } + + hash, _ = Argon2idHash{ + time: time, + memory: memory, + threads: threads, + keyLen: keyLen, + }.Hash(plaintext) + hashString = string(hash) default: return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm) diff --git a/modules/caddyhttp/celmatcher_test.go b/modules/caddyhttp/celmatcher_test.go index a7e91529c..1bd8e527e 100644 --- a/modules/caddyhttp/celmatcher_test.go +++ b/modules/caddyhttp/celmatcher_test.go @@ -535,7 +535,7 @@ func BenchmarkMatchExpressionMatch(b *testing.B) { } } b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { tc.expression.MatchWithError(req) } }) diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index bea86083a..e23d9109c 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -50,7 +50,7 @@ type Encode struct { // Only encode responses that are at least this many bytes long. MinLength int `json:"minimum_length,omitempty"` - // Only encode responses that match against this ResponseMmatcher. + // Only encode responses that match against this ResponseMatcher. // The default is a collection of text-based Content-Type headers. Matcher *caddyhttp.ResponseMatcher `json:"match,omitempty"` @@ -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*", @@ -176,7 +177,17 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh break } } - return next.ServeHTTP(w, r) + + err := next.ServeHTTP(w, r) + // If there was an error, disable encoding completely + // This prevents corruption when handle_errors processes the response + if err != nil { + if ew, ok := w.(*responseWriter); ok { + ew.disabled = true + } + } + + return err } func (enc *Encode) addEncoding(e Encoding) error { @@ -232,6 +243,7 @@ type responseWriter struct { statusCode int wroteHeader bool isConnect bool + disabled bool // disable encoding (for error responses) } // WriteHeader stores the status to write when the time comes @@ -424,7 +436,14 @@ func (rw *responseWriter) Unwrap() http.ResponseWriter { // init should be called before we write a response, if rw.buf has contents. func (rw *responseWriter) init() { + // Don't initialize encoder for error responses + // This prevents response corruption when handle_errors is used + if rw.disabled { + return + } + hdr := rw.Header() + if hdr.Get("Content-Encoding") == "" && isEncodeAllowed(hdr) && rw.config.Match(rw) { rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder) @@ -452,8 +471,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 } @@ -478,7 +496,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 83effa58c..818f76745 100644 --- a/modules/caddyhttp/encode/encode_test.go +++ b/modules/caddyhttp/encode/encode_test.go @@ -2,13 +2,14 @@ package encode import ( "net/http" + "slices" "sync" "testing" ) func BenchmarkOpenResponseWriter(b *testing.B) { enc := new(Encode) - for n := 0; n < b.N; n++ { + 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.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/matcher.go b/modules/caddyhttp/fileserver/matcher.go index 2bc665d4f..fbcd36e0a 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -252,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", @@ -274,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}" @@ -616,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 } } diff --git a/modules/caddyhttp/fileserver/matcher_test.go b/modules/caddyhttp/fileserver/matcher_test.go index b6697b9d8..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}/"}, } @@ -229,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"}, @@ -273,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" diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 2b0caecfc..3daf8daef 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}" @@ -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 for non-range requests if Content-Encoding is set. + // see: https://github.com/caddyserver/caddy/issues/7040 + // Setting the Range header manually will result in 206 Partial Content. + // see: https://github.com/caddyserver/caddy/issues/7250 + if r.Header.Get("Range") == "" { + respHeader.Set("Content-Length", strconv.FormatInt(compressedInfo.Size(), 10)) + } // 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] == "" { @@ -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/headers/headers.go b/modules/caddyhttp/headers/headers.go index c66bd4144..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 { @@ -266,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) } @@ -288,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 9808c29c9..a303c92dd 100644 --- a/modules/caddyhttp/headers/headers_test.go +++ b/modules/caddyhttp/headers/headers_test.go @@ -272,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..ad5991790 100644 --- a/modules/caddyhttp/http2listener.go +++ b/modules/caddyhttp/http2listener.go @@ -1,102 +1,131 @@ 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 - } - } + // *tls.Conn doesn't need to be wrapped because we already removed unwanted alpns + // and handshake won't succeed without mutually supported alpns + if tlsConn, ok := conn.(*tls.Conn); ok { + return tlsConn, nil + } + _, 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 { + if isConnectionStater { + return tlsStateConn{conn}, nil + } 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, + logger: h.logger, + Conn: conn, } + if isConnectionStater { + return tlsStateConn{http2StateConn{h2Conn}}, nil + } + return h2Conn, nil +} - timer := time.NewTimer(nextPollInterval()) - defer timer.Stop() - for { - if atomic.LoadUint64(&h.cnt) == 0 { - return nil +// tlsStateConn wraps a net.Conn that implements connectionStater to hide that method +// we can call netConn to get the original net.Conn and get the tls connection state +// golang 1.25 will call that method, and it breaks h2 with connections other than *tls.Conn +type tlsStateConn struct { + net.Conn +} + +func (conn tlsStateConn) tlsNetConn() net.Conn { + return conn.Conn +} + +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 5e0b356e7..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" @@ -109,7 +108,7 @@ func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) { []*cel.Type{cel.ListType(cel.StringType)}, // function to convert a constant list of strings to a MatchPath instance. func(data ref.Val) (RequestMatcherWithError, error) { - refStringList := reflect.TypeOf([]string{}) + refStringList := stringSliceType strList, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -222,7 +221,7 @@ func (MatchClientIP) CELLibrary(ctx caddy.Context) (cel.Library, error) { []*cel.Type{cel.ListType(cel.StringType)}, // function to convert a constant list of strings to a MatchPath instance. func(data ref.Val) (RequestMatcherWithError, error) { - refStringList := reflect.TypeOf([]string{}) + refStringList := stringSliceType strList, err := data.ConvertToNative(refStringList) if err != nil { return nil, err diff --git a/modules/caddyhttp/logging.go b/modules/caddyhttp/logging.go index 87298ac3c..e8a1316bd 100644 --- a/modules/caddyhttp/logging.go +++ b/modules/caddyhttp/logging.go @@ -209,7 +209,7 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi zap.String("err_trace", handlerErr.Trace), } } - return + return status, msg, fields } fields = func() []zapcore.Field { return []zapcore.Field{ @@ -218,7 +218,7 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi } status = http.StatusInternalServerError msg = err.Error() - return + return status, msg, fields } // ExtraLogFields is a list of extra fields to log with every request. diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 01bd7b8b4..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" @@ -373,7 +372,7 @@ func (MatchHost) CELLibrary(ctx caddy.Context) (cel.Library, error) { "host_match_request_list", []*cel.Type{cel.ListType(cel.StringType)}, func(data ref.Val) (RequestMatcherWithError, error) { - refStringList := reflect.TypeOf([]string{}) + refStringList := stringSliceType strList, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -552,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]) @@ -655,7 +653,7 @@ func (MatchPath) CELLibrary(ctx caddy.Context) (cel.Library, error) { []*cel.Type{cel.ListType(cel.StringType)}, // function to convert a constant list of strings to a MatchPath instance. func(data ref.Val) (RequestMatcherWithError, error) { - refStringList := reflect.TypeOf([]string{}) + refStringList := stringSliceType strList, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -734,7 +732,7 @@ func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { "path_regexp_request_string_string", []*cel.Type{cel.StringType, cel.StringType}, func(data ref.Val) (RequestMatcherWithError, error) { - refStringList := reflect.TypeOf([]string{}) + refStringList := stringSliceType params, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -803,7 +801,7 @@ func (MatchMethod) CELLibrary(_ caddy.Context) (cel.Library, error) { "method_request_list", []*cel.Type{cel.ListType(cel.StringType)}, func(data ref.Val) (RequestMatcherWithError, error) { - refStringList := reflect.TypeOf([]string{}) + refStringList := stringSliceType strList, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -1174,7 +1172,7 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { "header_regexp_request_string_string", []*cel.Type{cel.StringType, cel.StringType}, func(data ref.Val) (RequestMatcherWithError, error) { - refStringList := reflect.TypeOf([]string{}) + refStringList := stringSliceType params, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -1197,7 +1195,7 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { "header_regexp_request_string_string_string", []*cel.Type{cel.StringType, cel.StringType, cel.StringType}, func(data ref.Val) (RequestMatcherWithError, error) { - refStringList := reflect.TypeOf([]string{}) + refStringList := stringSliceType params, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -1547,7 +1545,7 @@ 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]any) diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index f7be6909e..b15b6316d 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -947,7 +947,7 @@ 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++ { + for b.Loop() { match.MatchWithError(req) } } @@ -992,8 +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() // compile the regexp and validate its name @@ -1180,8 +1178,7 @@ func BenchmarkLargeHostMatcher(b *testing.B) { b.Fatal(err) } - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { matcher.MatchWithError(req) } } @@ -1194,8 +1191,7 @@ func BenchmarkHostMatcherWithoutPlaceholder(b *testing.B) { match := MatchHost{"localhost"} - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { match.MatchWithError(req) } } @@ -1212,8 +1208,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++ { + for b.Loop() { match.MatchWithError(req) } } diff --git a/modules/caddyhttp/metrics_test.go b/modules/caddyhttp/metrics_test.go index 4a0519b87..4e1aa8b30 100644 --- a/modules/caddyhttp/metrics_test.go +++ b/modules/caddyhttp/metrics_test.go @@ -9,8 +9,9 @@ import ( "sync" "testing" - "github.com/caddyserver/caddy/v2" "github.com/prometheus/client_golang/prometheus/testutil" + + "github.com/caddyserver/caddy/v2" ) func TestServerNameFromContext(t *testing.T) { 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 776aa6294..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,8 @@ 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 @@ -283,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) @@ -298,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 @@ -363,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 830050416..65b09ded0 100644 --- a/modules/caddyhttp/requestbody/requestbody.go +++ b/modules/caddyhttp/requestbody/requestbody.go @@ -18,6 +18,7 @@ import ( "errors" "io" "net/http" + "strings" "time" "go.uber.org/zap" @@ -43,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 } @@ -60,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) } @@ -99,7 +116,7 @@ func (ew errorWrapper) Read(p []byte) (n int, err error) { if errors.As(err, &mbe) { err = caddyhttp.Error(http.StatusRequestEntityTooLarge, err) } - return + return n, err } // Interface guard 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..3f6ffa8cb 100644 --- a/modules/caddyhttp/reverseproxy/command.go +++ b/modules/caddyhttp/reverseproxy/command.go @@ -75,8 +75,8 @@ For proxying: cmd.Flags().BoolP("insecure", "", false, "Disable TLS verification (WARNING: DISABLES SECURITY BY NOT VERIFYING TLS CERTIFICATES!)") cmd.Flags().BoolP("disable-redirects", "r", false, "Disable HTTP->HTTPS redirects") cmd.Flags().BoolP("internal-certs", "i", false, "Use internal CA for issuing certs") - cmd.Flags().StringSliceP("header-up", "H", []string{}, "Set a request header to send to the upstream (format: \"Field: value\")") - cmd.Flags().StringSliceP("header-down", "d", []string{}, "Set a response header to send back to the client (format: \"Field: value\")") + cmd.Flags().StringArrayP("header-up", "H", []string{}, "Set a request header to send to the upstream (format: \"Field: value\")") + cmd.Flags().StringArrayP("header-down", "d", []string{}, "Set a response header to send back to the client (format: \"Field: value\")") cmd.Flags().BoolP("access-log", "", false, "Enable the access log") cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs") cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdReverseProxy) @@ -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 } } @@ -181,7 +182,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) { } // set up header_up - headerUp, err := fs.GetStringSlice("header-up") + headerUp, err := fs.GetStringArray("header-up") if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err) } @@ -203,7 +204,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) { } // set up header_down - headerDown, err := fs.GetStringSlice("header-down") + headerDown, err := fs.GetStringArray("header-down") if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err) } diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go index 5db73a4a2..2325af9a7 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go @@ -17,6 +17,7 @@ package fastcgi import ( "encoding/json" "net/http" + "slices" "strconv" "strings" @@ -314,7 +315,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error // if the index is turned off, we skip the redirect and try_files if indexFile != "off" { - dirRedir := false + var dirRedir bool dirIndex := "{http.request.uri.path}/" + indexFile tryPolicy := "first_exist_fallback" @@ -328,13 +329,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error tryPolicy = "" } - for _, tf := range tryFiles { - if tf == dirIndex { - dirRedir = true - - break - } - } + dirRedir = slices.Contains(tryFiles, dirIndex) } if dirRedir { diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client.go b/modules/caddyhttp/reverseproxy/fastcgi/client.go index 684394f53..48599c27f 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/client.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/client.go @@ -154,13 +154,13 @@ func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error) err = writer.writeBeginRequest(uint16(Responder), 0) if err != nil { - return + return r, err } writer.recType = Params err = writer.writePairs(p) if err != nil { - return + return r, err } writer.recType = Stdin @@ -176,7 +176,7 @@ func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error) } r = &streamReader{c: c} - return + return r, err } // clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer @@ -213,7 +213,7 @@ func (f clientCloser) Close() error { func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) { r, err := c.Do(p, req) if err != nil { - return + return resp, err } rb := bufio.NewReader(r) @@ -223,7 +223,7 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons // Parse the response headers. mimeHeader, err := tp.ReadMIMEHeader() if err != nil && err != io.EOF { - return + return resp, err } resp.Header = http.Header(mimeHeader) @@ -231,7 +231,7 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons statusNumber, statusInfo, statusIsCut := strings.Cut(resp.Header.Get("Status"), " ") resp.StatusCode, err = strconv.Atoi(statusNumber) if err != nil { - return + return resp, err } if statusIsCut { resp.Status = statusInfo @@ -260,7 +260,7 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons } resp.Body = closer - return + return resp, err } // Get issues a GET request to the fcgi responder. @@ -329,7 +329,7 @@ func (c *client) PostFile(p map[string]string, data url.Values, file map[string] for _, v0 := range val { err = writer.WriteField(key, v0) if err != nil { - return + return resp, err } } } @@ -347,13 +347,13 @@ func (c *client) PostFile(p map[string]string, data url.Values, file map[string] } _, err = io.Copy(part, fd) if err != nil { - return + return resp, err } } err = writer.Close() if err != nil { - return + return resp, err } return c.Post(p, "POST", bodyType, buf, int64(buf.Len())) diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client_test.go b/modules/caddyhttp/reverseproxy/fastcgi/client_test.go index 14a1cf684..f850cfb9d 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/client_test.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/client_test.go @@ -120,7 +120,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[ conn, err := net.Dial("tcp", ipPort) if err != nil { log.Println("err:", err) - return + return content } fcgi := client{rwc: conn, reqID: 1} @@ -162,7 +162,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[ if err != nil { log.Println("err:", err) - return + return content } defer resp.Body.Close() @@ -176,7 +176,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[ globalt.Error("Server return failed message") } - return + return content } func generateRandFile(size int) (p string, m string) { @@ -206,7 +206,7 @@ func generateRandFile(size int) (p string, m string) { } } m = fmt.Sprintf("%x", h.Sum(nil)) - return + return p, m } func DisabledTest(t *testing.T) { diff --git a/modules/caddyhttp/reverseproxy/fastcgi/record.go b/modules/caddyhttp/reverseproxy/fastcgi/record.go index 46c1f17bb..57d8d83ee 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/record.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/record.go @@ -30,23 +30,23 @@ func (rec *record) fill(r io.Reader) (err error) { rec.lr.N = rec.padding rec.lr.R = r if _, err = io.Copy(io.Discard, rec); err != nil { - return + return err } if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil { - return + return err } if rec.h.Version != 1 { err = errors.New("fcgi: invalid header version") - return + return err } if rec.h.Type == EndRequest { err = io.EOF - return + return err } rec.lr.N = int64(rec.h.ContentLength) rec.padding = int64(rec.h.PaddingLength) - return + return err } func (rec *record) Read(p []byte) (n int, err error) { diff --git a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go index 347f6dfbf..f838c8702 100644 --- a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go @@ -84,7 +84,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) // create the reverse proxy handler rpHandler := &reverseproxy.Handler{ // set up defaults for header_up; reverse_proxy already deals with - // adding the other three X-Forwarded-* headers, but for this flow, + // adding the other three X-Forwarded-* headers, but for this flow, // we want to also send along the incoming method and URI since this // request will have a rewritten URI and method. Headers: &headers.Handler{ diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index f0ffee5b8..ac42570b2 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -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..6d35ed821 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 @@ -283,3 +281,7 @@ const proxyProtocolInfoVarKey = "reverse_proxy.proxy_protocol_info" type ProxyProtocolInfo struct { AddrPort netip.AddrPort } + +// tlsH1OnlyVarKey is the key used that indicates the connection will use h1 only for TLS. +// https://github.com/caddyserver/caddy/issues/7292 +const tlsH1OnlyVarKey = "reverse_proxy.tls_h1_only" diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 910033ca1..3031bda46 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,6 +143,22 @@ 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.Transport // TODO: EXPERIMENTAL (May 2024) } @@ -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,60 @@ 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, "") + + // h1 only + if caddyhttp.GetVar(ctx, tlsH1OnlyVarKey) == true { + // stdlib does this + // https://github.com/golang/go/blob/4837fbe4145cd47b43eed66fee9eed9c2b988316/src/net/http/transport.go#L1701 + tlsConfig.NextProtos = nil + } + + 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 } @@ -429,45 +516,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 +534,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 +561,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. @@ -628,7 +673,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/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 5bdafa070..0c4028ce7 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -409,12 +409,16 @@ 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 + + // websocket over http2 or http3 if extended connect is enabled, assuming backend doesn't support this, the request will be modified to http1.1 upgrade + // Both use the same upgrade mechanism: server advertizes extended connect support, and client sends the pseudo header :protocol in a CONNECT request + // The quic-go http3 implementation also puts :protocol in r.Proto for CONNECT requests (quic-go/http3/headers.go@70-72,185,203) // 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" { + if (r.ProtoMajor == 2 && r.Method == http.MethodConnect && r.Header.Get(":protocol") == "websocket") || + (r.ProtoMajor == 3 && r.Method == http.MethodConnect && r.Proto == "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) + caddyhttp.SetVar(clonedReq.Context(), "extended_connect_websocket_body", clonedReq.Body) clonedReq.Body = http.NoBody clonedReq.Method = http.MethodGet clonedReq.Header.Set("Upgrade", "websocket") @@ -532,7 +536,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) } @@ -726,6 +730,12 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http. proxyProtocolInfo := ProxyProtocolInfo{AddrPort: addrPort} caddyhttp.SetVar(req.Context(), proxyProtocolInfoVarKey, proxyProtocolInfo) + // some of the outbound requests require h1 (e.g. websocket) + // https://github.com/golang/go/blob/4837fbe4145cd47b43eed66fee9eed9c2b988316/src/net/http/request.go#L1579 + if isWebsocket(req) { + caddyhttp.SetVar(req.Context(), tlsH1OnlyVarKey, true) + } + // Add the supported X-Forwarded-* headers err = h.addForwardedHeaders(req) if err != nil { @@ -1150,7 +1160,7 @@ 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 @@ -1356,7 +1366,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) } diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go index fcf7f90f6..585fc3400 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go @@ -219,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() { @@ -808,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/streaming.go b/modules/caddyhttp/reverseproxy/streaming.go index d697eb402..66dd106d5 100644 --- a/modules/caddyhttp/reverseproxy/streaming.go +++ b/modules/caddyhttp/reverseproxy/streaming.go @@ -94,9 +94,9 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup, conn io.ReadWriteCloser brw *bufio.ReadWriter ) - // websocket over http2, assuming backend doesn't support this, the request will be modified to http1.1 upgrade + // websocket over http2 or http3 if extended connect is enabled, 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 { + if body, ok := caddyhttp.GetVar(req.Context(), "extended_connect_websocket_body").(io.ReadCloser); ok { req.Body = body rw.Header().Del("Upgrade") rw.Header().Del("Connection") @@ -146,7 +146,7 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup, // 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(): @@ -588,11 +588,11 @@ func (m *maxLatencyWriter) Write(p []byte) (n int, err error) { m.logger.Debug("flushing immediately") //nolint:errcheck m.flush() - return + return n, err } if m.flushPending { m.logger.Debug("delayed flush already pending") - return + return n, err } if m.t == nil { m.t = time.AfterFunc(m.latency, m.delayedFlush) @@ -603,7 +603,7 @@ func (m *maxLatencyWriter) Write(p []byte) (n int, err error) { c.Write(zap.Duration("duration", m.latency)) } m.flushPending = true - return + return n, err } func (m *maxLatencyWriter) delayedFlush() { diff --git a/modules/caddyhttp/reverseproxy/upstreams.go b/modules/caddyhttp/reverseproxy/upstreams.go index aa59dc41b..e9eb7e60a 100644 --- a/modules/caddyhttp/reverseproxy/upstreams.go +++ b/modules/caddyhttp/reverseproxy/upstreams.go @@ -213,12 +213,12 @@ func (su SRVUpstreams) expandedAddr(r *http.Request) (addr, service, proto, name name = repl.ReplaceAll(su.Name, "") if su.Service == "" && su.Proto == "" { addr = name - return + return addr, service, proto, name } service = repl.ReplaceAll(su.Service, "") proto = repl.ReplaceAll(su.Proto, "") addr = su.formattedAddr(service, proto, name) - return + return addr, service, proto, name } // formattedAddr the RFC 2782 representation of the SRV domain, in 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/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go index 31ebfb430..2b18744db 100644 --- a/modules/caddyhttp/rewrite/rewrite.go +++ b/modules/caddyhttp/rewrite/rewrite.go @@ -377,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/routes.go b/modules/caddyhttp/routes.go index ccb5f2515..3dd770938 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -302,13 +302,7 @@ func wrapRoute(route Route) Middleware { // wrapMiddleware wraps mh such that it can be correctly // appended to a list of middleware in preparation for -// compiling into a handler chain. We can't do this inline -// inside a loop, because it relies on a reference to mh -// not changing until the execution of its handler (which -// is deferred by multiple func closures). In other words, -// 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. +// compiling into a handler chain. func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler, metrics *Metrics) Middleware { handlerToUse := mh if metrics != nil { @@ -317,18 +311,12 @@ func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler, metrics *Metrics) M } return func(next Handler) Handler { - // copy the next handler (it's an interface, so it's - // just a very lightweight copy of a pointer); this - // is a safeguard against the handler changing the - // value, which could affect future requests (yikes) - nextCopy := next - return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { // EXPERIMENTAL: Trace each module that gets invoked if server, ok := r.Context().Value(ServerCtxKey).(*Server); ok && server != nil { server.logTrace(handlerToUse) } - return handlerToUse.ServeHTTP(w, r, nextCopy) + return handlerToUse.ServeHTTP(w, r, next) }) } } diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index a2b29d658..ac30f4028 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -76,9 +76,25 @@ type Server struct { // KeepAliveInterval is the interval at which TCP keepalive packets // are sent to keep the connection alive at the TCP layer when no other - // data is being transmitted. The default is 15s. + // data is being transmitted. + // If zero, the default is 15s. + // If negative, keepalive packets are not sent and other keepalive parameters + // are ignored. KeepAliveInterval caddy.Duration `json:"keepalive_interval,omitempty"` + // KeepAliveIdle is the time that the connection must be idle before + // the first TCP keep-alive probe is sent when no other data is being + // transmitted. + // If zero, the default is 15s. + // If negative, underlying socket value is unchanged. + KeepAliveIdle caddy.Duration `json:"keepalive_idle,omitempty"` + + // KeepAliveCount is the maximum number of TCP keep-alive probes that + // should be sent before dropping a connection. + // If zero, the default is 9. + // If negative, underlying socket value is unchanged. + KeepAliveCount int `json:"keepalive_count,omitempty"` + // MaxHeaderBytes is the maximum size to parse from a client's // HTTP request headers. MaxHeaderBytes int `json:"max_header_bytes,omitempty"` @@ -186,6 +202,13 @@ type Server struct { // This option is disabled by default. TrustedProxiesStrict int `json:"trusted_proxies_strict,omitempty"` + // If greater than zero, enables trusting socket connections + // (e.g. Unix domain sockets) as coming from a trusted + // proxy. + // + // This option is disabled by default. + TrustedProxiesUnix bool `json:"trusted_proxies_unix,omitempty"` + // Enables access logging and configures how access logs are handled // in this server. To minimally enable access logs, simply set this // to a non-null, empty struct. @@ -235,7 +258,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 @@ -245,10 +269,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 @@ -265,14 +288,9 @@ 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. 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 { - r.TLS = new(tls.ConnectionState) - *r.TLS = csc.ConnectionState() - } + if tlsConnStateFunc, ok := r.Context().Value(tlsConnectionStateFuncCtxKey).(func() *tls.ConnectionState); ok { + r.TLS = tlsConnStateFunc() } } @@ -626,6 +644,8 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error } } + s.quicListeners = append(s.quicListeners, h3ln) + //nolint:errcheck go s.h3server.ServeListener(h3ln) @@ -923,6 +943,17 @@ func determineTrustedProxy(r *http.Request, s *Server) (bool, string) { return false, "" } + if s.TrustedProxiesUnix && r.RemoteAddr == "@" { + if s.TrustedProxiesStrict > 0 { + ipRanges := []netip.Prefix{} + if s.trustedProxies != nil { + ipRanges = s.trustedProxies.GetIPRanges(r) + } + return true, strictUntrustedClientIp(r, s.ClientIPHeaders, ipRanges, "@") + } else { + return true, trustedRealClientIP(r, s.ClientIPHeaders, "@") + } + } // Parse the remote IP, ignore the error as non-fatal, // but the remote IP is required to continue, so we // just return early. This should probably never happen @@ -982,10 +1013,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 { @@ -1079,9 +1110,14 @@ const ( // originally came into the server's entry handler OriginalRequestCtxKey caddy.CtxKey = "original_request" - // For referencing underlying net.Conn + // DEPRECATED: not used anymore. + // To refer to the underlying connection, implement a middleware plugin + // that RegisterConnContext during provisioning. ConnCtxKey caddy.CtxKey = "conn" + // used to get the tls connection state in the context, if available + tlsConnectionStateFuncCtxKey caddy.CtxKey = "tls_connection_state_func" + // 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 53f35368f..eecb392e4 100644 --- a/modules/caddyhttp/server_test.go +++ b/modules/caddyhttp/server_test.go @@ -116,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) } } @@ -139,9 +137,7 @@ 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) } } @@ -165,12 +161,11 @@ func BenchmarkServer_LogRequest_WithTrace(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" @@ -302,6 +297,39 @@ func TestServer_DetermineTrustedProxy_TrustedLoopback(t *testing.T) { assert.Equal(t, clientIP, "31.40.0.10") } +func TestServer_DetermineTrustedProxy_UnixSocket(t *testing.T) { + server := &Server{ + ClientIPHeaders: []string{"X-Forwarded-For"}, + TrustedProxiesUnix: true, + } + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "@" + req.Header.Set("X-Forwarded-For", "2.2.2.2, 3.3.3.3") + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.True(t, trusted) + assert.Equal(t, "2.2.2.2", clientIP) +} + +func TestServer_DetermineTrustedProxy_UnixSocketStrict(t *testing.T) { + server := &Server{ + ClientIPHeaders: []string{"X-Forwarded-For"}, + TrustedProxiesUnix: true, + TrustedProxiesStrict: 1, + } + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "@" + req.Header.Set("X-Forwarded-For", "2.2.2.2, 3.3.3.3") + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.True(t, trusted) + assert.Equal(t, "3.3.3.3", clientIP) +} + func TestServer_DetermineTrustedProxy_UntrustedPrefix(t *testing.T) { loopbackPrefix, _ := netip.ParsePrefix("127.0.0.1/8") diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go index 1b93ede4b..d783d1b04 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" @@ -78,7 +79,7 @@ Response headers may be added using the --header flag for each header field. cmd.Flags().StringP("body", "b", "", "The body of the HTTP response") cmd.Flags().BoolP("access-log", "", false, "Enable the access log") cmd.Flags().BoolP("debug", "v", false, "Enable more verbose debug-level logging") - cmd.Flags().StringSliceP("header", "H", []string{}, "Set a header on the response (format: \"Field: value\")") + cmd.Flags().StringArrayP("header", "H", []string{}, "Set a header on the response (format: \"Field: value\")") cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdRespond) }, }) @@ -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 != "" { @@ -364,7 +359,7 @@ func cmdRespond(fl caddycmd.Flags) (int, error) { } // build headers map - headers, err := fl.GetStringSlice("header") + headers, err := fl.GetStringArray("header") if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err) } 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 7ab891fc0..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{}) @@ -353,7 +355,7 @@ func (MatchVarsRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { "vars_regexp_request_string_string", []*cel.Type{cel.StringType, cel.StringType}, func(data ref.Val) (RequestMatcherWithError, error) { - refStringList := reflect.TypeOf([]string{}) + refStringList := stringSliceType params, err := data.ConvertToNative(refStringList) if err != nil { return nil, err @@ -376,7 +378,7 @@ func (MatchVarsRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { "vars_regexp_request_string_string_string", []*cel.Type{cel.StringType, cel.StringType, cel.StringType}, func(data ref.Val) (RequestMatcherWithError, error) { - refStringList := reflect.TypeOf([]string{}) + refStringList := stringSliceType params, err := data.ConvertToNative(refStringList) if err != nil { return nil, err diff --git a/modules/caddypki/acmeserver/caddyfile.go b/modules/caddypki/acmeserver/caddyfile.go index c4d85716f..a7dc8e337 100644 --- a/modules/caddypki/acmeserver/caddyfile.go +++ b/modules/caddypki/acmeserver/caddyfile.go @@ -91,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 { @@ -112,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/caddypki/adminapi.go b/modules/caddypki/adminapi.go index c454f6458..463e31f35 100644 --- a/modules/caddypki/adminapi.go +++ b/modules/caddypki/adminapi.go @@ -220,13 +220,13 @@ func (a *adminAPI) getCAFromAPIRequestPath(r *http.Request) (*CA, error) { func rootAndIntermediatePEM(ca *CA) (root, inter []byte, err error) { root, err = pemEncodeCert(ca.RootCertificate().Raw) if err != nil { - return + return root, inter, err } inter, err = pemEncodeCert(ca.IntermediateCertificate().Raw) if err != nil { - return + return root, inter, err } - return + return root, inter, err } // caInfo is the response structure for the CA info API endpoint. diff --git a/modules/caddypki/ca.go b/modules/caddypki/ca.go index 6c48da6f9..5b17518ca 100644 --- a/modules/caddypki/ca.go +++ b/modules/caddypki/ca.go @@ -124,8 +124,6 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error { } if ca.IntermediateLifetime == 0 { ca.IntermediateLifetime = caddy.Duration(defaultIntermediateLifetime) - } else if time.Duration(ca.IntermediateLifetime) >= defaultRootLifetime { - return fmt.Errorf("intermediate certificate lifetime must be less than root certificate lifetime (%s)", defaultRootLifetime) } // load the certs and key that will be used for signing @@ -144,6 +142,10 @@ func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error { if err != nil { return err } + actualRootLifetime := time.Until(rootCert.NotAfter) + if time.Duration(ca.IntermediateLifetime) >= actualRootLifetime { + return fmt.Errorf("intermediate certificate lifetime must be less than actual root certificate lifetime (%s)", actualRootLifetime) + } if ca.Intermediate != nil { interCert, interKey, err = ca.Intermediate.Load() } else { diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index f0965855a..7f13fd71f 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -106,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 @@ -194,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 } @@ -202,7 +205,7 @@ 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, @@ -216,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 @@ -229,6 +244,9 @@ func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) { template.DNS01Solver = iss.Challenges.DNS.solver } template.ListenHost = iss.Challenges.BindHost + if iss.Challenges.Distributed != nil { + template.DisableDistributedSolvers = !*iss.Challenges.Distributed + } } if iss.PreferredChains != nil { @@ -465,6 +483,20 @@ func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } iss.Challenges.TLSALPN.Disabled = true + case "distributed": + if !d.NextArg() { + return d.ArgErr() + } + if d.Val() != "false" { + return d.Errf("only accepted value is 'false'") + } + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.Distributed == nil { + iss.Challenges.Distributed = new(bool) + } + case "alt_http_port": if !d.NextArg() { return d.ArgErr() @@ -507,21 +539,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 1bc86020d..e69b5ad2f 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -28,6 +28,7 @@ import ( "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 @@ -384,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 @@ -452,6 +456,22 @@ type ChallengesConfig struct { // Optionally customize the host to which a listener // is bound if required for solving a challenge. BindHost string `json:"bind_host,omitempty"` + + // Whether distributed solving is enabled. This is + // enabled by default, so this is only used to + // disable it, which should only need to be done if + // you cannot reliably or affordably use storage + // backend for writing/distributing challenge info. + // (Applies to HTTP and TLS-ALPN challenges.) + // If set to false, challenges can only be solved + // from the Caddy instance that initiated the + // challenge, with the exception of HTTP challenges + // initiated with the same ACME account that this + // config uses. (Caddy can still solve those challenges + // without explicitly writing the info to storage.) + // + // Default: true + Distributed *bool `json:"distributed,omitempty"` } // HTTPChallengeConfig configures the ACME HTTP challenge. 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 b686090e6..724271a8e 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -24,6 +24,8 @@ import ( "fmt" "io" "os" + "reflect" + "slices" "strings" "github.com/mholt/acmez/v3" @@ -368,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) } @@ -461,6 +457,14 @@ func (p ConnectionPolicy) SettingsEmpty() bool { 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 { @@ -798,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 @@ -936,17 +944,10 @@ func setDefaultTLSParams(cfg *tls.Config) { cfg.CurvePreferences = defaultCurves } - if cfg.MinVersion == 0 { - // crypto/tls docs: - // "If EncryptedClientHelloKeys is set, MinVersion, if set, must be VersionTLS13." - if cfg.EncryptedClientHelloKeys == nil { - cfg.MinVersion = tls.VersionTLS12 - } else { - cfg.MinVersion = tls.VersionTLS13 - } - } - 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 } } @@ -988,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") @@ -998,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") @@ -1051,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 index 10f325565..1b3bacbd2 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -44,6 +44,10 @@ func init() { // 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 @@ -134,7 +138,6 @@ func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) { // 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)) @@ -275,7 +278,7 @@ func (t *TLS) publishECHConfigs() error { // 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", + 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 @@ -288,37 +291,78 @@ func (t *TLS) publishECHConfigs() error { } 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) - if err != nil { - t.logger.Error("publishing ECH configuration list", - zap.Strings("for_domains", publication.Domains), + + 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)) } - // update publication history, so that we don't unnecessarily republish every time - 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) + 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 { - cfg.meta.Publications[publisherKey][name] = pubTime + if _, ok := publishErrs[name]; !ok { + successNames = append(successNames, name) + } } - 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) + 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)) } } } @@ -381,27 +425,33 @@ func loadECHConfig(ctx caddy.Context, configID string) (echConfig, error) { return echConfig{}, nil } metaBytes, err := storage.Load(ctx, metaKey) - if err != nil { + 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 metadata (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr) + 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 metadata; deleted its config folder", + 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 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) + 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 } - 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 @@ -617,70 +667,96 @@ func (dnsPub ECHDNSPublisher) PublisherKey() string { return string(dnsPub.provider.(caddy.Module).CaddyModule().ID) } -// PublishECHConfigList publishes the given ECH config list to the given DNS names. +// 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 { - dnsPub.logger.Error("could not determine zone for domain", - zap.String("domain", domain), - zap.Error(err)) + errs[domain] = fmt.Errorf("could not determine zone for domain: %w (domain=%s nameservers=%v)", err, domain, nameservers) continue } - // get any existing HTTPS record for this domain, and augment - // our ech SvcParamKey with any other existing SvcParams + 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 { - dnsPub.logger.Error("unable to get existing DNS records to publish ECH data to HTTPS DNS record", - zap.String("domain", domain), - zap.Error(err)) + errs[domain] = fmt.Errorf("unable to get existing DNS records to publish ECH data to HTTPS DNS record: %w", err) continue } - relName := libdns.RelativeName(domain+".", zone) - var httpsRec libdns.Record + var httpsRec libdns.ServiceBinding + var nameHasExistingRecord bool for _, rec := range recs { - if rec.Name == relName && rec.Type == "HTTPS" && (rec.Target == "" || rec.Target == ".") { - httpsRec = rec + 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 + } + } } } - params := make(svcParams) - if httpsRec.Value != "" { - params, err = parseSvcParams(httpsRec.Value) - if err != nil { - dnsPub.logger.Error("unable to parse existing DNS record to publish ECH data to HTTPS DNS record", - zap.String("domain", domain), - zap.String("https_rec_value", httpsRec.Value), - zap.Error(err)) - continue - } + 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 + // 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) - Type: "HTTPS", + Scheme: "https", Name: relName, - Priority: 2, // allows a manual override with priority 1 + 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: ".", - Value: params.String(), - TTL: 1 * time.Minute, // TODO: for testing only + Params: params, }, }) if err != nil { - dnsPub.logger.Error("unable to publish ECH data to HTTPS DNS record", - zap.String("domain", domain), - zap.Error(err)) + 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 } @@ -906,172 +982,6 @@ func newECHConfigID(ctx caddy.Context) (uint8, error) { return 0, fmt.Errorf("depleted attempts to find an available config_id") } -// svcParams represents SvcParamKey and SvcParamValue pairs as -// described in https://www.rfc-editor.org/rfc/rfc9460 (section 2.1). -type svcParams map[string][]string - -// parseSvcParams parses service parameters into a structured type -// for safer manipulation. -func parseSvcParams(input string) (svcParams, error) { - if len(input) > 4096 { - return nil, fmt.Errorf("input too long: %d", len(input)) - } - - params := make(svcParams) - input = strings.TrimSpace(input) + " " - - for cursor := 0; cursor < len(input); cursor++ { - var key, rawVal string - - keyValPair: - for i := cursor; i < len(input); i++ { - switch input[i] { - case '=': - key = strings.ToLower(strings.TrimSpace(input[cursor:i])) - i++ - cursor = i - - var quoted bool - if input[cursor] == '"' { - quoted = true - i++ - cursor = i - } - - var escaped bool - - for j := cursor; j < len(input); j++ { - switch input[j] { - case '"': - if !quoted { - return nil, fmt.Errorf("illegal DQUOTE at position %d", j) - } - if !escaped { - // end of quoted value - rawVal = input[cursor:j] - j++ - cursor = j - break keyValPair - } - case '\\': - escaped = true - case ' ', '\t', '\n', '\r': - if !quoted { - // end of unquoted value - rawVal = input[cursor:j] - cursor = j - break keyValPair - } - default: - escaped = false - } - } - - case ' ', '\t', '\n', '\r': - // key with no value (flag) - key = input[cursor:i] - params[key] = []string{} - cursor = i - break keyValPair - } - } - - if rawVal == "" { - continue - } - - var sb strings.Builder - - var escape int // start of escape sequence (after \, so 0 is never a valid start) - for i := 0; i < len(rawVal); i++ { - ch := rawVal[i] - if escape > 0 { - // validate escape sequence - // (RFC 9460 Appendix A) - // escaped: "\" ( non-digit / dec-octet ) - // non-digit: "%x21-2F / %x3A-7E" - // dec-octet: "0-255 as a 3-digit decimal number" - if ch >= '0' && ch <= '9' { - // advance to end of decimal octet, which must be 3 digits - i += 2 - if i > len(rawVal) { - return nil, fmt.Errorf("value ends with incomplete escape sequence: %s", rawVal[escape:]) - } - decOctet, err := strconv.Atoi(rawVal[escape : i+1]) - if err != nil { - return nil, err - } - if decOctet < 0 || decOctet > 255 { - return nil, fmt.Errorf("invalid decimal octet in escape sequence: %s (%d)", rawVal[escape:i], decOctet) - } - sb.WriteRune(rune(decOctet)) - escape = 0 - continue - } else if (ch < 0x21 || ch > 0x2F) && (ch < 0x3A && ch > 0x7E) { - return nil, fmt.Errorf("illegal escape sequence %s", rawVal[escape:i]) - } - } - switch ch { - case ';', '(', ')': - // RFC 9460 Appendix A: - // > contiguous = 1*( non-special / escaped ) - // > non-special is VCHAR minus DQUOTE, ";", "(", ")", and "\". - return nil, fmt.Errorf("illegal character in value %q at position %d: %s", rawVal, i, string(ch)) - case '\\': - escape = i + 1 - default: - sb.WriteByte(ch) - escape = 0 - } - } - - params[key] = strings.Split(sb.String(), ",") - } - - return params, nil -} - -// String serializes svcParams into zone presentation format. -func (params svcParams) String() string { - var sb strings.Builder - for key, vals := range params { - if sb.Len() > 0 { - sb.WriteRune(' ') - } - sb.WriteString(key) - var hasVal, needsQuotes bool - for _, val := range vals { - if len(val) > 0 { - hasVal = true - } - if strings.ContainsAny(val, `" `) { - needsQuotes = true - } - if hasVal && needsQuotes { - break - } - } - if hasVal { - sb.WriteRune('=') - } - if needsQuotes { - sb.WriteRune('"') - } - for i, val := range vals { - if i > 0 { - sb.WriteRune(',') - } - val = strings.ReplaceAll(val, `"`, `\"`) - val = strings.ReplaceAll(val, `,`, `\,`) - sb.WriteString(val) - } - if needsQuotes { - sb.WriteRune('"') - } - } - return sb.String() -} - // ECHPublisher is an interface for publishing ECHConfigList values // so that they can be used by clients. type ECHPublisher interface { @@ -1080,13 +990,36 @@ type ECHPublisher interface { // It is used to prevent duplicating publications. PublisherKey() string - // Publishes the ECH config list 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. + // 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"` diff --git a/modules/caddytls/ech_test.go b/modules/caddytls/ech_test.go deleted file mode 100644 index b722d2fbf..000000000 --- a/modules/caddytls/ech_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package caddytls - -import ( - "reflect" - "testing" -) - -func TestParseSvcParams(t *testing.T) { - for i, test := range []struct { - input string - expect svcParams - shouldErr bool - }{ - { - input: `alpn="h2,h3" no-default-alpn ipv6hint=2001:db8::1 port=443`, - expect: svcParams{ - "alpn": {"h2", "h3"}, - "no-default-alpn": {}, - "ipv6hint": {"2001:db8::1"}, - "port": {"443"}, - }, - }, - { - input: `key=value quoted="some string" flag`, - expect: svcParams{ - "key": {"value"}, - "quoted": {"some string"}, - "flag": {}, - }, - }, - { - input: `key="nested \"quoted\" value,foobar"`, - expect: svcParams{ - "key": {`nested "quoted" value`, "foobar"}, - }, - }, - { - input: `alpn=h3,h2 tls-supported-groups=29,23 no-default-alpn ech="foobar"`, - expect: svcParams{ - "alpn": {"h3", "h2"}, - "tls-supported-groups": {"29", "23"}, - "no-default-alpn": {}, - "ech": {"foobar"}, - }, - }, - { - input: `escape=\097`, - expect: svcParams{ - "escape": {"a"}, - }, - }, - { - input: `escapes=\097\098c`, - expect: svcParams{ - "escapes": {"abc"}, - }, - }, - } { - actual, err := parseSvcParams(test.input) - if err != nil && !test.shouldErr { - t.Errorf("Test %d: Expected no error, but got: %v (input=%q)", i, err, test.input) - continue - } else if err == nil && test.shouldErr { - t.Errorf("Test %d: Expected an error, but got no error (input=%q)", i, test.input) - continue - } - if !reflect.DeepEqual(test.expect, actual) { - t.Errorf("Test %d: Expected %v, got %v (input=%q)", i, test.expect, actual, test.input) - continue - } - } -} - -func TestSvcParamsString(t *testing.T) { - // this test relies on the parser also working - // because we can't just compare string outputs - // since map iteration is unordered - for i, test := range []svcParams{ - - { - "alpn": {"h2", "h3"}, - "no-default-alpn": {}, - "ipv6hint": {"2001:db8::1"}, - "port": {"443"}, - }, - - { - "key": {"value"}, - "quoted": {"some string"}, - "flag": {}, - }, - { - "key": {`nested "quoted" value`, "foobar"}, - }, - { - "alpn": {"h3", "h2"}, - "tls-supported-groups": {"29", "23"}, - "no-default-alpn": {}, - "ech": {"foobar"}, - }, - } { - combined := test.String() - parsed, err := parseSvcParams(combined) - if err != nil { - t.Errorf("Test %d: Expected no error, but got: %v (input=%q)", i, err, test) - continue - } - if len(parsed) != len(test) { - t.Errorf("Test %d: Expected %d keys, but got %d", i, len(test), len(parsed)) - continue - } - for key, expectedVals := range test { - if expected, actual := len(expectedVals), len(parsed[key]); expected != actual { - t.Errorf("Test %d: Expected key %s to have %d values, but had %d", i, key, expected, actual) - continue - } - for j, expected := range expectedVals { - if actual := parsed[key][j]; actual != expected { - t.Errorf("Test %d key %q value %d: Expected '%s' but got '%s'", i, key, j, expected, actual) - continue - } - } - } - if !reflect.DeepEqual(parsed, test) { - t.Errorf("Test %d: Expected %#v, got %#v", i, test, combined) - continue - } - } -} 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/tls.go b/modules/caddytls/tls.go index 3b04b8785..7b49c0208 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -33,6 +33,7 @@ import ( "go.uber.org/zap/zapcore" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/internal" "github.com/caddyserver/caddy/v2/modules/caddyevents" ) @@ -55,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 @@ -123,7 +126,7 @@ type TLS struct { 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{} @@ -218,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 = append(t.automateNames, subjects...) } else { return fmt.Errorf("loading certificates with 'automate' requires array of strings, got: %T", modIface) } @@ -262,6 +266,18 @@ 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) @@ -271,7 +287,7 @@ func (t *TLS) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("provisioning default public automation policy: %v", err) } - for _, n := range t.automateNames { + 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!) @@ -294,18 +310,6 @@ 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) - } - // 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) @@ -339,8 +343,14 @@ func (t *TLS) Provision(ctx caddy.Context) error { // outer names should have certificates to reduce client brittleness for _, outerName := range outerNames { + if outerName == "" { + continue + } if !t.HasCertificateForSubject(outerName) { - t.automateNames = append(t.automateNames, outerNames...) + if t.automateNames == nil { + t.automateNames = make(map[string]struct{}) + } + t.automateNames[outerName] = struct{}{} } } } @@ -449,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. @@ -467,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{}{} } } } @@ -488,7 +499,7 @@ 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), ) } @@ -509,17 +520,31 @@ func (t *TLS) Cleanup() error { 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 @@ -530,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) } @@ -555,6 +580,43 @@ 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 @@ -563,7 +625,7 @@ func (t *TLS) Manage(names []string) error { // (keeping only the hotsname) and filters IP addresses, which can't be // used with ECH. // -// EXPERIMENTAL: This function and its behavior are subject to change. +// EXPERIMENTAL: This function and its semantics/behavior are subject to change. func (t *TLS) RegisterServerNames(dnsNames []string) { t.serverNamesMu.Lock() for _, name := range dnsNames { 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 0999bbfb2..c3df562cb 100644 --- a/modules/logging/filewriter.go +++ b/modules/logging/filewriter.go @@ -22,9 +22,11 @@ import ( "os" "path/filepath" "strconv" + "strings" + "time" + "github.com/DeRuina/timberjack" "github.com/dustin/go-humanize" - "gopkg.in/natefinch/lumberjack.v2" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" @@ -96,6 +98,21 @@ type FileWriter struct { // it will be rotated. RollSizeMB int `json:"roll_size_mb,omitempty"` + // Roll log file after some time + RollInterval time.Duration `json:"roll_interval,omitempty"` + + // Roll log file at fix minutes + // For example []int{0, 30} will roll file at xx:00 and xx:30 each hour + // Invalid value are ignored with a warning on stderr + // See https://github.com/DeRuina/timberjack#%EF%B8%8F-rotation-notes--warnings for caveats + RollAtMinutes []int `json:"roll_minutes,omitempty"` + + // Roll log file at fix time + // For example []string{"00:00", "12:00"} will roll file at 00:00 and 12:00 each day + // Invalid value are ignored with a warning on stderr + // See https://github.com/DeRuina/timberjack#%EF%B8%8F-rotation-notes--warnings for caveats + RollAt []string `json:"roll_at,omitempty"` + // Whether to compress rolled files. Default: true RollCompress *bool `json:"roll_gzip,omitempty"` @@ -109,6 +126,11 @@ type FileWriter struct { // How many days to keep rolled log files. Default: 90 RollKeepDays int `json:"roll_keep_days,omitempty"` + + // Rotated file will have format --.log + // Optional. If unset or invalid, defaults to 2006-01-02T15-04-05.000 (with fallback warning) + // must be a Go time compatible format, see https://pkg.go.dev/time#pkg-constants + BackupTimeFormat string `json:"backup_time_format,omitempty"` } // CaddyModule returns the Caddy module information. @@ -156,7 +178,7 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) { roll := fw.Roll == nil || *fw.Roll // 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) + // to restrictive if not set. (timberjack will reuse the file mode across log rotation) if err := os.MkdirAll(filepath.Dir(fw.Filename), 0o700); err != nil { return nil, err } @@ -166,7 +188,7 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) { } info, err := file.Stat() if roll { - file.Close() // lumberjack will reopen it on its own + file.Close() // timberjack will reopen it on its own } // Ensure already existing files have the right mode, since OpenFile will not set the mode in such case. @@ -201,13 +223,17 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) { 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, + return &timberjack.Logger{ + Filename: fw.Filename, + MaxSize: fw.RollSizeMB, + MaxAge: fw.RollKeepDays, + MaxBackups: fw.RollKeep, + LocalTime: fw.RollLocalTime, + Compress: *fw.RollCompress, + RotationInterval: fw.RollInterval, + RotateAtMinutes: fw.RollAtMinutes, + RotateAt: fw.RollAt, + BackupTimeFormat: fw.BackupTimeFormat, }, nil } @@ -314,6 +340,53 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } fw.RollKeepDays = int(math.Ceil(keepFor.Hours() / 24)) + case "roll_interval": + var durationStr string + if !d.AllArgs(&durationStr) { + return d.ArgErr() + } + duration, err := time.ParseDuration(durationStr) + if err != nil { + return d.Errf("parsing roll_interval duration: %v", err) + } + fw.RollInterval = duration + + case "roll_minutes": + var minutesArrayStr string + if !d.AllArgs(&minutesArrayStr) { + return d.ArgErr() + } + minutesStr := strings.Split(minutesArrayStr, ",") + minutes := make([]int, len(minutesStr)) + for i := range minutesStr { + ms := strings.Trim(minutesStr[i], " ") + m, err := strconv.Atoi(ms) + if err != nil { + return d.Errf("parsing roll_minutes number: %v", err) + } + minutes[i] = m + } + fw.RollAtMinutes = minutes + + case "roll_at": + var timeArrayStr string + if !d.AllArgs(&timeArrayStr) { + return d.ArgErr() + } + timeStr := strings.Split(timeArrayStr, ",") + times := make([]string, len(timeStr)) + for i := range timeStr { + times[i] = strings.Trim(timeStr[i], " ") + } + fw.RollAt = times + + case "backup_time_format": + var format string + if !d.AllArgs(&format) { + return d.ArgErr() + } + fw.BackupTimeFormat = format + default: return d.Errf("unrecognized subdirective '%s'", d.Val()) } diff --git a/modules/logging/filewriter_test.go b/modules/logging/filewriter_test.go index f9072f98a..2a246156c 100644 --- a/modules/logging/filewriter_test.go +++ b/modules/logging/filewriter_test.go @@ -317,7 +317,7 @@ func TestFileModeToJSON(t *testing.T) { }{ { name: "none zero", - mode: 0644, + mode: 0o644, want: `"0644"`, wantErr: false, }, @@ -358,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/filterencoder.go b/modules/logging/filterencoder.go index c46df0788..01333e195 100644 --- a/modules/logging/filterencoder.go +++ b/modules/logging/filterencoder.go @@ -152,6 +152,9 @@ func (fe *FilterEncoder) ConfigureDefaultFormat(wo caddy.WriterOpener) error { func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { d.Next() // consume encoder name + // Track regexp filters for automatic merging + regexpFilters := make(map[string][]*RegexpFilter) + // parse a field parseField := func() error { if fe.FieldsRaw == nil { @@ -171,6 +174,23 @@ func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if !ok { return d.Errf("module %s (%T) is not a logging.LogFieldFilter", moduleID, unm) } + + // Special handling for regexp filters to support multiple instances + if regexpFilter, isRegexp := filter.(*RegexpFilter); isRegexp { + regexpFilters[field] = append(regexpFilters[field], regexpFilter) + return nil // Don't set FieldsRaw yet, we'll merge them later + } + + // Check if we're trying to add a non-regexp filter to a field that already has regexp filters + if _, hasRegexpFilters := regexpFilters[field]; hasRegexpFilters { + return d.Errf("cannot mix regexp filters with other filter types for field %s", field) + } + + // Check if field already has a filter and it's not regexp-related + if _, exists := fe.FieldsRaw[field]; exists { + return d.Errf("field %s already has a filter; multiple non-regexp filters per field are not supported", field) + } + fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(filter, "filter", filterName, nil) return nil } @@ -210,6 +230,25 @@ func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } } } + + // After parsing all fields, merge multiple regexp filters into MultiRegexpFilter + for field, filters := range regexpFilters { + if len(filters) == 1 { + // Single regexp filter, use the original RegexpFilter + fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(filters[0], "filter", "regexp", nil) + } else { + // Multiple regexp filters, merge into MultiRegexpFilter + multiFilter := &MultiRegexpFilter{} + for _, regexpFilter := range filters { + err := multiFilter.AddOperation(regexpFilter.RawRegexp, regexpFilter.Value) + if err != nil { + return fmt.Errorf("adding regexp operation for field %s: %v", field, err) + } + } + fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(multiFilter, "filter", "multi_regexp", nil) + } + } + return nil } diff --git a/modules/logging/filters.go b/modules/logging/filters.go index 79d908fca..a2ce6502f 100644 --- a/modules/logging/filters.go +++ b/modules/logging/filters.go @@ -41,6 +41,7 @@ func init() { caddy.RegisterModule(CookieFilter{}) caddy.RegisterModule(RegexpFilter{}) caddy.RegisterModule(RenameFilter{}) + caddy.RegisterModule(MultiRegexpFilter{}) } // LogFieldFilter can filter (or manipulate) @@ -255,7 +256,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 { @@ -625,6 +626,222 @@ func (f *RegexpFilter) Filter(in zapcore.Field) zapcore.Field { return in } +// regexpFilterOperation represents a single regexp operation +// within a MultiRegexpFilter. +type regexpFilterOperation struct { + // The regular expression pattern defining what to replace. + RawRegexp string `json:"regexp,omitempty"` + + // The value to use as replacement + Value string `json:"value,omitempty"` + + regexp *regexp.Regexp +} + +// MultiRegexpFilter is a Caddy log field filter that +// can apply multiple regular expression replacements to +// the same field. This filter processes operations in the +// order they are defined, applying each regexp replacement +// sequentially to the result of the previous operation. +// +// This allows users to define multiple regexp filters for +// the same field without them overwriting each other. +// +// Security considerations: +// - Uses Go's regexp package (RE2 engine) which is safe from ReDoS attacks +// - Validates all patterns during provisioning +// - Limits the maximum number of operations to prevent resource exhaustion +// - Sanitizes input to prevent injection attacks +type MultiRegexpFilter struct { + // A list of regexp operations to apply in sequence. + // Maximum of 50 operations allowed for security and performance. + Operations []regexpFilterOperation `json:"operations"` +} + +// Security constants +const ( + maxRegexpOperations = 50 // Maximum operations to prevent resource exhaustion + maxPatternLength = 1000 // Maximum pattern length to prevent abuse +) + +// CaddyModule returns the Caddy module information. +func (MultiRegexpFilter) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "caddy.logging.encoders.filter.multi_regexp", + New: func() caddy.Module { return new(MultiRegexpFilter) }, + } +} + +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. +// Syntax: +// +// multi_regexp { +// regexp +// regexp +// ... +// } +func (f *MultiRegexpFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume filter name + for d.NextBlock(0) { + switch d.Val() { + case "regexp": + // Security check: limit number of operations + if len(f.Operations) >= maxRegexpOperations { + return d.Errf("too many regexp operations (maximum %d allowed)", maxRegexpOperations) + } + + op := regexpFilterOperation{} + if !d.NextArg() { + return d.ArgErr() + } + op.RawRegexp = d.Val() + + // Security validation: check pattern length + if len(op.RawRegexp) > maxPatternLength { + return d.Errf("regexp pattern too long (maximum %d characters)", maxPatternLength) + } + + // Security validation: basic pattern validation + if op.RawRegexp == "" { + return d.Errf("regexp pattern cannot be empty") + } + + if !d.NextArg() { + return d.ArgErr() + } + op.Value = d.Val() + f.Operations = append(f.Operations, op) + default: + return d.Errf("unrecognized subdirective %s", d.Val()) + } + } + + // Security check: ensure at least one operation is defined + if len(f.Operations) == 0 { + return d.Err("multi_regexp filter requires at least one regexp operation") + } + + return nil +} + +// Provision compiles all regexp patterns with security validation. +func (f *MultiRegexpFilter) Provision(ctx caddy.Context) error { + // Security check: validate operation count + if len(f.Operations) > maxRegexpOperations { + return fmt.Errorf("too many regexp operations: %d (maximum %d allowed)", len(f.Operations), maxRegexpOperations) + } + + if len(f.Operations) == 0 { + return fmt.Errorf("multi_regexp filter requires at least one operation") + } + + for i := range f.Operations { + // Security validation: pattern length check + if len(f.Operations[i].RawRegexp) > maxPatternLength { + return fmt.Errorf("regexp pattern %d too long: %d characters (maximum %d)", i, len(f.Operations[i].RawRegexp), maxPatternLength) + } + + // Security validation: empty pattern check + if f.Operations[i].RawRegexp == "" { + return fmt.Errorf("regexp pattern %d cannot be empty", i) + } + + // Compile and validate the pattern (uses RE2 engine - safe from ReDoS) + r, err := regexp.Compile(f.Operations[i].RawRegexp) + if err != nil { + return fmt.Errorf("compiling regexp pattern %d (%s): %v", i, f.Operations[i].RawRegexp, err) + } + f.Operations[i].regexp = r + } + return nil +} + +// Validate ensures the filter is properly configured with security checks. +func (f *MultiRegexpFilter) Validate() error { + if len(f.Operations) == 0 { + return fmt.Errorf("multi_regexp filter requires at least one operation") + } + + if len(f.Operations) > maxRegexpOperations { + return fmt.Errorf("too many regexp operations: %d (maximum %d allowed)", len(f.Operations), maxRegexpOperations) + } + + for i, op := range f.Operations { + if op.RawRegexp == "" { + return fmt.Errorf("regexp pattern %d cannot be empty", i) + } + if len(op.RawRegexp) > maxPatternLength { + return fmt.Errorf("regexp pattern %d too long: %d characters (maximum %d)", i, len(op.RawRegexp), maxPatternLength) + } + if op.regexp == nil { + return fmt.Errorf("regexp pattern %d not compiled (call Provision first)", i) + } + } + return nil +} + +// Filter applies all regexp operations sequentially to the input field. +// Input is sanitized and validated for security. +func (f *MultiRegexpFilter) Filter(in zapcore.Field) zapcore.Field { + if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok { + newArray := make(caddyhttp.LoggableStringArray, len(array)) + for i, s := range array { + newArray[i] = f.processString(s) + } + in.Interface = newArray + } else { + in.String = f.processString(in.String) + } + + return in +} + +// processString applies all regexp operations to a single string with input validation. +func (f *MultiRegexpFilter) processString(s string) string { + // Security: validate input string length to prevent resource exhaustion + const maxInputLength = 1000000 // 1MB max input size + if len(s) > maxInputLength { + // Log warning but continue processing (truncated) + s = s[:maxInputLength] + } + + result := s + for _, op := range f.Operations { + // Each regexp operation is applied sequentially + // Using RE2 engine which is safe from ReDoS attacks + result = op.regexp.ReplaceAllString(result, op.Value) + + // Ensure result doesn't exceed max length after each operation + if len(result) > maxInputLength { + result = result[:maxInputLength] + } + } + return result +} + +// AddOperation adds a single regexp operation to the filter with validation. +// This is used when merging multiple RegexpFilter instances. +func (f *MultiRegexpFilter) AddOperation(rawRegexp, value string) error { + // Security checks + if len(f.Operations) >= maxRegexpOperations { + return fmt.Errorf("cannot add operation: maximum %d operations allowed", maxRegexpOperations) + } + + if rawRegexp == "" { + return fmt.Errorf("regexp pattern cannot be empty") + } + + if len(rawRegexp) > maxPatternLength { + return fmt.Errorf("regexp pattern too long: %d characters (maximum %d)", len(rawRegexp), maxPatternLength) + } + + f.Operations = append(f.Operations, regexpFilterOperation{ + RawRegexp: rawRegexp, + Value: value, + }) + return nil +} + // RenameFilter is a Caddy log field filter that // renames the field's key with the indicated name. type RenameFilter struct { @@ -664,6 +881,7 @@ var ( _ LogFieldFilter = (*CookieFilter)(nil) _ LogFieldFilter = (*RegexpFilter)(nil) _ LogFieldFilter = (*RenameFilter)(nil) + _ LogFieldFilter = (*MultiRegexpFilter)(nil) _ caddyfile.Unmarshaler = (*DeleteFilter)(nil) _ caddyfile.Unmarshaler = (*HashFilter)(nil) @@ -673,9 +891,12 @@ var ( _ caddyfile.Unmarshaler = (*CookieFilter)(nil) _ caddyfile.Unmarshaler = (*RegexpFilter)(nil) _ caddyfile.Unmarshaler = (*RenameFilter)(nil) + _ caddyfile.Unmarshaler = (*MultiRegexpFilter)(nil) _ caddy.Provisioner = (*IPMaskFilter)(nil) _ caddy.Provisioner = (*RegexpFilter)(nil) + _ caddy.Provisioner = (*MultiRegexpFilter)(nil) _ caddy.Validator = (*QueryFilter)(nil) + _ caddy.Validator = (*MultiRegexpFilter)(nil) ) diff --git a/modules/logging/filters_test.go b/modules/logging/filters_test.go index 8f7ba0d70..42aa29757 100644 --- a/modules/logging/filters_test.go +++ b/modules/logging/filters_test.go @@ -1,11 +1,14 @@ package logging import ( + "fmt" + "strings" "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) { @@ -238,3 +241,198 @@ func TestHashFilterMultiValue(t *testing.T) { t.Fatalf("field entry 1 has not been filtered: %s", arr[1]) } } + +func TestMultiRegexpFilterSingleOperation(t *testing.T) { + f := MultiRegexpFilter{ + Operations: []regexpFilterOperation{ + {RawRegexp: `secret`, Value: "REDACTED"}, + }, + } + err := f.Provision(caddy.Context{}) + if err != nil { + t.Fatalf("unexpected error provisioning: %v", err) + } + + out := f.Filter(zapcore.Field{String: "foo-secret-bar"}) + if out.String != "foo-REDACTED-bar" { + t.Fatalf("field has not been filtered: %s", out.String) + } +} + +func TestMultiRegexpFilterMultipleOperations(t *testing.T) { + f := MultiRegexpFilter{ + Operations: []regexpFilterOperation{ + {RawRegexp: `secret`, Value: "REDACTED"}, + {RawRegexp: `password`, Value: "HIDDEN"}, + {RawRegexp: `token`, Value: "XXX"}, + }, + } + err := f.Provision(caddy.Context{}) + if err != nil { + t.Fatalf("unexpected error provisioning: %v", err) + } + + // Test sequential application + out := f.Filter(zapcore.Field{String: "my-secret-password-token-data"}) + expected := "my-REDACTED-HIDDEN-XXX-data" + if out.String != expected { + t.Fatalf("field has not been filtered correctly: got %s, expected %s", out.String, expected) + } +} + +func TestMultiRegexpFilterMultiValue(t *testing.T) { + f := MultiRegexpFilter{ + Operations: []regexpFilterOperation{ + {RawRegexp: `secret`, Value: "REDACTED"}, + {RawRegexp: `\d+`, Value: "NUM"}, + }, + } + err := f.Provision(caddy.Context{}) + if err != nil { + t.Fatalf("unexpected error provisioning: %v", err) + } + + out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{ + "foo-secret-123", + "bar-secret-456", + }}) + arr, ok := out.Interface.(caddyhttp.LoggableStringArray) + if !ok { + t.Fatalf("field is wrong type: %T", out.Interface) + } + if arr[0] != "foo-REDACTED-NUM" { + t.Fatalf("field entry 0 has not been filtered: %s", arr[0]) + } + if arr[1] != "bar-REDACTED-NUM" { + t.Fatalf("field entry 1 has not been filtered: %s", arr[1]) + } +} + +func TestMultiRegexpFilterAddOperation(t *testing.T) { + f := MultiRegexpFilter{} + err := f.AddOperation("secret", "REDACTED") + if err != nil { + t.Fatalf("unexpected error adding operation: %v", err) + } + err = f.AddOperation("password", "HIDDEN") + if err != nil { + t.Fatalf("unexpected error adding operation: %v", err) + } + err = f.Provision(caddy.Context{}) + if err != nil { + t.Fatalf("unexpected error provisioning: %v", err) + } + + if len(f.Operations) != 2 { + t.Fatalf("expected 2 operations, got %d", len(f.Operations)) + } + + out := f.Filter(zapcore.Field{String: "my-secret-password"}) + expected := "my-REDACTED-HIDDEN" + if out.String != expected { + t.Fatalf("field has not been filtered correctly: got %s, expected %s", out.String, expected) + } +} + +func TestMultiRegexpFilterSecurityLimits(t *testing.T) { + f := MultiRegexpFilter{} + + // Test maximum operations limit + for i := 0; i < 51; i++ { + err := f.AddOperation(fmt.Sprintf("pattern%d", i), "replacement") + if i < 50 { + if err != nil { + t.Fatalf("unexpected error adding operation %d: %v", i, err) + } + } else { + if err == nil { + t.Fatalf("expected error when adding operation %d (exceeds limit)", i) + } + } + } + + // Test empty pattern validation + f2 := MultiRegexpFilter{} + err := f2.AddOperation("", "replacement") + if err == nil { + t.Fatalf("expected error for empty pattern") + } + + // Test pattern length limit + f3 := MultiRegexpFilter{} + longPattern := strings.Repeat("a", 1001) + err = f3.AddOperation(longPattern, "replacement") + if err == nil { + t.Fatalf("expected error for pattern exceeding length limit") + } +} + +func TestMultiRegexpFilterValidation(t *testing.T) { + // Test validation with empty operations + f := MultiRegexpFilter{} + err := f.Validate() + if err == nil { + t.Fatalf("expected validation error for empty operations") + } + + // Test validation with valid operations + err = f.AddOperation("valid", "replacement") + if err != nil { + t.Fatalf("unexpected error adding operation: %v", err) + } + err = f.Provision(caddy.Context{}) + if err != nil { + t.Fatalf("unexpected error provisioning: %v", err) + } + err = f.Validate() + if err != nil { + t.Fatalf("unexpected validation error: %v", err) + } +} + +func TestMultiRegexpFilterInputSizeLimit(t *testing.T) { + f := MultiRegexpFilter{ + Operations: []regexpFilterOperation{ + {RawRegexp: `test`, Value: "REPLACED"}, + }, + } + err := f.Provision(caddy.Context{}) + if err != nil { + t.Fatalf("unexpected error provisioning: %v", err) + } + + // Test with very large input (should be truncated) + largeInput := strings.Repeat("test", 300000) // Creates ~1.2MB string + out := f.Filter(zapcore.Field{String: largeInput}) + + // The input should be truncated to 1MB and still processed + if len(out.String) > 1000000 { + t.Fatalf("output string not truncated: length %d", len(out.String)) + } + + // Should still contain replacements within the truncated portion + if !strings.Contains(out.String, "REPLACED") { + t.Fatalf("replacements not applied to truncated input") + } +} + +func TestMultiRegexpFilterOverlappingPatterns(t *testing.T) { + f := MultiRegexpFilter{ + Operations: []regexpFilterOperation{ + {RawRegexp: `secret.*password`, Value: "SENSITIVE"}, + {RawRegexp: `password`, Value: "HIDDEN"}, + }, + } + err := f.Provision(caddy.Context{}) + if err != nil { + t.Fatalf("unexpected error provisioning: %v", err) + } + + // The first pattern should match and replace the entire "secret...password" portion + // Then the second pattern should not find "password" anymore since it was already replaced + out := f.Filter(zapcore.Field{String: "my-secret-data-password-end"}) + expected := "my-SENSITIVE-end" + if out.String != expected { + t.Fatalf("field has not been filtered correctly: got %s, expected %s", out.String, expected) + } +} diff --git a/modules/logging/netwriter.go b/modules/logging/netwriter.go index 7d8481e3c..0ca866a8e 100644 --- a/modules/logging/netwriter.go +++ b/modules/logging/netwriter.go @@ -172,7 +172,7 @@ func (reconn *redialerConn) Write(b []byte) (n int, err error) { reconn.connMu.RUnlock() if conn != nil { if n, err = conn.Write(b); err == nil { - return + return n, err } } @@ -184,7 +184,7 @@ func (reconn *redialerConn) Write(b []byte) (n int, err error) { // one of them might have already re-dialed by now; try writing again if reconn.Conn != nil { if n, err = reconn.Conn.Write(b); err == nil { - return + return n, err } } @@ -198,7 +198,7 @@ func (reconn *redialerConn) Write(b []byte) (n int, err error) { if err2 != nil { // logger socket still offline; instead of discarding the log, dump it to stderr os.Stderr.Write(b) - return + return n, err } if n, err = conn2.Write(b); err == nil { if reconn.Conn != nil { @@ -211,7 +211,7 @@ func (reconn *redialerConn) Write(b []byte) (n int, err error) { os.Stderr.Write(b) } - return + return n, err } func (reconn *redialerConn) dial() (net.Conn, error) { 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 2c6306121..018a81165 100644 --- a/sigtrap_posix.go +++ b/sigtrap_posix.go @@ -18,6 +18,7 @@ package caddy import ( "context" + "errors" "os" "os/signal" "syscall" @@ -48,7 +49,31 @@ func trapSignalsPosix() { exitProcessFromSignal("SIGTERM") case syscall.SIGUSR1: - Log().Info("not implemented", zap.String("signal", "SIGUSR1")) + logger := Log().With(zap.String("signal", "SIGUSR1")) + // If we know the last source config file/adapter (set when starting + // via `caddy run --config --adapter `), attempt + // to reload from that source. Otherwise, ignore the signal. + file, adapter, reloadCallback := getLastConfig() + if file == "" { + logger.Info("last config unknown, ignored SIGUSR1") + break + } + logger = logger.With( + zap.String("file", file), + zap.String("adapter", adapter)) + if reloadCallback == nil { + logger.Warn("no reload helper available, ignored SIGUSR1") + break + } + logger.Info("reloading config from last-known source") + if err := reloadCallback(file, adapter); errors.Is(err, errReloadFromSourceUnavailable) { + // No reload helper available (likely not started via caddy run). + logger.Warn("reload from source unavailable in this process; ignored SIGUSR1") + } else if err != nil { + logger.Error("failed to reload config from file", zap.Error(err)) + } else { + logger.Info("successfully reloaded config from file") + } case syscall.SIGUSR2: Log().Info("not implemented", zap.String("signal", "SIGUSR2")) diff --git a/usagepool.go b/usagepool.go index e011be961..a6466b9b1 100644 --- a/usagepool.go +++ b/usagepool.go @@ -106,7 +106,7 @@ func (up *UsagePool) LoadOrNew(key any, construct Constructor) (value any, loade } upv.Unlock() } - return + return value, loaded, err } // LoadOrStore loads the value associated with key from the pool if it @@ -134,7 +134,7 @@ func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) { up.Unlock() value = val } - return + return value, loaded } // Range iterates the pool similarly to how sync.Map.Range() does: @@ -191,7 +191,7 @@ func (up *UsagePool) Delete(key any) (deleted bool, err error) { upv.value, upv.refs)) } } - return + return deleted, err } // References returns the number of references (count of usages) to a