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/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/auto-release-pr.yml b/.github/workflows/auto-release-pr.yml new file mode 100644 index 000000000..c8440d32c --- /dev/null +++ b/.github/workflows/auto-release-pr.yml @@ -0,0 +1,221 @@ +name: Release Proposal Approval Tracker + +on: + pull_request_review: + types: [submitted, dismissed] + pull_request: + types: [labeled, unlabeled, synchronize, closed] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + check-approvals: + name: Track Maintainer Approvals + runs-on: ubuntu-latest + # Only run on PRs with release-proposal label + if: contains(github.event.pull_request.labels.*.name, 'release-proposal') && github.event.pull_request.state == 'open' + + steps: + - name: Check approvals and update PR + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + MAINTAINER_LOGINS: ${{ secrets.MAINTAINER_LOGINS }} + with: + script: | + const pr = context.payload.pull_request; + + // Extract version from PR title (e.g., "Release Proposal: v1.2.3") + const versionMatch = pr.title.match(/Release Proposal:\s*(v[\d.]+(?:-[\w.]+)?)/); + const commitMatch = pr.body.match(/\*\*Target Commit:\*\*\s*`([a-f0-9]+)`/); + + if (!versionMatch || !commitMatch) { + console.log('Could not extract version from title or commit from body'); + return; + } + + const version = versionMatch[1]; + const targetCommit = commitMatch[1]; + + console.log(`Version: ${version}, Target Commit: ${targetCommit}`); + + // Get all reviews + const reviews = await github.rest.pulls.listReviews({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number + }); + + // Get list of maintainers + const maintainerLoginsRaw = process.env.MAINTAINER_LOGINS || ''; + const maintainerLogins = maintainerLoginsRaw + .split(/[,;]/) + .map(login => login.trim()) + .filter(login => login.length > 0); + + console.log(`Maintainer logins: ${maintainerLogins.join(', ')}`); + + // Get the latest review from each user + const latestReviewsByUser = {}; + reviews.data.forEach(review => { + const username = review.user.login; + if (!latestReviewsByUser[username] || new Date(review.submitted_at) > new Date(latestReviewsByUser[username].submitted_at)) { + latestReviewsByUser[username] = review; + } + }); + + // Count approvals from maintainers + const maintainerApprovals = Object.entries(latestReviewsByUser) + .filter(([username, review]) => + maintainerLogins.includes(username) && + review.state === 'APPROVED' + ) + .map(([username, review]) => username); + + const approvalCount = maintainerApprovals.length; + console.log(`Found ${approvalCount} maintainer approvals from: ${maintainerApprovals.join(', ')}`); + + // Get current labels + const currentLabels = pr.labels.map(label => label.name); + const hasApprovedLabel = currentLabels.includes('approved'); + const hasAwaitingApprovalLabel = currentLabels.includes('awaiting-approval'); + + if (approvalCount >= 2 && !hasApprovedLabel) { + console.log('✅ Quorum reached! Updating PR...'); + + // Remove awaiting-approval label if present + if (hasAwaitingApprovalLabel) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + name: 'awaiting-approval' + }).catch(e => console.log('Label not found:', e.message)); + } + + // Add approved label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: ['approved'] + }); + + // Add comment with tagging instructions + const approversList = maintainerApprovals.map(u => `@${u}`).join(', '); + const commentBody = [ + '## ✅ Approval Quorum Reached', + '', + `This release proposal has been approved by ${approvalCount} maintainers: ${approversList}`, + '', + '### Tagging Instructions', + '', + 'A maintainer should now create and push the signed tag:', + '', + '```bash', + `git checkout ${targetCommit}`, + `git tag -s ${version} -m "Release ${version}"`, + `git push origin ${version}`, + `git checkout -`, + '```', + '', + 'The release workflow will automatically start when the tag is pushed.' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: commentBody + }); + + console.log('Posted tagging instructions'); + } else if (approvalCount < 2 && hasApprovedLabel) { + console.log('⚠️ Approval count dropped below quorum, removing approved label'); + + // Remove approved label + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + name: 'approved' + }).catch(e => console.log('Label not found:', e.message)); + + // Add awaiting-approval label + if (!hasAwaitingApprovalLabel) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: ['awaiting-approval'] + }); + } + } else { + console.log(`⏳ Waiting for more approvals (${approvalCount}/2 required)`); + } + + handle-pr-closed: + name: Handle PR Closed Without Tag + runs-on: ubuntu-latest + if: | + contains(github.event.pull_request.labels.*.name, 'release-proposal') && + github.event.action == 'closed' && !contains(github.event.pull_request.labels.*.name, 'released') + + steps: + - name: Add cancelled label and comment + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const pr = context.payload.pull_request; + + // Check if the release-in-progress label is present + const hasReleaseInProgress = pr.labels.some(label => label.name === 'release-in-progress'); + + if (hasReleaseInProgress) { + // PR was closed while release was in progress - this is unusual + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: '⚠️ **Warning:** This PR was closed while a release was in progress. This may indicate an error. Please verify the release status.' + }); + } else { + // PR was closed before tag was created - this is normal cancellation + const versionMatch = pr.title.match(/Release Proposal:\s*(v[\d.]+(?:-[\w.]+)?)/); + const version = versionMatch ? versionMatch[1] : 'unknown'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: `## 🚫 Release Proposal Cancelled\n\nThis release proposal for ${version} was closed without creating the tag.\n\nIf you want to proceed with this release later, you can create a new release proposal.` + }); + } + + // Add cancelled label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: ['cancelled'] + }); + + // Remove other workflow labels if present + const labelsToRemove = ['awaiting-approval', 'approved', 'release-in-progress']; + for (const label of labelsToRemove) { + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + name: label + }); + } catch (e) { + console.log(`Label ${label} not found or already removed`); + } + } + + console.log('Added cancelled label and cleaned up workflow labels'); + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 893b12313..50501a0f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ on: - 2.* env: + GOFLAGS: '-tags=nobadger,nomysql,nopgx' # https://github.com/actions/setup-go/issues/491 GOTOOLCHAIN: local @@ -30,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) @@ -64,15 +65,15 @@ jobs: actions: write # to allow uploading artifacts and cache steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Install Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ matrix.GO_SEMVER }} check-latest: true @@ -110,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 @@ -133,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 @@ -161,13 +162,13 @@ jobs: continue-on-error: true # August 2020: s390x VM is down due to weather and power issues steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Run Tests run: | set +e @@ -190,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 @@ -220,27 +221,27 @@ jobs: if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]' steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 + - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 with: version: latest args: check - name: Install Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + 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@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 + - 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 90e3422c6..8aa9eaf59 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -11,6 +11,8 @@ on: - 2.* env: + GOFLAGS: '-tags=nobadger,nomysql,nopgx' + CGO_ENABLED: '0' # https://github.com/actions/setup-go/issues/491 GOTOOLCHAIN: local @@ -34,13 +36,13 @@ 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: @@ -49,15 +51,15 @@ jobs: continue-on-error: true steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Install Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: ${{ matrix.GO_SEMVER }} check-latest: true @@ -74,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 ea1246e00..849188c64 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -45,14 +45,14 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: - go-version: '~1.24' + go-version: '~1.25' check-latest: true - name: golangci-lint @@ -73,14 +73,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: govulncheck uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4 with: - go-version-input: '~1.24.1' + go-version-input: '~1.25.0' check-latest: true dependency-review: @@ -90,14 +90,14 @@ jobs: pull-requests: write steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: 'Checkout Repository' - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: 'Dependency Review' - uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 + 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 diff --git a/.github/workflows/release-proposal.yml b/.github/workflows/release-proposal.yml new file mode 100644 index 000000000..afde7965a --- /dev/null +++ b/.github/workflows/release-proposal.yml @@ -0,0 +1,248 @@ +name: Release Proposal + +# This workflow creates a release proposal as a PR that requires approval from maintainers +# Triggered manually by maintainers when ready to prepare a release +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g., v2.8.0)' + required: true + type: string + commit_hash: + description: 'Commit hash to release from' + required: true + type: string + +permissions: + contents: read + +jobs: + create-proposal: + name: Create Release Proposal + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + + - name: Trim and validate inputs + id: inputs + run: | + # Trim whitespace from inputs + VERSION=$(echo "${{ inputs.version }}" | xargs) + COMMIT_HASH=$(echo "${{ inputs.commit_hash }}" | xargs) + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT + + # Validate version format + if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then + echo "Error: Version must follow semver format (e.g., v2.8.0 or v2.8.0-beta.1)" + exit 1 + fi + + # Validate commit hash format + if [[ ! "$COMMIT_HASH" =~ ^[a-f0-9]{7,40}$ ]]; then + echo "Error: Commit hash must be a valid SHA (7-40 characters)" + exit 1 + fi + + # Check if commit exists + if ! git cat-file -e "$COMMIT_HASH"; then + echo "Error: Commit $COMMIT_HASH does not exist" + exit 1 + fi + + - name: Check if tag already exists + run: | + if git rev-parse "${{ steps.inputs.outputs.version }}" >/dev/null 2>&1; then + echo "Error: Tag ${{ steps.inputs.outputs.version }} already exists" + exit 1 + fi + + - name: Check for existing proposal PR + id: check_existing + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const version = '${{ steps.inputs.outputs.version }}'; + + // Search for existing open PRs with release-proposal label that match this version + const openPRs = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + sort: 'updated', + direction: 'desc' + }); + + const existingOpenPR = openPRs.data.find(pr => + pr.title.includes(version) && + pr.labels.some(label => label.name === 'release-proposal') + ); + + if (existingOpenPR) { + const hasReleased = existingOpenPR.labels.some(label => label.name === 'released'); + const hasReleaseInProgress = existingOpenPR.labels.some(label => label.name === 'release-in-progress'); + + if (hasReleased || hasReleaseInProgress) { + core.setFailed(`A release for ${version} is already in progress or completed: ${existingOpenPR.html_url}`); + } else { + core.setFailed(`An open release proposal already exists for ${version}: ${existingOpenPR.html_url}\n\nPlease use the existing PR or close it first.`); + } + return; + } + + // Check for closed PRs with this version that were cancelled + const closedPRs = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'closed', + sort: 'updated', + direction: 'desc' + }); + + const cancelledPR = closedPRs.data.find(pr => + pr.title.includes(version) && + pr.labels.some(label => label.name === 'release-proposal') && + pr.labels.some(label => label.name === 'cancelled') + ); + + if (cancelledPR) { + console.log(`Found previously cancelled proposal for ${version}: ${cancelledPR.html_url}`); + console.log('Creating new proposal to replace cancelled one...'); + } else { + console.log(`No existing proposal found for ${version}, proceeding...`); + } + + - name: Generate changelog and create branch + id: setup + run: | + VERSION="${{ steps.inputs.outputs.version }}" + COMMIT_HASH="${{ steps.inputs.outputs.commit_hash }}" + + # Create a new branch for the release proposal + BRANCH_NAME="release_proposal-$VERSION" + git checkout -b "$BRANCH_NAME" + + # Calculate how many commits behind HEAD + COMMITS_BEHIND=$(git rev-list --count ${COMMIT_HASH}..HEAD) + + if [ "$COMMITS_BEHIND" -eq 0 ]; then + BEHIND_INFO="This is the latest commit (HEAD)" + else + BEHIND_INFO="This commit is **${COMMITS_BEHIND} commits behind HEAD**" + fi + + echo "commits_behind=$COMMITS_BEHIND" >> $GITHUB_OUTPUT + echo "behind_info=$BEHIND_INFO" >> $GITHUB_OUTPUT + + # Get the last tag + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + + if [ -z "$LAST_TAG" ]; then + echo "No previous tag found, generating full changelog" + COMMITS=$(git log --pretty=format:"- %s (%h)" --reverse "$COMMIT_HASH") + else + echo "Generating changelog since $LAST_TAG" + COMMITS=$(git log --pretty=format:"- %s (%h)" --reverse "${LAST_TAG}..$COMMIT_HASH") + fi + + # Store changelog for PR body + echo "changelog<> $GITHUB_OUTPUT + echo "$COMMITS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Create empty commit for the PR + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git commit --allow-empty -m "Release proposal for $VERSION" + + # Push the branch + git push origin "$BRANCH_NAME" + + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + + - name: Create release proposal PR + id: create_pr + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const changelog = `${{ steps.setup.outputs.changelog }}`; + + const pr = await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `Release Proposal: ${{ steps.inputs.outputs.version }}`, + head: '${{ steps.setup.outputs.branch_name }}', + base: 'master', + body: `## Release Proposal: ${{ steps.inputs.outputs.version }} + + **Target Commit:** \`${{ steps.inputs.outputs.commit_hash }}\` + **Requested by:** @${{ github.actor }} + **Commit Status:** ${{ steps.setup.outputs.behind_info }} + + This PR proposes creating release tag \`${{ steps.inputs.outputs.version }}\` at commit \`${{ steps.inputs.outputs.commit_hash }}\`. + + ### Approval Process + + This PR requires **approval from 2+ maintainers** before the tag can be created. + + ### What happens next? + + 1. Maintainers review this proposal + 2. When 2+ maintainer approvals are received, an automated workflow will post tagging instructions + 3. A maintainer manually creates and pushes the signed tag + 4. The release workflow is triggered automatically by the tag push + 5. Upon release completion, this PR is closed and the branch is deleted + + ### Changes Since Last Release + + ${changelog} + + ### Release Checklist + + - [ ] All tests pass + - [ ] Security review completed + - [ ] Documentation updated + - [ ] Breaking changes documented + + --- + + **Note:** Tag creation is manual and requires a signed tag from a maintainer.`, + draft: true + }); + + // Add labels + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.data.number, + labels: ['release-proposal', 'awaiting-approval'] + }); + + console.log(`Created PR: ${pr.data.html_url}`); + + return { number: pr.data.number, url: pr.data.html_url }; + result-encoding: json + + - name: Post summary + run: | + echo "## Release Proposal PR Created! 🚀" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Version: **${{ steps.inputs.outputs.version }}**" >> $GITHUB_STEP_SUMMARY + echo "Commit: **${{ steps.inputs.outputs.commit_hash }}**" >> $GITHUB_STEP_SUMMARY + echo "Status: ${{ steps.setup.outputs.behind_info }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "PR: ${{ fromJson(steps.create_pr.outputs.result).url }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 39e088e05..e4880a64c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,20 +13,334 @@ permissions: contents: read jobs: + verify-tag: + name: Verify Tag Signature and Approvals + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + + outputs: + verification_passed: ${{ steps.verify.outputs.passed }} + tag_version: ${{ steps.info.outputs.version }} + proposal_issue_number: ${{ steps.find_proposal.outputs.result && fromJson(steps.find_proposal.outputs.result).number || '' }} + + steps: + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + # Force fetch upstream tags -- because 65 minutes + # tl;dr: actions/checkout@v3 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 + # which doesn't overwrite that tag because that would be destructive. + # Credit to @francislavoie for the investigation. + # https://github.com/actions/checkout/issues/290#issuecomment-680260080 + - name: Force fetch upstream tags + run: git fetch --tags --force + + - name: Get tag info + id: info + run: | + echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + + # https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 + - name: Print Go version and environment + id: vars + run: | + printf "Using go at: $(which go)\n" + printf "Go version: $(go version)\n" + printf "\n\nGo environment:\n\n" + go env + printf "\n\nSystem environment:\n\n" + env + echo "version_tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT + echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + # Add "pip install" CLI tools to PATH + echo ~/.local/bin >> $GITHUB_PATH + + # Parse semver + TAG=${GITHUB_REF/refs\/tags\//} + SEMVER_RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z\.-]*\)' + TAG_MAJOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\1#"` + TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"` + TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"` + TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"` + echo "tag_major=${TAG_MAJOR}" >> $GITHUB_OUTPUT + echo "tag_minor=${TAG_MINOR}" >> $GITHUB_OUTPUT + echo "tag_patch=${TAG_PATCH}" >> $GITHUB_OUTPUT + echo "tag_special=${TAG_SPECIAL}" >> $GITHUB_OUTPUT + + - name: Validate commits and tag signatures + id: verify + env: + signing_keys: ${{ secrets.SIGNING_KEYS }} + run: | + # Read the string into an array, splitting by IFS + IFS=";" read -ra keys_collection <<< "$signing_keys" + + # ref: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#example-usage-of-the-runner-context + touch "${{ runner.temp }}/allowed_signers" + + # Iterate and print the split elements + for item in "${keys_collection[@]}"; do + + # trim leading whitespaces + item="${item##*( )}" + + # trim trailing whitespaces + item="${item%%*( )}" + + IFS=" " read -ra key_components <<< "$item" + # git wants it in format: email address, type, public key + # ssh has it in format: type, public key, email address + echo "${key_components[2]} namespaces=\"git\" ${key_components[0]} ${key_components[1]}" >> "${{ runner.temp }}/allowed_signers" + done + + git config set --global gpg.ssh.allowedSignersFile "${{ runner.temp }}/allowed_signers" + + echo "Verifying the tag: ${{ steps.vars.outputs.version_tag }}" + + # Verify the tag is signed + if ! git verify-tag -v "${{ steps.vars.outputs.version_tag }}" 2>&1; then + echo "❌ Tag verification failed!" + echo "passed=false" >> $GITHUB_OUTPUT + git push --delete origin "${{ steps.vars.outputs.version_tag }}" + exit 1 + fi + # Run it again to capture the output + git verify-tag -v "${{ steps.vars.outputs.version_tag }}" 2>&1 | tee /tmp/verify-output.txt; + + # SSH verification output typically includes the key fingerprint + # Use GNU grep with Perl regex for cleaner extraction (Linux environment) + KEY_SHA256=$(grep -oP "SHA256:[\"']?\K[A-Za-z0-9+/=]+(?=[\"']?)" /tmp/verify-output.txt | head -1 || echo "") + + if [ -z "$KEY_SHA256" ]; then + # Try alternative pattern with "key" prefix + KEY_SHA256=$(grep -oP "key SHA256:[\"']?\K[A-Za-z0-9+/=]+(?=[\"']?)" /tmp/verify-output.txt | head -1 || echo "") + fi + + if [ -z "$KEY_SHA256" ]; then + # Fallback: extract any base64-like string (40+ chars) + KEY_SHA256=$(grep -oP '[A-Za-z0-9+/]{40,}=?' /tmp/verify-output.txt | head -1 || echo "") + fi + + if [ -z "$KEY_SHA256" ]; then + echo "Somehow could not extract SSH key fingerprint from git verify-tag output" + echo "Cancelling flow and deleting tag" + echo "passed=false" >> $GITHUB_OUTPUT + git push --delete origin "${{ steps.vars.outputs.version_tag }}" + exit 1 + fi + + echo "✅ Tag verification succeeded!" + echo "passed=true" >> $GITHUB_OUTPUT + echo "key_id=$KEY_SHA256" >> $GITHUB_OUTPUT + + - name: Find related release proposal + id: find_proposal + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const version = '${{ steps.vars.outputs.version_tag }}'; + + // Search for PRs with release-proposal label that match this version + const prs = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', // Changed to 'all' to find both open and closed PRs + sort: 'updated', + direction: 'desc' + }); + + // Find the most recent PR for this version + const proposal = prs.data.find(pr => + pr.title.includes(version) && + pr.labels.some(label => label.name === 'release-proposal') + ); + + if (!proposal) { + console.log(`⚠️ No release proposal PR found for ${version}`); + console.log('This might be a hotfix or emergency release'); + return { number: null, approved: true, approvals: 0, proposedCommit: null }; + } + + console.log(`Found proposal PR #${proposal.number} for version ${version}`); + + // Extract commit hash from PR body + const commitMatch = proposal.body.match(/\*\*Target Commit:\*\*\s*`([a-f0-9]+)`/); + const proposedCommit = commitMatch ? commitMatch[1] : null; + + if (proposedCommit) { + console.log(`Proposal was for commit: ${proposedCommit}`); + } else { + console.log('⚠️ No target commit hash found in PR body'); + } + + // Get PR reviews to extract approvers + let approvers = 'Validated by automation'; + let approvalCount = 2; // Minimum required + + try { + const reviews = await github.rest.pulls.listReviews({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: proposal.number + }); + + // Get latest review per user and filter for approvals + const latestReviewsByUser = {}; + reviews.data.forEach(review => { + const username = review.user.login; + if (!latestReviewsByUser[username] || new Date(review.submitted_at) > new Date(latestReviewsByUser[username].submitted_at)) { + latestReviewsByUser[username] = review; + } + }); + + const approvalReviews = Object.values(latestReviewsByUser).filter(review => + review.state === 'APPROVED' + ); + + if (approvalReviews.length > 0) { + approvers = approvalReviews.map(r => '@' + r.user.login).join(', '); + approvalCount = approvalReviews.length; + console.log(`Found ${approvalCount} approvals from: ${approvers}`); + } + } catch (error) { + console.log(`Could not fetch reviews: ${error.message}`); + } + + return { + number: proposal.number, + approved: true, + approvals: approvalCount, + approvers: approvers, + proposedCommit: proposedCommit + }; + result-encoding: json + + - name: Verify proposal commit + run: | + APPROVALS='${{ steps.find_proposal.outputs.result }}' + + # Parse JSON + PROPOSED_COMMIT=$(echo "$APPROVALS" | jq -r '.proposedCommit') + CURRENT_COMMIT="${{ steps.info.outputs.sha }}" + + echo "Proposed commit: $PROPOSED_COMMIT" + echo "Current commit: $CURRENT_COMMIT" + + # Check if commits match (if proposal had a target commit) + if [ "$PROPOSED_COMMIT" != "null" ] && [ -n "$PROPOSED_COMMIT" ]; then + # Normalize both commits to full SHA for comparison + PROPOSED_FULL=$(git rev-parse "$PROPOSED_COMMIT" 2>/dev/null || echo "") + CURRENT_FULL=$(git rev-parse "$CURRENT_COMMIT" 2>/dev/null || echo "") + + if [ -z "$PROPOSED_FULL" ]; then + echo "⚠️ Could not resolve proposed commit: $PROPOSED_COMMIT" + elif [ "$PROPOSED_FULL" != "$CURRENT_FULL" ]; then + echo "❌ Commit mismatch!" + echo "The tag points to commit $CURRENT_FULL but the proposal was for $PROPOSED_FULL" + echo "This indicates an error in tag creation." + # Delete the tag remotely + git push --delete origin "${{ steps.vars.outputs.version_tag }}" + echo "Tag ${{steps.vars.outputs.version_tag}} has been deleted" + exit 1 + else + echo "✅ Commit hash matches proposal" + fi + else + echo "⚠️ No target commit found in proposal (might be legacy release)" + fi + + echo "✅ Tag verification completed" + + - name: Update release proposal PR + if: fromJson(steps.find_proposal.outputs.result).number != null + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const result = ${{ steps.find_proposal.outputs.result }}; + + if (result.number) { + // Add in-progress label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: result.number, + labels: ['release-in-progress'] + }); + + // Remove approved label if present + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: result.number, + name: 'approved' + }); + } catch (e) { + console.log('Approved label not found:', e.message); + } + + const commentBody = [ + '## 🚀 Release Workflow Started', + '', + '- **Tag:** ${{ steps.info.outputs.version }}', + '- **Signed by key:** ${{ steps.verify.outputs.key_id }}', + '- **Commit:** ${{ steps.info.outputs.sha }}', + '- **Approved by:** ' + result.approvers, + '', + 'Release workflow is now running. This PR will be updated when the release is published.' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: result.number, + body: commentBody + }); + } + + - name: Summary + run: | + APPROVALS='${{ steps.find_proposal.outputs.result }}' + PROPOSED_COMMIT=$(echo "$APPROVALS" | jq -r '.proposedCommit // "N/A"') + APPROVERS=$(echo "$APPROVALS" | jq -r '.approvers // "N/A"') + + echo "## Tag Verification Summary 🔐" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Tag:** ${{ steps.info.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "- **Commit:** ${{ steps.info.outputs.sha }}" >> $GITHUB_STEP_SUMMARY + echo "- **Proposed Commit:** $PROPOSED_COMMIT" >> $GITHUB_STEP_SUMMARY + echo "- **Signature:** ✅ Verified" >> $GITHUB_STEP_SUMMARY + echo "- **Signed by:** ${{ steps.verify.outputs.key_id }}" >> $GITHUB_STEP_SUMMARY + echo "- **Approvals:** ✅ Sufficient" >> $GITHUB_STEP_SUMMARY + echo "- **Approved by:** $APPROVERS" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Proceeding with release build..." >> $GITHUB_STEP_SUMMARY + release: name: Release + needs: verify-tag + if: ${{ needs.verify-tag.outputs.verification_passed == 'true' }} strategy: matrix: 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 @@ -36,26 +350,28 @@ jobs: # https://docs.github.com/en/rest/overview/permissions-required-for-github-apps#permission-on-contents # "Releases" is part of `contents`, so it needs the `write` contents: write + issues: write + pull-requests: write steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 - name: Install Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 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 @@ -98,22 +414,12 @@ jobs: - name: Install Cloudsmith CLI run: pip install --upgrade cloudsmith-cli - - name: Validate commits and tag signatures - run: | - - # Import Matt Holt's key - curl 'https://github.com/mholt.gpg' | gpg --import - - echo "Verifying the tag: ${{ steps.vars.outputs.version_tag }}" - # tags are only accepted if signed by Matt's key - git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1 - - name: Install Cosign - uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # main + uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # main - name: Cosign version run: cosign version - name: Install Syft - uses: anchore/sbom-action/download-syft@7b36ad622f042cab6f59a75c2ac24ccb256e9b45 # main + uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # main - name: Syft version run: syft version - name: Install xcaddy @@ -122,7 +428,7 @@ jobs: xcaddy version # GoReleaser will take care of publishing those artifacts into the release - name: Run GoReleaser - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 with: version: latest args: release --clean --timeout 60m @@ -188,3 +494,72 @@ jobs: echo "Pushing $filename to 'testing'" cloudsmith push deb caddy/testing/any-distro/any-version $filename done + + - name: Update release proposal PR + if: needs.verify-tag.outputs.proposal_issue_number != '' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const prNumber = parseInt('${{ needs.verify-tag.outputs.proposal_issue_number }}'); + + if (prNumber) { + // Get PR details to find the branch + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + const branchName = pr.data.head.ref; + + // Remove in-progress label + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: 'release-in-progress' + }); + } catch (e) { + console.log('Label not found:', e.message); + } + + // Add released label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: ['released'] + }); + + // Add final comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: '## ✅ Release Published\n\nThe release has been successfully published and is now available.' + }); + + // Close the PR if it's still open + if (pr.data.state === 'open') { + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + state: 'closed' + }); + console.log(`Closed PR #${prNumber}`); + } + + // Delete the branch + try { + await github.rest.git.deleteRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `heads/${branchName}` + }); + console.log(`Deleted branch: ${branchName}`); + } catch (e) { + console.log(`Could not delete branch ${branchName}: ${e.message}`); + } + } diff --git a/.github/workflows/release_published.yml b/.github/workflows/release_published.yml index 2ff313223..8afc5c35e 100644 --- a/.github/workflows/release_published.yml +++ b/.github/workflows/release_published.yml @@ -24,12 +24,12 @@ jobs: # See https://github.com/peter-evans/repository-dispatch - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Trigger event on caddyserver/dist - uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 + uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0 with: token: ${{ secrets.REPO_DISPATCH_TOKEN }} repository: caddyserver/dist @@ -37,7 +37,7 @@ jobs: client-payload: '{"tag": "${{ github.event.release.tag_name }}"}' - name: Trigger event on caddyserver/caddy-docker - uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 + 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 index a03c3bb5b..bb49f935d 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -37,17 +37,17 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: "Checkout code" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif @@ -81,6 +81,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5 + uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5 with: sarif_file: results.sarif diff --git a/.golangci.yml b/.golangci.yml index 0f1082e72..4f4545054 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,6 +2,10 @@ version: "2" run: issues-exit-code: 1 tests: false + build-tags: + - nobadger + - nomysql + - nopgx output: formats: text: diff --git a/README.md b/README.md index e5d5a83d6..4c091f714 100644 --- a/README.md +++ b/README.md @@ -89,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 e6895c39f..6ccec41e7 100644 --- a/admin.go +++ b/admin.go @@ -1029,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 c637f92a9..92dd43a5c 100644 --- a/admin_test.go +++ b/admin_test.go @@ -149,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() } @@ -207,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) } } diff --git a/caddy.go b/caddy.go index abbd6d6f0..5f71d8e8b 100644 --- a/caddy.go +++ b/caddy.go @@ -975,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 { @@ -1059,7 +1059,7 @@ func Version() (simple, full string) { } } - return + return simple, full } // Event represents something that has happened or is happening. @@ -1197,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/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 0476a9b93..833aff353 100644 --- a/caddyconfig/caddyfile/formatter.go +++ b/caddyconfig/caddyfile/formatter.go @@ -52,17 +52,16 @@ func Format(input []byte) []byte { newLines int // count of newlines consumed - comment bool // whether we're in a comment - quoted bool // whether we're in a quoted segment - escaped bool // whether current char is escaped + comment bool // whether we're in a comment + quotes string // encountered quotes ('', '`', '"', '"`', '`"') + escaped bool // whether current char is escaped heredoc heredocState // whether we're in a heredoc heredocEscaped bool // whether heredoc is escaped heredocMarker []rune heredocClosingMarker []rune - nesting int // indentation level - withinBackquote bool + nesting int // indentation level ) write := func(ch rune) { @@ -89,12 +88,8 @@ 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 quotes == "" && (heredoc == heredocClosed && !heredocEscaped) && space && last == '<' && ch == '<' { write(ch) heredoc = heredocOpening @@ -180,16 +175,38 @@ func Format(input []byte) []byte { continue } - if quoted { + if ch == '`' { + switch quotes { + case "\"`": + quotes = "\"" + case "`": + quotes = "" + case "\"": + quotes = "\"`" + default: + quotes = "`" + } + } + + if quotes == "\"" { if ch == '"' { - quoted = false + quotes = "" } write(ch) continue } - if space && ch == '"' { - quoted = true + if ch == '"' { + switch quotes { + case "": + if space { + quotes = "\"" + } + case "`\"": + quotes = "`" + case "\"`": + quotes = "" + } } if unicode.IsSpace(ch) { @@ -224,7 +241,7 @@ func Format(input []byte) []byte { openBrace = false if beginningOfLine { indent() - } else if !openBraceSpace { + } else if !openBraceSpace || !unicode.IsSpace(last) { write(' ') } write('{') @@ -241,11 +258,11 @@ func Format(input []byte) []byte { case ch == '{': openBrace = true openBraceSpace = spacePrior && !beginningOfLine - if openBraceSpace { + if openBraceSpace && newLines == 0 { write(' ') } openBraceWritten = false - if withinBackquote { + if quotes == "`" { write('{') openBraceWritten = true continue @@ -253,7 +270,7 @@ func Format(input []byte) []byte { continue case ch == '}' && (spacePrior || !openBrace): - if withinBackquote { + if quotes == "`" { write('}') continue } diff --git a/caddyconfig/caddyfile/formatter_test.go b/caddyconfig/caddyfile/formatter_test.go index a64383c3c..0092d1311 100644 --- a/caddyconfig/caddyfile/formatter_test.go +++ b/caddyconfig/caddyfile/formatter_test.go @@ -444,6 +444,26 @@ block2 { input: "block {respond \"All braces should remain: {{now | date `2006`}}\"}", expect: "block {respond \"All braces should remain: {{now | date `2006`}}\"}", }, + { + description: "Preserve quoted backticks and backticked quotes", + input: "block { respond \"`\" } block { respond `\"`}", + expect: "block {\n\trespond \"`\"\n}\n\nblock {\n\trespond `\"`\n}", + }, + { + description: "No trailing space on line before env variable", + input: `{ + a + + {$ENV_VAR} +} +`, + expect: `{ + a + + {$ENV_VAR} +} +`, + }, } { // the formatter should output a trailing newline, // even if the tests aren't written to expect that 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 71aa0c2b8..061aaa48b 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -91,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 // } @@ -481,7 +481,7 @@ 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 + 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)", 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 65128efd6..30948f84f 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -554,6 +554,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e 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,21 +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 globalACMEDNSok && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) { - if globalACMEDNS == nil { - globalACMEDNS = options["dns"] - if globalACMEDNS == nil { - return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option") - } + 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") } - acmeIssuer.Challenges = &caddytls.ChallengesConfig{ - DNS: new(caddytls.DNSChallengeConfig), + if acmeIssuer.Challenges == nil { + acmeIssuer.Challenges = new(caddytls.ChallengesConfig) } - } else if globalACMEDNS != nil { - acmeIssuer.Challenges = &caddytls.ChallengesConfig{ - DNS: &caddytls.DNSChallengeConfig{ - ProviderRaw: caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil), - }, + 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 { @@ -606,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) } 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/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_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/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/import_block_anonymous.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_block_anonymous.caddyfiletest new file mode 100644 index 000000000..3ab1745e9 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/import_block_anonymous.caddyfiletest @@ -0,0 +1,13 @@ +(site) { + http://{args[0]} https://{args[0]} { + {block} + } +} +import site test.domain { + { + header_up Host {host} + header_up X-Real-IP {remote_host} + } +} +---------- +anonymous blocks are not supported \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/import_block_snippet_non_replaced_block.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_block_snippet_non_replaced_block.caddyfiletest new file mode 100644 index 000000000..bc01bb291 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/import_block_snippet_non_replaced_block.caddyfiletest @@ -0,0 +1,57 @@ +(snippet) { + header { + reverse_proxy localhost:3000 + {block} + } +} + +example.com { + import snippet +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "headers", + "response": { + "set": { + "Reverse_proxy": [ + "localhost:3000" + ] + } + } + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/import_block_snippet_non_replaced_key_block.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_block_snippet_non_replaced_key_block.caddyfiletest new file mode 100644 index 000000000..92e0d6b2a --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/import_block_snippet_non_replaced_key_block.caddyfiletest @@ -0,0 +1,57 @@ +(snippet) { + header { + reverse_proxy localhost:3000 + {blocks.content_type} + } +} + +example.com { + import snippet +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "headers", + "response": { + "set": { + "Reverse_proxy": [ + "localhost:3000" + ] + } + } + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/import_block_with_site_block.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_block_with_site_block.caddyfiletest new file mode 100644 index 000000000..7527ce4d6 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/import_block_with_site_block.caddyfiletest @@ -0,0 +1,65 @@ +(site) { + https://{args[0]} { + {block} + } +} + +import site test.domain { + reverse_proxy http://192.168.1.1:8080 { + header_up Host {host} + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "test.domain" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "headers": { + "request": { + "set": { + "Host": [ + "{http.request.host}" + ] + } + } + }, + "upstreams": [ + { + "dial": "192.168.1.1:8080" + } + ] + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/log_multiple_regexp_filters.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_multiple_regexp_filters.caddyfiletest new file mode 100644 index 000000000..c228c6812 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/log_multiple_regexp_filters.caddyfiletest @@ -0,0 +1,95 @@ +:80 + +log { + output stdout + format filter { + wrap console + + # Multiple regexp filters for the same field - this should work now! + request>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/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/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_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/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 31dc4be7b..e55a6df58 100644 --- a/caddytest/integration/mockdns_test.go +++ b/caddytest/integration/mockdns_test.go @@ -15,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 { @@ -31,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 } diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index 028ce5bd9..75d114992 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -172,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") @@ -187,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 } @@ -204,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 == "" { @@ -219,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 } } @@ -236,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 @@ -256,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 @@ -289,6 +331,9 @@ func cmdRun(fl Flags) (int, error) { } } + // release the last local logger reference + logger = nil //nolint:wastedassign,ineffassign + select {} } @@ -319,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 } @@ -337,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 { @@ -551,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 } @@ -766,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/main.go b/cmd/main.go index 47d702ca7..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 diff --git a/cmd/packagesfuncs.go b/cmd/packagesfuncs.go index cda6f31f6..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) { @@ -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 4c1139936..095598682 100644 --- a/context.go +++ b/context.go @@ -21,12 +21,14 @@ import ( "log" "log/slog" "reflect" + "sync" "github.com/caddyserver/certmagic" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "go.uber.org/zap" "go.uber.org/zap/exp/zapslog" + "go.uber.org/zap/zapcore" "github.com/caddyserver/caddy/v2/internal/filesystems" ) @@ -583,24 +585,57 @@ func (ctx Context) Logger(module ...Module) *zap.Logger { return ctx.cfg.Logging.Logger(mod) } +type slogHandlerFactory func(handler slog.Handler, core zapcore.Core, moduleID string) slog.Handler + +var ( + slogHandlerFactories []slogHandlerFactory + slogHandlerFactoriesMu sync.RWMutex +) + +// RegisterSlogHandlerFactory allows modules to register custom log/slog.Handler, +// for instance, to add contextual data to the logs. +func RegisterSlogHandlerFactory(factory slogHandlerFactory) { + slogHandlerFactoriesMu.Lock() + slogHandlerFactories = append(slogHandlerFactories, factory) + slogHandlerFactoriesMu.Unlock() +} + // Slogger returns a slog logger that is intended for use by // the most recent module associated with the context. func (ctx Context) Slogger() *slog.Logger { + var ( + handler slog.Handler + core zapcore.Core + moduleID string + ) if ctx.cfg == nil { // often the case in tests; just use a dev logger l, err := zap.NewDevelopment() if err != nil { panic("config missing, unable to create dev logger: " + err.Error()) } - return slog.New(zapslog.NewHandler(l.Core())) + + core = l.Core() + handler = zapslog.NewHandler(core) + } else { + mod := ctx.Module() + if mod == nil { + core = Log().Core() + handler = zapslog.NewHandler(core) + } else { + moduleID = string(mod.CaddyModule().ID) + core = ctx.cfg.Logging.Logger(mod).Core() + handler = zapslog.NewHandler(core, zapslog.WithName(moduleID)) + } } - mod := ctx.Module() - if mod == nil { - return slog.New(zapslog.NewHandler(Log().Core())) + + slogHandlerFactoriesMu.RLock() + for _, f := range slogHandlerFactories { + handler = f(handler, core, moduleID) } - return slog.New(zapslog.NewHandler(ctx.cfg.Logging.Logger(mod).Core(), - zapslog.WithName(string(mod.CaddyModule().ID)), - )) + slogHandlerFactoriesMu.RUnlock() + + return slog.New(handler) } // Modules returns the lineage of modules that this context provisioned, diff --git a/go.mod b/go.mod index 2a6c0b53a..d4e660b2a 100644 --- a/go.mod +++ b/go.mod @@ -1,82 +1,83 @@ module github.com/caddyserver/caddy/v2 -go 1.24 +go 1.25 require ( github.com/BurntSushi/toml v1.5.0 - github.com/KimMachineGun/automemlimit v0.7.4 + github.com/DeRuina/timberjack v1.3.9 + github.com/KimMachineGun/automemlimit v0.7.5 github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/chroma/v2 v2.20.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.23.0 + github.com/caddyserver/certmagic v0.25.0 github.com/caddyserver/zerossl v0.1.3 github.com/cloudflare/circl v1.6.1 github.com/dustin/go-humanize v1.0.1 - github.com/go-chi/chi/v5 v5.2.2 - github.com/google/cel-go v0.26.0 + 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/compress v1.18.1 github.com/klauspost/cpuid/v2 v2.3.0 - github.com/mholt/acmez/v3 v3.1.2 - github.com/prometheus/client_golang v1.23.0 - github.com/quic-go/quic-go v0.54.0 + github.com/mholt/acmez/v3 v3.1.4 + github.com/prometheus/client_golang v1.23.2 + github.com/quic-go/quic-go v0.57.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.7 - github.com/stretchr/testify v1.10.0 + github.com/spf13/cobra v1.10.1 + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.11.1 github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 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.61.0 - go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 - go.opentelemetry.io/otel v1.37.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 - go.opentelemetry.io/otel/sdk v1.37.0 + go.opentelemetry.io/contrib/exporters/autoexport v0.63.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/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.40.0 - golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 - golang.org/x/net v0.42.0 - golang.org/x/sync v0.16.0 - golang.org/x/term v0.33.0 - golang.org/x/time v0.12.0 - gopkg.in/natefinch/lumberjack.v2 v2.2.1 + golang.org/x/crypto v0.45.0 + golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99 + golang.org/x/net v0.47.0 + golang.org/x/sync v0.18.0 + golang.org/x/term v0.37.0 + golang.org/x/time v0.14.0 gopkg.in/yaml.v3 v3.0.1 ) require ( cel.dev/expr v0.24.0 // indirect - cloud.google.com/go/auth v0.16.2 // indirect + cloud.google.com/go/auth v0.17.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Microsoft/go-winio v0.6.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/ccoveille/go-safecast v1.6.1 // indirect - github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/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.8.0 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-jose/go-jose/v4 v4.1.2 // indirect github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect - github.com/google/go-tpm v0.9.5 // indirect + github.com/google/go-tpm v0.9.6 // indirect github.com/google/go-tspi v0.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect - github.com/googleapis/gax-go/v2 v2.14.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // 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/prometheus/otlptranslator v0.0.2 // indirect + github.com/quic-go/qpack v0.6.0 // 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 @@ -85,16 +86,30 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/propagators/aws v1.37.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.37.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 // indirect - go.opentelemetry.io/contrib/propagators/ot v1.37.0 // indirect - go.uber.org/mock v0.5.2 // indirect - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - google.golang.org/api v0.240.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.63.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.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect + go.opentelemetry.io/otel/log v0.14.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect + go.opentelemetry.io/otel/sdk/metric 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.32.0 // indirect + google.golang.org/api v0.254.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect ) @@ -123,41 +138,41 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/libdns/libdns v1.1.0 + 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.8.1 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.2 - github.com/prometheus/common v0.65.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/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.9.5 // indirect + github.com/slackhq/nebula v1.9.7 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/urfave/cli v1.22.17 // indirect go.etcd.io/bbolt v1.3.10 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 - go.opentelemetry.io/proto/otlp v1.7.0 // indirect - go.step.sm/crypto v0.67.0 + go.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.74.0 go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.25.0 // indirect - golang.org/x/sys v0.34.0 - golang.org/x/text v0.27.0 // indirect - golang.org/x/tools v0.34.0 // indirect - google.golang.org/grpc v1.73.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sys v0.38.0 + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.38.0 // indirect + google.golang.org/grpc v1.76.0 // 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 c97047aec..55245e72c 100644 --- a/go.sum +++ b/go.sum @@ -1,39 +1,32 @@ 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.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= -cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= -cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= 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/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k= +cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= -dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= -dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= -dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 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.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk= -github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= +github.com/DeRuina/timberjack v1.3.9 h1:6UXZ1I7ExPGTX/1UNYawR58LlOJUHKBPiYC7WQ91eBo= +github.com/DeRuina/timberjack v1.3.9/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE= +github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk= +github.com/KimMachineGun/automemlimit v0.7.5/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.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= @@ -52,53 +45,49 @@ github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iX github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= 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.36.4 h1:GySzjhVvx0ERP6eyfAbAuAXLtAda5TEy19E5q5W8I9E= -github.com/aws/aws-sdk-go-v2 v1.36.4/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/config v1.29.16 h1:XkruGnXX1nEZ+Nyo9v84TzsX+nj86icbFAeust6uo8A= -github.com/aws/aws-sdk-go-v2/config v1.29.16/go.mod h1:uCW7PNjGwZ5cOGZ5jr8vCWrYkGIhPoTNV23Q/tpHKzg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.69 h1:8B8ZQboRc3uaIKjshve/XlvJ570R7BKNy3gftSbS178= -github.com/aws/aws-sdk-go-v2/credentials v1.17.69/go.mod h1:gPME6I8grR1jCqBFEGthULiolzf/Sexq/Wy42ibKK9c= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 h1:oQWSGexYasNpYp4epLGZxxjsDo8BMBh6iNWkTXQvkwk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31/go.mod h1:nc332eGUU+djP3vrMI6blS0woaCfHTe3KiSQUVTMRq0= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 h1:o1v1VFfPcDVlK3ll1L5xHsaQAFdNtZ5GXnNR7SwueC4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35/go.mod h1:rZUQNYMNG+8uZxz9FOerQJ+FceCiodXvixpeRtdESrU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 h1:R5b82ubO2NntENm3SAm0ADME+H630HomNJdgv+yZ3xw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35/go.mod h1:FuA+nmgMRfkzVKYDNEqQadvEMxtxl9+RLT9ribCwEMs= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 h1:/ldKrPPXTC421bTNWrUIpq3CxwHwRI/kpc+jPUTJocM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16/go.mod h1:5vkf/Ws0/wgIMJDQbjI4p2op86hNW6Hie5QtebrDgT8= -github.com/aws/aws-sdk-go-v2/service/kms v1.41.0 h1:2jKyib9msVrAVn+lngwlSplG13RpUZmzVte2yDao5nc= -github.com/aws/aws-sdk-go-v2/service/kms v1.41.0/go.mod h1:RyhzxkWGcfixlkieewzpO3D4P4fTMxhIDqDZWsh0u/4= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 h1:EU58LP8ozQDVroOEyAfcq0cGc5R/FTZjVoYJ6tvby3w= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.4/go.mod h1:CrtOgCcysxMvrCoHnvNAD7PHWclmoFG78Q2xLK0KKcs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 h1:XB4z0hbQtpmBnb1FQYvKaCM7UsS6Y/u8jVBwIUGeCTk= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2/go.mod h1:hwRpqkRxnQ58J9blRDrB4IanlXCpcKmsC83EhG77upg= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 h1:nyLjs8sYJShFYj6aiyjCBI3EcLn1udWrQTjEF+SOXB0= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.21/go.mod h1:EhdxtZ+g84MSGrSrHzZiUm9PYiZkrADNja15wtRJSJo= -github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= -github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/aws/aws-sdk-go-v2 v1.39.5 h1:e/SXuia3rkFtapghJROrydtQpfQaaUgd1cUvyO1mp2w= +github.com/aws/aws-sdk-go-v2 v1.39.5/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM= +github.com/aws/aws-sdk-go-v2/config v1.31.16 h1:E4Tz+tJiPc7kGnXwIfCyUj6xHJNpENlY11oKpRTgsjc= +github.com/aws/aws-sdk-go-v2/config v1.31.16/go.mod h1:2S9hBElpCyGMifv14WxQ7EfPumgoeCPZUpuPX8VtW34= +github.com/aws/aws-sdk-go-v2/credentials v1.18.20 h1:KFndAnHd9NUuzikHjQ8D5CfFVO+bgELkmcGY8yAw98Q= +github.com/aws/aws-sdk-go-v2/credentials v1.18.20/go.mod h1:9mCi28a+fmBHSQ0UM79omkz6JtN+PEsvLrnG36uoUv0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12 h1:VO3FIM2TDbm0kqp6sFNR0PbioXJb/HzCDW6NtIZpIWE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12/go.mod h1:6C39gB8kg82tx3r72muZSrNhHia9rjGkX7ORaS2GKNE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12 h1:p/9flfXdoAnwJnuW9xHEAFY22R3A6skYkW19JFF9F+8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12/go.mod h1:ZTLHakoVCTtW8AaLGSwJ3LXqHD9uQKnOcv1TrpO6u2k= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12 h1:2lTWFvRcnWFFLzHWmtddu5MTchc5Oj2OOey++99tPZ0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12/go.mod h1:hI92pK+ho8HVcWMHKHrK3Uml4pfG7wvL86FzO0LVtQQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 h1:xtuxji5CS0JknaXoACOunXOYOQzgfTvGAc9s2QdCJA4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2/go.mod h1:zxwi0DIR0rcRcgdbl7E2MSOvxDyyXGBlScvBkARFaLQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12 h1:MM8imH7NZ0ovIVX7D2RxfMDv7Jt9OiUXkcQ+GqywA7M= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12/go.mod h1:gf4OGwdNkbEsb7elw2Sy76odfhwNktWII3WgvQgQQ6w= +github.com/aws/aws-sdk-go-v2/service/kms v1.47.0 h1:A97YCVyGz19rRs3+dWf3GpMPflCswgETA9r6/Q0JNSY= +github.com/aws/aws-sdk-go-v2/service/kms v1.47.0/go.mod h1:ZJ1ghBt9gQM8JoNscUua1siIgao8w74o3kvdWUU6N/Q= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.0 h1:xHXvxst78wBpJFgDW07xllOx0IAzbryrSdM4nMVQ4Dw= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.0/go.mod h1:/e8m+AO6HNPPqMyfKRtzZ9+mBF5/x1Wk8QiDva4m07I= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4 h1:tBw2Qhf0kj4ZwtsVpDiVRU3zKLvjvjgIjHMKirxXg8M= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4/go.mod h1:Deq4B7sRM6Awq/xyOBlxBdgW8/Z926KYNNaGMW2lrkA= +github.com/aws/aws-sdk-go-v2/service/sts v1.39.0 h1:C+BRMnasSYFcgDw8o9H5hzehKzXyAb9GY5v/8bP9DUY= +github.com/aws/aws-sdk-go-v2/service/sts v1.39.0/go.mod h1:4EjU+4mIx6+JqKQkruye+CaigV7alL3thVPfDd9VlMs= +github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= +github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= 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.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU= -github.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4= +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/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q= github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8= -github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/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= @@ -112,7 +101,6 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 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.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= @@ -120,7 +108,6 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz 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/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= @@ -148,23 +135,19 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp 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/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= -github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 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.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.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= -github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-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-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= +github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -172,56 +155,41 @@ 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.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/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= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -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.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= -github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +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/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.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-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA= +github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm-tools v0.4.6 h1:hwIwPG7w4z5eQEBq11gYw8YYr9xXLfBQ/0JsKyq5AJM= +github.com/google/go-tpm-tools v0.4.6/go.mod h1:MsVQbJnRhKDfWwf5zgr3cDGpj13P1uLAFF0wMEP/n5w= 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/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.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.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= -github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +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/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= +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= @@ -238,14 +206,10 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= 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= @@ -253,17 +217,14 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn 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/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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU= -github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= -github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= +github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= 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.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -271,14 +232,12 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk 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.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= -github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= -github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= -github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= -github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +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/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= @@ -287,13 +246,8 @@ github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= -github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/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= @@ -308,22 +262,20 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= -github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_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.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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= -github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= -github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= +github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= +github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= +github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= +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.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= +github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= @@ -333,38 +285,15 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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 v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= -github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= -github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= -github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= -github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= -github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= -github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= -github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= -github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= -github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= -github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= -github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= -github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= -github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= -github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= -github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 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.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.9.5 h1:ZrxcvP/lxwFglaijmiwXLuCSkybZMJnqSYI1S8DtGnY= -github.com/slackhq/nebula v1.9.5/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ= +github.com/slackhq/nebula v1.9.7 h1:v5u46efIyYHGdfjFnozQbRRhMdaB9Ma1SSTcUcE2lfE= +github.com/slackhq/nebula v1.9.7/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.28.4 h1:JTU6/A5Xes6m+OsR6fw1RACSA362vJc9SOFVG7poBEw= @@ -384,8 +313,6 @@ github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 h1:LyZqn24/ZiVg8v9H 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= -github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -394,13 +321,13 @@ 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/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/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= @@ -416,16 +343,14 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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.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= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= @@ -443,90 +368,103 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= 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.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/bridges/prometheus v0.63.0 h1:/Rij/t18Y7rUayNg7Id6rPrEnHgorxYabm2E6wUdPP4= +go.opentelemetry.io/contrib/bridges/prometheus v0.63.0/go.mod h1:AdyDPn6pkbkt2w01n3BubRVk7xAsCRq1Yg1mpfyA/0E= +go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 h1:NLnZybb9KkfMXPwZhd5diBYJoVxiO9Qa06dacEA7ySY= +go.opentelemetry.io/contrib/exporters/autoexport v0.63.0/go.mod h1:OvRg7gm5WRSCtxzGSsrFHbDLToYlStHNZQ+iPNIyD6g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 h1:1+EHlhAe/tukctfePZRrDruB9vn7MdwyC+rf36nUSPM= -go.opentelemetry.io/contrib/propagators/autoprop v0.62.0/go.mod h1:skzESZBY3IYcqJgImc+fwXQWflvVe+jZxoA/uw60NaI= -go.opentelemetry.io/contrib/propagators/aws v1.37.0 h1:cp8AFiM/qjBm10C/ATIRnEDXpD5MBknrA0ANw4T2/ss= -go.opentelemetry.io/contrib/propagators/aws v1.37.0/go.mod h1:Cy8Hk2E2iSGEbsLnPUdeigrexaAOAGIAmBFK919EQs0= -go.opentelemetry.io/contrib/propagators/b3 v1.37.0 h1:0aGKdIuVhy5l4GClAjl72ntkZJhijf2wg1S7b5oLoYA= -go.opentelemetry.io/contrib/propagators/b3 v1.37.0/go.mod h1:nhyrxEJEOQdwR15zXrCKI6+cJK60PXAkJ/jRyfhr2mg= -go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 h1:pW+qDVo0jB0rLsNeaP85xLuz20cvsECUcN7TE+D8YTM= -go.opentelemetry.io/contrib/propagators/jaeger v1.37.0/go.mod h1:x7bd+t034hxLTve1hF9Yn9qQJlO/pP8H5pWIt7+gsFM= -go.opentelemetry.io/contrib/propagators/ot v1.37.0 h1:tVjnBF6EiTDMXoq2Xuc2vK0I7MTbEs05II/0j9mMK+E= -go.opentelemetry.io/contrib/propagators/ot v1.37.0/go.mod h1:MQjyNXtxAC8PGN9gzPtO4GY5zuP+RI3XX53uWbCTvEQ= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= -go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= -go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= -go.step.sm/crypto v0.67.0 h1:1km9LmxMKG/p+mKa1R4luPN04vlJYnRLlLQrWv7egGU= -go.step.sm/crypto v0.67.0/go.mod h1:+AoDpB0mZxbW/PmOXuwkPSpXRgaUaoIK+/Wx/HGgtAU= +go.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/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= +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/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= +go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= +go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +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/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= +go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= +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.74.0 h1:/APBEv45yYR4qQFg47HA8w1nesIGcxh44pGyQNw6JRA= +go.step.sm/crypto v0.74.0/go.mod h1:UoXqCAJjjRgzPte0Llaqen7O9P7XjPmgjgTHQGkKCDk= 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.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= 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/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= -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= +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= 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-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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 h1:V5+zy0jmgNYmK1uW/sPpBw8ioFvalrhaUrYWmu1Fpe4= -golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= -golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +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-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE= +golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -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-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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-20190620200207-3b0461eec859/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= @@ -535,19 +473,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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= @@ -556,15 +485,11 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-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/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -582,8 +507,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.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= @@ -594,10 +519,9 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 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.3/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= @@ -607,74 +531,44 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +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-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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +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= -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.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU= -google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -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-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= -google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +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.254.0 h1:jl3XrGj7lRjnlUvZAbAdhINTLbsg5dbjmR90+pTQvt4= +google.golang.org/api v0.254.0/go.mod h1:5BkSURm3D9kAqjGvBNgf0EcbX6Rnrf6UArKkwBzAyqQ= +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-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +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-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/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= -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= 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= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= 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/listen.go b/listen.go index e84a197ac..fba9c3a6b 100644 --- a/listen.go +++ b/listen.go @@ -261,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 c0d018bb3..a1540521d 100644 --- a/listeners.go +++ b/listeners.go @@ -31,13 +31,17 @@ import ( "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/http3" - "github.com/quic-go/quic-go/qlog" + h3qlog "github.com/quic-go/quic-go/http3/qlog" "go.uber.org/zap" "golang.org/x/time/rate" "github.com/caddyserver/caddy/v2/internal" ) +// listenFdsStart is the first file descriptor number for systemd socket activation. +// File descriptors 0, 1, 2 are reserved for stdin, stdout, stderr. +const listenFdsStart = 3 + // NetworkAddress represents one or more network addresses. // It contains the individual components for a parsed network // address of the form accepted by ParseNetworkAddress(). @@ -305,6 +309,64 @@ func IsFdNetwork(netw string) bool { return strings.HasPrefix(netw, "fd") } +// getFdByName returns the file descriptor number for the given +// socket name from systemd's LISTEN_FDNAMES environment variable. +// Socket names are provided by systemd via socket activation. +// +// The name can optionally include an index to handle multiple sockets +// with the same name: "web:0" for first, "web:1" for second, etc. +// If no index is specified, defaults to index 0 (first occurrence). +func getFdByName(nameWithIndex string) (int, error) { + if nameWithIndex == "" { + return 0, fmt.Errorf("socket name cannot be empty") + } + + fdNamesStr := os.Getenv("LISTEN_FDNAMES") + if fdNamesStr == "" { + return 0, fmt.Errorf("LISTEN_FDNAMES environment variable not set") + } + + // Parse name and optional index + parts := strings.Split(nameWithIndex, ":") + if len(parts) > 2 { + return 0, fmt.Errorf("invalid socket name format '%s': too many colons", nameWithIndex) + } + + name := parts[0] + targetIndex := 0 + + if len(parts) > 1 { + var err error + targetIndex, err = strconv.Atoi(parts[1]) + if err != nil { + return 0, fmt.Errorf("invalid socket index '%s': %v", parts[1], err) + } + if targetIndex < 0 { + return 0, fmt.Errorf("socket index cannot be negative: %d", targetIndex) + } + } + + // Parse the socket names + names := strings.Split(fdNamesStr, ":") + + // Find the Nth occurrence of the requested name + matchCount := 0 + for i, fdName := range names { + if fdName == name { + if matchCount == targetIndex { + return listenFdsStart + i, nil + } + matchCount++ + } + } + + if matchCount == 0 { + return 0, fmt.Errorf("socket name '%s' not found in LISTEN_FDNAMES", name) + } + + return 0, fmt.Errorf("socket name '%s' found %d times, but index %d requested", name, matchCount, targetIndex) +} + // ParseNetworkAddress parses addr into its individual // components. The input string is expected to be of // the form "network/host:port-range" where any part is @@ -336,9 +398,27 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui }, err } if IsFdNetwork(network) { + fdAddr := host + + // Handle named socket activation (fdname/name, fdgramname/name) + if strings.HasPrefix(network, "fdname") || strings.HasPrefix(network, "fdgramname") { + fdNum, err := getFdByName(host) + if err != nil { + return NetworkAddress{}, fmt.Errorf("named socket activation: %v", err) + } + fdAddr = strconv.Itoa(fdNum) + + // Normalize network to standard fd/fdgram + if strings.HasPrefix(network, "fdname") { + network = "fd" + } else { + network = "fdgram" + } + } + return NetworkAddress{ Network: network, - Host: host, + Host: fdAddr, }, nil } var start, end uint64 @@ -382,7 +462,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 +482,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,6 +510,7 @@ func JoinNetworkAddress(network, host, port string) string { // address instead. // // NOTE: This API is EXPERIMENTAL and may be changed or removed. +// 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)) @@ -466,7 +547,7 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{ Allow0RTT: true, - Tracer: qlog.DefaultConnectionTracer, + Tracer: h3qlog.DefaultConnectionTracer, }, ) if err != nil { @@ -626,6 +707,7 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error) 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 diff --git a/listeners_test.go b/listeners_test.go index a4cadd3aa..c2cc255f2 100644 --- a/listeners_test.go +++ b/listeners_test.go @@ -15,6 +15,7 @@ package caddy import ( + "os" "reflect" "testing" @@ -652,3 +653,286 @@ func TestSplitUnixSocketPermissionsBits(t *testing.T) { } } } + +// TestGetFdByName tests the getFdByName function for systemd socket activation. +func TestGetFdByName(t *testing.T) { + // Save original environment + originalFdNames := os.Getenv("LISTEN_FDNAMES") + + // Restore environment after test + defer func() { + if originalFdNames != "" { + os.Setenv("LISTEN_FDNAMES", originalFdNames) + } else { + os.Unsetenv("LISTEN_FDNAMES") + } + }() + + tests := []struct { + name string + fdNames string + socketName string + expectedFd int + expectError bool + }{ + { + name: "simple http socket", + fdNames: "http", + socketName: "http", + expectedFd: 3, + }, + { + name: "multiple different sockets - first", + fdNames: "http:https:dns", + socketName: "http", + expectedFd: 3, + }, + { + name: "multiple different sockets - second", + fdNames: "http:https:dns", + socketName: "https", + expectedFd: 4, + }, + { + name: "multiple different sockets - third", + fdNames: "http:https:dns", + socketName: "dns", + expectedFd: 5, + }, + { + name: "duplicate names - first occurrence (no index)", + fdNames: "web:web:api", + socketName: "web", + expectedFd: 3, + }, + { + name: "duplicate names - first occurrence (explicit index 0)", + fdNames: "web:web:api", + socketName: "web:0", + expectedFd: 3, + }, + { + name: "duplicate names - second occurrence (index 1)", + fdNames: "web:web:api", + socketName: "web:1", + expectedFd: 4, + }, + { + name: "complex duplicates - first api", + fdNames: "web:api:web:api:dns", + socketName: "api:0", + expectedFd: 4, + }, + { + name: "complex duplicates - second api", + fdNames: "web:api:web:api:dns", + socketName: "api:1", + expectedFd: 6, + }, + { + name: "complex duplicates - first web", + fdNames: "web:api:web:api:dns", + socketName: "web:0", + expectedFd: 3, + }, + { + name: "complex duplicates - second web", + fdNames: "web:api:web:api:dns", + socketName: "web:1", + expectedFd: 5, + }, + { + name: "socket not found", + fdNames: "http:https", + socketName: "missing", + expectError: true, + }, + { + name: "empty socket name", + fdNames: "http", + socketName: "", + expectError: true, + }, + { + name: "missing LISTEN_FDNAMES", + fdNames: "", + socketName: "http", + expectError: true, + }, + { + name: "index out of range", + fdNames: "web:web", + socketName: "web:2", + expectError: true, + }, + { + name: "negative index", + fdNames: "web", + socketName: "web:-1", + expectError: true, + }, + { + name: "invalid index format", + fdNames: "web", + socketName: "web:abc", + expectError: true, + }, + { + name: "too many colons", + fdNames: "web", + socketName: "web:0:extra", + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Set up environment + if tc.fdNames != "" { + os.Setenv("LISTEN_FDNAMES", tc.fdNames) + } else { + os.Unsetenv("LISTEN_FDNAMES") + } + + // Test the function + fd, err := getFdByName(tc.socketName) + + if tc.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + } + if fd != tc.expectedFd { + t.Errorf("Expected FD %d but got %d", tc.expectedFd, fd) + } + } + }) + } +} + +// TestParseNetworkAddressFdName tests parsing of fdname and fdgramname addresses. +func TestParseNetworkAddressFdName(t *testing.T) { + // Save and restore environment + originalFdNames := os.Getenv("LISTEN_FDNAMES") + defer func() { + if originalFdNames != "" { + os.Setenv("LISTEN_FDNAMES", originalFdNames) + } else { + os.Unsetenv("LISTEN_FDNAMES") + } + }() + + // Set up test environment + os.Setenv("LISTEN_FDNAMES", "http:https:dns") + + tests := []struct { + input string + expectAddr NetworkAddress + expectErr bool + }{ + { + input: "fdname/http", + expectAddr: NetworkAddress{ + Network: "fd", + Host: "3", + }, + }, + { + input: "fdname/https", + expectAddr: NetworkAddress{ + Network: "fd", + Host: "4", + }, + }, + { + input: "fdname/dns", + expectAddr: NetworkAddress{ + Network: "fd", + Host: "5", + }, + }, + { + input: "fdname/http:0", + expectAddr: NetworkAddress{ + Network: "fd", + Host: "3", + }, + }, + { + input: "fdname/https:0", + expectAddr: NetworkAddress{ + Network: "fd", + Host: "4", + }, + }, + { + input: "fdgramname/http", + expectAddr: NetworkAddress{ + Network: "fdgram", + Host: "3", + }, + }, + { + input: "fdgramname/https", + expectAddr: NetworkAddress{ + Network: "fdgram", + Host: "4", + }, + }, + { + input: "fdgramname/http:0", + expectAddr: NetworkAddress{ + Network: "fdgram", + Host: "3", + }, + }, + { + input: "fdname/nonexistent", + expectErr: true, + }, + { + input: "fdgramname/nonexistent", + expectErr: true, + }, + { + input: "fdname/http:99", + expectErr: true, + }, + { + input: "fdname/invalid:abc", + expectErr: true, + }, + // Test that old fd/N syntax still works + { + input: "fd/7", + expectAddr: NetworkAddress{ + Network: "fd", + Host: "7", + }, + }, + { + input: "fdgram/8", + expectAddr: NetworkAddress{ + Network: "fdgram", + Host: "8", + }, + }, + } + + for i, tc := range tests { + actualAddr, err := ParseNetworkAddress(tc.input) + + if tc.expectErr && err == nil { + t.Errorf("Test %d (%s): Expected error but got none", i, tc.input) + } + if !tc.expectErr && err != nil { + t.Errorf("Test %d (%s): Expected no error but got: %v", i, tc.input, err) + } + if !tc.expectErr && !reflect.DeepEqual(tc.expectAddr, actualAddr) { + t.Errorf("Test %d (%s): Expected %+v but got %+v", i, tc.input, tc.expectAddr, actualAddr) + } + } +} diff --git a/logging.go b/logging.go index 1a7b0ce29..2734b5425 100644 --- a/logging.go +++ b/logging.go @@ -28,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() { @@ -190,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 } @@ -773,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/modules.go b/modules.go index 37b56a988..24c452589 100644 --- a/modules.go +++ b/modules.go @@ -345,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. diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 3e14ddb25..6ad18d051 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" @@ -151,6 +150,11 @@ 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 map[string]struct{} } @@ -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 { @@ -192,6 +198,8 @@ func (app *App) Provision(ctx caddy.Context) error { if app.Metrics != nil { app.Metrics.init = sync.Once{} app.Metrics.httpMetrics = &httpMetrics{} + // Scan config for allowed hosts to prevent cardinality explosion + app.Metrics.scanConfigForHosts(app) } // prepare each server oldContext := ctx.Context @@ -231,15 +239,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 +272,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 +429,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 +466,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 +513,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 { @@ -533,8 +545,10 @@ func (app *App) Start() error { // create the listener for this socket lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{ KeepAliveConfig: net.KeepAliveConfig{ - Enable: srv.KeepAliveInterval != 0, + Enable: srv.KeepAliveInterval >= 0, Interval: time.Duration(srv.KeepAliveInterval), + Idle: time.Duration(srv.KeepAliveIdle), + Count: srv.KeepAliveCount, }, }) if err != nil { @@ -565,15 +579,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; @@ -591,11 +603,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 { @@ -708,6 +717,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), @@ -722,31 +736,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; @@ -773,9 +792,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 c34954f92..05f8a7517 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -265,6 +265,22 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er } } + // 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: 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/hashes.go b/modules/caddyhttp/caddyauth/bcrypt.go similarity index 97% rename from modules/caddyhttp/caddyauth/hashes.go rename to modules/caddyhttp/caddyauth/bcrypt.go index ea91f24e2..f6940996e 100644 --- a/modules/caddyhttp/caddyauth/hashes.go +++ b/modules/caddyhttp/caddyauth/bcrypt.go @@ -27,7 +27,10 @@ func init() { } // defaultBcryptCost cost 14 strikes a solid balance between security, usability, and hardware performance -const defaultBcryptCost = 14 +const ( + bcryptName = "bcrypt" + defaultBcryptCost = 14 +) // BcryptHash implements the bcrypt hash. type BcryptHash struct { 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 2acaee6c4..e9c513005 100644 --- a/modules/caddyhttp/caddyauth/command.go +++ b/modules/caddyhttp/caddyauth/command.go @@ -32,28 +32,55 @@ import ( func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "hash-password", - Usage: "[--plaintext ] [--algorithm ] [--bcrypt-cost ]", + 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-cost sets the bcrypt hashing difficulty. -Higher values increase security by making the hash computation slower and more CPU-intensive. -If the provided cost is not within the valid range [bcrypt.MinCost, bcrypt.MaxCost], -the default value (defaultBcryptCost) will be used instead. -Note: Higher cost values can significantly degrade performance on slower systems. +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) }, }) @@ -115,8 +142,34 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) { var hash []byte var hashString string switch algorithm { - case "bcrypt": + 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.go b/modules/caddyhttp/celmatcher.go index 3d118ea79..66a60b817 100644 --- a/modules/caddyhttp/celmatcher.go +++ b/modules/caddyhttp/celmatcher.go @@ -665,7 +665,7 @@ func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander { // map literals containing heterogeneous values, in this case string and list // of string. func CELValueToMapStrList(data ref.Val) (map[string][]string, error) { - mapStrType := reflect.TypeOf(map[string]any{}) + mapStrType := reflect.TypeFor[map[string]any]() mapStrRaw, err := data.ConvertToNative(mapStrType) if err != nil { return nil, err 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..ac995c37b 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*", @@ -167,8 +168,8 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh // caches without knowing about our changes... if etag := r.Header.Get("If-None-Match"); etag != "" && !strings.HasPrefix(etag, "W/") { ourSuffix := "-" + encName + `"` - if strings.HasSuffix(etag, ourSuffix) { - etag = strings.TrimSuffix(etag, ourSuffix) + `"` + if before, ok := strings.CutSuffix(etag, ourSuffix); ok { + etag = before + `"` r.Header.Set("If-None-Match", etag) } } @@ -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 90be1e932..818f76745 100644 --- a/modules/caddyhttp/encode/encode_test.go +++ b/modules/caddyhttp/encode/encode_test.go @@ -9,7 +9,7 @@ import ( func BenchmarkOpenResponseWriter(b *testing.B) { enc := new(Encode) - for n := 0; n < b.N; n++ { + for b.Loop() { enc.openResponseWriter("test", nil, false) } } diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index b871fe995..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 @@ -455,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 { diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go index def508ec9..33d9e39ee 100644 --- a/modules/caddyhttp/headers/headers.go +++ b/modules/caddyhttp/headers/headers.go @@ -159,7 +159,7 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error { return nil } -// containsCaddyPlaceholders checks if the string contains Caddy placeholder syntax {key} +// containsPlaceholders checks if the string contains Caddy placeholder syntax {key} func containsPlaceholders(s string) bool { openIdx := strings.Index(s, "{") if openIdx == -1 { 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 cb23adf0a..bacdc74b5 100644 --- a/modules/caddyhttp/intercept/intercept.go +++ b/modules/caddyhttp/intercept/intercept.go @@ -17,6 +17,7 @@ package intercept import ( "bytes" "fmt" + "io" "net/http" "strconv" "strings" @@ -175,10 +176,35 @@ func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy c.Write(zap.Int("handler", rec.handlerIndex)) } - // pass the request through the response handler routes - return rec.handler.Routes.Compile(next).ServeHTTP(w, r) + // response recorder doesn't create a new copy of the original headers, they're + // present in the original response writer + // create a new recorder to see if any response body from the new handler is present, + // if not, use the already buffered response body + recorder := caddyhttp.NewResponseRecorder(w, nil, nil) + if err := rec.handler.Routes.Compile(emptyHandler).ServeHTTP(recorder, r); err != nil { + return err + } + + // no new response status and the status is not 0 + if recorder.Status() == 0 && rec.Status() != 0 { + w.WriteHeader(rec.Status()) + } + + // no new response body and there is some in the original response + // TODO: what if the new response doesn't have a body by design? + // see: https://github.com/caddyserver/caddy/pull/6232#issue-2235224400 + if recorder.Size() == 0 && buf.Len() > 0 { + _, err := io.Copy(w, buf) + return err + } + return nil } +// this handler does nothing because everything we need is already buffered +var emptyHandler caddyhttp.Handler = caddyhttp.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) error { + return nil +}) + // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // intercept [] { 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..b937a6f1e 100644 --- a/modules/caddyhttp/logging.go +++ b/modules/caddyhttp/logging.go @@ -15,18 +15,28 @@ package caddyhttp import ( + "context" "encoding/json" "errors" + "log/slog" "net" "net/http" "strings" + "sync" "go.uber.org/zap" + "go.uber.org/zap/exp/zapslog" "go.uber.org/zap/zapcore" "github.com/caddyserver/caddy/v2" ) +func init() { + caddy.RegisterSlogHandlerFactory(func(handler slog.Handler, core zapcore.Core, moduleID string) slog.Handler { + return &extraFieldsSlogHandler{defaultHandler: handler, core: core, moduleID: moduleID} + }) +} + // ServerLogConfig describes a server's logging configuration. If // enabled without customization, all requests to this server are // logged to the default logger; logger destinations may be @@ -209,7 +219,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,22 +228,26 @@ 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. type ExtraLogFields struct { - fields []zapcore.Field + fields []zapcore.Field + handlers sync.Map } // Add adds a field to the list of extra fields to log. func (e *ExtraLogFields) Add(field zap.Field) { + e.handlers.Clear() e.fields = append(e.fields, field) } // Set sets a field in the list of extra fields to log. // If the field already exists, it is replaced. func (e *ExtraLogFields) Set(field zap.Field) { + e.handlers.Clear() + for i := range e.fields { if e.fields[i].Key == field.Key { e.fields[i] = field @@ -243,6 +257,29 @@ func (e *ExtraLogFields) Set(field zap.Field) { e.fields = append(e.fields, field) } +func (e *ExtraLogFields) getSloggerHandler(handler *extraFieldsSlogHandler) (h slog.Handler) { + if existing, ok := e.handlers.Load(handler); ok { + return existing.(slog.Handler) + } + + if handler.moduleID == "" { + h = zapslog.NewHandler(handler.core.With(e.fields)) + } else { + h = zapslog.NewHandler(handler.core.With(e.fields), zapslog.WithName(handler.moduleID)) + } + + if handler.group != "" { + h = h.WithGroup(handler.group) + } + if handler.attrs != nil { + h = h.WithAttrs(handler.attrs) + } + + e.handlers.Store(handler, h) + + return h +} + const ( // Variable name used to indicate that this request // should be omitted from the access logs @@ -254,3 +291,43 @@ const ( // Variable name used to indicate the logger to be used AccessLoggerNameVarKey string = "access_logger_names" ) + +type extraFieldsSlogHandler struct { + defaultHandler slog.Handler + core zapcore.Core + moduleID string + group string + attrs []slog.Attr +} + +func (e *extraFieldsSlogHandler) Enabled(ctx context.Context, level slog.Level) bool { + return e.defaultHandler.Enabled(ctx, level) +} + +func (e *extraFieldsSlogHandler) Handle(ctx context.Context, record slog.Record) error { + if elf, ok := ctx.Value(ExtraLogFieldsCtxKey).(*ExtraLogFields); ok { + return elf.getSloggerHandler(e).Handle(ctx, record) + } + + return e.defaultHandler.Handle(ctx, record) +} + +func (e *extraFieldsSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &extraFieldsSlogHandler{ + e.defaultHandler.WithAttrs(attrs), + e.core, + e.moduleID, + e.group, + append(e.attrs, attrs...), + } +} + +func (e *extraFieldsSlogHandler) WithGroup(name string) slog.Handler { + return &extraFieldsSlogHandler{ + e.defaultHandler.WithGroup(name), + e.core, + e.moduleID, + name, + e.attrs, + } +} diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index dd8529fce..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 @@ -654,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 @@ -733,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 @@ -802,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 @@ -1173,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 @@ -1196,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 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.go b/modules/caddyhttp/metrics.go index 9bb97e0b4..424170732 100644 --- a/modules/caddyhttp/metrics.go +++ b/modules/caddyhttp/metrics.go @@ -17,14 +17,60 @@ import ( // Metrics configures metrics observations. // EXPERIMENTAL and subject to change or removal. +// +// Example configuration: +// +// { +// "apps": { +// "http": { +// "metrics": { +// "per_host": true, +// "allow_catch_all_hosts": false +// }, +// "servers": { +// "srv0": { +// "routes": [{ +// "match": [{"host": ["example.com", "www.example.com"]}], +// "handle": [{"handler": "static_response", "body": "Hello"}] +// }] +// } +// } +// } +// } +// } +// +// In this configuration: +// - Requests to example.com and www.example.com get individual host labels +// - All other hosts (e.g., attacker.com) are aggregated under "_other" label +// - This prevents unlimited cardinality from arbitrary Host headers type Metrics struct { // Enable per-host metrics. Enabling this option may // incur high-memory consumption, depending on the number of hosts // managed by Caddy. + // + // CARDINALITY PROTECTION: To prevent unbounded cardinality attacks, + // only explicitly configured hosts (via host matchers) are allowed + // by default. Other hosts are aggregated under the "_other" label. + // See AllowCatchAllHosts to change this behavior. PerHost bool `json:"per_host,omitempty"` - init sync.Once - httpMetrics *httpMetrics `json:"-"` + // Allow metrics for catch-all hosts (hosts without explicit configuration). + // When false (default), only hosts explicitly configured via host matchers + // will get individual metrics labels. All other hosts will be aggregated + // under the "_other" label to prevent cardinality explosion. + // + // This is automatically enabled for HTTPS servers (since certificates provide + // some protection against unbounded cardinality), but disabled for HTTP servers + // by default to prevent cardinality attacks from arbitrary Host headers. + // + // Set to true to allow all hosts to get individual metrics (NOT RECOMMENDED + // for production environments exposed to the internet). + AllowCatchAllHosts bool `json:"allow_catch_all_hosts,omitempty"` + + init sync.Once + httpMetrics *httpMetrics + allowedHosts map[string]struct{} + hasHTTPSServer bool } type httpMetrics struct { @@ -101,6 +147,63 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) { }, httpLabels) } +// scanConfigForHosts scans the HTTP app configuration to build a set of allowed hosts +// for metrics collection, similar to how auto-HTTPS scans for domain names. +func (m *Metrics) scanConfigForHosts(app *App) { + if !m.PerHost { + return + } + + m.allowedHosts = make(map[string]struct{}) + m.hasHTTPSServer = false + + for _, srv := range app.Servers { + // Check if this server has TLS enabled + serverHasTLS := len(srv.TLSConnPolicies) > 0 + if serverHasTLS { + m.hasHTTPSServer = true + } + + // Collect hosts from route matchers + for _, route := range srv.Routes { + for _, matcherSet := range route.MatcherSets { + for _, matcher := range matcherSet { + if hm, ok := matcher.(*MatchHost); ok { + for _, host := range *hm { + // Only allow non-fuzzy hosts to prevent unbounded cardinality + if !hm.fuzzy(host) { + m.allowedHosts[strings.ToLower(host)] = struct{}{} + } + } + } + } + } + } + } +} + +// shouldAllowHostMetrics determines if metrics should be collected for the given host. +// This implements the cardinality protection by only allowing metrics for: +// 1. Explicitly configured hosts +// 2. Catch-all requests on HTTPS servers (if AllowCatchAllHosts is true or auto-enabled) +// 3. Catch-all requests on HTTP servers only if explicitly allowed +func (m *Metrics) shouldAllowHostMetrics(host string, isHTTPS bool) bool { + if !m.PerHost { + return true // host won't be used in labels anyway + } + + normalizedHost := strings.ToLower(host) + + // Always allow explicitly configured hosts + if _, exists := m.allowedHosts[normalizedHost]; exists { + return true + } + + // For catch-all requests (not in allowed hosts) + allowCatchAll := m.AllowCatchAllHosts || (isHTTPS && m.hasHTTPSServer) + return allowCatchAll +} + // serverNameFromContext extracts the current server name from the context. // Returns "UNKNOWN" if none is available (should probably never happen). func serverNameFromContext(ctx context.Context) string { @@ -133,9 +236,19 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re // of a panic statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""} + // Determine if this is an HTTPS request + isHTTPS := r.TLS != nil + if h.metrics.PerHost { - labels["host"] = strings.ToLower(r.Host) - statusLabels["host"] = strings.ToLower(r.Host) + // Apply cardinality protection for host metrics + if h.metrics.shouldAllowHostMetrics(r.Host, isHTTPS) { + labels["host"] = strings.ToLower(r.Host) + statusLabels["host"] = strings.ToLower(r.Host) + } else { + // Use a catch-all label for unallowed hosts to prevent cardinality explosion + labels["host"] = "_other" + statusLabels["host"] = "_other" + } } inFlight := h.metrics.httpMetrics.requestInFlight.With(labels) diff --git a/modules/caddyhttp/metrics_test.go b/modules/caddyhttp/metrics_test.go index 4e1aa8b30..9f6f59858 100644 --- a/modules/caddyhttp/metrics_test.go +++ b/modules/caddyhttp/metrics_test.go @@ -2,6 +2,7 @@ package caddyhttp import ( "context" + "crypto/tls" "errors" "net/http" "net/http/httptest" @@ -206,9 +207,11 @@ func TestMetricsInstrumentedHandler(t *testing.T) { func TestMetricsInstrumentedHandlerPerHost(t *testing.T) { ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) metrics := &Metrics{ - PerHost: true, - init: sync.Once{}, - httpMetrics: &httpMetrics{}, + PerHost: true, + AllowCatchAllHosts: true, // Allow all hosts for testing + init: sync.Once{}, + httpMetrics: &httpMetrics{}, + allowedHosts: make(map[string]struct{}), } handlerErr := errors.New("oh noes") response := []byte("hello world!") @@ -379,6 +382,112 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) { } } +func TestMetricsCardinalityProtection(t *testing.T) { + ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) + + // Test 1: Without AllowCatchAllHosts, arbitrary hosts should be mapped to "_other" + metrics := &Metrics{ + PerHost: true, + AllowCatchAllHosts: false, // Default - should map unknown hosts to "_other" + init: sync.Once{}, + httpMetrics: &httpMetrics{}, + allowedHosts: make(map[string]struct{}), + } + + // Add one allowed host + metrics.allowedHosts["allowed.com"] = struct{}{} + + mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { + w.Write([]byte("hello")) + return nil + }) + + ih := newMetricsInstrumentedHandler(ctx, "test", mh, metrics) + + // Test request to allowed host + r1 := httptest.NewRequest("GET", "http://allowed.com/", nil) + r1.Host = "allowed.com" + w1 := httptest.NewRecorder() + ih.ServeHTTP(w1, r1, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil })) + + // Test request to unknown host (should be mapped to "_other") + r2 := httptest.NewRequest("GET", "http://attacker.com/", nil) + r2.Host = "attacker.com" + w2 := httptest.NewRecorder() + ih.ServeHTTP(w2, r2, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil })) + + // Test request to another unknown host (should also be mapped to "_other") + r3 := httptest.NewRequest("GET", "http://evil.com/", nil) + r3.Host = "evil.com" + w3 := httptest.NewRecorder() + ih.ServeHTTP(w3, r3, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil })) + + // Check that metrics contain: + // - One entry for "allowed.com" + // - One entry for "_other" (aggregating attacker.com and evil.com) + expected := ` + # HELP caddy_http_requests_total Counter of HTTP(S) requests made. + # TYPE caddy_http_requests_total counter + caddy_http_requests_total{handler="test",host="_other",server="UNKNOWN"} 2 + caddy_http_requests_total{handler="test",host="allowed.com",server="UNKNOWN"} 1 + ` + + if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected), + "caddy_http_requests_total", + ); err != nil { + t.Errorf("Cardinality protection test failed: %s", err) + } +} + +func TestMetricsHTTPSCatchAll(t *testing.T) { + ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) + + // Test that HTTPS requests allow catch-all even when AllowCatchAllHosts is false + metrics := &Metrics{ + PerHost: true, + AllowCatchAllHosts: false, + hasHTTPSServer: true, // Simulate having HTTPS servers + init: sync.Once{}, + httpMetrics: &httpMetrics{}, + allowedHosts: make(map[string]struct{}), // Empty - no explicitly allowed hosts + } + + mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { + w.Write([]byte("hello")) + return nil + }) + + ih := newMetricsInstrumentedHandler(ctx, "test", mh, metrics) + + // Test HTTPS request (should be allowed even though not in allowedHosts) + r1 := httptest.NewRequest("GET", "https://unknown.com/", nil) + r1.Host = "unknown.com" + r1.TLS = &tls.ConnectionState{} // Mark as TLS/HTTPS + w1 := httptest.NewRecorder() + ih.ServeHTTP(w1, r1, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil })) + + // Test HTTP request (should be mapped to "_other") + r2 := httptest.NewRequest("GET", "http://unknown.com/", nil) + r2.Host = "unknown.com" + // No TLS field = HTTP request + w2 := httptest.NewRecorder() + ih.ServeHTTP(w2, r2, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil })) + + // Check that HTTPS request gets real host, HTTP gets "_other" + expected := ` + # HELP caddy_http_requests_total Counter of HTTP(S) requests made. + # TYPE caddy_http_requests_total counter + caddy_http_requests_total{handler="test",host="_other",server="UNKNOWN"} 1 + caddy_http_requests_total{handler="test",host="unknown.com",server="UNKNOWN"} 1 + ` + + if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected), + "caddy_http_requests_total", + ); err != nil { + t.Errorf("HTTPS catch-all test failed: %s", err) + } +} + type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error { 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 69779e6ed..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 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/requestbody.go b/modules/caddyhttp/requestbody/requestbody.go index 1fa654811..65b09ded0 100644 --- a/modules/caddyhttp/requestbody/requestbody.go +++ b/modules/caddyhttp/requestbody/requestbody.go @@ -116,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 8439d1d51..12d610800 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -888,8 +888,11 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if commonScheme == "http" && te.TLSEnabled() { return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)") } - if te, ok := transport.(*HTTPTransport); ok && commonScheme == "h2c" { - te.Versions = []string{"h2c", "2"} + if h2ct, ok := transport.(H2CTransport); ok && commonScheme == "h2c" { + err := h2ct.EnableH2C() + if err != nil { + return err + } } } else if commonScheme == "https" { return d.Errf("upstreams are configured for HTTPS but transport module does not support TLS: %T", transport) diff --git a/modules/caddyhttp/reverseproxy/command.go b/modules/caddyhttp/reverseproxy/command.go index 54955bcf7..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) @@ -182,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) } @@ -204,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/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/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go index d451dd380..5c68c3ad5 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go @@ -112,6 +112,20 @@ func (t *Transport) Provision(ctx caddy.Context) error { return nil } +// DefaultBufferSizes enables request buffering for fastcgi if not configured. +// This is because most fastcgi servers are php-fpm that require the content length to be set to read the body, golang +// std has fastcgi implementation that doesn't need this value to process the body, but we can safely assume that's +// not used. +// http3 requests have a negative content length for GET and HEAD requests, if that header is not sent. +// see: https://github.com/caddyserver/caddy/issues/6678#issuecomment-2472224182 +// Though it appears even if CONTENT_LENGTH is invalid, php-fpm can handle just fine if the body is empty (no Stdin records sent). +// php-fpm will hang if there is any data in the body though, https://github.com/caddyserver/caddy/issues/5420#issuecomment-2415943516 + +// TODO: better default buffering for fastcgi requests without content length, in theory a value of 1 should be enough, make it bigger anyway +func (t Transport) DefaultBufferSizes() (int64, int64) { + return 4096, 0 +} + // RoundTrip implements http.RoundTripper. func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) { server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server) @@ -427,6 +441,7 @@ var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_") var ( _ zapcore.ObjectMarshaler = (*loggableEnv)(nil) - _ caddy.Provisioner = (*Transport)(nil) - _ http.RoundTripper = (*Transport)(nil) + _ caddy.Provisioner = (*Transport)(nil) + _ http.RoundTripper = (*Transport)(nil) + _ reverseproxy.BufferedTransport = (*Transport)(nil) ) 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/fastcgi/writer.go b/modules/caddyhttp/reverseproxy/fastcgi/writer.go index 3af00d9a1..225d8f5f8 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/writer.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/writer.go @@ -112,7 +112,7 @@ func encodeSize(b []byte, size uint32) int { binary.BigEndian.PutUint32(b, size) return 4 } - b[0] = byte(size) + b[0] = byte(size) //nolint:gosec // false positive; b is made 8 bytes long, then this function is always called with b being at least 4 or 1 byte long return 1 } 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 ac42570b2..b72e723e0 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -23,7 +23,6 @@ import ( "net/url" "regexp" "runtime/debug" - "slices" "strconv" "strings" "time" @@ -405,14 +404,9 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ u.Host = net.JoinHostPort(host, port) } - // this is kind of a hacky way to know if we should use HTTPS, but whatever - if tt, ok := h.Transport.(TLSTransport); ok && tt.TLSEnabled() { - u.Scheme = "https" - - // if the port is in the except list, flip back to HTTP - if ht, ok := h.Transport.(*HTTPTransport); ok && slices.Contains(ht.TLS.ExceptPorts, port) { - u.Scheme = "http" - } + // override health check schemes if applicable + if hcsot, ok := h.Transport.(HealthCheckSchemeOverriderTransport); ok { + hcsot.OverrideHealthCheckScheme(u, port) } // if we have a provisioned uri, use that, otherwise use diff --git a/modules/caddyhttp/reverseproxy/hosts.go b/modules/caddyhttp/reverseproxy/hosts.go index 300003f2b..fea85946d 100644 --- a/modules/caddyhttp/reverseproxy/hosts.go +++ b/modules/caddyhttp/reverseproxy/hosts.go @@ -281,3 +281,10 @@ 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" + +// proxyVarKey is the key used that indicates the proxy server used for a request. +const proxyVarKey = "reverse_proxy.proxy" diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 6729ea2fb..8edc585e7 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -24,6 +24,7 @@ import ( weakrand "math/rand" "net" "net/http" + "net/url" "os" "reflect" "slices" @@ -159,8 +160,7 @@ type HTTPTransport struct { // `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) + h3Transport *http3.Transport // TODO: EXPERIMENTAL (May 2024) } // CaddyModule returns the Caddy module information. @@ -204,11 +204,16 @@ func (h *HTTPTransport) Provision(ctx caddy.Context) error { func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, error) { // Set keep-alive defaults if it wasn't otherwise configured if h.KeepAlive == nil { - h.KeepAlive = &KeepAlive{ - ProbeInterval: caddy.Duration(30 * time.Second), - IdleConnTimeout: caddy.Duration(2 * time.Minute), - MaxIdleConnsPerHost: 32, // seems about optimal, see #2805 - } + h.KeepAlive = new(KeepAlive) + } + if h.KeepAlive.ProbeInterval == 0 { + h.KeepAlive.ProbeInterval = caddy.Duration(30 * time.Second) + } + if h.KeepAlive.IdleConnTimeout == 0 { + h.KeepAlive.IdleConnTimeout = caddy.Duration(2 * time.Minute) + } + if h.KeepAlive.MaxIdleConnsPerHost == 0 { + h.KeepAlive.MaxIdleConnsPerHost = 32 // seems about optimal, see #2805 } // Set a relatively short default dial timeout. @@ -267,15 +272,15 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e } dialContext := func(ctx context.Context, network, address string) (net.Conn, error) { - // For unix socket upstreams, we need to recover the dial info from - // the request's context, because the Host on the request's URL - // will have been modified by directing the request, overwriting - // the unix socket filename. - // Also, we need to avoid overwriting the address at this point - // when not necessary, because http.ProxyFromEnvironment may have - // modified the address according to the user's env proxy config. + // The network is usually tcp, and the address is the host in http.Request.URL.Host + // and that's been overwritten in directRequest + // However, if proxy is used according to http.ProxyFromEnvironment or proxy providers, + // address will be the address of the proxy server. + + // This means we can safely use the address in dialInfo if proxy is not used (the address and network will be same any way) + // or if the upstream is unix (because there is no way socks or http proxy can be used for unix address). if dialInfo, ok := GetDialInfo(ctx); ok { - if strings.HasPrefix(dialInfo.Network, "unix") { + if caddyhttp.GetVar(ctx, proxyVarKey) == nil || strings.HasPrefix(dialInfo.Network, "unix") { network = dialInfo.Network address = dialInfo.Address } @@ -376,9 +381,19 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e return nil, fmt.Errorf("network_proxy module is not `(func(*http.Request) (*url.URL, error))``") } } + // we need to keep track if a proxy is used for a request + proxyWrapper := func(req *http.Request) (*url.URL, error) { + u, err := proxy(req) + if u == nil || err != nil { + return u, err + } + // there must be a proxy for this request + caddyhttp.SetVar(req.Context(), proxyVarKey, u) + return u, nil + } rt := &http.Transport{ - Proxy: proxy, + Proxy: proxyWrapper, DialContext: dialContext, MaxConnsPerHost: h.MaxConnsPerHost, ResponseHeaderTimeout: time.Duration(h.ResponseHeaderTimeout), @@ -409,6 +424,14 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e 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 @@ -428,7 +451,19 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e } 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 } @@ -437,24 +472,10 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e rt.IdleConnTimeout = time.Duration(h.KeepAlive.IdleConnTimeout) } - // The proxy protocol header can only be sent once right after opening the connection. - // So single connection must not be used for multiple requests, which can potentially - // come from different clients. - if !rt.DisableKeepAlives && h.ProxyProtocol != "" { - caddyCtx.Logger().Warn("disabling keepalives, they are incompatible with using PROXY protocol") - rt.DisableKeepAlives = true - } - if h.Compression != nil { rt.DisableCompression = !*h.Compression } - if slices.Contains(h.Versions, "2") { - if err := http2.ConfigureTransport(rt); err != nil { - return nil, err - } - } - // configure HTTP/3 transport if enabled; however, this does not // automatically fall back to lower versions like most web browsers // do (that'd add latency and complexity, besides, we expect that @@ -472,25 +493,22 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e return nil, fmt.Errorf("if HTTP/3 is enabled to the upstream, no other HTTP versions are supported") } - // if h2c is enabled, configure its transport (std lib http.Transport - // does not "HTTP/2 over cleartext TCP") - if slices.Contains(h.Versions, "h2c") { - // crafting our own http2.Transport doesn't allow us to utilize - // most of the customizations/preferences on the http.Transport, - // because, for some reason, only http2.ConfigureTransport() - // is allowed to set the unexported field that refers to a base - // http.Transport config; oh well - h2t := &http2.Transport{ - // kind of a hack, but for plaintext/H2C requests, pretend to dial TLS - DialTLSContext: func(ctx context.Context, network, address string, _ *tls.Config) (net.Conn, error) { - return dialContext(ctx, network, address) - }, - AllowHTTP: true, + // if h2/c is enabled, configure it explicitly + if slices.Contains(h.Versions, "2") || slices.Contains(h.Versions, "h2c") { + if err := http2.ConfigureTransport(rt); err != nil { + return nil, err } - if h.Compression != nil { - h2t.DisableCompression = !*h.Compression + + // DisableCompression from h2 is configured by http2.ConfigureTransport + // Likewise, DisableKeepAlives from h1 is used too. + + // Protocols field is only used when the request is not using TLS, + // http1/2 over tls is still allowed + if slices.Contains(h.Versions, "h2c") { + rt.Protocols = new(http.Protocols) + rt.Protocols.SetUnencryptedHTTP2(true) + rt.Protocols.SetHTTP1(false) } - h.h2cTransport = h2t } return rt, nil @@ -505,15 +523,6 @@ func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { return h.h3Transport.RoundTrip(req) } - // if H2C ("HTTP/2 over cleartext") is enabled and the upstream request is - // HTTP without TLS, use the alternate H2C-capable transport instead - if req.URL.Scheme == "http" && h.h2cTransport != nil { - // There is no dedicated DisableKeepAlives field in *http2.Transport. - // This is an alternative way to disable keep-alive. - req.Close = h.Transport.DisableKeepAlives - return h.h2cTransport.RoundTrip(req) - } - return h.Transport.RoundTrip(req) } @@ -555,6 +564,26 @@ func (h *HTTPTransport) EnableTLS(base *TLSConfig) error { return nil } +// EnableH2C enables H2C (HTTP/2 over Cleartext) on the transport. +func (h *HTTPTransport) EnableH2C() error { + h.Versions = []string{"h2c", "2"} + return nil +} + +// OverrideHealthCheckScheme overrides the scheme of the given URL +// used for health checks. +func (h HTTPTransport) OverrideHealthCheckScheme(base *url.URL, port string) { + // if tls is enabled and the port isn't in the except list, use HTTPs + if h.TLSEnabled() && !slices.Contains(h.TLS.ExceptPorts, port) { + base.Scheme = "https" + } +} + +// ProxyProtocolEnabled returns true if proxy protocol is enabled. +func (h HTTPTransport) ProxyProtocolEnabled() bool { + return h.ProxyProtocol != "" +} + // Cleanup implements caddy.CleanerUpper and closes any idle connections. func (h HTTPTransport) Cleanup() error { if h.Transport == nil { @@ -811,8 +840,11 @@ func decodeBase64DERCert(certStr string) (*x509.Certificate, error) { // Interface guards var ( - _ caddy.Provisioner = (*HTTPTransport)(nil) - _ http.RoundTripper = (*HTTPTransport)(nil) - _ caddy.CleanerUpper = (*HTTPTransport)(nil) - _ TLSTransport = (*HTTPTransport)(nil) + _ caddy.Provisioner = (*HTTPTransport)(nil) + _ http.RoundTripper = (*HTTPTransport)(nil) + _ caddy.CleanerUpper = (*HTTPTransport)(nil) + _ TLSTransport = (*HTTPTransport)(nil) + _ H2CTransport = (*HTTPTransport)(nil) + _ HealthCheckSchemeOverriderTransport = (*HTTPTransport)(nil) + _ ProxyProtocolTransport = (*HTTPTransport)(nil) ) diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 88fba55a1..13bbee422 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -243,18 +243,16 @@ func (h *Handler) Provision(ctx caddy.Context) error { return fmt.Errorf("loading transport: %v", err) } h.Transport = mod.(http.RoundTripper) - // enable request buffering for fastcgi if not configured - // This is because most fastcgi servers are php-fpm that require the content length to be set to read the body, golang - // std has fastcgi implementation that doesn't need this value to process the body, but we can safely assume that's - // not used. - // http3 requests have a negative content length for GET and HEAD requests, if that header is not sent. - // see: https://github.com/caddyserver/caddy/issues/6678#issuecomment-2472224182 - // Though it appears even if CONTENT_LENGTH is invalid, php-fpm can handle just fine if the body is empty (no Stdin records sent). - // php-fpm will hang if there is any data in the body though, https://github.com/caddyserver/caddy/issues/5420#issuecomment-2415943516 - // TODO: better default buffering for fastcgi requests without content length, in theory a value of 1 should be enough, make it bigger anyway - if module, ok := h.Transport.(caddy.Module); ok && module.CaddyModule().ID.Name() == "fastcgi" && h.RequestBuffers == 0 { - h.RequestBuffers = 4096 + // set default buffer sizes if applicable + if bt, ok := h.Transport.(BufferedTransport); ok { + reqBuffers, respBuffers := bt.DefaultBufferSizes() + if h.RequestBuffers == 0 { + h.RequestBuffers = reqBuffers + } + if h.ResponseBuffers == 0 { + h.ResponseBuffers = respBuffers + } } } if h.LoadBalancing != nil && h.LoadBalancing.SelectionPolicyRaw != nil { @@ -409,12 +407,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") @@ -435,6 +437,20 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht reqHost := clonedReq.Host reqHeader := clonedReq.Header + // If the cloned request body was fully buffered, keep a reference to its + // buffer so we can reuse it across retries and return it to the pool + // once we’re done. + var bufferedReqBody *bytes.Buffer + if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil { + bufferedReqBody = reqBodyBuf.buf + reqBodyBuf.buf = nil + + defer func() { + bufferedReqBody.Reset() + bufPool.Put(bufferedReqBody) + }() + } + start := time.Now() defer func() { // total proxying duration, including time spent on LB and retries @@ -453,8 +469,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht // and reusable, so if a backend partially or fully reads the body but then // produces an error, the request can be repeated to the next backend with // the full body (retries should only happen for idempotent requests) (see #6259) - if reqBodyBuf, ok := r.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil { - r.Body = io.NopCloser(bytes.NewReader(reqBodyBuf.buf.Bytes())) + if bufferedReqBody != nil { + clonedReq.Body = io.NopCloser(bytes.NewReader(bufferedReqBody.Bytes())) } var done bool @@ -726,6 +742,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 { @@ -1188,7 +1210,7 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int // directRequest modifies only req.URL so that it points to the upstream // in the given DialInfo. It must modify ONLY the request URL. -func (Handler) directRequest(req *http.Request, di DialInfo) { +func (h *Handler) directRequest(req *http.Request, di DialInfo) { // we need a host, so set the upstream's host address reqHost := di.Address @@ -1199,6 +1221,13 @@ func (Handler) directRequest(req *http.Request, di DialInfo) { reqHost = di.Host } + // add client address to the host to let transport differentiate requests from different clients + if ppt, ok := h.Transport.(ProxyProtocolTransport); ok && ppt.ProxyProtocolEnabled() { + if proxyProtocolInfo, ok := caddyhttp.GetVar(req.Context(), proxyProtocolInfoVarKey).(ProxyProtocolInfo); ok { + reqHost = proxyProtocolInfo.AddrPort.String() + "->" + reqHost + } + } + req.URL.Host = reqHost } @@ -1356,7 +1385,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) } @@ -1484,6 +1513,32 @@ type TLSTransport interface { EnableTLS(base *TLSConfig) error } +// H2CTransport is implemented by transports +// that are capable of using h2c. +type H2CTransport interface { + EnableH2C() error +} + +// ProxyProtocolTransport is implemented by transports +// that are capable of using proxy protocol. +type ProxyProtocolTransport interface { + ProxyProtocolEnabled() bool +} + +// HealthCheckSchemeOverriderTransport is implemented by transports +// that can override the scheme used for health checks. +type HealthCheckSchemeOverriderTransport interface { + OverrideHealthCheckScheme(base *url.URL, port string) +} + +// BufferedTransport is implemented by transports +// that needs to buffer requests and/or responses. +type BufferedTransport interface { + // DefaultBufferSizes returns the default buffer sizes + // for requests and responses, respectively if buffering isn't enabled. + DefaultBufferSizes() (int64, int64) +} + // roundtripSucceededError is an error type that is returned if the // roundtrip succeeded, but an error occurred after-the-fact. type roundtripSucceededError struct{ error } @@ -1497,7 +1552,12 @@ type bodyReadCloser struct { } func (brc bodyReadCloser) Close() error { - bufPool.Put(brc.buf) + // Inside this package this will be set to nil for fully-buffered + // requests due to the possibility of retrial. + if brc.buf != nil { + bufPool.Put(brc.buf) + } + // For fully-buffered bodies, body is nil, so Close is a no-op. if brc.body != nil { return brc.body.Close() } diff --git a/modules/caddyhttp/reverseproxy/streaming.go b/modules/caddyhttp/reverseproxy/streaming.go index 374c208eb..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") @@ -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/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..94b8febfa 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -33,7 +33,7 @@ import ( "github.com/caddyserver/certmagic" "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/http3" - "github.com/quic-go/quic-go/qlog" + h3qlog "github.com/quic-go/quic-go/http3/qlog" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -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 @@ -262,30 +285,28 @@ type Server struct { onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023) } +var ( + ServerHeader = "Caddy" + serverHeader = []string{ServerHeader} +) + // 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() } } - w.Header().Set("Server", "Caddy") + h := w.Header() + h["Server"] = serverHeader // advertise HTTP/3, if enabled - if s.h3server != nil { - if r.ProtoMajor < 3 { - err := s.h3server.SetQUICHeaders(w.Header()) - if err != nil { - if c := s.logger.Check(zapcore.ErrorLevel, "setting HTTP/3 Alt-Svc header"); c != nil { - c.Write(zap.Error(err)) - } + if s.h3server != nil && r.ProtoMajor < 3 { + if err := s.h3server.SetQUICHeaders(h); err != nil { + if c := s.logger.Check(zapcore.ErrorLevel, "setting HTTP/3 Alt-Svc header"); c != nil { + c.Write(zap.Error(err)) } } } @@ -310,9 +331,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // enable full-duplex for HTTP/1, ensuring the entire // request body gets consumed before writing the response if s.EnableFullDuplex && r.ProtoMajor == 1 { - //nolint:bodyclose - err := http.NewResponseController(w).EnableFullDuplex() - if err != nil { + if err := http.NewResponseController(w).EnableFullDuplex(); err != nil { //nolint:bodyclose if c := s.logger.Check(zapcore.WarnLevel, "failed to enable full duplex"); c != nil { c.Write(zap.Error(err)) } @@ -399,8 +418,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { var fields []zapcore.Field if s.Errors != nil && len(s.Errors.Routes) > 0 { // execute user-defined error handling route - err2 := s.errorHandlerChain.ServeHTTP(w, r) - if err2 == nil { + if err2 := s.errorHandlerChain.ServeHTTP(w, r); err2 == nil { // user's error route handled the error response // successfully, so now just log the error for _, logger := range errLoggers { @@ -620,12 +638,14 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error MaxHeaderBytes: s.MaxHeaderBytes, QUICConfig: &quic.Config{ Versions: []quic.Version{quic.Version1, quic.Version2}, - Tracer: qlog.DefaultConnectionTracer, + Tracer: h3qlog.DefaultConnectionTracer, }, IdleTimeout: time.Duration(s.IdleTimeout), } } + s.quicListeners = append(s.quicListeners, h3ln) + //nolint:errcheck go s.h3server.ServeListener(h3ln) @@ -773,8 +793,10 @@ func (s *Server) logRequest( accLog *zap.Logger, r *http.Request, wrec ResponseRecorder, duration *time.Duration, repl *caddy.Replacer, bodyReader *lengthReader, shouldLogCredentials bool, ) { + ctx := r.Context() + // this request may be flagged as omitted from the logs - if skip, ok := GetVar(r.Context(), LogSkipVar).(bool); ok && skip { + if skip, ok := GetVar(ctx, LogSkipVar).(bool); ok && skip { return } @@ -792,7 +814,7 @@ func (s *Server) logRequest( } message := "handled request" - if nop, ok := GetVar(r.Context(), "unhandled").(bool); ok && nop { + if nop, ok := GetVar(ctx, "unhandled").(bool); ok && nop { message = "NOP" } @@ -816,7 +838,7 @@ func (s *Server) logRequest( reqBodyLength = bodyReader.Length } - extra := r.Context().Value(ExtraLogFieldsCtxKey).(*ExtraLogFields) + extra := ctx.Value(ExtraLogFieldsCtxKey).(*ExtraLogFields) fieldCount := 6 fields = make([]zapcore.Field, 0, fieldCount+len(extra.fields)) @@ -923,6 +945,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 +1015,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 +1112,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 6ce09974b..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,9 +161,7 @@ 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) } } @@ -303,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 12108ac03..d783d1b04 100644 --- a/modules/caddyhttp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -79,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) }, }) @@ -359,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/tracing/tracer.go b/modules/caddyhttp/tracing/tracer.go index 261952aa6..ab2ddf8a2 100644 --- a/modules/caddyhttp/tracing/tracer.go +++ b/modules/caddyhttp/tracing/tracer.go @@ -5,9 +5,9 @@ import ( "fmt" "net/http" + "go.opentelemetry.io/contrib/exporters/autoexport" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/contrib/propagators/autoprop" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -59,7 +59,7 @@ func newOpenTelemetryWrapper( return ot, fmt.Errorf("creating resource error: %w", err) } - traceExporter, err := otlptracegrpc.New(ctx) + traceExporter, err := autoexport.NewSpanExporter(ctx) if err != nil { return ot, fmt.Errorf("creating trace exporter error: %w", err) } 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/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 4830570bf..7f13fd71f 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -244,6 +244,9 @@ func (iss *ACMEIssuer) makeIssuerTemplate(ctx caddy.Context) (certmagic.ACMEIssu 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 { @@ -480,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() diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index c9274878a..e69b5ad2f 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -173,6 +173,9 @@ type AutomationPolicy struct { subjects []string magic *certmagic.Config storage certmagic.Storage + + // Whether this policy had explicit managers configured directly on it. + hadExplicitManagers bool } // Provision sets up ap and builds its underlying CertMagic config. @@ -209,9 +212,8 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { // store them on the policy before putting it on the config // load and provision any cert manager modules - var hadExplicitManagers bool if ap.ManagersRaw != nil { - hadExplicitManagers = true + ap.hadExplicitManagers = true vals, err := tlsApp.ctx.LoadModule(ap, "ManagersRaw") if err != nil { return fmt.Errorf("loading external certificate manager modules: %v", err) @@ -271,9 +273,9 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { // prevent issuance from Issuers (when Managers don't provide a certificate) if there's no // permission module configured noProtections := ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.permission == nil) - failClosed := noProtections && !hadExplicitManagers // don't allow on-demand issuance (other than implicit managers) if no managers have been explicitly configured + failClosed := noProtections && !ap.hadExplicitManagers // don't allow on-demand issuance (other than implicit managers) if no managers have been explicitly configured if noProtections { - if !hadExplicitManagers { + if !ap.hadExplicitManagers { // no managers, no explicitly-configured permission module, this is a config error return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details") } @@ -454,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/leaffolderloader.go b/modules/caddytls/leaffolderloader.go index 20f5aa82c..fe5e9e244 100644 --- a/modules/caddytls/leaffolderloader.go +++ b/modules/caddytls/leaffolderloader.go @@ -29,9 +29,9 @@ func init() { caddy.RegisterModule(LeafFolderLoader{}) } -// LeafFolderLoader loads certificates and their associated keys from disk +// LeafFolderLoader loads certificates from disk // by recursively walking the specified directories, looking for PEM -// files which contain both a certificate and a key. +// files which contain a certificate. type LeafFolderLoader struct { Folders []string `json:"folders,omitempty"` } 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/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 a929617d7..cf35e7178 100644 --- a/modules/logging/filters_test.go +++ b/modules/logging/filters_test.go @@ -1,6 +1,8 @@ package logging import ( + "fmt" + "strings" "testing" "go.uber.org/zap/zapcore" @@ -239,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_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