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 e7961281e..50501a0f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ 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 @@ -73,7 +73,7 @@ jobs: 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 @@ -162,7 +162,7 @@ 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 @@ -221,7 +221,7 @@ 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 @@ -233,7 +233,7 @@ jobs: 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.25" check-latest: true diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml index b2e172833..8aa9eaf59 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -51,7 +51,7 @@ 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 @@ -59,7 +59,7 @@ jobs: 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 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3d54bc546..849188c64 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -45,12 +45,12 @@ 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version: '~1.25' check-latest: true @@ -73,7 +73,7 @@ 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 @@ -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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: 'Dependency Review' - uses: actions/dependency-review-action@595b5aeba73380359d98a5e087f648dbb0edce1b # v4.7.3 + 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 b72987e87..e4880a64c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,8 +13,322 @@ 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: @@ -36,10 +350,12 @@ 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 @@ -49,7 +365,7 @@ jobs: 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 @@ -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@da167eac915b4e86f08b264dbdbc867b61be6f0c # main + uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # main - name: Syft version run: syft version - name: Install xcaddy @@ -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 43311829e..bb49f935d 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -37,7 +37,7 @@ 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 @@ -47,7 +47,7 @@ jobs: 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@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.29.5 + uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5 with: sarif_file: results.sarif 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/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/formatter.go b/caddyconfig/caddyfile/formatter.go index 8a757ea58..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) { @@ -245,7 +262,7 @@ func Format(input []byte) []byte { 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 29b910ff1..0092d1311 100644 --- a/caddyconfig/caddyfile/formatter_test.go +++ b/caddyconfig/caddyfile/formatter_test.go @@ -444,6 +444,11 @@ 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: `{ 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/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/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 1023a9d21..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,23 +565,22 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e if globalACMECARoot != nil && !slices.Contains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) { acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) } - if globalACMEDNSok { + if globalACMEDNSok && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil || acmeIssuer.Challenges.DNS.ProviderRaw == nil) { globalDNS := options["dns"] - if globalDNS != nil { - // If global `dns` is set, do NOT set provider in issuer, just set empty dns config - acmeIssuer.Challenges = &caddytls.ChallengesConfig{ - DNS: &caddytls.DNSChallengeConfig{}, - } - } else if globalACMEDNS != nil { - // Set a global DNS provider if `acme_dns` is set and `dns` is NOT set - acmeIssuer.Challenges = &caddytls.ChallengesConfig{ - DNS: &caddytls.DNSChallengeConfig{ - ProviderRaw: caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil), - }, - } - } else { + if globalDNS == nil && globalACMEDNS == nil { return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option") } + if acmeIssuer.Challenges == nil { + acmeIssuer.Challenges = new(caddytls.ChallengesConfig) + } + if acmeIssuer.Challenges.DNS == nil { + acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) + } + // If global `dns` is set, do NOT set provider in issuer, just set empty dns config + if globalDNS == nil && acmeIssuer.Challenges.DNS.ProviderRaw == nil { + // Set a global DNS provider if `acme_dns` is set and `dns` is NOT set + acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil) + } } if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil { acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB) @@ -607,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 index eaf8d0623..3f43a082a 100644 --- a/caddytest/integration/caddyfile_adapt/acme_dns_configured.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/acme_dns_configured.caddyfiletest @@ -53,6 +53,7 @@ example.com { "challenges": { "dns": { "provider": { + "argument": "foo", "name": "mock" } } 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/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 28e33f01b..75d114992 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -231,8 +231,9 @@ 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 @@ -249,6 +250,19 @@ 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 { @@ -295,7 +309,7 @@ func cmdRun(fl Flags) (int, error) { // 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 @@ -350,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 } @@ -368,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 { @@ -582,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 } @@ -797,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 3d24dd821..d4e660b2a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,8 @@ 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 @@ -15,43 +16,42 @@ require ( 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.3 - 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.10.1 - github.com/spf13/pflag v1.0.9 + 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/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/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 go.opentelemetry.io/otel/sdk v1.38.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.0 go.uber.org/zap/exp v0.3.0 - golang.org/x/crypto v0.41.0 - golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 - golang.org/x/net v0.43.0 - golang.org/x/sync v0.16.0 - golang.org/x/term v0.34.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.4 // 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.8.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 @@ -59,16 +59,16 @@ require ( 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.1.1 // 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.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 @@ -76,7 +76,8 @@ require ( 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/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.uber.org/mock v0.5.2 // indirect - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/oauth2 v0.30.0 // indirect - google.golang.org/api v0.247.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-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 ) @@ -135,14 +150,14 @@ require ( 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 @@ -151,13 +166,13 @@ require ( go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 go.opentelemetry.io/proto/otlp v1.7.1 // indirect - go.step.sm/crypto v0.70.0 + go.step.sm/crypto v0.74.0 go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.26.0 // indirect - golang.org/x/sys v0.35.0 - golang.org/x/text v0.28.0 // indirect - golang.org/x/tools v0.35.0 // indirect - google.golang.org/grpc v1.75.0 // indirect - google.golang.org/protobuf v1.36.8 // 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 8a1c8c779..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.4 h1:fXOAIQmkApVvcIn7Pc2+5J8QTMVbUGLscnSVNl11su8= -cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M= +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.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= -cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= +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,45 +45,41 @@ 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.38.0 h1:UCRQ5mlqcFk9HJDIqENSLR3wiG1VTWlyUfLDEvY7RxU= -github.com/aws/aws-sdk-go-v2 v1.38.0/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= -github.com/aws/aws-sdk-go-v2/config v1.31.0 h1:9yH0xiY5fUnVNLRWO0AtayqwU1ndriZdN78LlhruJR4= -github.com/aws/aws-sdk-go-v2/config v1.31.0/go.mod h1:VeV3K72nXnhbe4EuxxhzsDc/ByrCSlZwUnWH52Nde/I= -github.com/aws/aws-sdk-go-v2/credentials v1.18.4 h1:IPd0Algf1b+Qy9BcDp0sCUcIWdCQPSzDoMK3a8pcbUM= -github.com/aws/aws-sdk-go-v2/credentials v1.18.4/go.mod h1:nwg78FjH2qvsRM1EVZlX9WuGUJOL5od+0qvm0adEzHk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3 h1:GicIdnekoJsjq9wqnvyi2elW6CGMSYKhdozE7/Svh78= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3/go.mod h1:R7BIi6WNC5mc1kfRM7XM/VHC3uRWkjc396sfabq4iOo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3 h1:o9RnO+YZ4X+kt5Z7Nvcishlz0nksIt2PIzDglLMP0vA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3/go.mod h1:+6aLJzOG1fvMOyzIySYjOFjcguGvVRL68R+uoRencN4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3 h1:joyyUFhiTQQmVK6ImzNU9TQSNRNeD9kOklqTzyk5v6s= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3/go.mod h1:+vNIyZQP3b3B1tSLI0lxvrU9cfM7gpdRXMFfm67ZcPc= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3 h1:ieRzyHXypu5ByllM7Sp4hC5f/1Fy5wqxqY0yB85hC7s= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3/go.mod h1:O5ROz8jHiOAKAwx179v+7sHMhfobFVi6nZt8DEyiYoM= -github.com/aws/aws-sdk-go-v2/service/kms v1.44.0 h1:Z95XCqqSnwXr0AY7PgsiOUBhUG2GoDM5getw6RfD1Lg= -github.com/aws/aws-sdk-go-v2/service/kms v1.44.0/go.mod h1:DqcSngL7jJeU1fOzh5Ll5rSvX/MlMV6OZlE4mVdFAQc= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.0 h1:Mc/MKBf2m4VynyJkABoVEN+QzkfLqGj0aiJuEe7cMeM= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.0/go.mod h1:iS5OmxEcN4QIPXARGhavH7S8kETNL11kym6jhoS7IUQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0 h1:6csaS/aJmqZQbKhi1EyEMM7yBW653Wy/B9hnBofW+sw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0/go.mod h1:59qHWaY5B+Rs7HGTuVGaC32m0rdpQ68N8QCN3khYiqs= -github.com/aws/aws-sdk-go-v2/service/sts v1.37.0 h1:MG9VFW43M4A8BYeAfaJJZWrroinxeTi2r3+SnmLQfSA= -github.com/aws/aws-sdk-go-v2/service/sts v1.37.0/go.mod h1:JdeBDPgpJfuS6rU/hNglmOigKhyEZtBmbraLE4GK1J8= -github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= -github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/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.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= @@ -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.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= -github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= +github.com/go-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,19 +155,12 @@ 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.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= @@ -192,34 +168,26 @@ github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PU 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.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/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= @@ -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,7 +217,6 @@ 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= @@ -261,9 +224,7 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= -github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -271,12 +232,10 @@ 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.3 h1:gUl789rjbJSuM5hYzOFnNaGgWPV1xVfnOs59o0dZEcc= -github.com/mholt/acmez/v3 v3.1.3/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= -github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +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= @@ -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= @@ -398,8 +325,9 @@ 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.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 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= @@ -420,12 +348,9 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu 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,9 +368,12 @@ 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.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= @@ -462,71 +390,81 @@ go.opentelemetry.io/contrib/propagators/ot v1.38.0 h1:k4gSyyohaDXI8F9BDXYC3uO2vr 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.70.0 h1:Q9Ft7N637mucyZcHZd1+0VVQJVwDCKqcb9CYcYi7cds= -go.step.sm/crypto v0.70.0/go.mod h1:pzfUhS5/ue7ev64PLlEgXvhx1opwbhFCjkvlhsxVds0= +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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -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-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= -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.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= -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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.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.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +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,76 +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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -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.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +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= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= -google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc= -google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -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/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-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= -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.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= -google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +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.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +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/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 01adc615d..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 @@ -467,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 { 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/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 45f99c4d7..6ad18d051 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -198,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 @@ -466,7 +468,14 @@ func (app *App) Start() error { 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 }, } @@ -538,6 +547,8 @@ func (app *App) Start() error { KeepAliveConfig: net.KeepAliveConfig{ Enable: srv.KeepAliveInterval >= 0, Interval: time.Duration(srv.KeepAliveInterval), + Idle: time.Duration(srv.KeepAliveIdle), + Count: srv.KeepAliveCount, }, }) if err != nil { diff --git a/modules/caddyhttp/caddyauth/argon2id.go b/modules/caddyhttp/caddyauth/argon2id.go new file mode 100644 index 000000000..f1070ce48 --- /dev/null +++ b/modules/caddyhttp/caddyauth/argon2id.go @@ -0,0 +1,188 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyauth + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "fmt" + "strconv" + "strings" + + "golang.org/x/crypto/argon2" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(Argon2idHash{}) +} + +const ( + argon2idName = "argon2id" + defaultArgon2idTime = 1 + defaultArgon2idMemory = 46 * 1024 + defaultArgon2idThreads = 1 + defaultArgon2idKeylen = 32 + defaultSaltLength = 16 +) + +// Argon2idHash implements the Argon2id password hashing. +type Argon2idHash struct { + salt []byte + time uint32 + memory uint32 + threads uint8 + keyLen uint32 +} + +// CaddyModule returns the Caddy module information. +func (Argon2idHash) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "http.authentication.hashes.argon2id", + New: func() caddy.Module { return new(Argon2idHash) }, + } +} + +// Compare checks if the plaintext password matches the given Argon2id hash. +func (Argon2idHash) Compare(hashed, plaintext []byte) (bool, error) { + argHash, storedKey, err := DecodeHash(hashed) + if err != nil { + return false, err + } + + computedKey := argon2.IDKey( + plaintext, + argHash.salt, + argHash.time, + argHash.memory, + argHash.threads, + argHash.keyLen, + ) + + return subtle.ConstantTimeCompare(storedKey, computedKey) == 1, nil +} + +// Hash generates an Argon2id hash of the given plaintext using the configured parameters and salt. +func (b Argon2idHash) Hash(plaintext []byte) ([]byte, error) { + if b.salt == nil { + s, err := generateSalt(defaultSaltLength) + if err != nil { + return nil, err + } + b.salt = s + } + + key := argon2.IDKey( + plaintext, + b.salt, + b.time, + b.memory, + b.threads, + b.keyLen, + ) + + hash := fmt.Sprintf( + "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + argon2.Version, + b.memory, + b.time, + b.threads, + base64.RawStdEncoding.EncodeToString(b.salt), + base64.RawStdEncoding.EncodeToString(key), + ) + + return []byte(hash), nil +} + +// DecodeHash parses an Argon2id PHC string into an Argon2idHash struct and returns the struct along with the derived key. +func DecodeHash(hash []byte) (*Argon2idHash, []byte, error) { + parts := strings.Split(string(hash), "$") + if len(parts) != 6 { + return nil, nil, fmt.Errorf("invalid hash format") + } + + if parts[1] != argon2idName { + return nil, nil, fmt.Errorf("unsupported variant: %s", parts[1]) + } + + version, err := strconv.Atoi(strings.TrimPrefix(parts[2], "v=")) + if err != nil { + return nil, nil, fmt.Errorf("invalid version: %w", err) + } + if version != argon2.Version { + return nil, nil, fmt.Errorf("incompatible version: %d", version) + } + + params := strings.Split(parts[3], ",") + if len(params) != 3 { + return nil, nil, fmt.Errorf("invalid parameters") + } + + mem, err := strconv.ParseUint(strings.TrimPrefix(params[0], "m="), 10, 32) + if err != nil { + return nil, nil, fmt.Errorf("invalid memory parameter: %w", err) + } + + iter, err := strconv.ParseUint(strings.TrimPrefix(params[1], "t="), 10, 32) + if err != nil { + return nil, nil, fmt.Errorf("invalid iterations parameter: %w", err) + } + + threads, err := strconv.ParseUint(strings.TrimPrefix(params[2], "p="), 10, 8) + if err != nil { + return nil, nil, fmt.Errorf("invalid parallelism parameter: %w", err) + } + + salt, err := base64.RawStdEncoding.Strict().DecodeString(parts[4]) + if err != nil { + return nil, nil, fmt.Errorf("decode salt: %w", err) + } + + key, err := base64.RawStdEncoding.Strict().DecodeString(parts[5]) + if err != nil { + return nil, nil, fmt.Errorf("decode key: %w", err) + } + + return &Argon2idHash{ + salt: salt, + time: uint32(iter), + memory: uint32(mem), + threads: uint8(threads), + keyLen: uint32(len(key)), + }, key, nil +} + +// FakeHash returns a constant fake hash for timing attacks mitigation. +func (Argon2idHash) FakeHash() []byte { + // hashed with the following command: + // caddy hash-password --plaintext "antitiming" --algorithm "argon2id" + return []byte("$argon2id$v=19$m=47104,t=1,p=1$P2nzckEdTZ3bxCiBCkRTyA$xQL3Z32eo5jKl7u5tcIsnEKObYiyNZQQf5/4sAau6Pg") +} + +// Interface guards +var ( + _ Comparer = (*Argon2idHash)(nil) + _ Hasher = (*Argon2idHash)(nil) +) + +func generateSalt(length int) ([]byte, error) { + salt := make([]byte, length) + if _, err := rand.Read(salt); err != nil { + return nil, fmt.Errorf("failed to generate salt: %w", err) + } + return salt, nil +} diff --git a/modules/caddyhttp/caddyauth/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/encode/encode.go b/modules/caddyhttp/encode/encode.go index 421f956e8..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"` @@ -168,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) } } diff --git a/modules/caddyhttp/http2listener.go b/modules/caddyhttp/http2listener.go index afc5e51ab..ad5991790 100644 --- a/modules/caddyhttp/http2listener.go +++ b/modules/caddyhttp/http2listener.go @@ -36,6 +36,12 @@ func (h *http2Listener) Accept() (net.Conn, error) { return nil, err } + // *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 { @@ -46,6 +52,9 @@ func (h *http2Listener) Accept() (net.Conn, error) { // 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 } @@ -53,14 +62,26 @@ func (h *http2Listener) Accept() (net.Conn, error) { // or else the listener wouldn't be created h2Conn := &http2Conn{ h2Expected: h.useH2, + logger: h.logger, Conn: conn, } if isConnectionStater { - return http2StateConn{h2Conn}, nil + return tlsStateConn{http2StateConn{h2Conn}}, nil } return h2Conn, 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 } 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/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_test.go b/modules/caddyhttp/matchers_test.go index 068207e85..b15b6316d 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -992,7 +992,6 @@ func TestVarREMatcher(t *testing.T) { expect: true, }, } { - tc := tc // capture range value t.Run(tc.desc, func(t *testing.T) { t.Parallel() // compile the regexp and validate its name 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/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 1a6be4d05..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 @@ -449,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 @@ -484,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 @@ -517,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) } @@ -567,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 { @@ -823,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 c8b10581a..d207e240e 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") @@ -726,6 +728,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 +1196,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 +1207,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 } @@ -1484,6 +1499,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 } 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/server.go b/modules/caddyhttp/server.go index f6666c14f..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. @@ -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: Scheduled to be removed later because https://github.com/golang/go/pull/56110 has been merged. if r.TLS == nil { - // not all requests have a conn (like virtual requests) - see #5698 - if conn, ok := r.Context().Value(ConnCtxKey).(net.Conn); ok { - if csc, ok := conn.(connectionStater); 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,7 +638,7 @@ 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), } @@ -775,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 } @@ -794,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" } @@ -818,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)) @@ -925,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 @@ -1081,11 +1112,14 @@ const ( // originally came into the server's entry handler OriginalRequestCtxKey caddy.CtxKey = "original_request" - // For referencing underlying net.Conn - // This will eventually be deprecated and not used. To refer to the underlying connection, implement a middleware plugin + // 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 d47f55c63..eecb392e4 100644 --- a/modules/caddyhttp/server_test.go +++ b/modules/caddyhttp/server_test.go @@ -297,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 d4b7c1d4c..bb0f81fc3 100644 --- a/modules/caddyhttp/tracing/tracer.go +++ b/modules/caddyhttp/tracing/tracer.go @@ -5,10 +5,10 @@ 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/attribute" - "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" @@ -63,7 +63,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/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/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 4c74bb95b..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) @@ -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/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